Ejemplo n.º 1
0
 def update(self,
            req,
            id,
            comment,
            attributes={},
            notify=False,
            author='',
            when=None):
     """ Update a ticket, returning the new ticket in the same form as
     get(). 'New-style' call requires two additional items in attributes:
     (1) 'action' for workflow support (including any supporting fields
     as retrieved by getActions()),
     (2) '_ts' changetime token for detecting update collisions (as received
     from get() or update() calls).
     ''Calling update without 'action' and '_ts' changetime token is
     deprecated, and will raise errors in a future version.'' """
     t = model.Ticket(self.env, id)
     # custom author?
     if author and not (req.authname == 'anonymous' \
                         or 'TICKET_ADMIN' in req.perm(t.resource)):
         # only allow custom author if anonymous is permitted or user is admin
         self.log.warn(
             "RPC ticket.update: %r not allowed to change author "
             "to %r for comment on #%d", req.authname, author, id)
         author = ''
     author = author or req.authname
     # custom change timestamp?
     if when and not 'TICKET_ADMIN' in req.perm(t.resource):
         self.log.warn(
             "RPC ticket.update: %r not allowed to update #%d with "
             "non-current timestamp (%r)", author, id, when)
         when = None
     when = when or to_datetime(None, utc)
     # never try to update 'time' and 'changetime' attributes directly
     if 'time' in attributes:
         del attributes['time']
     if 'changetime' in attributes:
         del attributes['changetime']
     # and action...
     if not 'action' in attributes:
         # FIXME: Old, non-restricted update - remove soon!
         self.log.warning("Rpc ticket.update for ticket %d by user %s " \
                 "has no workflow 'action'." % (id, req.authname))
         req.perm(t.resource).require('TICKET_MODIFY')
         time_changed = attributes.pop('_ts', None)
         if time_changed and \
                 str(time_changed) != str(to_utimestamp(t.time_changed)):
             raise TracError("Ticket has been updated since last get().")
         for k, v in attributes.iteritems():
             t[k] = v
         t.save_changes(author, comment, when=when)
     else:
         ts = TicketSystem(self.env)
         tm = TicketModule(self.env)
         # TODO: Deprecate update without time_changed timestamp
         time_changed = attributes.pop('_ts', to_utimestamp(t.time_changed))
         try:
             time_changed = int(time_changed)
         except ValueError:
             raise TracError("RPC ticket.update: Wrong '_ts' token " \
                             "in attributes (%r)." % time_changed)
         action = attributes.get('action')
         avail_actions = ts.get_available_actions(req, t)
         if not action in avail_actions:
             raise TracError("Rpc: Ticket %d by %s " \
                     "invalid action '%s'" % (id, req.authname, action))
         controllers = list(tm._get_action_controllers(req, t, action))
         all_fields = [field['name'] for field in ts.get_ticket_fields()]
         for k, v in attributes.iteritems():
             if k in all_fields and k != 'status':
                 t[k] = v
         # TicketModule reads req.args - need to move things there...
         req.args.update(attributes)
         req.args['comment'] = comment
         # Collision detection: 0.11+0.12 timestamp
         req.args['ts'] = str(from_utimestamp(time_changed))
         # Collision detection: 0.13/1.0+ timestamp
         req.args['view_time'] = str(time_changed)
         changes, problems = tm.get_ticket_changes(req, t, action)
         for warning in problems:
             add_warning(req, "Rpc ticket.update: %s" % warning)
         valid = problems and False or tm._validate_ticket(req, t)
         if not valid:
             raise TracError(" ".join(
                 [warning for warning in req.chrome['warnings']]))
         else:
             tm._apply_ticket_changes(t, changes)
             self.log.debug("Rpc ticket.update save: %s" % repr(t.values))
             t.save_changes(author, comment, when=when)
             # Apply workflow side-effects
             for controller in controllers:
                 controller.apply_action_side_effects(req, t, action)
     if notify:
         try:
             tn = TicketNotifyEmail(self.env)
             tn.notify(t, newticket=False, modtime=when)
         except Exception, e:
             self.log.exception("Failure sending notification on change of "
                                "ticket #%s: %s" % (t.id, e))
Ejemplo n.º 2
0
    def save_changes(self, author=None, comment=None, when=None, db=None,
                     cnum='', replyto=None):
        """
        Store ticket changes in the database. The ticket must already exist in
        the database.  Returns False if there were no changes to save, True
        otherwise.

        :since 0.13: the `db` parameter is no longer needed and will be removed
        in version 0.14
        :since 0.13: the `cnum` parameter is deprecated, and threading should
        be controlled with the `replyto` argument
        """
        assert self.exists, "Cannot update a new ticket"

        if 'cc' in self.values:
            self['cc'] = _fixup_cc_list(self.values['cc'])

        if not self._old and not comment:
            return False # Not modified

        if when is None:
            when = datetime.now(utc)
        when_ts = to_utimestamp(when)

        if 'component' in self.values:
            # If the component is changed on a 'new' ticket
            # then owner field is updated accordingly. (#623).
            if self.values.get('status') == 'new' \
                    and 'component' in self._old \
                    and 'owner' not in self._old:
                try:
                    old_comp = Component(self.env, self._old['component'])
                    old_owner = old_comp.owner or ''
                    current_owner = self.values.get('owner') or ''
                    if old_owner == current_owner:
                        new_comp = Component(self.env, self['component'])
                        if new_comp.owner:
                            self['owner'] = new_comp.owner
                except TracError:
                    # If the old component has been removed from the database
                    # we just leave the owner as is.
                    pass

        with self.env.db_transaction as db:
            db("UPDATE ticket SET changetime=%s WHERE id=%s",
               (when_ts, self.id))
            
            # find cnum if it isn't provided
            if not cnum:
                num = 0
                for ts, old in db("""
                        SELECT DISTINCT tc1.time, COALESCE(tc2.oldvalue,'')
                        FROM ticket_change AS tc1
                        LEFT OUTER JOIN ticket_change AS tc2
                        ON tc2.ticket=%s AND tc2.time=tc1.time
                           AND tc2.field='comment'
                        WHERE tc1.ticket=%s ORDER BY tc1.time DESC
                        """, (self.id, self.id)):
                    # Use oldvalue if available, else count edits
                    try:
                        num += int(old.rsplit('.', 1)[-1])
                        break
                    except ValueError:
                        num += 1
                cnum = str(num + 1)
                if replyto:
                    cnum = '%s.%s' % (replyto, cnum)

            # store fields
            for name in self._old.keys():
                if name in self.custom_fields:
                    for row in db("""SELECT * FROM ticket_custom 
                                     WHERE ticket=%s and name=%s
                                     """, (self.id, name)):
                        db("""UPDATE ticket_custom SET value=%s
                              WHERE ticket=%s AND name=%s
                              """, (self[name], self.id, name))
                        break
                    else:
                        db("""INSERT INTO ticket_custom (ticket,name,value)
                              VALUES(%s,%s,%s)
                              """, (self.id, name, self[name]))
                else:
                    db("UPDATE ticket SET %s=%%s WHERE id=%%s" 
                       % name, (self[name], self.id))
                db("""INSERT INTO ticket_change
                        (ticket,time,author,field,oldvalue,newvalue)
                      VALUES (%s, %s, %s, %s, %s, %s)
                      """, (self.id, when_ts, author, name, self._old[name],
                            self[name]))

            # always save comment, even if empty 
            # (numbering support for timeline)
            db("""INSERT INTO ticket_change
                    (ticket,time,author,field,oldvalue,newvalue)
                  VALUES (%s,%s,%s,'comment',%s,%s)
                  """, (self.id, when_ts, author, cnum, comment))

        old_values = self._old
        self._old = {}
        self.values['changetime'] = when

        for listener in TicketSystem(self.env).change_listeners:
            listener.ticket_changed(self, comment, author, old_values)
        return int(cnum.rsplit('.', 1)[-1])
Ejemplo n.º 3
0
 def setUp(self):
     self.env = EnvironmentStub(default_data=True)
     for ctlr in TicketSystem(self.env).action_controllers:
         if isinstance(ctlr, ConfigurableTicketWorkflow):
             self.ctlr = ctlr
