Beispiel #1
0
    def editor(self, filename):
        """Spawn the default editor ($EDITOR env var or editor configuration
        item)."""

        if not self.config_editor:
            raise exceptions.FatalError("no editor configured (EDITOR "
                                        "environment variable or editor "
                                        "configuration item)")

        if subprocess.call([self.config_editor, filename]) != 0:
            raise exceptions.FatalError("there was a problem running the "
                                        "editor")
Beispiel #2
0
    def post(self, query_string, data=None, handle_errors=True):
        """Generates a POST query on the target Trac system.

        This also alters the given data to include the form token stored on the
        cookies. Without this token, Trac will refuse form submissions.

        :param query_string: Starts with a slash, part of the URL between the
                             domain and the parameters (before the ?).
        :param data: Dictionary of parameters to encode and transmit to the
                     target page.
        :param handle_errors: Crash with a proper exception according to the
                              HTTP return code (default: True).

        """
        if data:
            data["__FORM_TOKEN"] = self.get_form_token()

        r = self.session.post(self.base_url + query_string, data=data)

        if r.status_code >= 400 and handle_errors:
            message = text.extract_message(r.text)
            if not message:
                message = "{} returned {}".format(self.base_url, r)
            raise exceptions.FatalError(message)

        return r
Beispiel #3
0
    def get(self, query_string, data=None, handle_errors=True):
        """Generates a GET query on the target Trac system.

        TODO: extract all the possible error elements as message.

        :param query_string: Starts with a slash, part of the URL between the
                             domain and the parameters (before the ?).
        :param data: Dictionary of parameters to encode at the end of the
                     ``query_string``.
        :param handle_errors: Crash with a proper exception according to the
                              HTTP return code (default: True).

        """

        r = self.session.get(self.base_url + query_string, data=data)

        if r.status_code >= 400 and handle_errors:
            message = text.extract_message(r.text)
            if not message:
                message = "{} returned {}".format(self.base_url, r)
            raise exceptions.FatalError(message)

        # Check the version if we can.
        self.check_version(r.text)

        return r
Beispiel #4
0
    def run_change(self, ticket_id, *values):
        """Make change to the given ticket_id.

        This command does not return anything if successful.

        TODO: support spawning an editor to change field values.

        usage: cm change ticket_id field=value [field=value...]

        """
        ticket_id = text.validate_id(ticket_id)

        if not values:
            raise exceptions.InvalidParameter("should provide at least one "
                                              "field change")
        fields_data = {}
        for v in values:
            s = v.split('=', 1)
            if len(s) != 2:
                raise exceptions.InvalidParameter(
                    "invalid value '{}', should be a field=value "
                    "pair".format(v))
            field = s[0].strip()
            value = s[1]
            fields_data["field_" + field] = value

        self.login()

        # Load the timestamps from the ticket page.
        r = self.get("/ticket/{}".format(ticket_id))
        timestamps = self._extract_timestamps(r.text)

        if self.message:
            comment = self.message
        elif self.add_comment:
            comment = self._read_comment()
        else:
            comment = ""

        data = {
            "action": "leave",
            "comment": comment,
            "submit": "Submit changes",
        }
        data.update(timestamps)
        data.update(fields_data)

        r = self.post("/ticket/{}".format(ticket_id), data)

        # Starting from 1.0+, the system-message element is always on the page,
        # only the style is changed.
        if self.trac_version >= (1, 0):
            token = 'system-message" style=""'
        else:
            token = "system-message"

        if token in r.text or r.status_code != 200:
            raise exceptions.FatalError("unable to save change")
Beispiel #5
0
    def run_comment(self, ticket_id):
        """Add a comment to the given ticket_id. This command does not return
        anything if successful. Command is cancelled if the content of the
        comment is empty.

        usage: cm comment ticket_id

        """
        ticket_id = text.validate_id(ticket_id)

        if self.message:
            comment = self.message
        else:
            comment = self._read_comment()

        if not comment.strip():
            raise exceptions.FatalError("empty comment, cancelling")

        self.login()

        # Load the timestamps from the ticket page.
        r = self.get("/ticket/{}".format(ticket_id))
        timestamps = self._extract_timestamps(r.text)

        data = {
            "comment": comment,
            "action": "leave",
            "submit": "Submit changes",
        }
        data.update(timestamps)

        r = self.post("/ticket/{}".format(ticket_id), data)

        # Starting from 1.0+, the system-message element is always on the page,
        # only the style is changed.
        if self.trac_version >= (1, 0):
            token = 'system-message" style=""'
        else:
            token = "system-message"

        if token in r.text or r.status_code != 200:
            raise exceptions.FatalError("unable to save comment")
Beispiel #6
0
def extract_timestamps_common(token, raw_html):
    """Given a dump of HTML data, extract the timestamp and return it as a
    string value.

    :param raw_html: Dump from the ticket page.

    """
    regex = r"""name="{}" value="([^"]+)""".format(token)

    m = re.search(regex, raw_html, re.MULTILINE)

    if m:
        timestamp = m.group(1)
    else:
        raise exceptions.FatalError("unable to fetch timestamp")

    return timestamp
Beispiel #7
0
    def run_status(self, ticket_id, status=None):
        """Updates the status of a ticket.

        usage: cm status ticket_id [new_status]

        """
        output = []
        ticket_id = text.validate_id(ticket_id)

        self.login()

        # Get all the available actions for this ticket
        r = self.get("/ticket/{}".format(ticket_id))
        statuses = text.extract_statuses(r.text)

        # Just display current status.
        if not status:
            status = self.extract_status_from_ticket_page(r.text)
            output.append("Current status: {}".format(status))
            if statuses:
                output.append("Available statuses: {}".format(
                    ", ".join(statuses)))
            return output

        if not status:
            raise exceptions.FatalError("bad status (acceptable: {})".format(
                ", ".join(statuses)))

        if self.message:
            comment = self.message
        elif self.add_comment:
            comment = self._read_comment()
        else:
            comment = ""

        # Not having a value for submit causes Trac to ignore the request.
        data = {
            "action": status,
            "comment": comment,
            "submit": "anything",
        }
        data.update(self._extract_timestamps(r.text))

        r = self.post("/ticket/{}".format(ticket_id), data)
