def _render_property(self, name, mode, context, props): repos, revs = props[name] def link(rev): chgset = repos.get_changeset(rev) return tag.a(rev, class_="changeset", title=shorten_line(chgset.message), href=context.href.changeset(rev, repos.reponame)) if name == 'Parents' and len(revs) == 2: # merge new = context.resource.id parent_links = [ (link(rev), ' (', tag.a('diff', title=_("Diff against this parent " "(show the changes merged from the other parents)"), href=context.href.changeset(new, repos.reponame, old=rev)), ')') for rev in revs] return tag([(parent, ', ') for parent in parent_links[:-1]], parent_links[-1], tag.br(), tag.span(tag_("Note: this is a %(merge)s changeset, " "the changes displayed below correspond " "to the merge itself.", merge=tag.strong('merge')), class_='hint'), tag.br(), # TODO: only keep chunks present in both parents # (conflicts) or in none (extra changes) # tag.span('No changes means the merge was clean.', # class_='hint'), tag.br(), tag.span(tag_("Use the %(diff)s links above to see all " "the changes relative to each parent.", diff=tag.tt('(diff)')), class_='hint')) return tag([tag(link(rev), ', ') for rev in revs[:-1]], link(revs[-1]))
def _provider_failure(self, exc, req, ep, current_filters, all_filters): """Raise a TracError exception explaining the failure of a provider. At the same time, the message will contain a link to the timeline without the filters corresponding to the guilty event provider `ep`. """ self.log.error('Timeline event provider failed: %s', exception_to_unicode(exc, traceback=True)) ep_kinds = dict((f[0], f[1]) for f in ep.get_timeline_filters(req) or []) ep_filters = set(ep_kinds.keys()) current_filters = set(current_filters) other_filters = set(current_filters) - ep_filters if not other_filters: other_filters = set(all_filters) - ep_filters args = [(a, req.args.get(a)) for a in ('from', 'format', 'max', 'daysback')] href = req.href.timeline(args + [(f, 'on') for f in other_filters]) # TRANSLATOR: ...want to see the 'other kinds of events' from... (link) other_events = tag.a(_('other kinds of events'), href=href) raise TracError(tag( tag.p(tag_("Event provider %(name)s failed for filters " "%(kinds)s: ", name=tag.tt(ep.__class__.__name__), kinds=', '.join('"%s"' % ep_kinds[f] for f in current_filters & ep_filters)), tag.b(exception_to_unicode(exc)), class_='message'), tag.p(tag_("You may want to see the %(other_events)s from the " "Timeline or notify your Trac administrator about the " "error (detailed information was written to the log).", other_events=other_events))))
def _do_repositories(self, req, category, page): # Check that the setttings have been set. parentpath = self.config.get('svnadmin', 'parent_path') client = self.config.get('svnadmin', 'svn_client_location') admin = self.config.get('svnadmin', 'svnadmin_location') if not parentpath or not client or not admin: add_warning(req, _('You must provide settings before continuing.')) req.redirect(req.href.admin(category, 'config')) data = {} svn_provider = self.env[SvnRepositoryProvider] db_provider = self.env[DbRepositoryProvider] if req.method == 'POST': # Add a repository if svn_provider and req.args.get('add_repos'): name = req.args.get('name') dir = os.path.join(parentpath, name) if name is None or name == "" or not dir: add_warning(req, _('Missing arguments to add a repository.')) elif self._check_dir(req, dir): try: svn_provider.add_repository(name) db_provider.add_repository(name, dir, 'svn') add_notice(req, _('The repository "%(name)s" has been ' 'added.', name=name)) resync = tag.tt('trac-admin $ENV repository resync ' '"%s"' % name) msg = tag_('You should now run %(resync)s to ' 'synchronize Trac with the repository.', resync=resync) add_notice(req, msg) cset_added = tag.tt('trac-admin $ENV changeset ' 'added "%s" $REV' % name) msg = tag_('You should also set up a post-commit hook ' 'on the repository to call %(cset_added)s ' 'for each committed changeset.', cset_added=cset_added) add_notice(req, msg) req.redirect(req.href.admin(category, page)) except TracError, why: add_warning(req, str(why)) # Remove repositories elif svn_provider and req.args.get('remove'): sel = req.args.getlist('sel') if sel: for name in sel: svn_provider.remove_repository(name) db_provider.remove_repository(name) add_notice(req, _('The selected repositories have ' 'been removed.')) req.redirect(req.href.admin(category, page)) add_warning(req, _('No repositories were selected.'))
def __get__(self, instance, owner): if instance is None: return self order = ListOption.__get__(self, instance, owner) components = [] implementing_classes = [] for impl in self.xtnpt.extensions(instance): implementing_classes.append(impl.__class__.__name__) if self.include_missing or impl.__class__.__name__ in order: components.append(impl) not_found = sorted(set(order) - set(implementing_classes)) if not_found: raise ConfigurationError( tag_("Cannot find implementation(s) of the %(interface)s " "interface named %(implementation)s. Please check " "that the Component is enabled or update the option " "%(option)s in trac.ini.", interface=tag.tt(self.xtnpt.interface.__name__), implementation=tag( (', ' if idx != 0 else None, tag.tt(impl)) for idx, impl in enumerate(not_found)), option=tag.tt("[%s] %s" % (self.section, self.name)))) def compare(x, y): x, y = x.__class__.__name__, y.__class__.__name__ if x not in order: return int(y in order) if y not in order: return -int(x in order) return cmp(order.index(x), order.index(y)) components.sort(compare) return components
def _do_save(self, req, page): if not page.exists: req.perm(page.resource).require('WIKI_CREATE') else: req.perm(page.resource).require('WIKI_MODIFY') if 'WIKI_ADMIN' in req.perm(page.resource): # Modify the read-only flag if it has been changed and the user is # WIKI_ADMIN page.readonly = int('readonly' in req.args) try: page.save(get_reporter_id(req, 'author'), req.args.get('comment'), req.remote_addr) href = req.href.wiki(page.name, action='diff', version=page.version) add_notice(req, tag_("Your changes have been saved in version " "%(version)s (%(diff)s).", version=page.version, diff=tag.a(_("diff"), href=href))) req.redirect(get_resource_url(self.env, page.resource, req.href, version=None)) except TracError: add_warning(req, _("Page not modified, showing latest version.")) return self._render_view(req, page)
def _render_source(self, context, stream, annotations, marks=None): from trac.web.chrome import add_warning annotators, labels, titles = {}, {}, {} for annotator in self.annotators: atype, alabel, atitle = annotator.get_annotation_type() if atype in annotations: labels[atype] = alabel titles[atype] = atitle annotators[atype] = annotator annotations = [a for a in annotations if a in annotators] if isinstance(stream, list): stream = HTMLParser(StringIO(u'\n'.join(stream))) elif isinstance(stream, unicode): text = stream def linesplitter(): for line in text.splitlines(True): yield TEXT, line, (None, -1, -1) stream = linesplitter() annotator_datas = [] for a in annotations: annotator = annotators[a] try: data = (annotator, annotator.get_annotation_data(context)) except TracError, e: self.log.warning("Can't use annotator '%s': %s", a, e.message) add_warning(context.req, tag.strong( tag_("Can't use %(annotator)s annotator: %(error)s", annotator=tag.em(a), error=tag.pre(e.message)))) data = (None, None) annotator_datas.append(data)
def _do_delete(self, req, milestone): req.perm(milestone.resource).require('MILESTONE_DELETE') retarget_to = req.args.get('target') or None # Don't translate ticket comment (comment:40:ticket:5658) retargeted_tickets = \ milestone.move_tickets(retarget_to, req.authname, "Ticket retargeted after milestone deleted") milestone.delete(author=req.authname) add_notice(req, _('The milestone "%(name)s" has been deleted.', name=milestone.name)) if retargeted_tickets: add_notice(req, _('The tickets associated with milestone ' '"%(name)s" have been retargeted to milestone ' '"%(retarget)s".', name=milestone.name, retarget=retarget_to)) new_values = {'milestone': retarget_to} comment = _("Tickets retargeted after milestone deleted") tn = BatchTicketNotifyEmail(self.env) try: tn.notify(retargeted_tickets, new_values, comment, None, req.authname) except Exception, e: self.log.error("Failure sending notification on ticket batch " "change: %s", exception_to_unicode(e)) add_warning(req, tag_("The changes have been saved, but an " "error occurred while sending " "notifications: %(message)s", message=to_unicode(e)))
def notify(self, resid, subject, author=None): self.subject = subject config = self.config['notification'] if not config.getbool('smtp_enabled'): return from_email, from_name = '', '' if author and config.getbool('smtp_from_author'): from_email = self.get_smtp_address(author) if from_email: from_name = self.name_map.get(author, '') if not from_name: mo = self.longaddr_re.search(author) if mo: from_name = mo.group(1) if not from_email: from_email = config.get('smtp_from') from_name = config.get('smtp_from_name') or self.env.project_name self.replyto_email = config.get('smtp_replyto') self.from_email = from_email or self.replyto_email self.from_name = from_name if not self.from_email and not self.replyto_email: message = tag( tag.p(_('Unable to send email due to identity crisis.')), # convert explicitly to `Fragment` to avoid breaking message # when passing `LazyProxy` object to `Fragment` tag.p(to_fragment(tag_( "Neither %(from_)s nor %(reply_to)s are specified in the " "configuration.", from_=tag.strong("[notification] smtp_from"), reply_to=tag.strong("[notification] smtp_replyto"))))) raise TracError(message, _("SMTP Notification Error")) Notify.notify(self, resid)
def send(self, from_addr, recipients, message): # Ensure the message complies with RFC2822: use CRLF line endings message = fix_eol(message, CRLF) self.log.info("Sending notification through SMTP at %s:%d to %s", self.smtp_server, self.smtp_port, recipients) try: server = smtplib.SMTP(self.smtp_server, self.smtp_port) except smtplib.socket.error as e: raise ConfigurationError( tag_("SMTP server connection error (%(error)s). Please " "modify %(option1)s or %(option2)s in your " "configuration.", error=to_unicode(e), option1=tag.code("[notification] smtp_server"), option2=tag.code("[notification] smtp_port"))) # server.set_debuglevel(True) if self.use_tls: server.ehlo() if 'starttls' not in server.esmtp_features: raise TracError(_("TLS enabled but server does not support" " TLS")) server.starttls() server.ehlo() if self.smtp_user: server.login(self.smtp_user.encode('utf-8'), self.smtp_password.encode('utf-8')) start = time.time() resp = sendmail(server, from_addr, recipients, message) t = time.time() - start if t > 5: self.log.warning("Slow mail submission (%.2f s), " "check your mail setup", t) if self.use_tls: # avoid false failure detection when the server closes # the SMTP connection with TLS enabled import socket try: server.quit() except socket.sslerror: pass else: server.quit() msg = email.message_from_string(message) ticket_id = int(msg['x-trac-ticket-id']) msgid = msg['message-id'] aws_re = r'^email-smtp\.([a-z0-9-]+)\.amazonaws\.com$' m = re.match(aws_re, self.smtp_server) if m: parts = resp.split() if len(parts) == 2 and parts[0] == 'Ok': region = m.group(1) msgid = '<%s@%s.amazonses.com>' % (parts[1], region) with self.env.db_transaction as db: cursor = db.cursor() cursor.execute(""" INSERT OR IGNORE INTO messageid (ticket,messageid) VALUES (%s, %s) """, (ticket_id, msgid))
def _do_login(self, req): """Log the remote user in. This function expects to be called when the remote user name is available. The user name is inserted into the `auth_cookie` table and a cookie identifying the user on subsequent requests is sent back to the client. If the Authenticator was created with `ignore_case` set to true, then the authentication name passed from the web server in req.remote_user will be converted to lower case before being used. This is to avoid problems on installations authenticating against Windows which is not case sensitive regarding user names and domain names """ if not req.remote_user: # TRANSLATOR: ... refer to the 'installation documentation'. (link) inst_doc = tag.a(_('installation documentation'), title=_("Configuring Authentication"), href=req.href.wiki('TracInstall') + "#ConfiguringAuthentication") raise TracError(tag_("Authentication information not available. " "Please refer to the %(inst_doc)s.", inst_doc=inst_doc)) remote_user = req.remote_user if self.ignore_case: remote_user = remote_user.lower() if req.authname not in ('anonymous', remote_user): raise TracError(_('Already logged in as %(user)s.', user=req.authname)) with self.env.db_transaction as db: # Delete cookies older than 10 days db("DELETE FROM auth_cookie WHERE time < %s", (int(time.time()) - 86400 * 10,)) # Insert a new cookie if we haven't already got one cookie = None trac_auth = req.incookie.get('trac_auth') if trac_auth is not None: name = self._cookie_to_name(req, trac_auth) cookie = trac_auth.value if name == remote_user else None if cookie is None: cookie = hex_entropy() db(""" INSERT INTO auth_cookie (cookie, name, ipnr, time) VALUES (%s, %s, %s, %s) """, (cookie, remote_user, req.remote_addr, int(time.time()))) req.authname = remote_user req.outcookie['trac_auth'] = cookie req.outcookie['trac_auth']['path'] = self.auth_cookie_path \ or req.base_path or '/' if self.env.secure_cookies: req.outcookie['trac_auth']['secure'] = True if sys.version_info >= (2, 6): req.outcookie['trac_auth']['httponly'] = True if self.auth_cookie_lifetime > 0: req.outcookie['trac_auth']['expires'] = self.auth_cookie_lifetime
def render_timeline_event(self, context, field, event): url, title, desc = event[3] if field == 'url': return url elif field == 'title': return tag_('%(page)s created', page=tag.em(title)) elif field == 'description': return tag(desc)
def _render_source(self, context, stream, annotations): from trac.web.chrome import add_warning annotators, labels, titles = {}, {}, {} for annotator in self.annotators: atype, alabel, atitle = annotator.get_annotation_type() if atype in annotations: labels[atype] = alabel titles[atype] = atitle annotators[atype] = annotator annotations = [a for a in annotations if a in annotators] if isinstance(stream, list): stream = HTMLParser(StringIO(u"\n".join(stream))) elif isinstance(stream, unicode): text = stream def linesplitter(): for line in text.splitlines(True): yield TEXT, line, (None, -1, -1) stream = linesplitter() annotator_datas = [] for a in annotations: annotator = annotators[a] try: data = (annotator, annotator.get_annotation_data(context)) except TracError as e: self.log.warning("Can't use annotator '%s': %s", a, e) add_warning( context.req, tag.strong( tag_("Can't use %(annotator)s annotator: %(error)s", annotator=tag.em(a), error=tag.pre(e)) ), ) data = None, None annotator_datas.append(data) def _head_row(): return tag.tr( [tag.th(labels[a], class_=a, title=titles[a]) for a in annotations] + [tag.th(u"\xa0", class_="content")] ) def _body_rows(): for idx, line in enumerate(_group_lines(stream)): row = tag.tr() for annotator, data in annotator_datas: if annotator: annotator.annotate_row(context, row, idx + 1, line, data) else: row.append(tag.td()) row.append(tag.td(line)) yield row return tag.table(class_="code")(tag.thead(_head_row()), tag.tbody(_body_rows()))
def render_timeline_event(self, context, field, event): milestone, description = event[3] if field == 'url': return context.href.milestone(milestone.id) elif field == 'title': return tag_('Milestone %(name)s completed', name=tag.em(milestone.id)) elif field == 'description': return format_to(self.env, None, context.child(resource=milestone), description)
def render_timeline_event(self, context, field, event): wiki_page, comment = event[3] if field == 'url': return context.href.wiki(wiki_page.id, version=wiki_page.version) elif field == 'title': name = tag.em(get_resource_name(self.env, wiki_page)) if wiki_page.version > 1: return tag_('%(page)s edited', page=name) else: return tag_('%(page)s created', page=name) elif field == 'description': markup = format_to(self.env, None, context.child(resource=wiki_page), comment) if wiki_page.version > 1: diff_href = context.href.wiki( wiki_page.id, version=wiki_page.version, action='diff') markup = tag(markup, ' (', tag.a(_('diff'), href=diff_href), ')') return markup
def validate(self, relation): rls = RelationsSystem(self.env) existing_relations = rls._select_relations(resource_type=relation.type, destination=relation.destination) if existing_relations: raise ValidationError( tag_("Another resource is already related to %(destination)s " "with %(relation)s relation.", destination=tag.em(relation.destination), relation=tag.b(self.render_relation_type(relation.type))) )
def _dispatch_request(req, env, env_error): resp = [] # fixup env.abs_href if `[trac] base_url` was not specified if env and not env.abs_href.base: env._abs_href = req.abs_href try: if not env and env_error: raise HTTPInternalError(env_error) try: dispatcher = RequestDispatcher(env) dispatcher.dispatch(req) except RequestDone: pass resp = req._response or [] except HTTPException, e: # This part is a bit more complex than it should be. # See trac/web/api.py for the definition of HTTPException subclasses. if env: env.log.warn(exception_to_unicode(e)) try: # We try to get localized error messages here, # but we should ignore secondary errors title = _('Error') if e.reason: if title.lower() in e.reason.lower(): title = e.reason else: title = _('Error: %(message)s', message=e.reason) except: title = 'Error' # The message is based on the e.detail, which can be an Exception # object, but not a TracError one: when creating HTTPException, # a TracError.message is directly assigned to e.detail if isinstance(e.detail, Exception): # not a TracError message = exception_to_unicode(e.detail) elif isinstance(e.detail, Fragment): # markup coming from a TracError message = e.detail else: message = to_unicode(e.detail) data = {'title': title, 'type': 'TracError', 'message': message, 'frames': [], 'traceback': None} if e.code == 403 and req.authname == 'anonymous': # TRANSLATOR: ... not logged in, you may want to 'do so' now (link) do_so = tag.a(_("do so"), href=req.href.login()) req.chrome['notices'].append( tag_("You are currently not logged in. You may want to " "%(do_so)s now.", do_so=do_so)) try: req.send_error(sys.exc_info(), status=e.code, env=env, data=data) except RequestDone: pass
def process_request(self, req): req.perm.assert_permission('TICKET_BATCH_MODIFY') comment = req.args.get('batchmod_value_comment', '') action = req.args.get('action') try: new_values = self._get_new_ticket_values(req) except TracError, e: new_values = None add_warning(req, tag_("The changes could not be saved: " "%(message)s", message=to_unicode(e)))
def get_existing_node(req, repos, path, rev): try: return repos.get_node(path, rev) except NoSuchNode, e: # TRANSLATOR: You can 'search' in the repository history... (link) search_a = tag.a(_("search"), href=req.href.log(path, rev=rev, mode='path_history')) raise ResourceNotFound(tag( tag.p(e.message, class_="message"), tag.p(tag_("You can %(search)s in the repository history to see " "if that path existed but was later removed", search=search_a))))
def _validate(self, req, page): valid = True # Validate page size if len(req.args.get('text', '')) > self.max_size: add_warning(req, _("The wiki page is too long (must be less " "than %(num)s characters)", num=self.max_size)) valid = False # Give the manipulators a pass at post-processing the page for manipulator in self.page_manipulators: for field, message in manipulator.validate_wiki_page(req, page): valid = False if field: add_warning(req, tag_("The Wiki page field '%(field)s'" " is invalid: %(message)s", field=field, message=message)) else: add_warning(req, tag_("Invalid Wiki page: %(message)s", message=message)) return valid
def generate_prefix(prefix): intertrac = intertracs[prefix] if isinstance(intertrac, basestring): yield tag.tr(tag.td(tag.strong(prefix)), tag.td(tag_("Alias for %(name)s", name=tag.strong(intertrac)))) else: url = intertrac.get('url', '') if url: title = intertrac.get('title', url) yield tag.tr(tag.td(tag.a(tag.strong(prefix), href=url + '/timeline')), tag.td(tag.a(title, href=url)))
def _do_login(self, req): """Log the remote user in. This function expects to be called when the remote user name is available. The user name is inserted into the `auth_cookie` table and a cookie identifying the user on subsequent requests is sent back to the client. If the Authenticator was created with `ignore_case` set to true, then the authentication name passed from the web server in req.remote_user will be converted to lower case before being used. This is to avoid problems on installations authenticating against Windows which is not case sensitive regarding user names and domain names """ if not req.remote_user: # TRANSLATOR: ... refer to the 'installation documentation'. (link) inst_doc = tag.a( _("installation documentation"), title=_("Configuring Authentication"), href=req.href.wiki("TracInstall") + "#ConfiguringAuthentication", ) raise TracError( tag_( "Authentication information not available. " "Please refer to the %(inst_doc)s.", inst_doc=inst_doc ) ) remote_user = req.remote_user if self.ignore_case: remote_user = remote_user.lower() assert req.authname in ("anonymous", remote_user), _("Already logged in as %(user)s.", user=req.authname) cookie = hex_entropy() @self.env.with_transaction() def store_session_cookie(db): cursor = db.cursor() # Delete cookies older than 10 days cursor.execute("DELETE FROM auth_cookie WHERE time < %s", (int(time.time()) - 86400 * 10,)) cursor.execute( "INSERT INTO auth_cookie (cookie,name,ipnr,time) " "VALUES (%s, %s, %s, %s)", (cookie, remote_user, req.remote_addr, int(time.time())), ) req.authname = remote_user req.outcookie["trac_auth"] = cookie req.outcookie["trac_auth"]["path"] = self.auth_cookie_path or req.base_path or "/" if self.env.secure_cookies: req.outcookie["trac_auth"]["secure"] = True if self.auth_cookie_lifetime > 0: req.outcookie["trac_auth"]["expires"] = self.auth_cookie_lifetime
def get_search_results(self, req, terms, filters): """Overriding search results for Tickets""" if not "ticket" in filters: return ticket_realm = Resource("ticket") with self.env.db_query as db: sql, args = search_to_sql( db, ["summary", "keywords", "description", "reporter", "cc", db.cast("id", "text")], terms ) sql2, args2 = search_to_sql(db, ["newvalue"], terms) sql3, args3 = search_to_sql(db, ["value"], terms) ticketsystem = TicketSystem(self.env) if req.args.get("product"): productsql = "product='%s' AND" % req.args.get("product") else: productsql = "" for summary, desc, author, type, tid, ts, status, resolution in db( """SELECT summary, description, reporter, type, id, time, status, resolution FROM ticket WHERE (%s id IN ( SELECT id FROM ticket WHERE %s UNION SELECT ticket FROM ticket_change WHERE field='comment' AND %s UNION SELECT ticket FROM ticket_custom WHERE %s )) """ % (productsql, sql, sql2, sql3), args + args2 + args3, ): t = ticket_realm(id=tid) if "TICKET_VIEW" in req.perm(t): yield ( req.href.ticket(tid), tag_( "%(title)s: %(message)s", title=tag.span(get_resource_shortname(self.env, t), class_=status), message=ticketsystem.format_summary(summary, status, resolution, type), ), from_utimestamp(ts), author, shorten_result(desc, terms), ) # Attachments for result in AttachmentModule(self.env).get_search_results(req, ticket_realm, terms): yield result
def _send_user_error(req, env, e): # See trac/web/api.py for the definition of HTTPException subclasses. if env: env.log.warn('[%s] %s', req.remote_addr, exception_to_unicode(e)) data = {'title': e.title, 'type': 'TracError', 'message': e.message, 'frames': [], 'traceback': None} if e.code == 403 and req.authname == 'anonymous': # TRANSLATOR: ... not logged in, you may want to 'do so' now (link) do_so = tag.a(_("do so"), href=req.href.login()) add_notice(req, tag_("You are currently not logged in. You may want " "to %(do_so)s now.", do_so=do_so)) try: req.send_error(sys.exc_info(), status=e.code, env=env, data=data) except RequestDone: pass
def render_ticket_action_control(self, req, ticket, action): this_action = ConfigurableTicketWorkflow(self.env).actions[action] status = this_action['newstate'] operations = this_action['operations'] control = [] # default to nothing hints = [] if 'set_evaluation' in operations: evaluations = self._get_evaluation_options() if not evaluations: raise TracError(_('Your workflow attempts to set an evaluation ' 'but none is defined (configuration issue, ' 'please contact your Trac admin).')) id = 'action_%s_evaluate_evaluation' % action if len(evaluations) == 1: evaluation = tag.input(type='hidden', id=id, name=id, value=evaluations[0]) control.append(tag_('as %(evaluation)s', evaluation=tag(evaluations[0], evaluation))) hints.append(_('The evaluation will be set to %(name)s', name=evaluations[0])) else: selected_option = 1 control.append(tag_('as %(evaluation)s', evaluation=tag.select( [tag.option(x, value=x, selected=(x == selected_option or None)) for x in evaluations], id=id, name=id))) hints.append(_('The evaluation will be set')) #if 'del_evaluation' in operations: # hints.append(_('The evaluation will be deleted')) return (this_action['name'], tag(*control), '. '.join(hints) + '.' if hints else '')
def get_delete_event(self, req, download_str, dir_or_file, type_to_can_view, show_downloads_events, show_files_events, filename, path): target_path = '/' + path target_type = download_str can_view_target = type_to_can_view[target_type] if not can_view_target: return if target_type == 'download': if not show_downloads_events: return event_class = 'downloadsevent-rm' else: if not show_files_events: return event_class = 'filesevent-rm' parent_path = get_normalized_relative_path( 'base_path_not_used', os.path.join(target_path[1:], '..'), assume_relative_path=True) if dir_or_file == 'dir': title = _("Directory %(filename)s deleted", filename=filename) description = tag_("Directory %(filename)s deleted from %(target_path)s", filename=filename, target_path=tag.a(target_path, href=req.href.files(parent_path))) else: if target_type == 'download': title = _("Download %(filename)s deleted", filename=filename) description = tag_("Download %(filename)s deleted from %(target_path)s", filename=filename, target_path=tag.a(target_path, href=req.href.files(parent_path))) else: title = _("File %(filename)s deleted", filename=filename) description = tag_("File %(filename)s deleted from %(target_path)s", filename=filename, target_path=tag.a(target_path, href=req.href.files(parent_path))) return event_class, title, description
def __get__(self, instance, owner): if instance is None: return self value = Option.__get__(self, instance, owner) for impl in self.xtnpt.extensions(instance): if impl.__class__.__name__ == value: return impl raise ConfigurationError( tag_("Cannot find an implementation of the %(interface)s " "interface named %(implementation)s. Please check " "that the Component is enabled or update the option " "%(option)s in trac.ini.", interface=tag.tt(self.xtnpt.interface.__name__), implementation=tag.tt(value), option=tag.tt("[%s] %s" % (self.section, self.name))))
def get_search_results(self, req, terms, filters): """Return the entry whose 'keyword' or 'text' tag contains one or more word among the terms. """ if 'crashdump' not in filters: return self.log.debug('search for %s and %s', terms, filters) #ticket_realm = Resource(self.realm) with self.env.db_query as db: sql, args = search_to_sql(db, ['summary', 'keywords', 'description', 'reporter', 'cc', 'applicationname', 'applicationfile', 'uploadhostname', 'uploadusername', 'crashhostname', 'crashusername', 'systemname', 'uuid', db.cast('id', 'text')], terms) for id, uuid, summary, description, reporter, type, \ crashhostname, crashusername, applicationname, applicationfile, systemname, \ crashtime, reporttime, status, resolution in \ db("""SELECT id, uuid, summary, description, reporter, type, crashhostname, crashusername, applicationname, applicationfile, systemname, crashtime, reporttime, status, resolution FROM crashdump WHERE id IN ( SELECT id FROM crashdump WHERE %s ) """ % (sql), args): if 'TICKET_VIEW' in req.perm: # The events returned by this function must be tuples of the form (href, title, date, author, excerpt). full_desc = '%s on %s (%s)' % ( applicationname if applicationname else applicationfile, '%s@%s' % (crashusername, crashhostname), systemname ) #excerpt = shorten_result(full_desc, terms) excerpt = full_desc yield (req.href('crash', uuid), tag_("%(title)s: %(uuid)s", uuid=uuid, title='CrashId#%i' % id, ), from_utimestamp(crashtime), reporter, excerpt)
def send(self, from_addr, recipients, message): # Ensure the message complies with RFC2822: use CRLF line endings message = fix_eol(message, CRLF) self.log.info("Sending notification through SMTP at %s:%d to %s", self.smtp_server, self.smtp_port, recipients) try: server = smtplib.SMTP(self.smtp_server, self.smtp_port) except smtplib.socket.error, e: raise ConfigurationError( tag_("SMTP server connection error (%(error)s). Please " "modify %(option1)s or %(option2)s in your " "configuration.", error=to_unicode(e), option1=tag.tt("[notification] smtp_server"), option2=tag.tt("[notification] smtp_port")))
def send(self, from_addr, recipients, message): # Use native line endings in message message = fix_eol(message, os.linesep) self.log.info("Sending notification through sendmail at %s to %s", self.sendmail_path, recipients) cmdline = [self.sendmail_path, "-i", "-f", from_addr] cmdline.extend(recipients) self.log.debug("Sendmail command line: %s", cmdline) try: child = Popen(cmdline, bufsize=-1, stdin=PIPE, stdout=PIPE, stderr=PIPE, close_fds=close_fds) except OSError, e: raise ConfigurationError( tag_("Sendmail error (%(error)s). Please modify %(option)s " "in your configuration.", error=to_unicode(e), option=tag.tt("[notification] sendmail_path")))
def send(self, from_addr, recipients, message): # Ensure the message complies with RFC2822: use CRLF line endings message = fix_eol(message, CRLF) self.log.info("Sending notification through SMTP at %s:%d to %s", self.smtp_server, self.smtp_port, recipients) try: server = smtplib.SMTP(self.smtp_server, self.smtp_port) except smtplib.socket.error as e: raise ConfigurationError( tag_("SMTP server connection error (%(error)s). Please " "modify %(option1)s or %(option2)s in your " "configuration.", error=to_unicode(e), option1=tag.code("[notification] smtp_server"), option2=tag.code("[notification] smtp_port"))) # server.set_debuglevel(True) if self.use_tls: server.ehlo() if 'starttls' not in server.esmtp_features: raise TracError(_("TLS enabled but server does not support" " TLS")) server.starttls() server.ehlo() if self.smtp_user: server.login(self.smtp_user.encode('utf-8'), self.smtp_password.encode('utf-8')) start = time.time() server.sendmail(from_addr, recipients, message) t = time.time() - start if t > 5: self.log.warning("Slow mail submission (%.2f s), " "check your mail setup", t) if self.use_tls: # avoid false failure detection when the server closes # the SMTP connection with TLS enabled import socket try: server.quit() except socket.sslerror: pass else: server.quit()
def send(self, from_addr, recipients, message): # Use native line endings in message message = fix_eol(message, os.linesep) self.log.info("Sending notification through sendmail at %s to %s", self.sendmail_path, recipients) cmdline = [self.sendmail_path, '-i', '-f', from_addr] + recipients self.log.debug("Sendmail command line: %s", cmdline) try: child = Popen(cmdline, bufsize=-1, stdin=PIPE, stdout=PIPE, stderr=PIPE, close_fds=close_fds) except OSError as e: raise ConfigurationError( tag_("Sendmail error (%(error)s). Please modify %(option)s " "in your configuration.", error=to_unicode(e), option=tag.code("[notification] sendmail_path"))) out, err = child.communicate(message) if child.returncode or err: raise Exception("Sendmail failed with (%s, %s), command: '%s'" % (child.returncode, err.strip(), cmdline))
def _get_valid_default_handler(self, req): # Use default_handler from the Session if it is a valid value. name = req.session.get('default_handler') handler = self._request_handlers.get(name) if handler and not is_valid_default_handler(handler): handler = None if not handler: # Use default_handler from project configuration. handler = self.default_handler if not is_valid_default_handler(handler): raise ConfigurationError( tag_( "%(handler)s is not a valid default handler. Please " "update %(option)s through the %(page)s page or by " "directly editing trac.ini.", handler=tag.code(handler.__class__.__name__), option=tag.code("[trac] default_handler"), page=tag.a(_("Basic Settings"), href=req.href.admin('general/basics')))) return handler
def __init__(self, path, log=None, params={}): self.cnx = None if path != ':memory:': if not os.access(path, os.F_OK): raise ConfigurationError( _('Database "%(path)s" not found.', path=path)) dbdir = os.path.dirname(path) if not os.access(path, os.R_OK + os.W_OK) or \ not os.access(dbdir, os.R_OK + os.W_OK): raise ConfigurationError( tag_( "The user %(user)s requires read _and_ write permissions " "to the database file %(path)s and the directory it is " "located in.", user=tag.code(getuser()), path=tag.code(path))) self._active_cursors = weakref.WeakKeyDictionary() timeout = int(params.get('timeout', 10.0)) self._eager = params.get('cursor', 'eager') == 'eager' # eager is default, can be turned off by specifying ?cursor= cnx = sqlite.connect(path, detect_types=sqlite.PARSE_DECLTYPES, isolation_level=None, check_same_thread=sqlite_version < (3, 3, 1), timeout=timeout) # load extensions extensions = params.get('extensions', []) if len(extensions) > 0: cnx.enable_load_extension(True) for ext in extensions: cnx.load_extension(ext) cnx.enable_load_extension(False) with closing(cnx.cursor()) as cursor: _set_journal_mode(cursor, params.get('journal_mode')) set_synchronous(cursor, params.get('synchronous')) cnx.isolation_level = 'DEFERRED' ConnectionWrapper.__init__(self, cnx, log)
def _do_delete(self, req, milestone): req.perm(milestone.resource).require('MILESTONE_DELETE') retarget_to = req.args.get('target') or None # Don't translate ticket comment (comment:40:ticket:5658) retargeted_tickets = \ milestone.move_tickets(retarget_to, req.authname, "Ticket retargeted after milestone deleted") milestone.delete(author=req.authname) add_notice( req, _('The milestone "%(name)s" has been deleted.', name=milestone.name)) if retargeted_tickets: add_notice( req, _( 'The tickets associated with milestone ' '"%(name)s" have been retargeted to milestone ' '"%(retarget)s".', name=milestone.name, retarget=retarget_to)) new_values = {'milestone': retarget_to} comment = _("Tickets retargeted after milestone deleted") tn = BatchTicketNotifyEmail(self.env) try: tn.notify(retargeted_tickets, new_values, comment, None, req.authname) except Exception, e: self.log.error( "Failure sending notification on ticket batch " "change: %s", exception_to_unicode(e)) add_warning( req, tag_( "The changes have been saved, but an " "error occurred while sending " "notifications: %(message)s", message=to_unicode(e)))
def _filter_resolutions(self, req, items): for item in items: if item[0] != 'resolve': yield item return resolutions = [ val.name for val in Resolution.select(self.env) if int(val.value) > 0 ] ts = TicketSystem(self.env) selected_option = req.args.get('action_resolve_resolve_resolution', ts.default_resolution) control = tag.select([ tag.option(x, value=x, selected=(x == selected_option or None)) for x in resolutions ], id='action_resolve_resolve_resolution', name='action_resolve_resolve_resolution') yield ('resolve', tag_('as %(resolution)s', resolution=control), item[2])
def getint(self, name, default=None, min=None, max=None): """Return the value as an integer. Raise an `HTTPBadRequest` exception if an exception occurs while converting the value to an integer. :param name: the name of the request parameter :keyword default: the value to return if the parameter is not specified :keyword min: lower bound to which the value is limited :keyword max: upper bound to which the value is limited :since: 1.2 """ if name not in self: return default value = as_int(self[name], None, min, max) if value is None: raise HTTPBadRequest( tag_("Invalid value for request argument " "%(name)s.", name=tag.em(name))) return value
def _save_ticket_changes(self, req, selected_tickets, new_values, comment, action): """Save all of the changes to tickets.""" when = datetime.now(utc) list_fields = self._get_list_fields() with self.env.db_transaction as db: for id in selected_tickets: t = Ticket(self.env, int(id)) _values = new_values.copy() for field in list_fields: if field in new_values: old = t.values[field] if field in t.values else '' new = new_values[field] mode = req.args.get('batchmod_value_' + field + '_mode') new2 = req.args.get('batchmod_value_' + field + '_secondary', '') _values[field] = self._change_list(old, new, new2, mode) controllers = list(self._get_action_controllers(req, t, action)) for controller in controllers: _values.update(controller.get_ticket_changes(req, t, action)) t.populate(_values) t.save_changes(req.authname, comment, when=when) for controller in controllers: controller.apply_action_side_effects(req, t, action) try: tn = BatchTicketNotifyEmail(self.env) tn.notify(selected_tickets, new_values, comment, action, req.authname) except Exception, e: self.log.error("Failure sending notification on ticket batch" "change: %s", exception_to_unicode(e)) add_warning(req, tag_("The changes have been saved, but an " "error occurred while sending " "notifications: %(message)s", message=to_unicode(e)))
def process_request(self, req): req.perm.assert_permission('TICKET_BATCH_MODIFY') comment = req.args.get('batchmod_value_comment', '') action = req.args.get('action') try: new_values = self._get_new_ticket_values(req) except TracError as e: new_values = None add_warning( req, tag_("The changes could not be saved: " "%(message)s", message=to_unicode(e))) if new_values is not None: selected_tickets = self._get_selected_tickets(req) self._save_ticket_changes(req, selected_tickets, new_values, comment, action) #Always redirect back to the query page we came from. req.redirect(req.session['query_href'])
def _send_user_error(req, env, e): # See trac/web/api.py for the definition of HTTPException subclasses. if env: env.log.warning('[%s] %s, %r, referrer %r', req.remote_addr, exception_to_unicode(e), req, req.environ.get('HTTP_REFERER')) data = { 'title': e.title, 'type': 'TracError', 'message': e.message, 'frames': [], 'traceback': None } if e.code == 403 and not req.is_authenticated: # TRANSLATOR: ... not logged in, you may want to 'do so' now (link) do_so = tag.a(_("do so"), href=req.href.login()) add_notice( req, tag_( "You are currently not logged in. You may want " "to %(do_so)s now.", do_so=do_so)) _send_error(req, sys.exc_info(), status=e.code, env=env, data=data)
def _do_save(self, req, page): if not page.exists: req.perm(page.resource).require('WIKI_CREATE') else: req.perm(page.resource).require('WIKI_MODIFY') if 'WIKI_CHANGE_READONLY' in req.perm(page.resource): # Modify the read-only flag if it has been changed and the user is # WIKI_ADMIN page.readonly = int('readonly' in req.args) try: page.save(get_reporter_id(req, 'author'), req.args.get('comment')) except TracError: add_warning(req, _("Page not modified, showing latest version.")) return self._render_view(req, page) href = req.href.wiki(page.name, action='diff', version=page.version) add_notice(req, tag_("Your changes have been saved in version " "%(version)s (%(diff)s).", version=page.version, diff=tag.a(_("diff"), href=href))) req.redirect(get_resource_url(self.env, page.resource, req.href, version=None))
def _render_source(self, context, stream, annotations, marks=None): from trac.web.chrome import add_warning annotators, labels, titles = {}, {}, {} for annotator in self.annotators: atype, alabel, atitle = annotator.get_annotation_type() if atype in annotations: labels[atype] = alabel titles[atype] = atitle annotators[atype] = annotator annotations = [a for a in annotations if a in annotators] if isinstance(stream, list): stream = HTMLParser(StringIO(u'\n'.join(stream))) elif isinstance(stream, unicode): text = stream def linesplitter(): for line in text.splitlines(True): yield TEXT, line, (None, -1, -1) stream = linesplitter() annotator_datas = [] for a in annotations: annotator = annotators[a] try: data = (annotator, annotator.get_annotation_data(context)) except TracError, e: self.log.warning("Can't use annotator '%s': %s", a, e.message) add_warning( context.req, tag.strong( tag_("Can't use %(annotator)s annotator: %(error)s", annotator=tag.em(a), error=tag.pre(e.message)))) data = (None, None) annotator_datas.append(data)
def _send_user_error(req, env, e): # See trac/web/api.py for the definition of HTTPException subclasses. if env: env.log.warn('[%s] %s' % (req.remote_addr, exception_to_unicode(e))) try: # We first try to get localized error messages here, but we # should ignore secondary errors if the main error was also # due to i18n issues title = _('Error') if e.reason: if title.lower() in e.reason.lower(): title = e.reason else: title = _('Error: %(message)s', message=e.reason) except Exception: title = 'Error' # The message is based on the e.detail, which can be an Exception # object, but not a TracError one: when creating HTTPException, # a TracError.message is directly assigned to e.detail if isinstance(e.detail, Exception): # not a TracError message = exception_to_unicode(e.detail) elif isinstance(e.detail, Fragment): # markup coming from a TracError message = e.detail else: message = to_unicode(e.detail) data = {'title': title, 'type': 'TracError', 'message': message, 'frames': [], 'traceback': None} if e.code == 403 and req.authname == 'anonymous': # TRANSLATOR: ... not logged in, you may want to 'do so' now (link) do_so = tag.a(_("do so"), href=req.href.login()) req.chrome['notices'].append( tag_("You are currently not logged in. You may want to " "%(do_so)s now.", do_so=do_so)) try: req.send_error(sys.exc_info(), status=e.code, env=env, data=data) except RequestDone: pass
def _process_fork_request(self, req, data): """Fork an existing repository.""" rm = RepositoryManager(self.env) origin_name = req.args.get('local_origin', req.args.get('reponame')) if self.restrict_forks and origin_name: name = req.authname + '/' + origin_name if not 'REPOSITORY_ADMIN' in req.perm: if rm.get_repository(name): req.redirect(req.href.browser(name)) req.args['local_name'] = name local_fork = self._get_repository_data_from_request(req, 'local_') local_fork['origin'] = origin_name if req.args.get('fork_local'): origin = self._get_checked_repository(req, local_fork['origin'], False, 'REPOSITORY_FORK') local_fork.update({'type': origin.type}) self._create(req, local_fork, rm.fork_local) repo_link = tag.a(origin_name, href=req.href.browser(origin_name)) data.update({'title': tag_("Fork Repository %(link)s", link=repo_link), 'local_fork': local_fork})
def _send_user_error(req, env, e): # See trac/web/api.py for the definition of HTTPException subclasses. if env: env.log.warn('[%s] %s', req.remote_addr, exception_to_unicode(e)) data = { 'title': e.title, 'type': 'TracError', 'message': e.message, 'frames': [], 'traceback': None } if e.code == 403 and req.authname == 'anonymous': # TRANSLATOR: ... not logged in, you may want to 'do so' now (link) do_so = tag.a(_("do so"), href=req.href.login()) add_notice( req, tag_( "You are currently not logged in. You may want " "to %(do_so)s now.", do_so=do_so)) try: req.send_error(sys.exc_info(), status=e.code, env=env, data=data) except RequestDone: pass
def notify(self, resid, subject, author=None): self.subject = subject config = self.config['notification'] if not config.getbool('smtp_enabled'): return from_email, from_name = '', '' if author and config.getbool('smtp_from_author'): from_email = self.get_smtp_address(author) if from_email: from_name = self.name_map.get(author, '') if not from_name: mo = self.longaddr_re.search(author) if mo: from_name = mo.group(1) if not from_email: from_email = config.get('smtp_from') from_name = config.get('smtp_from_name') or self.env.project_name self.replyto_email = config.get('smtp_replyto') self.from_email = from_email or self.replyto_email self.from_name = from_name if not self.from_email and not self.replyto_email: message = tag( tag.p(_('Unable to send email due to identity crisis.')), # convert explicitly to `Fragment` to avoid breaking message # when passing `LazyProxy` object to `Fragment` tag.p( to_fragment( tag_( "Neither %(from_)s nor %(reply_to)s are specified in the " "configuration.", from_=tag.strong("[notification] smtp_from"), reply_to=tag.strong( "[notification] smtp_replyto"))))) raise TracError(message, _("SMTP Notification Error")) Notify.notify(self, resid)
def render_admin_panel(self, req, cat, page, path_info): log_type = self.env.log_type log_level = self.env.log_level log_file = self.env.log_file log_dir = self.env.log_dir log_types = [ dict(name='none', label=_("None"), selected=log_type == 'none', disabled=False), dict(name='stderr', label=_("Console"), selected=log_type == 'stderr', disabled=False), dict(name='file', label=_("File"), selected=log_type == 'file', disabled=False), dict(name='syslog', label=_("Syslog"), selected=log_type in ('unix', 'syslog'), disabled=os.name != 'posix'), dict(name='eventlog', label=_("Windows event log"), selected=log_type in ('winlog', 'eventlog', 'nteventlog'), disabled=os.name != 'nt'), ] if req.method == 'POST': changed = False new_type = req.args.get('log_type') if new_type not in [t['name'] for t in log_types]: raise TracError(_("Unknown log type %(type)s", type=new_type), _("Invalid log type")) new_file = req.args.get('log_file', 'trac.log') if not new_file: raise TracError(_("You must specify a log file"), _("Missing field")) new_level = req.args.get('log_level') if new_level not in LOG_LEVELS: raise TracError( _("Unknown log level %(level)s", level=new_level), _("Invalid log level")) # Create logger to be sure the configuration is valid. new_file_path = new_file if not os.path.isabs(new_file_path): new_file_path = os.path.join(self.env.log_dir, new_file) try: logger, handler = \ self.env.create_logger(new_type, new_file_path, new_level, self.env.log_format) except Exception as e: add_warning( req, tag_( "Changes not saved. Logger configuration " "error: %(error)s. Inspect the log for more " "information.", error=tag.code(exception_to_unicode(e)))) self.log.error("Logger configuration error: %s", exception_to_unicode(e, traceback=True)) else: handler.close() if new_type != log_type: self.config.set('logging', 'log_type', new_type) changed = True log_type = new_type if log_type == 'none': self.config.remove('logging', 'log_level') changed = True else: if new_level != log_level: self.config.set('logging', 'log_level', new_level) changed = True log_level = new_level if log_type == 'file': if new_file != log_file: self.config.set('logging', 'log_file', new_file) changed = True log_file = new_file else: self.config.remove('logging', 'log_file') changed = True if changed: _save_config(self.config, req, self.log), req.redirect(req.href.admin(cat, page)) data = { 'type': log_type, 'types': log_types, 'level': log_level, 'levels': LOG_LEVELS, 'file': log_file, 'dir': log_dir } return 'admin_logging.html', {'log': data}
def save_milestone(self, req, milestone): # Instead of raising one single error, check all the constraints # and let the user fix them by going back to edit mode and showing # the warnings warnings = [] def warn(msg): add_warning(req, msg) warnings.append(msg) milestone.description = req.args.get('description', '') if 'due' in req.args: duedate = req.args.get('duedate') milestone.due = user_time(req, parse_date, duedate, hint='datetime') \ if duedate else None else: milestone.due = None # -- check completed date if 'completed' in req.args: completed = req.args.get('completeddate', '') completed = user_time(req, parse_date, completed, hint='datetime') if completed else None if completed and completed > datetime_now(utc): warn(_("Completion date may not be in the future")) else: completed = None milestone.completed = completed # -- check the name # If the name has changed, check that the milestone doesn't already # exist # FIXME: the whole .exists business needs to be clarified # (#4130) and should behave like a WikiPage does in # this respect. new_name = req.args.get('name') try: new_milestone = Milestone(self.env, new_name) except ResourceNotFound: milestone.name = new_name else: if new_milestone.name != milestone.name: if new_milestone.name: warn( _( 'Milestone "%(name)s" already exists, please ' 'choose another name.', name=new_milestone.name)) else: warn(_("You must provide a name for the milestone.")) if warnings: return False # -- actually save changes if milestone.exists: milestone.update(author=req.authname) if completed and 'retarget' in req.args: comment = req.args.get('comment', '') retarget_to = req.args.get('target') or None retargeted_tickets = \ milestone.move_tickets(retarget_to, req.authname, comment, exclude_closed=True) add_notice( req, _( 'The open tickets associated with ' 'milestone "%(name)s" have been retargeted ' 'to milestone "%(retarget)s".', name=milestone.name, retarget=retarget_to)) new_values = {'milestone': retarget_to} comment = comment or \ _("Open tickets retargeted after milestone closed") event = BatchTicketChangeEvent(retargeted_tickets, None, req.authname, comment, new_values, None) try: NotificationSystem(self.env).notify(event) except Exception as e: self.log.error( "Failure sending notification on ticket " "batch change: %s", exception_to_unicode(e)) add_warning( req, tag_( "The changes have been saved, but " "an error occurred while sending " "notifications: %(message)s", message=to_unicode(e))) add_notice(req, _("Your changes have been saved.")) else: milestone.insert() add_notice( req, _('The milestone "%(name)s" has been added.', name=milestone.name)) return True
def _do_login(self, req): """Log the remote user in. This function expects to be called when the remote user name is available. The user name is inserted into the `auth_cookie` table and a cookie identifying the user on subsequent requests is sent back to the client. If the Authenticator was created with `ignore_case` set to true, then the authentication name passed from the web server in req.remote_user will be converted to lower case before being used. This is to avoid problems on installations authenticating against Windows which is not case sensitive regarding user names and domain names """ if not req.remote_user: # TRANSLATOR: ... refer to the 'installation documentation'. (link) inst_doc = tag.a(_("installation documentation"), title=_("Configuring Authentication"), href=req.href.wiki('TracInstall') + "#ConfiguringAuthentication") raise TracError( tag_( "Authentication information not available. " "Please refer to the %(inst_doc)s.", inst_doc=inst_doc)) remote_user = req.remote_user if self.ignore_case: remote_user = remote_user.lower() if req.authname not in ('anonymous', remote_user): raise TracError( _("Already logged in as %(user)s.", user=req.authname)) with self.env.db_transaction as db: # Delete cookies older than 10 days db("DELETE FROM auth_cookie WHERE time < %s", (int(time.time()) - 86400 * 10, )) # Insert a new cookie if we haven't already got one cookie = None trac_auth = req.incookie.get('trac_auth') if trac_auth is not None: name = self._cookie_to_name(req, trac_auth) cookie = trac_auth.value if name == remote_user else None if cookie is None: cookie = hex_entropy() db( """ INSERT INTO auth_cookie (cookie, name, ipnr, time) VALUES (%s, %s, %s, %s) """, (cookie, remote_user, req.remote_addr, int(time.time()))) req.authname = remote_user req.outcookie['trac_auth'] = cookie if self.auth_cookie_domain: req.outcookie['trac_auth']['domain'] = self.auth_cookie_domain req.outcookie['trac_auth']['path'] = self.auth_cookie_path \ or req.base_path or '/' if self.env.secure_cookies: req.outcookie['trac_auth']['secure'] = True req.outcookie['trac_auth']['httponly'] = True if self.auth_cookie_lifetime > 0: req.outcookie['trac_auth']['expires'] = self.auth_cookie_lifetime
'title': title, 'description': description, 'max': limit, 'args': args, 'show_args_form': False, 'message': None, 'paginator': None, 'report_href': report_href, } res = None with self.env.db_query as db: res = self.execute_paginated_report(req, db, 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),
def process_request(self, req): parent_id = None parent_realm = req.args.get('realm') path = req.args.get('path') filename = None if not parent_realm or not path: raise HTTPBadRequest(_('Bad request')) if parent_realm == 'attachment': raise TracError( tag_("%(realm)s is not a valid parent realm", realm=tag.code(parent_realm))) parent_realm = Resource(parent_realm) action = req.args.get('action', 'view') if action == 'new': parent_id = path.rstrip('/') else: last_slash = path.rfind('/') if last_slash == -1: parent_id, filename = path, '' else: parent_id, filename = path[:last_slash], path[last_slash + 1:] parent = parent_realm(id=parent_id) if not resource_exists(self.env, parent): raise ResourceNotFound( _("Parent resource %(parent)s doesn't exist", parent=get_resource_name(self.env, parent))) # Link the attachment page to parent resource parent_name = get_resource_name(self.env, parent) parent_url = get_resource_url(self.env, parent, req.href) add_link(req, 'up', parent_url, parent_name) add_ctxtnav(req, _('Back to %(parent)s', parent=parent_name), parent_url) if not filename: # there's a trailing '/' if req.args.get('format') == 'zip': self._download_as_zip(req, parent) elif action != 'new': return self._render_list(req, parent) attachment = Attachment(self.env, parent.child(self.realm, filename)) if req.method == 'POST': if action == 'new': data = self._do_save(req, attachment) elif action == 'delete': self._do_delete(req, attachment) else: raise HTTPBadRequest(_("Invalid request arguments.")) elif action == 'delete': data = self._render_confirm_delete(req, attachment) elif action == 'new': data = self._render_form(req, attachment) else: data = self._render_view(req, attachment) add_stylesheet(req, 'common/css/code.css') return 'attachment.html', data, None
def _do_save(self, req, attachment): req.perm(attachment.resource).require('ATTACHMENT_CREATE') parent_resource = attachment.resource.parent if 'cancel' in req.args: req.redirect(get_resource_url(self.env, parent_resource, req.href)) upload = req.args.getfirst('attachment') if not hasattr(upload, 'filename') or not upload.filename: raise TracError(_("No file uploaded")) if hasattr(upload.file, 'fileno'): size = os.fstat(upload.file.fileno())[6] else: upload.file.seek(0, 2) # seek to end of file size = upload.file.tell() upload.file.seek(0) if size == 0: raise TracError(_("Can't upload empty file")) # Maximum attachment size (in bytes) max_size = self.max_size if 0 <= max_size < size: raise TracError( _("Maximum attachment size: %(num)s", num=pretty_size(max_size)), _("Upload failed")) filename = _normalized_filename(upload.filename) if not filename: raise TracError(_("No file uploaded")) # Now the filename is known, update the attachment resource attachment.filename = filename attachment.description = req.args.get('description', '') attachment.author = get_reporter_id(req, 'author') attachment.ipnr = req.remote_addr # Validate attachment valid = True for manipulator in self.manipulators: for field, message in manipulator.validate_attachment( req, attachment): valid = False if field: add_warning( req, tag_( "Attachment field %(field)s is invalid: " "%(message)s", field=tag.strong(field), message=message)) else: add_warning( req, tag_("Invalid attachment: %(message)s", message=message)) if not valid: # Display the attach form with pre-existing data # NOTE: Local file path not known, file field cannot be repopulated add_warning(req, _('Note: File must be selected again.')) data = self._render_form(req, attachment) data['is_replace'] = req.args.get('replace') return data if req.args.get('replace'): try: old_attachment = Attachment(self.env, attachment.resource(id=filename)) if not (req.authname and req.authname != 'anonymous' and old_attachment.author == req.authname) \ and 'ATTACHMENT_DELETE' \ not in req.perm(attachment.resource): raise PermissionError(msg=_( "You don't have permission to " "replace the attachment %(name)s. You can only " "replace your own attachments. Replacing other's " "attachments requires ATTACHMENT_DELETE permission.", name=filename)) if (not attachment.description.strip() and old_attachment.description): attachment.description = old_attachment.description old_attachment.delete() except TracError: pass # don't worry if there's nothing to replace attachment.insert(filename, upload.file, size) req.redirect( get_resource_url(self.env, attachment.resource(id=None), req.href))
def _save_ticket_changes(self, req, selected_tickets, new_values, comment, action): """Save changes to tickets.""" valid = True for manipulator in self.ticket_manipulators: if hasattr(manipulator, 'validate_comment'): for message in manipulator.validate_comment(req, comment): valid = False add_warning( req, tag_("The ticket comment is invalid: " "%(message)s", message=message)) tickets = [] for id_ in selected_tickets: t = Ticket(self.env, id_) values = self._get_updated_ticket_values(req, t, new_values) for ctlr in self._get_action_controllers(req, t, action): values.update(ctlr.get_ticket_changes(req, t, action)) t.populate(values) for manipulator in self.ticket_manipulators: for field, message in manipulator.validate_ticket(req, t): valid = False if field: add_warning( req, tag_( "The ticket field %(field)s is " "invalid: %(message)s", field=tag.strong(field), message=message)) else: add_warning(req, message) tickets.append(t) if not valid: return when = datetime_now(utc) with self.env.db_transaction: for t in tickets: t.save_changes(req.authname, comment, when=when) for ctlr in self._get_action_controllers(req, t, action): ctlr.apply_action_side_effects(req, t, action) event = BatchTicketChangeEvent(selected_tickets, when, req.authname, comment, new_values, action) try: NotificationSystem(self.env).notify(event) except Exception as e: self.log.error( "Failure sending notification on ticket batch" "change: %s", exception_to_unicode(e)) add_warning( req, tag_( "The changes have been saved, but an error " "occurred while sending notifications: " "%(message)s", message=to_unicode(e)))
def render_admin_panel(self, req, category, page, path_info): req.perm.require('VERSIONCONTROL_ADMIN') # Retrieve info for all repositories rm = RepositoryManager(self.env) all_repos = rm.get_all_repositories() db_provider = self.env[DbRepositoryProvider] if path_info: # Detail view reponame = path_info if not is_default(path_info) else '' info = all_repos.get(reponame) if info is None: raise TracError( _("Repository '%(repo)s' not found", repo=path_info)) if req.method == 'POST': if req.args.get('cancel'): req.redirect(req.href.admin(category, page)) elif db_provider and req.args.get('save'): # Modify repository changes = {} for field in db_provider.repository_attrs: value = normalize_whitespace(req.args.get(field)) if (value is not None or field == 'hidden') \ and value != info.get(field): changes[field] = value if 'dir' in changes \ and not self._check_dir(req, changes['dir']): changes = {} if changes: db_provider.modify_repository(reponame, changes) add_notice(req, _('Your changes have been saved.')) name = req.args.get('name') resync = tag.tt('trac-admin $ENV repository resync "%s"' % (name or '(default)')) if 'dir' in changes: msg = tag_( 'You should now run %(resync)s to ' 'synchronize Trac with the repository.', resync=resync) add_notice(req, msg) elif 'type' in changes: msg = tag_( 'You may have to run %(resync)s to ' 'synchronize Trac with the repository.', resync=resync) add_notice(req, msg) if name and name != path_info and not 'alias' in info: cset_added = tag.tt('trac-admin $ENV changeset ' 'added "%s" $REV' % (name or '(default)')) msg = tag_( 'You will need to update your post-commit ' 'hook to call %(cset_added)s with the new ' 'repository name.', cset_added=cset_added) add_notice(req, msg) if changes: req.redirect(req.href.admin(category, page)) Chrome(self.env).add_wiki_toolbars(req) data = {'view': 'detail', 'reponame': reponame} else: # List view if req.method == 'POST': # Add a repository if db_provider and req.args.get('add_repos'): name = req.args.get('name') type_ = req.args.get('type') # Avoid errors when copy/pasting paths dir = normalize_whitespace(req.args.get('dir', '')) if name is None or type_ is None or not dir: add_warning( req, _('Missing arguments to add a ' 'repository.')) elif self._check_dir(req, dir): db_provider.add_repository(name, dir, type_) name = name or '(default)' add_notice( req, _('The repository "%(name)s" has been ' 'added.', name=name)) resync = tag.tt('trac-admin $ENV repository resync ' '"%s"' % name) msg = tag_( 'You should now run %(resync)s to ' 'synchronize Trac with the repository.', resync=resync) add_notice(req, msg) cset_added = tag.tt('trac-admin $ENV changeset ' 'added "%s" $REV' % name) msg = tag_( 'You should also set up a post-commit hook ' 'on the repository to call %(cset_added)s ' 'for each committed changeset.', cset_added=cset_added) add_notice(req, msg) req.redirect(req.href.admin(category, page)) # Add a repository alias elif db_provider and req.args.get('add_alias'): name = req.args.get('name') alias = req.args.get('alias') if name is not None and alias is not None: db_provider.add_alias(name, alias) add_notice( req, _('The alias "%(name)s" has been ' 'added.', name=name or '(default)')) req.redirect(req.href.admin(category, page)) add_warning(req, _('Missing arguments to add an ' 'alias.')) # Refresh the list of repositories elif req.args.get('refresh'): req.redirect(req.href.admin(category, page)) # Remove repositories elif db_provider and req.args.get('remove'): sel = req.args.getlist('sel') if sel: for name in sel: db_provider.remove_repository(name) add_notice( req, _('The selected repositories have ' 'been removed.')) req.redirect(req.href.admin(category, page)) add_warning(req, _('No repositories were selected.')) data = {'view': 'list'} # Find repositories that are editable db_repos = {} if db_provider is not None: db_repos = dict(db_provider.get_repositories()) # Prepare common rendering data repositories = dict( (reponame, self._extend_info(reponame, info.copy(), reponame in db_repos)) for (reponame, info) in all_repos.iteritems()) types = sorted([''] + rm.get_supported_types()) data.update({ 'types': types, 'default_type': rm.repository_type, 'repositories': repositories }) return 'admin_repositories.html', data
def process_request(self, req): realm = req.args['realm'] id_ = req.args['id'] if not which(self.dot_path): raise TracError( _("Path to dot executable is invalid: %(path)s", path=self.dot_path)) # Urls to generate the depgraph for a ticket is /depgraph/ticketnum # Urls to generate the depgraph for a milestone is # /depgraph/milestone/milestone_name # List of tickets to generate the depgraph. if realm == 'milestone': # We need to query the list of tickets in the milestone query = Query(self.env, constraints={'milestone': [id_]}, max=0) tkt_ids = [fields['id'] for fields in query.execute(req)] else: tid = as_int(id_, None) if tid is None: raise TracError( tag_("%(id)s is not a valid ticket id.", id=html.tt(id_))) tkt_ids = [tid] # The summary argument defines whether we place the ticket id or # its summary in the node's label label_summary = 0 if 'summary' in req.args: label_summary = int(req.args.get('summary')) g = self._build_graph(req, tkt_ids, label_summary=label_summary) if req.path_info.endswith('/depgraph.png') or 'format' in req.args: format_ = req.args.get('format') if format_ == 'text': # In case g.__str__ returns unicode, convert it in ascii req.send( to_unicode(g).encode('ascii', 'replace'), 'text/plain') elif format_ == 'debug': import pprint req.send( pprint.pformat( [TicketLinks(self.env, tkt_id) for tkt_id in tkt_ids]), 'text/plain') elif format_ is not None: if format_ in self.acceptable_formats: req.send(g.render(self.dot_path, format_), 'text/plain') else: raise TracError( _("The %(format)s format is not allowed.", format=format_)) if self.use_gs: ps = g.render(self.dot_path, 'ps2') gs = subprocess.Popen([ self.gs_path, '-q', '-dTextAlphaBits=4', '-dGraphicsAlphaBits=4', '-sDEVICE=png16m', '-sOutputFile=%stdout%', '-' ], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) img, err = gs.communicate(ps) if err: self.log.debug('MasterTickets: Error from gs: %s', err) else: img = g.render(self.dot_path) req.send(img, 'image/png') else: data = {} # Add a context link to enable/disable labels in nodes. if label_summary: add_ctxtnav(req, 'Without labels', req.href(req.path_info, summary=0)) else: add_ctxtnav(req, 'With labels', req.href(req.path_info, summary=1)) if realm == 'milestone': add_ctxtnav(req, 'Back to Milestone: %s' % id_, req.href.milestone(id_)) data['milestone'] = id_ else: data['ticket'] = id_ add_ctxtnav(req, 'Back to Ticket #%s' % id_, req.href.ticket(id_)) data['graph'] = g data['graph_render'] = functools.partial(g.render, self.dot_path) data['use_gs'] = self.use_gs return 'depgraph.html', data, None
def expand_macro(self, formatter, name, content): req = formatter.req query_string, kwargs, format = self.parse_args(content) if query_string: query_string += '&' query_string += '&'.join('%s=%s' % item for item in kwargs.iteritems()) env = ProductEnvironment.lookup_global_env(self.env) query = ProductQuery.from_string(env, query_string) if format == 'count': cnt = query.count(req) return tag.span(cnt, title='%d tickets for which %s' % (cnt, query_string), class_='query_count') tickets = query.execute(req) if format == 'table': data = query.template_data(formatter.context, tickets, req=formatter.context.req) add_stylesheet(req, 'common/css/report.css') return Chrome(env).render_template( req, 'query_results.html', data, None, fragment=True) if format == 'progress': from trac.ticket.roadmap import (RoadmapModule, apply_ticket_permissions, get_ticket_stats, grouped_stats_data) add_stylesheet(req, 'common/css/roadmap.css') def query_href(extra_args, group_value = None): q = ProductQuery.from_string(env, query_string) if q.group: extra_args[q.group] = group_value q.group = None for constraint in q.constraints: constraint.update(extra_args) if not q.constraints: q.constraints.append(extra_args) return q.get_href(formatter.context) chrome = Chrome(env) tickets = apply_ticket_permissions(env, req, tickets) stats_provider = RoadmapModule(env).stats_provider by = query.group if not by: stat = get_ticket_stats(stats_provider, tickets) data = { 'stats': stat, 'stats_href': query_href(stat.qry_args), 'interval_hrefs': [query_href(interval['qry_args']) for interval in stat.intervals], 'legend': True, } return tag.div( chrome.render_template(req, 'progress_bar.html', data, None, fragment=True), class_='trac-progress') def per_group_stats_data(gstat, group_name): return { 'stats': gstat, 'stats_href': query_href(gstat.qry_args, group_name), 'interval_hrefs': [query_href(interval['qry_args'], group_name) for interval in gstat.intervals], 'percent': '%d / %d' % (gstat.done_count, gstat.count), 'legend': False, } groups = grouped_stats_data(env, stats_provider, tickets, by, per_group_stats_data) data = { 'groups': groups, 'grouped_by': by, 'summary': _("Ticket completion status for each %(group)s", group=by), } return tag.div( chrome.render_template(req, 'progress_bar_grouped.html', data, None, fragment=True), class_='trac-groupprogress') # Formats above had their own permission checks, here we need to # do it explicitly: tickets = [t for t in tickets if 'TICKET_VIEW' in req.perm('ticket', t['id'])] if not tickets: return tag.span(_("No results"), class_='query_no_results') # Cache resolved href targets hrefcache = {} def ticket_anchor(ticket): try: pvalue = ticket.get('product') or GLOBAL_PRODUCT envhref = hrefcache[pvalue] except KeyError: try: env = lookup_product_env(self.env, prefix= pvalue, name=pvalue) except LookupError: return tag.a('#%s' % ticket['id'], class_='missing product') hrefcache[pvalue] = envhref = resolve_product_href( to_env=env, at_env=self.env) return tag.a('#%s' % ticket['id'], class_=ticket['status'], href=envhref.ticket(int(ticket['id'])), title=shorten_line(ticket['summary'])) def ticket_groups(): groups = [] for v, g in groupby(tickets, lambda t: t[query.group]): q = ProductQuery.from_string(env, query_string) # produce the hint for the group q.group = q.groupdesc = None order = q.order q.order = None title = _("%(groupvalue)s %(groupname)s tickets matching " "%(query)s", groupvalue=v, groupname=query.group, query=q.to_string()) # produce the href for the query corresponding to the group for constraint in q.constraints: constraint[str(query.group)] = v q.order = order href = q.get_href(formatter.context) groups.append((v, [t for t in g], href, title)) return groups if format == 'compact': if query.group: groups = [(v, ' ', tag.a('#%s' % u',\u200b'.join(str(t['id']) for t in g), href=href, class_='query', title=title)) for v, g, href, title in ticket_groups()] return tag(groups[0], [(', ', g) for g in groups[1:]]) else: alist = [ticket_anchor(ticket) for ticket in tickets] return tag.span(alist[0], *[(', ', a) for a in alist[1:]]) else: if query.group: return tag.div( [(tag.p(tag_('%(groupvalue)s %(groupname)s tickets:', groupvalue=tag.a(v, href=href, class_='query', title=title), groupname=query.group)), tag.dl([(tag.dt(ticket_anchor(t)), tag.dd(t['summary'])) for t in g], class_='wiki compact')) for v, g, href, title in ticket_groups()]) else: return tag.div(tag.dl([(tag.dt(ticket_anchor(ticket)), tag.dd(ticket['summary'])) for ticket in tickets], class_='wiki compact'))
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'] ticket_owner = ticket._old.get('owner', ticket['owner']) ticket_status = ticket._old.get('status', ticket['status']) 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(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 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 != 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: control.append(tag_("as %(status)s", status=ticket_status)) if len(operations) == 1: 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 not operations: if status != '*': if ticket['status'] is None: hints.append(tag_("The status will be '%(name)s'", name=status)) else: 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, content, args=None): if content is not None: content = content.strip() if not args and not content: raw_actions = self.config.options('ticket-workflow') else: is_macro = args is None if is_macro: kwargs = parse_args(content)[1] file = kwargs.get('file') else: file = args.get('file') if not file and not content: raise ProcessorError("Invalid argument(s).") if file: print(file) text = RepositoryManager(self.env).read_file_by_path(file) if text is None: raise ProcessorError( tag_("The file %(file)s does not exist.", file=tag.code(file))) elif is_macro: text = '\n'.join(line.lstrip() for line in content.split(';')) else: text = content if '[ticket-workflow]' not in text: text = '[ticket-workflow]\n' + text parser = RawConfigParser() try: parser.readfp(io.StringIO(text)) except ParsingError as e: if is_macro: raise MacroError(exception_to_unicode(e)) else: raise ProcessorError(exception_to_unicode(e)) raw_actions = list(parser.items('ticket-workflow')) actions = parse_workflow_config(raw_actions) states = list( {state for action in actions.itervalues() for state in action['oldstates']} | {action['newstate'] for action in actions.itervalues()}) action_labels = [attrs['label'] for attrs in actions.values()] action_names = list(actions) edges = [] for name, action in actions.items(): new_index = states.index(action['newstate']) name_index = action_names.index(name) for old_state in action['oldstates']: old_index = states.index(old_state) edges.append((old_index, new_index, name_index)) args = args or {} width = args.get('width', 800) height = args.get('height', 600) graph = {'nodes': states, 'actions': action_labels, 'edges': edges, 'width': width, 'height': height} graph_id = '%012x' % id(graph) req = formatter.req add_script(req, 'common/js/excanvas.js', ie_if='IE') add_script(req, 'common/js/workflow_graph.js') add_script_data(req, {'graph_%s' % graph_id: graph}) return tag( tag.div('', class_='trac-workflow-graph trac-noscript', id='trac-workflow-graph-%s' % graph_id, style="display:inline-block;width:%spx;height:%spx" % (width, height)), tag.noscript( tag.div(_("Enable JavaScript to display the workflow graph."), class_='system-message')))
def render_property_diff(self, name, old_context, old_props, new_context, new_props, options): # Build 5 columns table showing modifications on merge sources # || source || added || removed || added (ni) || removed (ni) || # || source || removed || rm = RepositoryManager(self.env) repos = rm.get_repository(old_context.resource.parent.id) def parse_sources(props): sources = {} value = props[name] lines = value.splitlines() if name == 'svn:mergeinfo' \ else value.split() for line in lines: path, revs = line.split(':', 1) spath = _path_within_scope(repos.scope, path) if spath is not None: inheritable, non_inheritable = _partition_inheritable(revs) sources[spath] = (set(Ranges(inheritable)), set(Ranges(non_inheritable))) return sources old_sources = parse_sources(old_props) new_sources = parse_sources(new_props) # Go through new sources, detect modified ones or added ones blocked = name.endswith('blocked') added_label = [_("merged: "), _("blocked: ")][blocked] removed_label = [_("reverse-merged: "), _("un-blocked: ")][blocked] added_ni_label = _("marked as non-inheritable: ") removed_ni_label = _("unmarked as non-inheritable: ") sources = [] changed_revs = {} changed_nodes = [] for spath, (new_revs, new_revs_ni) in new_sources.iteritems(): new_spath = spath not in old_sources if new_spath: old_revs = old_revs_ni = set() else: old_revs, old_revs_ni = old_sources.pop(spath) added = new_revs - old_revs removed = old_revs - new_revs # unless new revisions differ from old revisions if not added and not removed: continue added_ni = new_revs_ni - old_revs_ni removed_ni = old_revs_ni - new_revs_ni revs = sorted(added | removed | added_ni | removed_ni) try: node = repos.get_node(spath, revs[-1]) changed_nodes.append((node, revs[0])) except NoSuchNode: pass sources.append( (spath, new_spath, added, removed, added_ni, removed_ni)) if changed_nodes: changed_revs = repos._get_changed_revs(changed_nodes) def revs_link(revs, context): if revs: revs = to_ranges(revs) return _get_revs_link(revs.replace(',', u',\u200b'), context, spath, revs) modified_sources = [] for spath, new_spath, added, removed, added_ni, removed_ni in sources: if spath in changed_revs: revs = set(changed_revs[spath]) added &= revs removed &= revs added_ni &= revs removed_ni &= revs if added or removed: if new_spath: status = _(" (added)") else: status = None modified_sources.append( (spath, [_get_source_link(spath, new_context), status], added and tag(added_label, revs_link(added, new_context)), removed and tag(removed_label, revs_link(removed, old_context)), added_ni and tag(added_ni_label, revs_link(added_ni, new_context)), removed_ni and tag(removed_ni_label, revs_link(removed_ni, old_context)))) # Go through remaining old sources, those were deleted removed_sources = [] for spath, old_revs in old_sources.iteritems(): removed_sources.append( (spath, _get_source_link(spath, old_context))) if modified_sources or removed_sources: modified_sources.sort() removed_sources.sort() changes = tag.table(tag.tbody([ tag.tr(tag.td(c) for c in cols[1:]) for cols in modified_sources ], [ tag.tr(tag.td(src), tag.td(_('removed'), colspan=4)) for spath, src in removed_sources ]), class_='props') else: changes = tag.em(_(' (with no actual effect on merging)')) return tag.li(tag_('Property %(prop)s changed', prop=tag.strong(name)), changes)
def render_admin_panel(self, req, category, page, path_info): # Retrieve info for all repositories rm = RepositoryManager(self.env) all_repos = rm.get_all_repositories() db_provider = self.env[DbRepositoryProvider] if path_info: # Detail view reponame = path_info if not is_default(path_info) else '' info = all_repos.get(reponame) if info is None: raise TracError( _("Repository '%(repo)s' not found", repo=path_info)) if req.method == 'POST': if req.args.get('cancel'): req.redirect(req.href.admin(category, page)) elif db_provider and req.args.get('save'): # Modify repository changes = {} valid = True for field in db_provider.repository_attrs: value = normalize_whitespace(req.args.get(field)) if (value is not None or field in ('hidden', 'sync_per_request')) \ and value != info.get(field): changes[field] = value if 'dir' in changes and not \ self._check_dir(req, changes['dir']): valid = False if valid and changes: db_provider.modify_repository(reponame, changes) add_notice(req, _('Your changes have been saved.')) name = req.args.get('name') pretty_name = name or '(default)' resync = tag.code('trac-admin "%s" repository resync ' '"%s"' % (self.env.path, pretty_name)) if 'dir' in changes: msg = tag_( 'You should now run %(resync)s to ' 'synchronize Trac with the repository.', resync=resync) add_notice(req, msg) elif 'type' in changes: msg = tag_( 'You may have to run %(resync)s to ' 'synchronize Trac with the repository.', resync=resync) add_notice(req, msg) if name and name != path_info and 'alias' not in info: cset_added = tag.code('trac-admin "%s" changeset ' 'added "%s" $REV' % (self.env.path, pretty_name)) msg = tag_( 'You will need to update your ' 'post-commit hook to call ' '%(cset_added)s with the new ' 'repository name.', cset_added=cset_added) add_notice(req, msg) if valid: req.redirect(req.href.admin(category, page)) chrome = Chrome(self.env) chrome.add_wiki_toolbars(req) chrome.add_auto_preview(req) data = {'view': 'detail', 'reponame': reponame} else: # List view if req.method == 'POST': # Add a repository if db_provider and req.args.get('add_repos'): name = req.args.get('name') pretty_name = name or '(default)' if name in all_repos: raise TracError( _('The repository "%(name)s" already ' 'exists.', name=pretty_name)) type_ = req.args.get('type') # Avoid errors when copy/pasting paths dir = normalize_whitespace(req.args.get('dir', '')) if name is None or type_ is None or not dir: add_warning( req, _('Missing arguments to add a ' 'repository.')) elif self._check_dir(req, dir): db_provider.add_repository(name, dir, type_) add_notice( req, _('The repository "%(name)s" has been ' 'added.', name=pretty_name)) resync = tag.code('trac-admin "%s" repository resync ' '"%s"' % (self.env.path, pretty_name)) msg = tag_( 'You should now run %(resync)s to ' 'synchronize Trac with the repository.', resync=resync) add_notice(req, msg) cset_added = tag.code('trac-admin "%s" changeset ' 'added "%s" $REV' % (self.env.path, pretty_name)) doc = tag.a(_("documentation"), href=req.href.wiki('TracRepositoryAdmin') + '#Synchronization') msg = tag_( 'You should also set up a post-commit hook ' 'on the repository to call %(cset_added)s ' 'for each committed changeset. See the ' '%(doc)s for more information.', cset_added=cset_added, doc=doc) add_notice(req, msg) # Add a repository alias elif db_provider and req.args.get('add_alias'): name = req.args.get('name') pretty_name = name or '(default)' alias = req.args.get('alias') if name is not None and alias is not None: try: db_provider.add_alias(name, alias) except self.env.db_exc.IntegrityError: raise TracError( _('The alias "%(name)s" already ' 'exists.', name=pretty_name)) add_notice( req, _('The alias "%(name)s" has been ' 'added.', name=pretty_name)) else: add_warning(req, _('Missing arguments to add an ' 'alias.')) # Refresh the list of repositories elif req.args.get('refresh'): pass # Remove repositories elif db_provider and req.args.get('remove'): sel = req.args.getlist('sel') if sel: for name in sel: db_provider.remove_repository(name) add_notice( req, _('The selected repositories have ' 'been removed.')) else: add_warning(req, _('No repositories were selected.')) req.redirect(req.href.admin(category, page)) data = {'view': 'list'} # Find repositories that are editable db_repos = {} if db_provider is not None: db_repos = dict(db_provider.get_repositories()) # Prepare common rendering data repositories = { reponame: self._extend_info(reponame, info.copy(), reponame in db_repos) for (reponame, info) in all_repos.iteritems() } types = sorted([''] + rm.get_supported_types()) data.update({ 'types': types, 'default_type': rm.default_repository_type, 'repositories': repositories, 'can_add_alias': any('alias' not in info for info in repositories.itervalues()) }) return 'admin_repositories.html', data
def _render_view(self, req, id): """Retrieve the report results and pre-process them for rendering.""" r = Report(self.env, id) title, description, sql = r.title, r.description, r.query # 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 report_id not 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:'): from trac.ticket.query import Query, QuerySyntaxError try: query = Query.from_string(self.env, query[6:], report=id) except QuerySyntaxError as e: req.redirect(req.href.report(id, action='edit', error=to_unicode(e))) else: req.redirect(query.get_href(req.href)) format = req.args.get('format') if format == 'sql': self._send_sql(req, id, title, description, sql) title = '{%i} %s' % (id, title) report_resource = Resource(self.realm, id) req.perm(report_resource).require('REPORT_VIEW') context = web_context(req, report_resource) page = req.args.getint('page', 1) default_max = {'rss': self.items_per_page_rss, 'csv': 0, 'tab': 0}.get(format, self.items_per_page) max = req.args.getint('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.getint('asc', 0, min=0, max=1) args = {} 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 if page != 1: params['page'] = page if max != default_max: params['max'] = max params.update(kwargs) params['asc'] = 1 if params.get('asc', asc) else None return req.href.report(id, params) data = {'action': 'view', 'report': {'id': id, 'resource': report_resource}, 'context': context, 'title': title, 'description': description, 'max': limit, 'args': args, 'show_args_form': False, 'message': None, 'paginator': None, 'report_href': report_href} try: args = self.get_var_args(req) sql = self.get_default_var_args(args, sql) except ValueError as e: data['message'] = _("Report failed: %(error)s", error=e) return 'report_view.html', data, None data.update({'args': args, 'title': sub_vars(title, args), 'description': sub_vars(description or '', args)}) try: res = self.execute_paginated_report(req, id, sql, args, limit, offset) except TracError as e: data['message'] = _("Report failed: %(error)s", error=e) else: 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"))) if data['message']: 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: if asc: data['asc'] = asc data['sort'] = sort_col header['asc'] = bool(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 = TicketSystem.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(self.realm, 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']) - {'USER'}: data['show_args_form'] = True # Add values of all select-type ticket fields for autocomplete. fields = TicketSystem(self.env).get_ticket_fields() arg_values = {} for arg in set(data['args']) - {'USER'}: attrs = fields.by_name(arg.lower()) if attrs and 'options' in attrs: arg_values[attrs['name']] = attrs['options'] if arg_values: add_script_data(req, arg_values=arg_values) Chrome(self.env).add_jquery_ui(req) if missing_args: add_warning(req, _( 'The following arguments are missing: %(args)s', args=", ".join(missing_args))) return 'report_view.html', data, None