Ejemplo n.º 4
0
    def _render_view(self, req, id):
        """Retrieve the report results and pre-process them for rendering."""
        title, description, sql = self.get_report(id)
        try:
            args = self.get_var_args(req)
        except ValueError as e:
            raise TracError(_("Report failed: %(error)s", error=e))

        # If this is a saved custom query, redirect to the query module
        #
        # A saved query is either an URL query (?... or query:?...),
        # or a query language expression (query:...).
        #
        # It may eventually contain newlines, for increased clarity.
        #
        query = ''.join([line.strip() for line in sql.splitlines()])
        if query and (query[0] == '?' or query.startswith('query:?')):
            query = query if query[0] == '?' else query[6:]
            report_id = 'report=%s' % id
            if 'report=' in query:
                if not report_id in query:
                    err = _(
                        'When specified, the report number should be '
                        '"%(num)s".',
                        num=id)
                    req.redirect(req.href.report(id, action='edit', error=err))
            else:
                if query[-1] != '?':
                    query += '&'
                query += report_id
            req.redirect(req.href.query() + quote_query_string(query))
        elif query.startswith('query:'):
            try:
                from trac.ticket.query import Query, QuerySyntaxError
                query = Query.from_string(self.env, query[6:], report=id)
                req.redirect(query.get_href(req))
            except QuerySyntaxError as e:
                req.redirect(
                    req.href.report(id, action='edit', error=to_unicode(e)))

        format = req.args.get('format')
        if format == 'sql':
            self._send_sql(req, id, title, description, sql)

        title = '{%i} %s' % (id, title)

        report_resource = Resource('report', id)
        req.perm(report_resource).require('REPORT_VIEW')
        context = web_context(req, report_resource)

        page = int(req.args.get('page', '1'))
        default_max = {
            'rss': self.items_per_page_rss,
            'csv': 0,
            'tab': 0
        }.get(format, self.items_per_page)
        max = req.args.get('max')
        limit = as_int(max, default_max, min=0)  # explict max takes precedence
        offset = (page - 1) * limit

        sort_col = req.args.get('sort', '')
        asc = req.args.get('asc', 1)
        asc = bool(int(asc))  # string '0' or '1' to int/boolean

        def report_href(**kwargs):
            """Generate links to this report preserving user variables,
            and sorting and paging variables.
            """
            params = args.copy()
            if sort_col:
                params['sort'] = sort_col
            params['page'] = page
            if max:
                params['max'] = max
            params.update(kwargs)
            params['asc'] = '1' if params.get('asc', asc) else '0'
            return req.href.report(id, params)

        data = {
            'action': 'view',
            'report': {
                'id': id,
                'resource': report_resource
            },
            'context': context,
            'title': sub_vars(title, args),
            'description': sub_vars(description or '', args),
            'max': limit,
            'args': args,
            'show_args_form': False,
            'message': None,
            'paginator': None,
            'report_href': report_href,
        }

        res = self.execute_paginated_report(req, id, sql, args, limit, offset)

        if len(res) == 2:
            e, sql = res
            data['message'] = \
                tag_("Report execution failed: %(error)s %(sql)s",
                     error=tag.pre(exception_to_unicode(e)),
                     sql=tag(tag.hr(),
                             tag.pre(sql, style="white-space: pre")))
            return 'report_view.html', data, None

        cols, results, num_items, missing_args, limit_offset = res
        need_paginator = limit > 0 and limit_offset
        need_reorder = limit_offset is None
        results = [list(row) for row in results]
        numrows = len(results)

        paginator = None
        if need_paginator:
            paginator = Paginator(results, page - 1, limit, num_items)
            data['paginator'] = paginator
            if paginator.has_next_page:
                add_link(req, 'next', report_href(page=page + 1),
                         _('Next Page'))
            if paginator.has_previous_page:
                add_link(req, 'prev', report_href(page=page - 1),
                         _('Previous Page'))

            pagedata = []
            shown_pages = paginator.get_shown_pages(21)
            for p in shown_pages:
                pagedata.append([
                    report_href(page=p), None,
                    str(p),
                    _('Page %(num)d', num=p)
                ])
            fields = ['href', 'class', 'string', 'title']
            paginator.shown_pages = [dict(zip(fields, p)) for p in pagedata]
            paginator.current_page = {
                'href': None,
                'class': 'current',
                'string': str(paginator.page + 1),
                'title': None
            }
            numrows = paginator.num_items

        # Place retrieved columns in groups, according to naming conventions
        #  * _col_ means fullrow, i.e. a group with one header
        #  * col_ means finish the current group and start a new one

        field_labels = TicketSystem(self.env).get_ticket_field_labels()

        header_groups = [[]]
        for idx, col in enumerate(cols):
            if col in field_labels:
                title = field_labels[col]
            else:
                title = col.strip('_').capitalize()
            header = {
                'col': col,
                'title': title,
                'hidden': False,
                'asc': None,
            }

            if col == sort_col:
                header['asc'] = asc
                if not paginator and need_reorder:
                    # this dict will have enum values for sorting
                    # and will be used in sortkey(), if non-empty:
                    sort_values = {}
                    if sort_col in ('status', 'resolution', 'priority',
                                    'severity'):
                        # must fetch sort values for that columns
                        # instead of comparing them as strings
                        with self.env.db_query as db:
                            for name, value in db(
                                    "SELECT name, %s FROM enum WHERE type=%%s"
                                    % db.cast('value', 'int'), (sort_col, )):
                                sort_values[name] = value

                    def sortkey(row):
                        val = row[idx]
                        # check if we have sort_values, then use them as keys.
                        if sort_values:
                            return sort_values.get(val)
                        # otherwise, continue with string comparison:
                        if isinstance(val, basestring):
                            val = val.lower()
                        return val

                    results = sorted(results, key=sortkey, reverse=(not asc))

            header_group = header_groups[-1]

            if col.startswith('__') and col.endswith('__'):  # __col__
                header['hidden'] = True
            elif col[0] == '_' and col[-1] == '_':  # _col_
                header_group = []
                header_groups.append(header_group)
                header_groups.append([])
            elif col[0] == '_':  # _col
                header['hidden'] = True
            elif col[-1] == '_':  # col_
                header_groups.append([])
            header_group.append(header)

        # Structure the rows and cells:
        #  - group rows according to __group__ value, if defined
        #  - group cells the same way headers are grouped
        chrome = Chrome(self.env)
        row_groups = []
        authorized_results = []
        prev_group_value = None
        for row_idx, result in enumerate(results):
            col_idx = 0
            cell_groups = []
            row = {'cell_groups': cell_groups}
            realm = self.realm
            parent_realm = ''
            parent_id = ''
            email_cells = []
            for header_group in header_groups:
                cell_group = []
                for header in header_group:
                    value = cell_value(result[col_idx])
                    cell = {'value': value, 'header': header, 'index': col_idx}
                    col = header['col']
                    col_idx += 1
                    # Detect and create new group
                    if col == '__group__' and value != prev_group_value:
                        prev_group_value = value
                        # Brute force handling of email in group by header
                        row_groups.append(
                            (value and chrome.format_author(req, value), []))
                    # Other row properties
                    row['__idx__'] = row_idx
                    if col in self._html_cols:
                        row[col] = value
                    if col in ('report', 'ticket', 'id', '_id'):
                        row['id'] = value
                    # Special casing based on column name
                    col = col.strip('_')
                    if col in ('reporter', 'cc', 'owner'):
                        email_cells.append(cell)
                    elif col == 'realm':
                        realm = value
                    elif col == 'parent_realm':
                        parent_realm = value
                    elif col == 'parent_id':
                        parent_id = value
                    cell_group.append(cell)
                cell_groups.append(cell_group)
            if parent_realm:
                resource = Resource(realm,
                                    row.get('id'),
                                    parent=Resource(parent_realm, parent_id))
            else:
                resource = Resource(realm, row.get('id'))
            # FIXME: for now, we still need to hardcode the realm in the action
            if resource.realm.upper() + '_VIEW' not in req.perm(resource):
                continue
            authorized_results.append(result)
            if email_cells:
                for cell in email_cells:
                    emails = chrome.format_emails(context.child(resource),
                                                  cell['value'])
                    result[cell['index']] = cell['value'] = emails
            row['resource'] = resource
            if row_groups:
                row_group = row_groups[-1][1]
            else:
                row_group = []
                row_groups = [(None, row_group)]
            row_group.append(row)

        data.update({
            'header_groups': header_groups,
            'row_groups': row_groups,
            'numrows': numrows
        })

        if format == 'rss':
            data['context'] = web_context(req, report_resource, absurls=True)
            return 'report.rss', data, 'application/rss+xml'
        elif format == 'csv':
            filename = 'report_%s.csv' % id if id else 'report.csv'
            self._send_csv(req,
                           cols,
                           authorized_results,
                           mimetype='text/csv',
                           filename=filename)
        elif format == 'tab':
            filename = 'report_%s.tsv' % id if id else 'report.tsv'
            self._send_csv(req,
                           cols,
                           authorized_results,
                           '\t',
                           mimetype='text/tab-separated-values',
                           filename=filename)
        else:
            p = page if max is not None else None
            add_link(req, 'alternate',
                     auth_link(req, report_href(format='rss', page=None)),
                     _('RSS Feed'), 'application/rss+xml', 'rss')
            add_link(req, 'alternate', report_href(format='csv', page=p),
                     _('Comma-delimited Text'), 'text/plain')
            add_link(req, 'alternate', report_href(format='tab', page=p),
                     _('Tab-delimited Text'), 'text/plain')
            if 'REPORT_SQL_VIEW' in req.perm('report', id):
                add_link(req, 'alternate', req.href.report(id=id,
                                                           format='sql'),
                         _('SQL Query'), 'text/plain')

            # reuse the session vars of the query module so that
            # the query navigation links on the ticket can be used to
            # navigate report results as well
            try:
                req.session['query_tickets'] = \
                    ' '.join([str(int(row['id']))
                              for rg in row_groups for row in rg[1]])
                req.session['query_href'] = \
                    req.session['query_href'] = report_href()
                # Kludge: we have to clear the other query session
                # variables, but only if the above succeeded
                for var in ('query_constraints', 'query_time'):
                    if var in req.session:
                        del req.session[var]
            except (ValueError, KeyError):
                pass
            if set(data['args']) - set(['USER']):
                data['show_args_form'] = True
                add_script(req, 'common/js/folding.js')
            if missing_args:
                add_warning(
                    req,
                    _('The following arguments are missing: %(args)s',
                      args=", ".join(missing_args)))
            return 'report_view.html', data, None
