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))
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])
def setUp(self): self.env = EnvironmentStub(default_data=True) for ctlr in TicketSystem(self.env).action_controllers: if isinstance(ctlr, ConfigurableTicketWorkflow): self.ctlr = ctlr
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
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
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 ''))
def setUp(self): self.env = EnvironmentStub() self.ticket_system = TicketSystem(self.env)
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))
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)
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)
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))
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))
def getAll(self, req): """ Returns all ticket states described by active workflow. """ return TicketSystem(self.env).get_all_status()
def getTicketFields(self, req): """ Return a list of all ticket fields fields. """ return TicketSystem(self.env).get_ticket_fields()
def get_user_list(self): return TicketSystem(self.env).get_allowed_owners()
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')
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
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 ''))
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')
]) 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)
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
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 '')
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
def setUp(self): self.env = EnvironmentStub() self.perm = PermissionSystem(self.env) self.ticket_system = TicketSystem(self.env) self.req = Mock()
def select(cls, env, db=None): for state in TicketSystem(env).get_all_status(): status = cls(env) status.name = state yield status
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))
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
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]))