def _test_edit_ticket_comment(self, commenter, editor): ticket = self._insert_ticket(commenter) ticket.save_changes(commenter, comment='The comment') comment_resource = Resource('comment', 1, parent=ticket.resource) perm_cache = PermissionCache(self.env, editor, comment_resource) return perm_cache, comment_resource
def resource(self): return Resource(self.realm, self.name, self._resource_version)
def test_describe_tagged_resource(self): resource = Resource('ticket', 1) self.assertEquals( self.provider.describe_tagged_resource(self.req, resource), 'defect: summary')
def expand_macro(self, formatter, name, content): env = self.env req = formatter.req args, kw = parse_args(content) # Use macro arguments (most likely wiki macro calls). realms = 'realm' in kw and kw['realm'].split('|') or [] tag_system = TagSystem(env) all_realms = [p.get_taggable_realm() for p in tag_system.tag_providers] self.all_realms = all_realms self.realms = realms if name == 'TagCloud': args.append(' or '.join(['realm:%s' % r for r in realms])) all_tags = tag_system.get_all_tags(req, ' '.join(args)) mincount = 'mincount' in kw and kw['mincount'] or None return self.render_cloud(req, all_tags, caseless_sort=self.caseless_sort, mincount=mincount) elif name == 'ListTagged': if _OBSOLETE_ARGS_RE.search(content): data = {'warning': 'obsolete_args'} else: data = {'warning': None} context = formatter.context # Use TagsQuery arguments (most likely wiki macro calls). cols = 'cols' in kw and kw['cols'] or self.default_cols format = 'format' in kw and kw['format'] or self.default_format query = args and args[0].strip() or None if query and not realms: # First read query arguments (most likely a web-UI call). for realm in all_realms: if re.search('(^|\W)realm:%s(\W|$)' % (realm), query): realms = realms and realms.append(realm) or [realm] if not realms: # Apply ListTagged defaults to macro call w/o realm. realms = list(set(all_realms) - set(self.exclude_realms)) if not realms: return '' else: self.query = query self.realms = realms query = '(%s) (%s)' % (query or '', ' or '.join( ['realm:%s' % (r) for r in realms])) env.log.debug('LISTTAGGED_QUERY: ' + query) query_result = tag_system.query(req, query) if not query_result: return '' def _link(resource): if resource.realm == 'tag': # Keep realm selection in tag links. return builder.a(resource.id, href=self.get_href(req, tag=resource)) elif resource.realm == 'ticket': # Return resource link including ticket status dependend # class to allow for common Trac ticket link style. ticket = Ticket(env, resource.id) return builder.a('#%s' % ticket.id, class_=ticket['status'], href=formatter.href.ticket(ticket.id), title=shorten_line(ticket['summary'])) return render_resource_link(env, context, resource, 'compact') if format == 'table': cols = [ col for col in cols.split('|') if col in self.supported_cols ] # Use available translations from Trac core. try: labels = TicketSystem(env).get_ticket_field_labels() labels['id'] = _('Id') except AttributeError: # Trac 0.11 neither has the attribute nor uses i18n. labels = {'id': 'Id', 'description': 'Description'} labels['realm'] = _('Realm') labels['tags'] = _('Tags') headers = [{'label': labels.get(col)} for col in cols] data.update({'cols': cols, 'headers': headers}) results = sorted(query_result, key=lambda r: \ embedded_numbers(to_unicode(r[0].id))) results = self._paginate(req, results) rows = [] for resource, tags in results: desc = tag_system.describe_tagged_resource(req, resource) tags = sorted(tags) if tags: rendered_tags = [ _link(Resource('tag', tag)) for tag in tags ] if 'oldlist' == format: resource_link = _link(resource) else: desc = desc or \ get_resource_description(env, resource, context=context) resource_link = builder.a(desc, href=get_resource_url( env, resource, context.href)) if 'table' == format: cells = [] for col in cols: if col == 'id': cells.append(_link(resource)) # Don't duplicate links to resource in both. elif col == 'description' and 'id' in cols: cells.append(desc) elif col == 'description': cells.append(resource_link) elif col == 'realm': cells.append(resource.realm) elif col == 'tags': cells.append( builder([(tag, ' ') for tag in rendered_tags])) rows.append({'cells': cells}) continue rows.append({ 'desc': desc, 'rendered_tags': None, 'resource_link': _link(resource) }) data.update({ 'format': format, 'paginator': results, 'results': rows, 'tags_url': req.href('tags') }) # Work around a bug in trac/templates/layout.html, that causes a # TypeError for the wiki macro call, if we use add_link() alone. add_stylesheet(req, 'common/css/search.css') return Chrome(env).render_template(req, 'listtagged_results.html', data, 'text/html', True)
def process_request(self, req): req.perm.require('TAGS_VIEW') match = re.match(r'/tags/?(.*)', req.path_info) tag_id = match.group(1) and match.group(1) or None query = req.args.get('q', '') # Consider only providers, that are permitted for display. realms = [p.get_taggable_realm() for p in self.tag_providers if (not hasattr(p, 'check_permission') or \ p.check_permission(req.perm, 'view'))] if not (tag_id or query) or [r for r in realms if r in req.args] == []: for realm in realms: if not realm in self.exclude_realms: req.args[realm] = 'on' checked_realms = [r for r in realms if r in req.args] if query: # Add permitted realms from query expression. checked_realms.extend(query_realms(query, realms)) realm_args = dict( zip([r for r in checked_realms], ['on' for r in checked_realms])) # Switch between single tag and tag query expression mode. if tag_id and not re.match(r"""(['"]?)(\S+)\1$""", tag_id, re.UNICODE): # Convert complex, invalid tag ID's --> query expression. req.redirect(req.href.tags(realm_args, q=tag_id)) elif query: single_page = re.match(r"""(['"]?)(\S+)\1$""", query, re.UNICODE) if single_page: # Convert simple query --> single tag. req.redirect(req.href.tags(single_page.group(2), realm_args)) data = dict(page_title=_("Tags"), checked_realms=checked_realms) # Populate the TagsQuery form field. data['tag_query'] = tag_id and tag_id or query data['tag_realms'] = list( dict(name=realm, checked=realm in checked_realms) for realm in realms) if tag_id: page_name = tag_id page = WikiPage(self.env, page_name) data['tag_page'] = page macros = TagWikiMacros(self.env) if query or tag_id: # TRANSLATOR: The meta-nav link label. add_ctxtnav(req, _("Back to Cloud"), req.href.tags()) macro = 'ListTagged' args = "%s,format=%s,cols=%s" % \ (tag_id and tag_id or query, self.default_format, self.default_cols) data['mincount'] = None else: macro = 'TagCloud' mincount = as_int(req.args.get('mincount', None), self.cloud_mincount) args = mincount and "mincount=%s" % mincount or None data['mincount'] = mincount formatter = Formatter(self.env, Context.from_request(req, Resource('tag'))) self.env.log.debug("Tag macro arguments: %s", args and args or '(none)') try: # Query string without realm throws 'NotImplementedError'. data['tag_body'] = checked_realms and \ macros.expand_macro(formatter, macro, args, realms=checked_realms) \ or '' except InvalidQuery, e: data['tag_query_error'] = to_unicode(e) data['tag_body'] = macros.expand_macro(formatter, 'TagCloud', '')
def test_actor_has_email_view_for_resource_negative(self): format_author = Chrome(self.env).format_author req = MockRequest(self.env, authname='user2') resource = Resource('wiki', 'TracGuide') author = format_author(req, '*****@*****.**', resource) self.assertEqual(u'user@\u2026', author)
def resource(self): return Resource('milestone', self.name) ### .version !!!
def test_action_revoked(self): resource = Resource('wiki', 'RestrictedPage') self.assertEquals( self.check('WIKI_VIEW', 'anonymous', resource, PermissionCache(self.env)), False)
def process_request(self, req): tid = req.args.get('id') if not tid: raise TracError(_('No ticket id provided.')) try: ticket = Ticket(self.env, tid) except ValueError: raise TracError(_('Invalid ticket id.')) # For access to the relation management, TICKET_MODIFY is required. req.perm.require('TICKET_MODIFY') relsys = RelationsSystem(self.env) data = { 'relation': {}, } if req.method == 'POST': # for modifying the relations TICKET_MODIFY is required for # both the source and the destination tickets if 'remove' in req.args: rellist = req.args.get('sel') if rellist: if isinstance(rellist, basestring): rellist = [ rellist, ] self.remove_relations(req, rellist) elif 'add' in req.args: relation = dict( destination=req.args.get('dest_tid', ''), type=req.args.get('reltype', ''), comment=req.args.get('comment', ''), ) try: trs = TicketRelationsSpecifics(self.env) dest_ticket = trs.find_ticket(relation['destination']) except NoSuchTicketError: data['error'] = _('Invalid ticket ID.') else: req.perm.require('TICKET_MODIFY', Resource(dest_ticket.id)) try: dbrel = relsys.add(ticket, dest_ticket, relation['type'], relation['comment'], req.authname) except NoSuchTicketError: data['error'] = _('Invalid ticket ID.') except UnknownRelationType: data['error'] = _('Unknown relation type.') except ValidationError as ex: data['error'] = ex.message else: # Notify try: self.notify_relation_changed(dbrel) except Exception, e: self.log.error( "Failure sending notification on" "creation of relation: %s", exception_to_unicode(e)) add_warning( req, _( "The relation has been added, " "but an error occurred while " "sending notifications: " "%(message)s", message=to_unicode(e))) if 'error' in data: data['relation'] = relation else: raise TracError(_('Invalid operation.'))
def test_set_tags(self): resource = Resource('wiki', 'WikiStart') tags = ['tag1'] self.req.perm = PermissionCache(self.env, username='******') # Shouldn't raise an error with appropriate permission. self.tag_s.set_tags(self.req, resource, tags)
def test_action_granted(self): resource = Resource('wiki', 'PublicPage') self.assertEquals( self.check('WIKI_MODIFY', 'anonymous', resource, PermissionCache(self.env)), True)
def _do_actions(self, context, actions): for action in actions: if action == 'get-file': context.req.perm.require('DOWNLOADS_VIEW') # Get request arguments. download_id = context.req.args.get('id') or 0 download_file = context.req.args.get('file') # Get download. if download_id: download = self.get_download(context, download_id) else: download = self.get_download_by_file( context, download_file) # Check if requested download exists. if not download: raise TracError('File not found.') # Check resource based permission. context.req.perm.require('DOWNLOADS_VIEW', Resource('downloads', download['id'])) # Get download file path. path = os.path.normpath( os.path.join(self.path, to_unicode(download['id']), download['file'])) self.log.debug('path: %s' % (path, )) # Increase downloads count. new_download = {'count': download['count'] + 1} # Edit download. self.edit_download(context, download['id'], new_download) # Notify change listeners. for listener in self.change_listeners: listener.download_changed(context, new_download, download) # Commit DB before file send. db = self.env.get_db_cnx() db.commit() # Guess mime type. file = open(path.encode('utf-8'), "r") file_data = file.read(1000) file.close() mimeview = Mimeview(self.env) mime_type = mimeview.get_mimetype(path, file_data) if not mime_type: mime_type = 'application/octet-stream' if 'charset=' not in mime_type: charset = mimeview.get_charset(file_data, mime_type) mime_type = mime_type + '; charset=' + charset # Return uploaded file to request. context.req.send_header( 'Content-Disposition', 'attachment;filename="%s"' % (os.path.normpath(download['file']))) context.req.send_header('Content-Description', download['description']) context.req.send_file(path.encode('utf-8'), mime_type) elif action == 'downloads-list': context.req.perm.require('DOWNLOADS_VIEW') self.log.debug('visible_fields: %s' % (self.visible_fields, )) # Get form values. order = context.req.args.get('order') or self.download_sort if context.req.args.has_key('desc'): desc = context.req.args.get('desc') == '1' else: desc = self.download_sort_direction == 'desc' self.data['order'] = order self.data['desc'] = desc self.data['has_tags'] = self.env.is_component_enabled( 'tractags.api.TagEngine') self.data['visible_fields'] = self.visible_fields self.data['title'] = self.title self.data['description'] = self.get_description(context) self.data['downloads'] = self.get_downloads( context, order, desc) self.data['visible_fields'] = [ visible_field for visible_field in self.visible_fields ] # Component, versions, etc. are needed only for new download # add form. if context.req.perm.has_permission('DOWNLOADS_ADD'): self.data['components'] = self.get_components(context) self.data['versions'] = self.get_versions(context) self.data['architectures'] = self.get_architectures( context) self.data['platforms'] = self.get_platforms(context) self.data['types'] = self.get_types(context) elif action == 'admin-downloads-list': context.req.perm.require('DOWNLOADS_ADMIN') # Get form values order = context.req.args.get('order') or self.download_sort if context.req.args.has_key('desc'): desc = context.req.args.get('desc') == '1' else: desc = self.download_sort_direction == 'desc' download_id = int(context.req.args.get('download') or 0) self.data['order'] = order self.data['desc'] = desc self.data['has_tags'] = self.env.is_component_enabled( 'tractags.api.TagEngine') self.data['download'] = self.get_download(context, download_id) self.data['downloads'] = self.get_downloads( context, order, desc) self.data['components'] = self.get_components(context) self.data['versions'] = self.get_versions(context) self.data['architectures'] = self.get_architectures(context) self.data['platforms'] = self.get_platforms(context) self.data['types'] = self.get_types(context) elif action == 'description-edit': context.req.perm.require('DOWNLOADS_ADMIN') elif action == 'description-post-edit': context.req.perm.require('DOWNLOADS_ADMIN') # Get form values. description = context.req.args.get('description') # Set new description. self.edit_description(context, description) elif action == 'downloads-post-add': context.req.perm.require('DOWNLOADS_ADD') # Get form values. file, filename, file_size = self._get_file_from_req(context) download = { 'file': filename, 'description': context.req.args.get('description'), 'size': file_size, 'time': to_timestamp(datetime.now(utc)), 'count': 0, 'author': context.req.authname, 'tags': context.req.args.get('tags'), 'component': context.req.args.get('component'), 'version': context.req.args.get('version'), 'architecture': context.req.args.get('architecture'), 'platform': context.req.args.get('platform'), 'type': context.req.args.get('type') } # Upload file to DB and file storage. self._add_download(context, download, file) # Close input file. file.close() elif action == 'downloads-post-edit': context.req.perm.require('DOWNLOADS_ADMIN') # Get form values. download_id = context.req.args.get('id') old_download = self.get_download(context, download_id) download = { 'description': context.req.args.get('description'), 'tags': context.req.args.get('tags'), 'component': context.req.args.get('component'), 'version': context.req.args.get('version'), 'architecture': context.req.args.get('architecture'), 'platform': context.req.args.get('platform'), 'type': context.req.args.get('type') } # Edit Download. self.edit_download(context, download_id, download) # Notify change listeners. for listener in self.change_listeners: listener.download_changed(context, download, old_download) elif action == 'downloads-delete': context.req.perm.require('DOWNLOADS_ADMIN') # Get selected downloads. selection = context.req.args.get('selection') if isinstance(selection, (str, unicode)): selection = [selection] # Delete download. if selection: for download_id in selection: download = self.get_download(context, download_id) self.log.debug('download: %s' % (download, )) self._delete_download(context, download) elif action == 'admin-architectures-list': context.req.perm.require('DOWNLOADS_ADMIN') # Get form values order = context.req.args.get('order') or self.architecture_sort if context.req.args.has_key('desc'): desc = context.req.args.get('desc') == '1' else: desc = self.architecture_sort_direction == 'desc' architecture_id = int( context.req.args.get('architecture') or 0) # Display architectures. self.data['order'] = order self.data['desc'] = desc self.data['architecture'] = self.get_architecture( context, architecture_id) self.data['architectures'] = self.get_architectures( context, order, desc) elif action == 'architectures-post-add': context.req.perm.require('DOWNLOADS_ADMIN') # Get form values. architecture = { 'name': context.req.args.get('name'), 'description': context.req.args.get('description') } # Add architecture. self.add_architecture(context, architecture) elif action == 'architectures-post-edit': context.req.perm.require('DOWNLOADS_ADMIN') # Get form values. architecture_id = context.req.args.get('id') architecture = { 'name': context.req.args.get('name'), 'description': context.req.args.get('description') } # Add architecture. self.edit_architecture(context, architecture_id, architecture) elif action == 'architectures-delete': context.req.perm.require('DOWNLOADS_ADMIN') # Get selected architectures. selection = context.req.args.get('selection') if isinstance(selection, (str, unicode)): selection = [selection] # Delete architectures. if selection: for architecture_id in selection: self.delete_architecture(context, architecture_id) elif action == 'admin-platforms-list': context.req.perm.require('DOWNLOADS_ADMIN') # Get form values. order = context.req.args.get('order') or self.platform_sort if context.req.args.has_key('desc'): desc = context.req.args.get('desc') == '1' else: desc = self.platform_sort_direction == 'desc' platform_id = int(context.req.args.get('platform') or 0) # Display platforms. self.data['order'] = order self.data['desc'] = desc self.data['platform'] = self.get_platform(context, platform_id) self.data['platforms'] = self.get_platforms( context, order, desc) elif action == 'platforms-post-add': context.req.perm.require('DOWNLOADS_ADMIN') # Get form values. platform = { 'name': context.req.args.get('name'), 'description': context.req.args.get('description') } # Add platform. self.add_platform(context, platform) elif action == 'platforms-post-edit': context.req.perm.require('DOWNLOADS_ADMIN') # Get form values. platform_id = context.req.args.get('id') platform = { 'name': context.req.args.get('name'), 'description': context.req.args.get('description') } # Add platform. self.edit_platform(context, platform_id, platform) elif action == 'platforms-delete': context.req.perm.require('DOWNLOADS_ADMIN') # Get selected platforms. selection = context.req.args.get('selection') if isinstance(selection, (str, unicode)): selection = [selection] # Delete platforms. if selection: for platform_id in selection: self.delete_platform(context, platform_id) elif action == 'admin-types-list': context.req.perm.require('DOWNLOADS_ADMIN') # Get form values order = context.req.args.get('order') or self.type_sort if context.req.args.has_key('desc'): desc = context.req.args.get('desc') == '1' else: desc = self.type_sort_direction == 'desc' platform_id = int(context.req.args.get('type') or 0) # Display platforms. self.data['order'] = order self.data['desc'] = desc self.data['type'] = self.get_type(context, platform_id) self.data['types'] = self.get_types(context, order, desc) elif action == 'types-post-add': context.req.perm.require('DOWNLOADS_ADMIN') # Get form values. type = { 'name': context.req.args.get('name'), 'description': context.req.args.get('description') } # Add type. self.add_type(context, type) elif action == 'types-post-edit': context.req.perm.require('DOWNLOADS_ADMIN') # Get form values. type_id = context.req.args.get('id') type = { 'name': context.req.args.get('name'), 'description': context.req.args.get('description') } # Add platform. self.edit_type(context, type_id, type) elif action == 'types-delete': context.req.perm.require('DOWNLOADS_ADMIN') # Get selected types. selection = context.req.args.get('selection') if isinstance(selection, (str, unicode)): selection = [selection] # Delete types. if selection: for type_id in selection: self.delete_type(context, type_id)
def expand_macro(self, formatter, name, content): args = None if content: content = stripws(content) # parse arguments # we expect the 1st argument to be a filename (filespec) args = [stripws(arg) for arg in self._split_args_re.split(content)[1::2]] if not args: return '' # strip unicode white-spaces and ZWSPs are copied from attachments # section (#10668) filespec = args.pop(0) # style information attr = {} style = {} link = '' # helper for the special case `source:` # from trac.versioncontrol.web_ui import BrowserModule # FIXME: somehow use ResourceSystem.get_known_realms() # ... or directly trac.wiki.extract_link try: browser_links = [res[0] for res in BrowserModule(self.env).get_link_resolvers()] except Exception: browser_links = [] while args: arg = args.pop(0) if self._size_re.match(arg): # 'width' keyword attr['width'] = arg elif arg == 'nolink': link = None elif arg.startswith('link='): val = arg.split('=', 1)[1] elt = extract_link(self.env, formatter.context, val.strip()) elt = find_element(elt, 'href') link = None if elt is not None: link = elt.attrib.get('href') elif arg in ('left', 'right'): style['float'] = arg elif arg == 'center': style['margin-left'] = style['margin-right'] = 'auto' style['display'] = 'block' style.pop('margin', '') elif arg in ('top', 'bottom', 'middle'): style['vertical-align'] = arg else: match = self._attr_re.match(arg) if match: key, val = match.groups() if (key == 'align' and val in ('left', 'right', 'center')) or \ (key == 'valign' and val in ('top', 'middle', 'bottom')): args.append(val) elif key in ('margin-top', 'margin-bottom'): style[key] = ' %dpx' % _arg_as_int(val, key, min=1) elif key in ('margin', 'margin-left', 'margin-right') \ and 'display' not in style: style[key] = ' %dpx' % _arg_as_int(val, key, min=1) elif key == 'border': style['border'] = ' %dpx solid' % _arg_as_int(val, key) else: m = self._quoted_re.search(val) # unquote "..." and '...' if m: val = m.group(1) attr[str(key)] = val # will be used as a __call__ kwd if self._quoted_re.match(filespec): filespec = filespec.strip('\'"') # parse filespec argument to get realm and id if contained. parts = [i.strip('\'"') for i in self._split_filespec_re.split(filespec)[1::2]] realm = parts[0] if parts else None url = raw_url = desc = None attachment = None interwikimap = InterWikiMap(self.env) if realm in ('http', 'https', 'ftp', 'data'): # absolute raw_url = url = filespec desc = url.rsplit('?')[0] elif realm in interwikimap: url, desc = interwikimap.url(realm, ':'.join(parts[1:])) raw_url = url elif filespec.startswith('//'): # server-relative raw_url = url = filespec[1:] desc = url.rsplit('?')[0] elif filespec.startswith('/'): # project-relative params = '' if '?' in filespec: filespec, params = filespec.rsplit('?', 1) url = formatter.href(filespec) if params: url += '?' + params raw_url, desc = url, filespec elif len(parts) == 3: # realm:id:attachment-filename # # or intertrac:realm:id realm, id, filename = parts intertrac_target = "%s:%s" % (id, filename) it = formatter.get_intertrac_url(realm, intertrac_target) if it: url, desc = it raw_url = url + unicode_quote('?format=raw') else: attachment = Resource(realm, id).child('attachment', filename) elif len(parts) == 2: realm, filename = parts if realm in browser_links: # source:path # TODO: use context here as well rev = None if '@' in filename: filename, rev = filename.rsplit('@', 1) url = formatter.href.browser(filename, rev=rev) raw_url = formatter.href.browser(filename, rev=rev, format='raw') desc = filespec else: # #ticket:attachment or WikiPage:attachment # FIXME: do something generic about shorthand forms... realm = None id, filename = parts if id and id[0] == '#': realm = 'ticket' id = id[1:] elif id == 'htdocs': raw_url = url = formatter.href.chrome('site', filename) desc = os.path.basename(filename) elif id == 'shared': raw_url = url = formatter.href.chrome('shared', filename) desc = os.path.basename(filename) else: realm = 'wiki' if realm: attachment = Resource(realm, id).child('attachment', filename) elif len(parts) == 1: # it's an attachment of the current resource attachment = formatter.resource.child('attachment', filespec) else: return system_message(_("No filespec given")) if attachment: try: desc = get_resource_summary(self.env, attachment) except ResourceNotFound: link = None raw_url = chrome_resource_path(formatter.context.req, 'common/attachment.png') desc = _('No image "%(id)s" attached to %(parent)s', id=attachment.id, parent=get_resource_name(self.env, attachment.parent)) else: if 'ATTACHMENT_VIEW' in formatter.perm(attachment): url = get_resource_url(self.env, attachment, formatter.href) raw_url = get_resource_url(self.env, attachment, formatter.href, format='raw') for key in ('title', 'alt'): if desc and key not in attr: attr[key] = desc if style: attr['style'] = '; '.join('%s:%s' % (k, escape(v)) for k, v in style.items()) if not WikiSystem(self.env).is_safe_origin(raw_url, formatter.context.req): attr['crossorigin'] = 'anonymous' # avoid password prompt result = tag.img(src=raw_url, **attr) if link is not None: result = tag.a(result, href=link or url, style='padding:0; border:none') return result
class Node(object): """Represents a directory or file in the repository at a given revision.""" DIRECTORY = "dir" FILE = "file" resource = property(lambda self: Resource('source', self.created_path, version=self.created_rev, parent=self.repos.resource)) # created_path and created_rev properties refer to the Node "creation" # in the Subversion meaning of a Node in a versioned tree (see #3340). # # Those properties must be set by subclasses. # created_rev = None created_path = None def __init__(self, repos, path, rev, kind): assert kind in (Node.DIRECTORY, Node.FILE), \ "Unknown node kind %s" % kind self.repos = repos self.path = to_unicode(path) self.rev = rev self.kind = kind def get_content(self): """Return a stream for reading the content of the node. This method will return `None` for directories. The returned object must support a `read([len])` method. """ raise NotImplementedError def get_entries(self): """Generator that yields the immediate child entries of a directory. The entries are returned in no particular order. If the node is a file, this method returns `None`. """ raise NotImplementedError def get_history(self, limit=None): """Provide backward history for this Node. Generator that yields `(path, rev, chg)` tuples, one for each revision in which the node was changed. This generator will follow copies and moves of a node (if the underlying version control system supports that), which will be indicated by the first element of the tuple (i.e. the path) changing. Starts with an entry for the current revision. :param limit: if given, yield at most ``limit`` results. """ raise NotImplementedError def get_previous(self): """Return the change event corresponding to the previous revision. This returns a `(path, rev, chg)` tuple. """ skip = True for p in self.get_history(2): if skip: skip = False else: return p def get_annotations(self): """Provide detailed backward history for the content of this Node. Retrieve an array of revisions, one `rev` for each line of content for that node. Only expected to work on (text) FILE nodes, of course. """ raise NotImplementedError def get_properties(self): """Returns the properties (meta-data) of the node, as a dictionary. The set of properties depends on the version control system. """ raise NotImplementedError def get_content_length(self): """The length in bytes of the content. Will be `None` for a directory. """ raise NotImplementedError content_length = property(lambda x: x.get_content_length()) def get_content_type(self): """The MIME type corresponding to the content, if known. Will be `None` for a directory. """ raise NotImplementedError content_type = property(lambda x: x.get_content_type()) def get_name(self): return self.path.split('/')[-1] name = property(lambda x: x.get_name()) def get_last_modified(self): raise NotImplementedError last_modified = property(lambda x: x.get_last_modified()) isdir = property(lambda x: x.kind == Node.DIRECTORY) isfile = property(lambda x: x.kind == Node.FILE) def can_view(self, perm): """Return True if view permission is granted on the node.""" return (self.isdir and 'BROWSER_VIEW' or 'FILE_VIEW') \ in perm(self.resource)
elif query.startswith('query:'): try: from trac.ticket.query import Query, QuerySyntaxError query = Query.from_string(self.env, query[6:], report=id) req.redirect(query.get_href(req)) except QuerySyntaxError, e: req.redirect(req.href.report(id, action='edit', error=to_unicode(e))) format = req.args.get('format') if format == 'sql': self._send_sql(req, id, title, description, sql) title = '{%i} %s' % (id, title) report_resource = Resource('report', id) req.perm(report_resource).require('REPORT_VIEW') context = web_context(req, report_resource) page = as_int(req.args.get('page'), 1) default_max = {'rss': self.items_per_page_rss, 'csv': 0, 'tab': 0}.get(format, self.items_per_page) max = req.args.get('max') limit = as_int(max, default_max, min=0) # explict max takes precedence offset = (page - 1) * limit sort_col = req.args.get('sort', '') asc = as_bool(req.args.get('asc', 1)) def report_href(**kwargs): """Generate links to this report preserving user variables,
def test_resource_doesnt_exist(self): """Non-existent resource returns False from resource_exists.""" r = Resource('wiki', 'WikiStart').child('attachment', 'file.txt') self.assertFalse(AttachmentModule(self.env).resource_exists(r))
def test_actor_has_email_view_for_resource(self): format_author = Chrome(self.env).format_author req = MockRequest(self.env, authname='user2') resource = Resource('wiki', 'WikiStart') author = format_author(req, '*****@*****.**', resource) self.assertEqual('*****@*****.**', author)
def expand_macro(self, formatter, name, content): # args will be null if the macro is called without parenthesis. if not content: return '' # parse arguments # we expect the 1st argument to be a filename (filespec) args = content.split(',') if len(args) == 0: raise Exception("No argument.") # strip unicode white-spaces and ZWSPs are copied from attachments # section (#10668) filespec = stripws(args.pop(0)) # style information size_re = re.compile('[0-9]+(%|px)?$') attr_re = re.compile('(align|valign|border|width|height|alt' '|margin(?:-(?:left|right|top|bottom))?' '|title|longdesc|class|id|usemap)=(.+)') quoted_re = re.compile("(?:[\"'])(.*)(?:[\"'])$") attr = {} style = {} link = '' # helper for the special case `source:` # from trac.versioncontrol.web_ui import BrowserModule # FIXME: somehow use ResourceSystem.get_known_realms() # ... or directly trac.wiki.extract_link try: browser_links = [ res[0] for res in BrowserModule(self.env).get_link_resolvers() ] except Exception: browser_links = [] while args: arg = stripws(args.pop(0)) if size_re.match(arg): # 'width' keyword attr['width'] = arg elif arg == 'nolink': link = None elif arg.startswith('link='): val = arg.split('=', 1)[1] elt = extract_link(self.env, formatter.context, val.strip()) elt = find_element(elt, 'href') link = None if elt is not None: link = elt.attrib.get('href') elif arg in ('left', 'right'): style['float'] = arg elif arg == 'center': style['margin-left'] = style['margin-right'] = 'auto' style['display'] = 'block' style.pop('margin', '') elif arg in ('top', 'bottom', 'middle'): style['vertical-align'] = arg else: match = attr_re.match(arg) if match: key, val = match.groups() if (key == 'align' and val in ('left', 'right', 'center')) or \ (key == 'valign' and \ val in ('top', 'middle', 'bottom')): args.append(val) elif key in ('margin-top', 'margin-bottom'): style[key] = ' %dpx' % int(val) elif key in ('margin', 'margin-left', 'margin-right') \ and 'display' not in style: style[key] = ' %dpx' % int(val) elif key == 'border': style['border'] = ' %dpx solid' % int(val) else: m = quoted_re.search(val) # unquote "..." and '...' if m: val = m.group(1) attr[str(key)] = val # will be used as a __call__ kwd # parse filespec argument to get realm and id if contained. parts = [ i.strip('''['"]''') for i in self._split_filespec_re.split(filespec) ] url = raw_url = desc = None attachment = None if (parts and parts[0] in ('http', 'https', 'ftp')): # absolute raw_url = url = filespec desc = url.rsplit('?')[0] elif filespec.startswith('//'): # server-relative raw_url = url = filespec[1:] desc = url.rsplit('?')[0] elif filespec.startswith('/'): # project-relative params = '' if '?' in filespec: filespec, params = filespec.rsplit('?', 1) url = formatter.href(filespec) if params: url += '?' + params raw_url, desc = url, filespec elif len(parts) == 3: # realm:id:attachment-filename # # or intertrac:realm:id realm, id, filename = parts intertrac_target = "%s:%s" % (id, filename) it = formatter.get_intertrac_url(realm, intertrac_target) if it: url, desc = it raw_url = url + unicode_quote('?format=raw') else: attachment = Resource(realm, id).child('attachment', filename) elif len(parts) == 2: realm, filename = parts if realm in browser_links: # source:path # TODO: use context here as well rev = None if '@' in filename: filename, rev = filename.rsplit('@', 1) url = formatter.href.browser(filename, rev=rev) raw_url = formatter.href.browser(filename, rev=rev, format='raw') desc = filespec else: # #ticket:attachment or WikiPage:attachment # FIXME: do something generic about shorthand forms... realm = None id, filename = parts if id and id[0] == '#': realm = 'ticket' id = id[1:] elif id == 'htdocs': raw_url = url = formatter.href.chrome('site', filename) desc = os.path.basename(filename) elif id == 'shared': raw_url = url = formatter.href.chrome('shared', filename) desc = os.path.basename(filename) else: realm = 'wiki' if realm: attachment = Resource(realm, id).child('attachment', filename) elif len(parts) == 1: # it's an attachment of the current resource attachment = formatter.resource.child('attachment', filespec) else: raise TracError('No filespec given') if attachment and 'ATTACHMENT_VIEW' in formatter.perm(attachment): url = get_resource_url(self.env, attachment, formatter.href) raw_url = get_resource_url(self.env, attachment, formatter.href, format='raw') try: desc = get_resource_summary(self.env, attachment) except ResourceNotFound as e: raw_url = formatter.href.chrome('common/attachment.png') desc = _('No image "%(id)s" attached to %(parent)s', id=attachment.id, parent=get_resource_name(self.env, attachment.parent)) for key in ('title', 'alt'): if desc and not key in attr: attr[key] = desc if style: attr['style'] = '; '.join('%s:%s' % (k, escape(v)) for k, v in style.iteritems()) result = tag.img(src=raw_url, **attr) if link is not None: result = tag.a(result, href=link or url, style='padding:0; border:none') return result
data.update( dict(header_groups=[headers], numrows=len(data['tickets']), row_groups=[(group_value, [{ '__color__' : t['priority_value'], '__idx__' : idxs.next(), 'cell_groups' : [[ { 'header' : h, 'index' : hidx, 'value' : t[h['col']] } \ for hidx, h in enumerate(headers)]], 'id' : t['id'], 'resource' : Resource('ticket', t['id']) } for t in tickets]) \ for group_value, tickets in data['groups'] ])) return 'widget_grid.html', \ { 'title' : title or _('Custom Query'), 'data' : data, 'ctxtnav' : [ tag.a(_('More'), href=query.get_href(req.href))], 'altlinks' : fakereq.chrome.get('links', {}).get('alternate') }, \ qryctx render_widget = pretty_wrapper(render_widget, check_widget_name)
def get_list(self, realm, wl, req, fields=None): db = self.env.get_db_cnx() cursor = db.cursor() context = Context.from_request(req) locale = getattr(req, 'locale', LC_TIME) ticketlist = [] extradict = {} if not fields: fields = set(self.default_fields['ticket']) else: fields = set(fields) if 'changetime' in fields: max_changetime = datetime(1970, 1, 1, tzinfo=utc) min_changetime = datetime.now(utc) if 'time' in fields: max_time = datetime(1970, 1, 1, tzinfo=utc) min_time = datetime.now(utc) for sid, last_visit in wl.get_watched_resources( 'ticket', req.authname): ticketdict = {} try: ticket = Ticket(self.env, sid, db) exists = ticket.exists except: exists = False if not exists: ticketdict['deleted'] = True if 'id' in fields: ticketdict['id'] = sid ticketdict['ID'] = '#' + sid if 'author' in fields: ticketdict['author'] = '?' if 'changetime' in fields: ticketdict['changedsincelastvisit'] = 1 ticketdict['changetime'] = '?' ticketdict['ichangetime'] = 0 if 'time' in fields: ticketdict['time'] = '?' ticketdict['itime'] = 0 if 'comment' in fields: ticketdict['comment'] = tag.strong(t_("deleted"), class_='deleted') if 'notify' in fields: ticketdict['notify'] = wl.is_notify(req, 'ticket', sid) if 'description' in fields: ticketdict['description'] = '' if 'owner' in fields: ticketdict['owner'] = '' if 'reporter' in fields: ticketdict['reporter'] = '' ticketlist.append(ticketdict) continue render_elt = lambda x: x if not (Chrome(self.env).show_email_addresses or \ 'EMAIL_VIEW' in req.perm(ticket.resource)): render_elt = obfuscate_email_address # Copy all requested fields from ticket if fields: for f in fields: ticketdict[f] = ticket.values.get(f, u'') else: ticketdict = ticket.values.copy() changetime = ticket.time_changed if wl.options['attachment_changes']: for attachment in Attachment.select(self.env, 'ticket', sid, db): if attachment.date > changetime: changetime = attachment.date if 'attachment' in fields: attachments = [] for attachment in Attachment.select(self.env, 'ticket', sid, db): wikitext = u'[attachment:"' + u':'.join([ attachment.filename, 'ticket', sid ]) + u'" ' + attachment.filename + u']' attachments.extend([ tag(', '), format_to_oneliner(self.env, context, wikitext, shorten=False) ]) if attachments: attachments.reverse() attachments.pop() ticketdict['attachment'] = moreless(attachments, 5) # Changes are special. Comment, commentnum and last author are included in them. if 'changes' in fields or 'author' in fields or 'comment' in fields or 'commentnum' in fields: changes = [] # If there are now changes the reporter is the last author author = ticket.values['reporter'] commentnum = u"0" comment = u"" want_changes = 'changes' in fields for date, cauthor, field, oldvalue, newvalue, permanent in ticket.get_changelog( changetime, db): author = cauthor if field == 'comment': if 'commentnum' in fields: ticketdict['commentnum'] = to_unicode(oldvalue) if 'comment' in fields: comment = to_unicode(newvalue) comment = moreless(comment, 200) ticketdict['comment'] = comment if not want_changes: break else: if want_changes: label = self.fields['ticket'].get(field, u'') if label: changes.extend([ tag( tag.strong(label), ' ', render_property_diff( self.env, req, ticket, field, oldvalue, newvalue)), tag('; ') ]) if want_changes: # Remove the last tag('; '): if changes: changes.pop() changes = moreless(changes, 5) ticketdict['changes'] = tag(changes) if 'id' in fields: ticketdict['id'] = sid ticketdict['ID'] = format_to_oneliner(self.env, context, '#' + sid, shorten=True) if 'cc' in fields: if render_elt == obfuscate_email_address: ticketdict['cc'] = ', '.join( [render_elt(c) for c in ticketdict['cc'].split(', ')]) if 'author' in fields: ticketdict['author'] = render_elt(author) if 'changetime' in fields: ichangetime = to_timestamp(changetime) ticketdict.update( changetime=format_datetime(changetime, locale=locale, tzinfo=req.tz), ichangetime=ichangetime, changedsincelastvisit=(last_visit < ichangetime and 1 or 0), changetime_delta=pretty_timedelta(changetime), changetime_link=req.href.timeline( precision='seconds', from_=trac_format_datetime(changetime, 'iso8601', tzinfo=req.tz))) if changetime > max_changetime: max_changetime = changetime if changetime < min_changetime: min_changetime = changetime if 'time' in fields: time = ticket.time_created ticketdict.update(time=format_datetime(time, locale=locale, tzinfo=req.tz), itime=to_timestamp(time), time_delta=pretty_timedelta(time), time_link=req.href.timeline( precision='seconds', from_=trac_format_datetime( time, 'iso8601', tzinfo=req.tz))) if time > max_time: max_time = time if time < min_time: min_time = time if 'description' in fields: description = ticket.values['description'] description = moreless(description, 200) ticketdict['description'] = description if 'notify' in fields: ticketdict['notify'] = wl.is_notify(req, 'ticket', sid) if 'owner' in fields: ticketdict['owner'] = render_elt(ticket.values['owner']) if 'reporter' in fields: ticketdict['reporter'] = render_elt(ticket.values['reporter']) if 'tags' in fields and self.tagsystem: tags = [] for t in self.tagsystem.get_tags(req, Resource('ticket', sid)): tags.extend( [tag.a(t, href=req.href('tags', q=t)), tag(', ')]) if tags: tags.pop() ticketdict['tags'] = moreless(tags, 10) ticketlist.append(ticketdict) if 'changetime' in fields: extradict['max_changetime'] = format_datetime(max_changetime, locale=locale, tzinfo=req.tz) extradict['min_changetime'] = format_datetime(min_changetime, locale=locale, tzinfo=req.tz) if 'time' in fields: extradict['max_time'] = format_datetime(max_time, locale=locale, tzinfo=req.tz) extradict['min_time'] = format_datetime(min_time, locale=locale, tzinfo=req.tz) return ticketlist, extradict
def _normalize_resource(self, realm_or_resource, id, version): if realm_or_resource: return Resource(realm_or_resource, id, version) else: return self._resource
def _render_repository_index(self, context, all_repositories, order, desc): # Color scale for the age column timerange = custom_colorizer = None if self.color_scale: custom_colorizer = self.get_custom_colorizer() rm = RepositoryManager(self.env) repositories = [] for reponame, repoinfo in all_repositories.iteritems(): if not reponame or as_bool(repoinfo.get('hidden')): continue try: repos = rm.get_repository(reponame) except TracError as err: entry = (reponame, repoinfo, None, None, exception_to_unicode(err), None) else: if repos: if not repos.is_viewable(context.perm): continue try: youngest = repos.get_changeset(repos.youngest_rev) except NoSuchChangeset: youngest = None if self.color_scale and youngest: if not timerange: timerange = TimeRange(youngest.date) else: timerange.insert(youngest.date) raw_href = self._get_download_href(context.href, repos, None, None) entry = (reponame, repoinfo, repos, youngest, None, raw_href) else: entry = (reponame, repoinfo, None, None, u"\u2013", None) if entry[4] is not None: # Check permission in case of error root = Resource('repository', reponame).child(self.realm, '/') if 'BROWSER_VIEW' not in context.perm(root): continue repositories.append(entry) # Ordering of repositories if order == 'date': def repo_order(args): reponame, repoinfo, repos, youngest, err, href = args return (youngest.date if youngest else to_datetime(0), embedded_numbers(reponame.lower())) elif order == 'author': def repo_order(args): reponame, repoinfo, repos, youngest, err, href = args return (youngest.author.lower() if youngest else '', embedded_numbers(reponame.lower())) else: def repo_order(args): reponame, repoinfo, repos, youngest, err, href = args return embedded_numbers(reponame.lower()) repositories = sorted(repositories, key=repo_order, reverse=desc) return {'repositories' : repositories, 'timerange': timerange, 'colorize_age': custom_colorizer}
def test_resource_doesnt_exist(self): """Non-existent resource returns False from resource_exists.""" parent = Resource('parent_realm', 'parent_id') self.assertTrue(resource_exists(self.env, parent)) r = parent.child('attachment', 'file.txt') self.assertFalse(resource_exists(self.env, r))
class Node(object): """Represents a directory or file in the repository at a given revision.""" __metaclass__ = ABCMeta DIRECTORY = "dir" FILE = "file" realm = 'source' resource = property(lambda self: Resource( self.realm, self.path, version=self.rev, parent=self.repos.resource)) # created_path and created_rev properties refer to the Node "creation" # in the Subversion meaning of a Node in a versioned tree (see #3340). # # Those properties must be set by subclasses. # created_rev = None created_path = None def __init__(self, repos, path, rev, kind): assert kind in (Node.DIRECTORY, Node.FILE), \ "Unknown node kind %s" % kind self.repos = repos self.path = to_unicode(path) self.rev = rev self.kind = kind @abstractmethod def get_content(self): """Return a stream for reading the content of the node. This method will return `None` for directories. The returned object must support a `read([len])` method. """ pass def get_processed_content(self, keyword_substitution=True, eol_hint=None): """Return a stream for reading the content of the node, with some standard processing applied. :param keyword_substitution: if `True`, meta-data keywords present in the content like ``$Rev$`` are substituted (which keyword are substituted and how they are substituted is backend specific) :param eol_hint: which style of line ending is expected if `None` was explicitly specified for the file itself in the version control backend (for example in Subversion, if it was set to ``'native'``). It can be `None`, ``'LF'``, ``'CR'`` or ``'CRLF'``. """ return self.get_content() @abstractmethod def get_entries(self): """Generator that yields the immediate child entries of a directory. The entries are returned in no particular order. If the node is a file, this method returns `None`. """ pass @abstractmethod def get_history(self, limit=None): """Provide backward history for this Node. Generator that yields `(path, rev, chg)` tuples, one for each revision in which the node was changed. This generator will follow copies and moves of a node (if the underlying version control system supports that), which will be indicated by the first element of the tuple (i.e. the path) changing. Starts with an entry for the current revision. :param limit: if given, yield at most ``limit`` results. """ pass def get_previous(self): """Return the change event corresponding to the previous revision. This returns a `(path, rev, chg)` tuple. """ skip = True for p in self.get_history(2): if skip: skip = False else: return p @abstractmethod def get_annotations(self): """Provide detailed backward history for the content of this Node. Retrieve an array of revisions, one `rev` for each line of content for that node. Only expected to work on (text) FILE nodes, of course. """ pass @abstractmethod def get_properties(self): """Returns the properties (meta-data) of the node, as a dictionary. The set of properties depends on the version control system. """ pass @abstractmethod def get_content_length(self): """The length in bytes of the content. Will be `None` for a directory. """ pass content_length = property(lambda self: self.get_content_length()) @abstractmethod def get_content_type(self): """The MIME type corresponding to the content, if known. Will be `None` for a directory. """ pass content_type = property(lambda self: self.get_content_type()) def get_name(self): return self.path.split('/')[-1] name = property(lambda self: self.get_name()) @abstractmethod def get_last_modified(self): pass last_modified = property(lambda self: self.get_last_modified()) isdir = property(lambda self: self.kind == Node.DIRECTORY) isfile = property(lambda self: self.kind == Node.FILE) def is_viewable(self, perm): """Return True if view permission is granted on the node.""" return ('BROWSER_VIEW' if self.isdir else 'FILE_VIEW') \ in perm(self.resource) can_view = is_viewable # 0.12 compatibility
def test_resource_doesnt_exist(self): r = Resource('wiki', 'WikiStart').child('attachment', 'file.txt') self.assertEqual(False, AttachmentModule(self.env).resource_exists(r))
class Changeset(object): """Represents a set of changes committed at once in a repository.""" __metaclass__ = ABCMeta ADD = 'add' COPY = 'copy' DELETE = 'delete' EDIT = 'edit' MOVE = 'move' # change types which can have diff associated to them DIFF_CHANGES = (EDIT, COPY, MOVE) # MERGE OTHER_CHANGES = (ADD, DELETE) ALL_CHANGES = DIFF_CHANGES + OTHER_CHANGES realm = 'changeset' resource = property(lambda self: Resource( self.realm, self.rev, parent=self.repos.resource)) def __init__(self, repos, rev, message, author, date): self.repos = repos self.rev = rev self.message = message or '' self.author = author or '' self.date = date def get_properties(self): """Returns the properties (meta-data) of the node, as a dictionary. The set of properties depends on the version control system. Warning: this used to yield 4-elements tuple (besides `name` and `text`, there were `wikiflag` and `htmlclass` values). This is now replaced by the usage of IPropertyRenderer (see #1601). """ return [] @abstractmethod def get_changes(self): """Generator that produces a tuple for every change in the changeset. The tuple will contain `(path, kind, change, base_path, base_rev)`, where `change` can be one of Changeset.ADD, Changeset.COPY, Changeset.DELETE, Changeset.EDIT or Changeset.MOVE, and `kind` is one of Node.FILE or Node.DIRECTORY. The `path` is the targeted path for the `change` (which is the ''deleted'' path for a DELETE change). The `base_path` and `base_rev` are the source path and rev for the action (`None` and `-1` in the case of an ADD change). """ pass def get_branches(self): """Yield branches to which this changeset belong. Each branch is given as a pair `(name, head)`, where `name` is the branch name and `head` a flag set if the changeset is a head for this branch (i.e. if it has no children changeset). """ return [] def get_tags(self): """Yield tags associated with this changeset. .. versionadded :: 1.0 """ return [] def is_viewable(self, perm): """Return True if view permission is granted on the changeset.""" return 'CHANGESET_VIEW' in perm(self.resource) can_view = is_viewable # 0.12 compatibility
def test_create_resource(self): so = FullTextSearchObject(self.project, Resource('wiki', 'WikiStart')) self.assertEquals('wiki', so.realm) self.assertEquals('WikiStart', so.id) self.assertEquals(Resource('wiki', 'WikiStart'), so.resource) self.assertEquals('project1:wiki:WikiStart', so.doc_id)
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
def _format_name(self, req, url): linkname = url name = "" missing = False path_info = url query_string = '' idx = path_info.find('?') if idx >= 0: path_info, query_string = path_info[:idx], path_info[idx:] href = req.href(path_info) + query_string args = arg_list_to_args(parse_arg_list(query_string.lstrip('?'))) version = args.get('version', False) path = path_info.strip('/').split('/') realm = path[0] class_ = realm if len(path) > 1: resource = Resource(realm, path[1]) if resource: if realm == 'ticket': linkname = get_resource_shortname(self.env, resource) try: name = get_resource_summary(self.env, resource) except ResourceNotFound: missing = True else: from trac.ticket.model import Ticket class_ = Ticket(self.env, resource.id)['status'] + \ ' ' + class_ elif realm == 'milestone': linkname = get_resource_name(self.env, resource) elif realm == 'wiki': resource = Resource(realm, '/'.join(path[1:]), version) linkname = get_resource_shortname(self.env, resource) if version: linkname += '@' + version elif realm == 'report': linkname = "{%s}" % path[1] name = self._format_report_name(path[1]) elif realm == 'changeset': rev = path[1] parent = Resource('source', '/'.join(path[2:])) resource = Resource(realm, rev, False, parent) linkname = "[%s]" % rev name = get_resource_description(self.env, resource) elif realm == 'browser': rm = RepositoryManager(self.env) reponame, repos, path = rm.get_repository_by_path('/'.join( path[1:])) parent = Resource('source', reponame) resource = Resource('source', path, False, parent) linkname = get_resource_description(self.env, resource) name = get_resource_summary(self.env, resource) elif realm == 'attachment': # Assume a file and check existence parent = Resource(path[1], '/'.join(path[2:-1])) resource = Resource(realm, path[-1], parent=parent) linkname = get_resource_name(self.env, resource) if not resource_exists(self.env, resource): # Assume an attachment list page and check existence parent = Resource(path[1], '/'.join(path[2:])) if resource_exists(self.env, parent): resource = Resource(realm, parent=parent) linkname = get_resource_name(self.env, resource) if not query_string: # Trailing slash needed for Trac < 1.0, t:#10280 href += '/' else: # Assume it's a missing attachment missing = True else: linkname = get_resource_shortname(self.env, resource) name = get_resource_summary(self.env, resource) elif len(path) == 1 and path[0] and path[0] != 'wiki': linkname = path[0].capitalize() else: class_ = 'wiki' linkname = 'WikiStart' if missing: href = None class_ = 'missing ' + realm return { 'class_': class_, 'href': href, 'linkname': linkname, 'name': name, 'delete': req.href.bookmark('delete_in_page', url), }
def _insert_attachment(self, author): parent_resource = Resource('parent_realm', 'parent_id') att = Attachment(self.env, 'parent_realm', 'parent_id') att.author = author att.insert('file.txt', io.BytesIO(), 1) return Resource('attachment', 'file.txt', parent=parent_resource)