Ejemplo n.º 5
0
    def get_ticket_group_stats(self, ticket_ids):
        total_cnt = len(ticket_ids)
        all_statuses = set(TicketSystem(self.env).get_all_status())
        status_cnt = {}
        for s in all_statuses:
            status_cnt[s] = 0
        if total_cnt:
            for status, count in self.env.db_query("""
                    SELECT status, count(status) FROM ticket
                    WHERE id IN (%s) GROUP BY status
                    """ % ",".join(str(x) for x in sorted(ticket_ids))):
                status_cnt[status] = count

        stat = TicketGroupStats(_("ticket status"), _("tickets"))
        remaining_statuses = set(all_statuses)
        groups = self._get_ticket_groups()
        catch_all_group = None
        # we need to go through the groups twice, so that the catch up group
        # doesn't need to be the last one in the sequence
        for group in groups:
            status_str = group['status'].strip()
            if status_str == '*':
                if catch_all_group:
                    raise TracError(
                        _(
                            "'%(group1)s' and '%(group2)s' milestone groups "
                            "both are declared to be \"catch-all\" groups. "
                            "Please check your configuration.",
                            group1=group['name'],
                            group2=catch_all_group['name']))
                catch_all_group = group
            else:
                group_statuses = {s.strip() for s in status_str.split(',')} \
                                 & all_statuses
                if group_statuses - remaining_statuses:
                    raise TracError(
                        _(
                            "'%(groupname)s' milestone group reused status "
                            "'%(status)s' already taken by other groups. "
                            "Please check your configuration.",
                            groupname=group['name'],
                            status=', '.join(group_statuses -
                                             remaining_statuses)))
                else:
                    remaining_statuses -= group_statuses
                group['statuses'] = group_statuses
        if catch_all_group:
            catch_all_group['statuses'] = remaining_statuses
        for group in groups:
            group_cnt = 0
            query_args = {}
            for s, cnt in status_cnt.iteritems():
                if s in group['statuses']:
                    group_cnt += cnt
                    query_args.setdefault('status', []).append(s)
            for arg in [
                    kv for kv in group.get('query_args', '').split(',')
                    if '=' in kv
            ]:
                k, v = [a.strip() for a in arg.split('=', 1)]
                query_args.setdefault(k, []).append(v)
            stat.add_interval(group.get('label', group['name']),
                              group_cnt, query_args,
                              group.get('css_class', group['name']),
                              as_bool(group.get('overall_completion')))
        stat.refresh_calcs()
        return stat
Ejemplo n.º 6
0
    def render_ticket_action_control(self, req, ticket, action):

        self.log.debug('render_ticket_action_control: action "%s"', action)

        this_action = self.actions[action]
        label = this_action['label']
        operations = this_action['operations']
        ticket_owner = ticket._old.get('owner', ticket['owner'])
        ticket_status = ticket._old.get('status', ticket['status'])
        next_status = this_action['newstate']
        author = get_reporter_id(req, 'author')
        author_info = partial(Chrome(self.env).authorinfo, req,
                              resource=ticket.resource)
        format_author = partial(Chrome(self.env).format_author, req,
                                resource=ticket.resource)
        formatted_current_owner = author_info(ticket_owner)
        exists = ticket_status is not None

        ticket_system = TicketSystem(self.env)
        control = []  # default to nothing
        hints = []
        if 'reset_workflow' in operations:
            control.append(_("from invalid state"))
            hints.append(_("Current state no longer exists"))
        if 'del_owner' in operations:
            hints.append(_("The ticket will be disowned"))
        if 'set_owner' in operations or 'may_set_owner' in operations:
            owners = self.get_allowed_owners(req, ticket, this_action)

            if 'set_owner' in operations:
                default_owner = author
            elif 'may_set_owner' in operations:
                if not exists:
                    default_owner = ticket_system.default_owner
                else:
                    default_owner = ticket_owner or None
                if owners is not None and default_owner not in owners:
                    owners.insert(0, default_owner)
            else:
                # Protect against future modification for case that another
                # operation is added to the outer conditional
                raise AssertionError(operations)

            id = 'action_%s_reassign_owner' % action

            if not owners:
                owner = req.args.get(id, default_owner)
                control.append(
                    tag_("to %(owner)s",
                         owner=tag.input(type='text', id=id, name=id,
                                         value=owner)))
                if not exists or ticket_owner is None:
                    hints.append(_("The owner will be the specified user"))
                else:
                    hints.append(tag_("The owner will be changed from "
                                      "%(current_owner)s to the specified "
                                      "user",
                                      current_owner=formatted_current_owner))
            elif len(owners) == 1:
                owner = tag.input(type='hidden', id=id, name=id,
                                  value=owners[0])
                formatted_new_owner = author_info(owners[0])
                control.append(tag_("to %(owner)s",
                                    owner=tag(formatted_new_owner, owner)))
                if not exists or ticket_owner is None:
                    hints.append(tag_("The owner will be %(new_owner)s",
                                      new_owner=formatted_new_owner))
                elif ticket['owner'] != owners[0]:
                    hints.append(tag_("The owner will be changed from "
                                      "%(current_owner)s to %(new_owner)s",
                                      current_owner=formatted_current_owner,
                                      new_owner=formatted_new_owner))
            else:
                selected_owner = req.args.get(id, default_owner)
                control.append(tag_("to %(owner)s", owner=tag.select(
                    [tag.option(text, value=value if value is not None else '',
                                selected=(value == selected_owner or None))
                     for text, value in sorted((format_author(owner), owner)
                                                for owner in owners)],
                    id=id, name=id)))
                if not exists or ticket_owner is None:
                    hints.append(_("The owner will be the selected user"))
                else:
                    hints.append(tag_("The owner will be changed from "
                                      "%(current_owner)s to the selected user",
                                      current_owner=formatted_current_owner))
        elif 'set_owner_to_self' in operations:
            formatted_author = author_info(author)
            if not exists or ticket_owner is None:
                hints.append(tag_("The owner will be %(new_owner)s",
                                  new_owner=formatted_author))
            elif ticket_owner != author:
                hints.append(tag_("The owner will be changed from "
                                  "%(current_owner)s to %(new_owner)s",
                                  current_owner=formatted_current_owner,
                                  new_owner=formatted_author))
            elif ticket_status != next_status:
                hints.append(tag_("The owner will remain %(current_owner)s",
                                  current_owner=formatted_current_owner))
        if 'set_resolution' in operations:
            resolutions = [r.name for r in Resolution.select(self.env)]
            if 'set_resolution' in this_action:
                valid_resolutions = set(resolutions)
                resolutions = this_action['set_resolution']
                if any(x not in valid_resolutions for x in resolutions):
                    raise ConfigurationError(_(
                        "Your workflow attempts to set a resolution but uses "
                        "undefined resolutions (configuration issue, please "
                        "contact your Trac admin)."))
            if not resolutions:
                raise ConfigurationError(_(
                    "Your workflow attempts to set a resolution but none is "
                    "defined (configuration issue, please contact your Trac "
                    "admin)."))
            id = 'action_%s_resolve_resolution' % action
            if len(resolutions) == 1:
                resolution = tag.input(type='hidden', id=id, name=id,
                                       value=resolutions[0])
                control.append(tag_("as %(resolution)s",
                                    resolution=tag(resolutions[0],
                                                   resolution)))
                hints.append(tag_("The resolution will be set to %(name)s",
                                  name=resolutions[0]))
            else:
                selected_option = req.args.get(id,
                                               ticket_system.default_resolution)
                control.append(tag_("as %(resolution)s",
                                    resolution=tag.select(
                    [tag.option(x, value=x,
                                selected=(x == selected_option or None))
                     for x in resolutions],
                    id=id, name=id)))
                hints.append(_("The resolution will be set"))
        if 'del_resolution' in operations:
            hints.append(_("The resolution will be deleted"))
        if 'leave_status' in operations:
            if len(operations) == 1:
                control.append(tag_("as %(status)s", status=ticket_status))
                hints.append(tag_("The owner will remain %(current_owner)s",
                                  current_owner=formatted_current_owner)
                             if ticket_owner else
                             _("The ticket will remain with no owner"))
        elif ticket['status'] is None:  # New ticket
            hints.append(tag_("The status will be '%(name)s'",
                              name=next_status))
        elif next_status != ticket_status:
            hints.append(tag_("Next status will be '%(name)s'",
                              name=next_status))
        return (label, tag(separated(control, ' ')),
                tag(separated(hints, '. ', '.') if hints else ''))