Beispiel #8
0
def extract_status_from_ticket_page_common(re_status, raw_html):
    """Given a dump of the HTML ticket page, extract the current status of
    a ticket.

    TODO: return resolution and display it if any.

    :param raw_html: Dump for the ticket page.

    """
    m = re.search(re_status, raw_html, re.MULTILINE)

    if m:
        status = m.group(1)
        # task_type = m.group(2)
        # resolution = m.group(3)
    else:
        raise exceptions.FatalError("unable to fetch ticket status")

    return status
Beispiel #9
0
    def run_new(self, owner=None):
        """Create a new ticket and return its id if successful.

        usage: cm new [owner]

        """
        template = self.resolve_template()

        if not template:
            if self.message_file:
                if self.message_file == "-":
                    template = sys.stdin.read()
                else:
                    with open(self.message_file) as fp:
                        template = fp.read()
            else:
                template = DEFAULT_TEMPLATE

        # Parse the template ahead of time, allowing us to insert the Owner/To.
        ep = email.parser.Parser()
        em = ep.parsestr(template)
        body = em.get_payload()
        headers = OrderedDict(em.items())

        # The owner specified on the command line always prevails.
        if owner:
            headers["To"] = owner

        # If all else fail, assign it to yourself.
        if not headers["To"]:
            headers["To"] = self.username

        self.login()

        valid = False
        while not valid:
            # Get the properties at each iteration, in case an admin updated
            # the list in the mean time.
            options = self.get_property_options()

            # Assume the user will produce a valid ticket
            valid = True

            # Load the current values in a temp file for editing
            (fd, filename) = tempfile.mkstemp(suffix=".cm.ticket")
            fp = os.fdopen(fd, "w")

            fp.write(self._format_headers(headers))
            fp.write("\n\n")
            fp.write(body)

            fp.close()

            # When reading the message from stdin, we can't edit, skip editor.
            if not self.message_file:
                self.editor(filename)

            # Use the email parser to get the headers.
            ep = email.parser.Parser()
            with open(filename, "r") as fp:
                em = ep.parse(fp)

            os.unlink(filename)

            body = em.get_payload()
            headers = OrderedDict(em.items())

            errors = []
            fuzzy_match_fields = ("Milestone", "Component", "Type", "Version",
                                  "Priority")

            # Ensures all the required fields are filled-in
            for key in self.required_fields:
                if key in fuzzy_match_fields:
                    continue
                if not headers.get(key) or "**ERROR**" in headers[key]:
                    errors.append("Invalid '{}': cannot be blank".format(key))

            # Some fields are tolerant to incomplete values, this is where we
            # try to complete them.
            for key in fuzzy_match_fields:
                lkey = key.lower()
                if lkey not in options:
                    continue

                valid_options = options[lkey]

                # The specified value is not available in the multi-choice.
                if key in headers and headers[key] not in valid_options:
                    m = text.fuzzy_find(headers[key], valid_options)
                    if m:
                        # We found a close match, update the value with it.
                        headers[key] = m
                    else:
                        # We didn't find a close match. If the user entered
                        # something explicitly or if this field is required,
                        # this is an error, else just wipe the value and move
                        # on.
                        if headers[key] or key in self.required_fields:
                            joined_options = ", ".join(valid_options)
                            errors.append(u"Invalid '{}': expected: {}".format(
                                key, joined_options))
                        else:
                            headers[key] = ""

            if errors:
                valid = False
                print("\nFound the following errors:")
                for error in errors:
                    print(u" - {}".format(error))

                try:
                    if not self.message_file:
                        self.input("\n-- Hit Enter to return to editor, "
                                   "^C to abort --\n")
                except KeyboardInterrupt:
                    raise exceptions.FatalError("ticket creation interrupted")

            # There is no editor loop when reading message from stdin, just
            # print the errors and exit.
            if self.message_file:
                break

        # Since the body is expected to be using CRLF line termination, we
        # replace newlines by CRLF if no CRLF is found.
        if "\r\n" not in body:
            body = body.replace("\n", "\r\n")

        fields_data = {
            "field_summary": headers.get("Subject", ""),
            "field_type": headers.get("Type", ""),
            "field_version": headers.get("Version", ""),
            "field_description": body,
            "field_milestone": headers.get("Milestone", ""),
            "field_component": headers.get("Component", ""),
            "field_owner": headers.get("To", ""),
            "field_keywords": headers.get("Keywords", ""),
            "field_cc": headers.get("Cc", ""),
            "field_attachment": "",
        }

        # Assume anything outside of the original headers it to be included as
        # fields.
        for key, value in headers.items():
            field_name = "field_" + key.lower()
            if field_name not in fields_data:
                fields_data[field_name] = value

        r = self.post("/newticket", fields_data)

        if r.status_code != 200:
            message = text.extract_message(r.text)
            if not message:
                message = "unable to create new ticket"
            raise exceptions.RequestException(message)

        try:
            ticket_id = int(r.url.split("/")[-1])
        except:
            raise exceptions.RequestException("returned ticket_id is invalid.")

        self.open_in_browser_on_request(ticket_id)

        return ["ticket #{} created".format(ticket_id)]