Ejemplo n.º 7
0
 def setUp(self):
     self.env = EnvironmentStub()
     self.ticket_system = TicketSystem(self.env)
Ejemplo n.º 8
0
 def _get_ticket_field(self, field_name):
     fields = TicketSystem(self.env).get_ticket_fields()
     return next((i for i in fields if i['name'] == field_name))
Ejemplo n.º 9
0
 def get_configurable_workflow(self):
     controllers = TicketSystem(self.env).action_controllers
     for controller in controllers:
         if isinstance(controller, ConfigurableTicketWorkflow):
             return controller
     return ConfigurableTicketWorkflow(self.env)
Ejemplo n.º 10
0
 def setUp(self):
     self.env = EnvironmentStub(default_data=True)
     self.perm = PermissionSystem(self.env)
     self.ticket_system = TicketSystem(self.env)
     self.req = MockRequest(self.env)
Ejemplo n.º 11
0
 def _get_actions(self, ticket_dict):
     ts = TicketSystem(self.env)
     ticket = insert_ticket(self.env, **ticket_dict)
     return ts.get_available_actions(self.req, Ticket(self.env, ticket.id))
Ejemplo n.º 12
0
 def test_custom_field_select_without_options(self):
     self.env.config.set('ticket-custom', 'test', 'select')
     self.env.config.set('ticket-custom', 'test.label', 'Test')
     self.env.config.set('ticket-custom', 'test.value', '1')
     fields = TicketSystem(self.env).get_custom_fields()
     self.assertEqual(0, len(fields))
Ejemplo n.º 13
0
 def getAll(self, req):
     """ Returns all ticket states described by active workflow. """
     return TicketSystem(self.env).get_all_status()
Ejemplo n.º 14
0
 def getTicketFields(self, req):
     """ Return a list of all ticket fields fields. """
     return TicketSystem(self.env).get_ticket_fields()
Ejemplo n.º 15
0
 def get_user_list(self):
     return TicketSystem(self.env).get_allowed_owners()
Ejemplo n.º 16
0
 def expand_macro(self, formatter, name, content, args=[]):
     try:
         cols = []  # Sentinel
         group = ''  # Sentinel
         groups = {}
         lines = content.split('\r\n')
         for line in lines:
             if line.startswith('||= href =||= '):
                 cols = line[14:].split(' =||= ')
             elif line.startswith('|| group: '):
                 group = line[10:]
                 if group in [u'', u'None']:
                     group = None
                 groups[group] = []  # initialize for the group
             elif line.startswith('|| '):
                 values = iter(line[3:].split(' || '))
                 ticket = {'href': values.next()}
                 for col in cols:
                     ticket[col] = values.next()
                 groups[group].append(ticket)
             else:
                 pass
         ticketsystem = TicketSystem(self.env)
         #
         labels = ticketsystem.get_ticket_field_labels()
         headers = [{
             'name': col,
             'label': labels.get(col, _('Ticket'))
         } for col in cols]
         #
         fields = {}
         ticket_fields = ticketsystem.get_ticket_fields()
         for field in ticket_fields:
             fields[field['name']] = {
                 'label': field['label']
             }  # transform list to expected dict
         # fail safe
         fields[None] = 'NONE'
         for group in groups.keys():
             if not 'group' in fields:
                 fields[group] = group
         #
         group_name = 'group' in args and args['group'] or None
         if group_name not in fields:
             group_name = None
         query = {'group': group_name}
         #
         groups = [(name, groups[name])
                   for name in groups]  # transform dict to expected tuple
         #
         data = {
             'paginator': None,
             'headers': headers,
             'query': query,
             'fields': fields,
             'groups': groups,
         }
         add_stylesheet(formatter.req, 'common/css/report.css')
         chrome = Chrome(self.env)
         data = chrome.populate_data(formatter.req, data)
         template = chrome.load_template('query_results.html')
         content = template.generate(**data)
         # ticket id list as static
         tickets = ''
         if 'id' in cols:
             ticket_id_list = [
                 ticket.get('id') for group in groups for ticket in group[1]
             ]
             if len(ticket_id_list) > 0:
                 tickets = '([ticket:' + ','.join(
                     ticket_id_list) + ' query by ticket id])'
         return tag.div(
             content, format_to_html(self.env, formatter.context, tickets))
     except StopIteration:
         errorinfo = _('Not Enough fields in ticket: %s') % line
     except Exception:
         errorinfo = sys.exc_info()
     return tag.div(tag.div(errorinfo, class_='message'),
                    class_='error',
                    id='content')
Ejemplo n.º 17
0
    def _render_admin_panel(self, req, cat, page, component):
        # Detail view?
        if component:
            comp = model.Component(self.env, component)
            if req.method == 'POST':
                if req.args.get('save'):
                    comp.name = name = req.args.get('name')
                    comp.owner = req.args.get('owner')
                    comp.description = req.args.get('description')
                    try:
                        comp.update()
                    except self.env.db_exc.IntegrityError:
                        raise TracError(_('Component "%(name)s" already '
                                          'exists.', name=name))
                    add_notice(req, _("Your changes have been saved."))
                    req.redirect(req.href.admin(cat, page))
                elif req.args.get('cancel'):
                    req.redirect(req.href.admin(cat, page))

            Chrome(self.env).add_wiki_toolbars(req)
            data = {'view': 'detail', 'component': comp}

        else:
            default = self.config.get('ticket', 'default_component')
            if req.method == 'POST':
                # Add Component
                if req.args.get('add') and req.args.get('name'):
                    name = req.args.get('name')
                    try:
                        comp = model.Component(self.env, name=name)
                    except ResourceNotFound:
                        comp = model.Component(self.env)
                        comp.name = name
                        if req.args.get('owner'):
                            comp.owner = req.args.get('owner')
                        comp.insert()
                        add_notice(req, _('The component "%(name)s" has been '
                                          'added.', name=name))
                        req.redirect(req.href.admin(cat, page))
                    else:
                        if comp.name is None:
                            raise TracError(_("Invalid component name."))
                        raise TracError(_('Component "%(name)s" already '
                                          'exists.', name=name))

                # Remove components
                elif req.args.get('remove'):
                    sel = req.args.get('sel')
                    if not sel:
                        raise TracError(_("No component selected"))
                    if not isinstance(sel, list):
                        sel = [sel]
                    with self.env.db_transaction:
                        for name in sel:
                            model.Component(self.env, name).delete()
                    add_notice(req, _("The selected components have been "
                                      "removed."))
                    req.redirect(req.href.admin(cat, page))

                # Set default component
                elif req.args.get('apply'):
                    name = req.args.get('default')
                    if name and name != default:
                        self.log.info("Setting default component to %s", name)
                        self.config.set('ticket', 'default_component', name)
                        self._save_config(req)
                        req.redirect(req.href.admin(cat, page))

                # Clear default component
                elif req.args.get('clear'):
                    self.log.info("Clearing default component")
                    self.config.set('ticket', 'default_component', '')
                    self._save_config(req)
                    req.redirect(req.href.admin(cat, page))

            data = {'view': 'list',
                    'components': list(model.Component.select(self.env)),
                    'default': default}

        owners = TicketSystem(self.env).get_allowed_owners()
        if owners is not None:
            owners.insert(0, '')
        data.update({'owners': owners})

        return 'admin_components.html', data
Ejemplo n.º 18
0
    def render_ticket_action_control(self, req, ticket, action):

        self.log.debug('render_ticket_action_control: action "%s"', action)

        this_action = self.actions[action]
        status = this_action['newstate']
        operations = this_action['operations']
        current_owner = ticket._old.get('owner', ticket['owner'])
        author = get_reporter_id(req, 'author')
        author_info = partial(Chrome(self.env).authorinfo,
                              req,
                              resource=ticket.resource)
        format_author = partial(Chrome(self.env).format_author,
                                req,
                                resource=ticket.resource)
        formatted_current_owner = author_info(current_owner)
        exists = ticket._old.get('status', ticket['status']) is not None

        control = []  # default to nothing
        hints = []
        if 'reset_workflow' in operations:
            control.append(_("from invalid state"))
            hints.append(_("Current state no longer exists"))
        if 'del_owner' in operations:
            hints.append(_("The ticket will be disowned"))
        if 'set_owner' in operations or 'may_set_owner' in operations:
            if 'set_owner' in this_action:
                owners = self._to_users(this_action['set_owner'], ticket)
            elif self.config.getbool('ticket', 'restrict_owner'):
                perm = PermissionSystem(self.env)
                owners = perm.get_users_with_permission('TICKET_MODIFY')
                owners = [
                    user for user in owners
                    if 'TICKET_MODIFY' in PermissionCache(
                        self.env, user, ticket.resource)
                ]
                owners = sorted(owners)
            else:
                owners = None

            if 'set_owner' in operations:
                default_owner = author
            elif 'may_set_owner' in operations:
                if not exists:
                    default_owner = TicketSystem(self.env).default_owner
                else:
                    default_owner = ticket._old.get('owner', ticket['owner']
                                                    or None)
                if owners is not None and default_owner not in owners:
                    owners.insert(0, default_owner)
            else:
                # Protect against future modification for case that another
                # operation is added to the outer conditional
                raise AssertionError(operations)

            id = 'action_%s_reassign_owner' % action

            if not owners:
                owner = req.args.get(id, default_owner)
                control.append(
                    tag_("to %(owner)s",
                         owner=tag.input(type='text',
                                         id=id,
                                         name=id,
                                         value=owner)))
                if not exists or current_owner is None:
                    hints.append(_("The owner will be the specified user"))
                else:
                    hints.append(
                        tag_(
                            "The owner will be changed from "
                            "%(current_owner)s to the specified "
                            "user",
                            current_owner=formatted_current_owner))
            elif len(owners) == 1:
                owner = tag.input(type='hidden',
                                  id=id,
                                  name=id,
                                  value=owners[0])
                formatted_new_owner = author_info(owners[0])
                control.append(
                    tag_("to %(owner)s", owner=tag(formatted_new_owner,
                                                   owner)))
                if not exists or current_owner is None:
                    hints.append(
                        tag_("The owner will be %(new_owner)s",
                             new_owner=formatted_new_owner))
                elif ticket['owner'] != owners[0]:
                    hints.append(
                        tag_(
                            "The owner will be changed from "
                            "%(current_owner)s to %(new_owner)s",
                            current_owner=formatted_current_owner,
                            new_owner=formatted_new_owner))
            else:
                selected_owner = req.args.get(id, default_owner)
                control.append(
                    tag_("to %(owner)s",
                         owner=tag.select([
                             tag.option(
                                 label,
                                 value=value if value is not None else '',
                                 selected=(value == selected_owner or None))
                             for label, value in sorted(
                                 (format_author(owner), owner)
                                 for owner in owners)
                         ],
                                          id=id,
                                          name=id)))
                if not exists or current_owner is None:
                    hints.append(_("The owner will be the selected user"))
                else:
                    hints.append(
                        tag_(
                            "The owner will be changed from "
                            "%(current_owner)s to the selected user",
                            current_owner=formatted_current_owner))
        elif 'set_owner_to_self' in operations and \
                ticket._old.get('owner', ticket['owner']) != author:
            formatted_author = author_info(author)
            if not exists or current_owner is None:
                hints.append(
                    tag_("The owner will be %(new_owner)s",
                         new_owner=formatted_author))
            else:
                hints.append(
                    tag_(
                        "The owner will be changed from "
                        "%(current_owner)s to %(new_owner)s",
                        current_owner=formatted_current_owner,
                        new_owner=formatted_author))
        if 'set_resolution' in operations:
            if 'set_resolution' in this_action:
                resolutions = this_action['set_resolution']
            else:
                resolutions = [r.name for r in Resolution.select(self.env)]
            if not resolutions:
                raise TracError(
                    _("Your workflow attempts to set a resolution "
                      "but none is defined (configuration issue, "
                      "please contact your Trac admin)."))
            id = 'action_%s_resolve_resolution' % action
            if len(resolutions) == 1:
                resolution = tag.input(type='hidden',
                                       id=id,
                                       name=id,
                                       value=resolutions[0])
                control.append(
                    tag_("as %(resolution)s",
                         resolution=tag(resolutions[0], resolution)))
                hints.append(
                    tag_("The resolution will be set to %(name)s",
                         name=resolutions[0]))
            else:
                selected_option = req.args.get(
                    id,
                    TicketSystem(self.env).default_resolution)
                control.append(
                    tag_(
                        "as %(resolution)s",
                        resolution=tag.select([
                            tag.option(x,
                                       value=x,
                                       selected=(x == selected_option or None))
                            for x in resolutions
                        ],
                                              id=id,
                                              name=id)))
                hints.append(_("The resolution will be set"))
        if 'del_resolution' in operations:
            hints.append(_("The resolution will be deleted"))
        if 'leave_status' in operations:
            control.append(
                tag_("as %(status)s",
                     status=ticket._old.get('status', ticket['status'])))
            if len(operations) == 1:
                hints.append(
                    tag_("The owner will remain %(current_owner)s",
                         current_owner=formatted_current_owner) if
                    current_owner else _("The ticket will remain with no owner"
                                         ))
        else:
            if ticket['status'] is None:
                hints.append(tag_("The status will be '%(name)s'",
                                  name=status))
            elif status != '*':
                hints.append(
                    tag_("Next status will be '%(name)s'", name=status))
        return (this_action['label'], tag(separated(control, ' ')),
                tag(separated(hints, '. ', '.') if hints else ''))
Ejemplo n.º 19
0
    def expand_macro(self, formatter, name, args):
        u"""パラメータを展開する。パラメータは、カンマで区切ることとする。
        パラメータのキーと値は以下のような感じで定義する(キーと値は":"でつなぐ)
        x -> 横軸の表示内容(抽出内容)を"|"で区切ってセット。
             なお、表示対象とするフィールド名は先頭に記載し、具体的な値は"="でつなぐ。値を省略した場合は項目の一覧を自動取得。
             グループ化したいときは "+"でつなぐ。改名したいときは AS で別名をつける。
             例)x:status=new|accepted + assigned AS doing|closed|reopened
        y -> 縦軸の表示内容。設定方法はxと同じ
        q -> 抽出条件(SQLのWHERE条件)。複数の条件を指定する場合は"&"でつなぐ。
             q:milestone=milestone1&owner=hoge
        xa-> x軸部分をクリックした際に表示するカスタムクエリのorderおよびgroup指定。
             xa:order=id&group=status のような感じで指定する。指定がない場合はorder=idのみを自動的に付加する。
        ya-> y軸部分をクリックした際に表示するカスタムクエリのorderおよびgroup指定。
             指定方法はxaと同様。
        m -> 表部分をクリックした際に表示するカスタムクエリのorderおよびgroup指定。
             指定方法はxa/yaと同様。
        """

        # とりあえず、パラメータ中に";"が含まれている場合はエラーとする
        if ';' in args:
            raise TracError('Parameter Error')

        # パラメータをカンマで区切って展開
        # ":"でキーと値に分割してDictionaryに格納(":"で2つに分割できなかったものは処理対象外)
        params = {}
        for value in args.split(','):
            v = value.split(':')
            if len(v) == 2:
                params[v[0]] = v[1]

        # 抽出条件を構築(SQLのWHERE節を構築)
        condition = self.createcondition(params)

        # x軸(横軸)の処理
        xaxiswork = params['x'].split('=')
        xaxiskey = xaxiswork[0]
        xaxiscolname = xaxiskey

        # y軸(縦軸)の処理
        yaxiswork = params['y'].split('=')
        yaxiskey = yaxiswork[0]
        yaxiscolname = yaxiskey

        # Ticketのフィールド一覧を取得する
        xfield = None
        yfield = None
        for field in TicketSystem(self.env).get_ticket_fields():
            if field['name'] == xaxiskey:
                xfield = field
                xaxislabel = xfield['label']
            if field['name'] == yaxiskey:
                yfield = field
                yaxislabel = yfield['label']

        # それぞれの軸がカスタムフィールドか否か
        xcustom = 'custom' in xfield and xfield['custom']
        ycustom = 'custom' in yfield and yfield['custom']
        if xcustom:
            xaxiskey = 'cx.value'
        if ycustom:
            yaxiskey = 'cy.value'

        yaxis = self.getaxisvalues(yaxiswork, yfield, ycustom, yaxiskey, yaxiscolname, condition)
        xaxis = self.getaxisvalues(xaxiswork, xfield, xcustom, xaxiskey, xaxiscolname, condition)

        if len(xaxis) == 0 or len(yaxis) == 0:
            return u'<ul><i>抽出条件に合致するチケットが存在しませんでした</i></ul>'

        query = u'SELECT '
        query += 'CASE '
        for y in yaxis:
            query += 'when %s in (%s) then \'%s\' \n' % (yaxiskey, ",".join(["'%s'" % v.strip() for v in y.split("+")]), y)
        query += 'END as %s' % yaxiskey

        for x in xaxis:
            query += ",sum(case when coalesce(%s,'') IN (%s) then 1 else 0 end)" % (xaxiskey, ",".join(["'%s'" % v.strip() for v in x.split("+")]))
        query += " from ticket "

        # カスタムフィールドを使用している場合はticket_customテーブルを連結する必要あり
        # なお、カスタムフィールドの場合は取得する際の列名がいったんvalueに置き換わってしまうので注意
        if xcustom:
            query += " left outer join ticket_custom as cx on cx.ticket=id and cx.name='%s'" % xaxiscolname
        if ycustom:
            query += " left outer join ticket_custom as cy on cy.ticket=id and cy.name='%s'" % yaxiscolname

        # 条件文及びgroup値を追加
        query += " %s group by %s" % (condition, yaxiskey)

        self.env.log.info(query)

        result = {}
        for row in self.env.db_query(query):
            result[row[0] or ''] = row[1:]

        # queryリンクのパラメータの各要素を構築する
        querycond = 'q' in params and "&%s" % params['q'] or '&order=priority'
        xquery = 'xa' in params and "&%s" % params['xa'] or "&order=id"
        yquery = 'ya' in params and "&%s" % params['ya'] or "&order=id"
        mquery = 'm' in params and "&%s" % params['m'] or "&order=id"

        # 出力テキスト構築
        # x軸のタイトル行を構築する
        xaxislabels = [(yaxislabel + u'\' + xaxislabel)] + xaxiswork[1] + [u"小計"]
        wikitext = "||%s||\n" % "||".join(["= %s =" % x for x in xaxislabels])

        # 縦計を格納する
        ysum = {}
        for x in xaxis:
            ysum[x] = 0
        for yindex, y in enumerate(yaxis):
            # タイトル列
            wikitext += "||= %s =||" % yaxiswork[1][yindex]
            if y in result:
                each = result[y]
                linesum = 0
                for cnt2, x in enumerate(xaxis):
                    # テーブル部分本体
                    eachval = each[cnt2]
                    wikitext += self.q_or_0("%s=%s&%s=%s%s%s", (yaxiscolname, y, xaxiscolname, x, querycond, mquery), each[cnt2])
                    linesum += eachval
                    ysum[x] += eachval
                # 小計列
                wikitext += self.q_or_0("%s=%s%s%s", (yaxiscolname, y, querycond, yquery), linesum, term="\n")
            else:
                # 該当するy軸に対応するデータがない場合はリンクは付加しない
                for x in xaxis:
                    wikitext += "0||"
                wikitext += "0||\n"

        # 合計行
        wikitext += "||= %s =||" % u"【合計】"
        linesum = 0
        for x in xaxis:
            ysumval = ysum[x]
            wikitext += self.q_or_0("%s=%s%s%s", (xaxiscolname, x, querycond, xquery), ysum[x])
            linesum += ysumval
        # 総合計
        wikitext += self.q_or_0("%s", (querycond[1:],), linesum, term="\n")

        # 構築したwikiテキストをhtmlに変換(expand_macroの戻り値はhtmlそのものとなる)。
        out = StringIO()
        Formatter(self.env, formatter.context).format(wikitext, out)
        return Markup(out.getvalue())
    def process_request(self, req):
        self.log.debug('HTTP request: %s, method: %s, user: %s' %
                       (req.path_info, req.method, req.authname))

        if req.method != 'GET' and req.method != 'POST':
            return req.send([], content_type='application/json')

        board_id = None
        is_ticket_call = False
        match = self.request_regexp.match(req.path_info)
        if match:
            board_id = match.group('bid')
            is_ticket_call = match.group('ticket') is not None

        if not self.ticket_fields:
            self.ticket_fields = TicketSystem(self.env).get_ticket_fields()

        if board_id is None:
            meta_data = {}
            meta_data['ticketFields'] = self.ticket_fields
            return req.send(json.dumps(meta_data),
                            content_type='application/json')

        arg_list = parse_arg_list(req.query_string)
        detailed_tickets = []
        added_tickets = []
        removed_tickets = []
        for arg in arg_list:
            if arg[0] == 'detailed':
                detailed_tickets = self._parse_id_list(arg[1])
            elif arg[0] == 'add':
                added_tickets = self._parse_id_list(arg[1])
            elif arg[0] == 'remove':
                removed_tickets = self._parse_id_list(arg[1])

        board = KanbanBoard(board_id, detailed_tickets, self.ticket_fields,
                            self.env, self.log)

        added = 0
        if len(added_tickets) > 0:
            added = board.add_tickets(added_tickets)

        removed = 0
        if len(removed_tickets) > 0:
            removed = board.remove_tickets(removed_tickets)

        # We need to update board data to match (possibly changed) ticket states
        is_editable = 'WIKI_MODIFY' in req.perm and 'TICKET_MODIFY' in req.perm
        board.fix_ticket_columns(req, is_editable, added > 0 or removed > 0)

        if req.method == 'GET':
            return req.send(board.get_json(True, False),
                            content_type='application/json')
        else:
            if is_ticket_call:
                ticket_data = json.loads(req.read())
                is_new = 'id' not in ticket_data
                id = self.save_ticket(ticket_data, req.authname)
                if is_new:
                    board.add_tickets([id])
                else:
                    board.update_tickets([id])
            else:
                modified_tickets = []
                column_data = json.loads(req.read())
                for col in column_data:
                    for ticket in col['tickets']:
                        for key, value in ticket.items():
                            if key != 'id':
                                self.save_ticket(ticket, req.authname)
                                modified_tickets.append(ticket['id'])
                                break

                board.update_columns(column_data)
                if modified_tickets:
                    board.update_tickets(modified_tickets)

            board.fix_ticket_columns(req, True, True)
            return req.send(board.get_json(True, False),
                            content_type='application/json')
Ejemplo n.º 21
0
                ])
            fields = ['href', 'class', 'string', 'title']
            paginator.shown_pages = [dict(zip(fields, p)) for p in pagedata]
            paginator.current_page = {
                'href': None,
                'class': 'current',
                'string': str(paginator.page + 1),
                'title': None
            }
            numrows = paginator.num_items

        # Place retrieved columns in groups, according to naming conventions
        #  * _col_ means fullrow, i.e. a group with one header
        #  * col_ means finish the current group and start a new one

        field_labels = TicketSystem(self.env).get_ticket_field_labels()

        header_groups = [[]]
        for idx, col in enumerate(cols):
            if col in field_labels:
                title = field_labels[col]
            else:
                title = col.strip('_').capitalize()
            header = {
                'col': col,
                'title': title,
                'hidden': False,
                'asc': None,
            }

            if col == sort_col:
    def expand_macro(self, formatter, name, text, args):
        template_data = {'css_class': 'trac-kanban-board'}
        template_file = 'kanbanboard.html'
        board = None

        template_data['height'] = '300px'
        if args:
            template_data['height'] = args.get('height', '300px')

        project_name = self.env.path.split('/')[-1]
        page_name = formatter.req.path_info.split('/')[-1]
        is_editable = 'WIKI_MODIFY' in formatter.req.perm and 'TICKET_MODIFY' in formatter.req.perm

        js_globals = {
            'KANBAN_BOARD_ID': page_name,
            'TRAC_PROJECT_NAME': project_name,
            'TRAC_USER_NAME': formatter.req.authname,
            'IS_EDITABLE': is_editable
        }

        if not self.ticket_fields:
            self.ticket_fields = TicketSystem(self.env).get_ticket_fields()

        if text is None:
            template_data['error'] = 'Board data is not defined'
            template_data['usage'] = format_to_html(self.env,
                                                    formatter.context,
                                                    self.__doc__)
        else:
            try:
                board = KanbanBoard(page_name, [], self.ticket_fields,
                                    self.env, self.log)
            except InvalidDataError as e:
                template_data['error'] = e.msg
                template_data['usage'] = format_to_html(
                    self.env, formatter.context, self.__doc__)
            except InvalidFieldError as e:
                template_data[
                    'error'] = 'Invalid ticket fields: %s' % ', '.join(
                        e.fields)
                valid_fields = map(lambda x: x['name'], self.ticket_fields)
                template_data[
                    'usage'] = 'Valid field names are: %s.' % ', '.join(
                        valid_fields)

        if board:
            # TICKET_FIELDS is comma-separated list of user defined ticket field names
            js_globals['TICKET_FIELDS'] = board.get_field_string()

        add_stylesheet(formatter.req, 'trackanbanboard/css/kanbanboard.css')
        add_script_data(formatter.req, js_globals)

        if 'error' in template_data:
            template_file = 'kanbanerror.html'
        else:
            add_script(formatter.req,
                       'trackanbanboard/js/libs/jquery-1.8.3.js')
            add_script(
                formatter.req,
                'trackanbanboard/js/libs/jquery-ui-1.9.2.custom.min.js')
            add_script(formatter.req,
                       'trackanbanboard/js/libs/knockout-2.2.0.js')
            add_script(formatter.req,
                       'trackanbanboard/js/libs/knockout.mapping.js')
            add_script(formatter.req,
                       'trackanbanboard/js/libs/knockout-sortable.min.js')
            add_script(formatter.req, 'trackanbanboard/js/kanbanutil.js')
            add_script(formatter.req, 'trackanbanboard/js/kanbanboard.js')
            add_stylesheet(
                formatter.req,
                'trackanbanboard/css/jquery-ui-1.9.2.custom.min.css')

        return Chrome(self.env).render_template(
            formatter.req, template_file, template_data, None,
            fragment=True).render(strip_whitespace=False)
Ejemplo n.º 23
0
    def _render_view(self, req, milestone):
        milestone_groups = []
        available_groups = []
        default_group_by_available = False
        ticket_fields = TicketSystem(self.env).get_ticket_fields()

        # collect fields that can be used for grouping
        for field in ticket_fields:
            if field['type'] == 'select' and field['name'] != 'milestone' \
                    or field['name'] in ('owner', 'reporter'):
                available_groups.append({
                    'name': field['name'],
                    'label': field['label']
                })
                if field['name'] == self.default_group_by:
                    default_group_by_available = True

        # determine the field currently used for grouping
        by = None
        if default_group_by_available:
            by = self.default_group_by
        elif available_groups:
            by = available_groups[0]['name']
        by = req.args.get('by', by)

        tickets = get_tickets_for_milestone(self.env,
                                            milestone=milestone.name,
                                            field=by)
        tickets = apply_ticket_permissions(self.env, req, tickets)
        stat = get_ticket_stats(self.stats_provider, tickets)

        context = web_context(req, milestone.resource)
        data = {
            'context': context,
            'milestone': milestone,
            'attachments': AttachmentModule(self.env).attachment_data(context),
            'available_groups': available_groups,
            'grouped_by': by,
            'groups': milestone_groups
        }
        data.update(milestone_stats_data(self.env, req, stat, milestone.name))

        if by:

            def per_group_stats_data(gstat, group_name):
                return milestone_stats_data(self.env, req, gstat,
                                            milestone.name, by, group_name)

            milestone_groups.extend(
                grouped_stats_data(self.env, self.stats_provider, tickets, by,
                                   per_group_stats_data))

        add_stylesheet(req, 'common/css/roadmap.css')

        def add_milestone_link(rel, milestone):
            href = req.href.milestone(milestone.name, by=req.args.get('by'))
            add_link(req, rel, href,
                     _('Milestone "%(name)s"', name=milestone.name))

        milestones = [
            m for m in Milestone.select(self.env)
            if 'MILESTONE_VIEW' in req.perm(m.resource)
        ]
        idx = [i for i, m in enumerate(milestones) if m.name == milestone.name]
        if idx:
            idx = idx[0]
            if idx > 0:
                add_milestone_link('first', milestones[0])
                add_milestone_link('prev', milestones[idx - 1])
            if idx < len(milestones) - 1:
                add_milestone_link('next', milestones[idx + 1])
                add_milestone_link('last', milestones[-1])
        prevnext_nav(req, _("Previous Milestone"), _("Next Milestone"),
                     _("Back to Roadmap"))

        return 'milestone_view.html', data
Ejemplo n.º 24
0
    def render_ticket_action_control(self, req, ticket, action):

        self.log.debug('render_ticket_action_control: action "%s"' % action)

        this_action = self.actions[action]
        status = this_action['newstate']
        operations = this_action['operations']
        current_owner_or_empty = ticket._old.get('owner', ticket['owner'])
        current_owner = current_owner_or_empty or '(none)'
        if not (Chrome(self.env).show_email_addresses
                or 'EMAIL_VIEW' in req.perm(ticket.resource)):
            format_user = obfuscate_email_address
        else:
            format_user = lambda address: address
        current_owner = format_user(current_owner)

        control = [] # default to nothing
        hints = []
        if 'reset_workflow' in operations:
            control.append(tag("from invalid state "))
            hints.append(_("Current state no longer exists"))
        if 'del_owner' in operations:
            hints.append(_("The ticket will be disowned"))
        if 'set_owner' in operations or 'may_set_owner' in operations:
            if 'set_owner' in operations:
                default_owner = req.authname
            elif 'may_set_owner' in operations:
                default_owner = \
                    ticket._old.get('owner', ticket['owner'] or None)
            else:
                # Protect against future modification for case that another
                # operation is added to the outer conditional
                raise AssertionError(operations)

            if 'set_owner' in this_action:
                owners = [x.strip() for x in
                          this_action['set_owner'].split(',')]
            elif self.config.getbool('ticket', 'restrict_owner'):
                perm = PermissionSystem(self.env)
                owners = perm.get_users_with_permission('TICKET_MODIFY')
                owners.sort()
            else:
                owners = None
            if owners is not None and default_owner not in owners:
                owners.insert(0, default_owner)

            id = 'action_%s_reassign_owner' % action
            selected_owner = req.args.get(id, default_owner)

            if owners is None:
                control.append(
                    tag_('to %(owner)s', owner=tag.input(type='text', id=id,
                         name=id, value=selected_owner)))
                hints.append(_("The owner will be changed from "
                               "%(current_owner)s to the specified user",
                               current_owner=current_owner))
            elif len(owners) == 1:
                owner = tag.input(type='hidden', id=id, name=id,
                                  value=owners[0])
                formatted_owner = format_user(owners[0])
                control.append(tag_('to %(owner)s ',
                                    owner=tag(formatted_owner, owner)))
                if ticket['owner'] != owners[0]:
                    hints.append(_("The owner will be changed from "
                                   "%(current_owner)s to %(selected_owner)s",
                                   current_owner=current_owner,
                                   selected_owner=formatted_owner))
            else:
                control.append(tag_('to %(owner)s', owner=tag.select(
                    [tag.option(x if x is not None else '(none)',
                                value=x if x is not None else '',
                                selected=(x == selected_owner or None))
                     for x in owners],
                    id=id, name=id)))
                hints.append(_("The owner will be changed from "
                               "%(current_owner)s to the selected user",
                               current_owner=current_owner))
        elif 'set_owner_to_self' in operations and \
                ticket._old.get('owner', ticket['owner']) != req.authname:
            hints.append(_("The owner will be changed from %(current_owner)s "
                           "to %(authname)s", current_owner=current_owner,
                           authname=req.authname))
        if 'set_resolution' in operations:
            if 'set_resolution' in this_action:
                resolutions = [x.strip() for x in
                               this_action['set_resolution'].split(',')]
            else:
                resolutions = [val.name for val in Resolution.select(self.env)]
            if not resolutions:
                raise TracError(_("Your workflow attempts to set a resolution "
                                  "but none is defined (configuration issue, "
                                  "please contact your Trac admin)."))
            id = 'action_%s_resolve_resolution' % action
            if len(resolutions) == 1:
                resolution = tag.input(type='hidden', id=id, name=id,
                                       value=resolutions[0])
                control.append(tag_('as %(resolution)s',
                                    resolution=tag(resolutions[0],
                                                   resolution)))
                hints.append(_("The resolution will be set to %(name)s",
                               name=resolutions[0]))
            else:
                selected_option = req.args.get(id,
                        TicketSystem(self.env).default_resolution)
                control.append(tag_('as %(resolution)s',
                                    resolution=tag.select(
                    [tag.option(x, value=x,
                                selected=(x == selected_option or None))
                     for x in resolutions],
                    id=id, name=id)))
                hints.append(_("The resolution will be set"))
        if 'del_resolution' in operations:
            hints.append(_("The resolution will be deleted"))
        if 'leave_status' in operations:
            control.append(_('as %(status)s ',
                             status=ticket._old.get('status',
                                                    ticket['status'])))
            if len(operations) == 1:
                hints.append(_("The owner will remain %(current_owner)s",
                               current_owner=current_owner)
                             if current_owner_or_empty else
                             _("The ticket will remain with no owner"))
        else:
            if status != '*':
                hints.append(_("Next status will be '%(name)s'", name=status))
        return (this_action['name'], tag(*control), '. '.join(hints) + '.'
                if hints else '')
Ejemplo n.º 25
0
    def insert(self, when=None, db=None):
        """Add ticket to database.

        :since 0.13: the `db` parameter is no longer needed and will be removed
        in version 0.14
        """
        assert not self.exists, 'Cannot insert an existing ticket'

        if 'cc' in self.values:
            self['cc'] = _fixup_cc_list(self.values['cc'])

        # Add a timestamp
        if when is None:
            when = datetime.now(utc)
        self.values['time'] = self.values['changetime'] = when

        # The owner field defaults to the component owner
        if self.values.get('owner') == '< default >':
            default_to_owner = ''
            if self.values.get('component'):
                try:
                    component = Component(self.env, self['component'])
                    default_to_owner = component.owner # even if it's empty
                except ResourceNotFound:
                    # No such component exists
                    pass
            # If the current owner is "< default >", we need to set it to
            # _something_ else, even if that something else is blank.
            self['owner'] = default_to_owner

        # Perform type conversions
        values = dict(self.values)
        for field in self.time_fields:
            if field in values:
                values[field] = to_utimestamp(values[field])

        # Insert ticket record
        std_fields = []
        custom_fields = []
        for f in self.fields:
            fname = f['name']
            if fname in self.values:
                if f.get('custom'):
                    custom_fields.append(fname)
                else:
                    std_fields.append(fname)
        with self.env.db_transaction as db:
            cursor = db.cursor()
            cursor.execute("INSERT INTO ticket (%s) VALUES (%s)"
                           % (','.join(std_fields),
                              ','.join(['%s'] * len(std_fields))),
                           [values[name] for name in std_fields])
            tkt_id = db.get_last_id(cursor, 'ticket')

            # Insert custom fields
            if custom_fields:
                db.executemany(
                    """INSERT INTO ticket_custom (ticket, name, value)
                      VALUES (%s, %s, %s)
                      """,
                    [(tkt_id, c, self[c]) for c in custom_fields])

        self.id = tkt_id
        self.resource = self.resource(id=tkt_id)
        self._old = {}

        for listener in TicketSystem(self.env).change_listeners:
            listener.ticket_created(self)

        return self.id
Ejemplo n.º 26
0
Archivo: api.py Proyecto: t2y/trac
 def setUp(self):
     self.env = EnvironmentStub()
     self.perm = PermissionSystem(self.env)
     self.ticket_system = TicketSystem(self.env)
     self.req = Mock()
Ejemplo n.º 27
0
 def select(cls, env, db=None):
     for state in TicketSystem(env).get_all_status():
         status = cls(env)
         status.name = state
         yield status
Ejemplo n.º 28
0
Archivo: api.py Proyecto: t2y/trac
 def _get_actions(self, ticket_dict):
     ts = TicketSystem(self.env)
     ticket = Ticket(self.env)
     ticket.populate(ticket_dict)
     id = ticket.insert()
     return ts.get_available_actions(self.req, Ticket(self.env, id))
Ejemplo n.º 29
0
 def _complete_transition_list(self, args):
     if len(args) == 1:
         return TicketSystem(self.env).get_all_status()
     if len(args) == 2:
         return self.common_days
Ejemplo n.º 30
0
    def ticket_changed(self, ticket, comment, author, old_values):
        link_fields = [f['name'] for f in ticket.fields if f.get('link')]
        ticket_system = TicketSystem(self.env)
        links_provider = LinksProvider(self.env)
        remote_tktsys = RemoteTicketSystem(self.env)

        # We go behind trac's back to augment the ticket with remote links
        # As a result trac doesn't provide a correct old_values so fetch
        # our own
        orig_old_vals = old_values
        if old_values is None:
            old_values = {}
        else:
            self._augment_values(ticket.id, old_values)

        @self.env.with_transaction()
        def do_changed(db):
            cursor = db.cursor()
            for end in link_fields:
                # Determine links added or removed in this change by taking the
                # set difference of new and old values
                new_rtkts = set(remote_tktsys.parse_links(ticket[end]))
                old_rtkts = set(remote_tktsys.parse_links(old_values.get(end)))

                links_added = new_rtkts - old_rtkts
                links_removed = old_rtkts - new_rtkts
                links_changed = old_rtkts ^ new_rtkts  # Additons and removals

                other_end = ticket_system.link_ends_map[end]

                # Add link records for remote links created in this change
                records = [('', ticket.id, end, rname, rid)
                           for rname, rid in links_added]
                if other_end:
                    records += [(rname, rid, other_end, '', ticket.id)
                                for rname, rid in links_added]
                cursor.executemany(
                    '''
                    INSERT INTO remote_ticket_links
                    (source_name, source, type, destination_name, destination)
                    VALUES (%s, %s, %s, %s, %s)''', records)

                # Remove link records for remote links removed in this change
                records = [('', ticket.id, end, rname, rid)
                           for rname, rid in links_removed]
                if other_end:
                    records += [(rname, rid, other_end, '', ticket.id)
                                for rname, rid in links_added]
                cursor.executemany(
                    '''
                    DELETE FROM remote_ticket_links 
                    WHERE source_name=%s AND source=%s AND type=%s
                    AND destination_name=%s AND destination=%s''', records)

                # Record change history in ticket_change
                # Again we're going behind trac's back, so take care not to
                # obliterate existing records:
                #  - If the field (end) has changed local links, as well as
                #    changed remote links then update the record
                #  - If the only change was to remote links then there is no
                #    ticket_change record to update, so insert one
                if links_changed and orig_old_vals is not None:
                    when_ts = to_utimestamp(ticket['changetime'])

                    cursor.execute(
                        '''
                        UPDATE ticket_change
                        SET oldvalue=%s, newvalue=%s
                        WHERE ticket=%s AND time=%s AND author=%s AND field=%s
                        ''', (old_values[end], ticket[end], ticket.id, when_ts,
                              author, end))

                    # Check that a row was updated, if so
                    if cursor.rowcount >= 1:
                        continue

                    cursor.execute(
                        '''
                        INSERT INTO ticket_change
                        (ticket, time, author, field, oldvalue, newvalue)
                        VALUES (%s, %s, %s, %s, %s, %s)
                        ''', (ticket.id, when_ts, author, end, old_values[end],
                              ticket[end]))