def _process_request(self, req): mod = TimelineModule(self.env) req = MockRequest(self.env, path_info='/timeline', args={'format': 'rss'}) self.assertTrue(mod.match_request(req)) return mod.process_request(req)
def _get_timeline_events(self, tracenviron): """ Get the timeline events for one project, based on the given in trac environment object. """ # skip empty projects if tracenviron is None: return None daterange = int(self.conf.activity_calculation_daterange) # end time of timeline is current time todate = datetime.now(datefmt.localtz) # start time of timeline is last update of if not known, last two monts fromdate = todate fromdate = fromdate - timedelta(days=daterange) stop = todate start = fromdate # events will continue the timeline events events = [] # Access event providers for timeline events event_providers = TimelineModule(tracenviron).event_providers # create a dummy request (see class above) req = DummyReq('user', 'password', 'method', 'uri', 'args') req.permissions = ('TICKET_VIEW', 'CHANGESET_VIEW', 'WIKI_VIEW', 'ATTACHMENT_VIEW', 'DISCUSSION_VIEW') req.authname = 'authname' # filters will contain the available timeline event types available_filters = [] for event_provider in event_providers: available_filters += event_provider.get_timeline_filters(req) or [] filters = [] # check the request or session for enabled filters, or use default for test in (lambda f: f[0] in req.args, lambda f: len(f) == 2 or f[2]): if filters: break filters = [f[0] for f in available_filters if test(f)] # do the actual event querying for provider in event_providers: # note: this is because if discussion is not in current project, do not fail try: for event in provider.get_timeline_events(req, start, stop, filters): events.append(self._event_data(provider, event)) except Exception: self.conf.log.exception( "ActivityCalculator.get_timeline_events(%s) couldn't get timeline events from %s." % (tracenviron.project_name, str(provider))) events.sort(lambda x, y: cmp(y['date'], x['date'])) return events
def _format_timeline(self, d, format, dateonly): data = Chrome(self.env).populate_data(self.req, {}) TimelineModule(self.env) \ .post_process_request(self.req, 'timeline.html', data, None) return plaintext(data['pretty_dateinfo'](d, format=format, dateonly=dateonly))
def _create_html_body(self, chrome, req, ticket, cnum, link): tktmod = TicketModule(self.env) attmod = AttachmentModule(self.env) data = tktmod._prepare_data(req, ticket) tktmod._insert_ticket_data(req, ticket, data, req.authname, {}) data['ticket']['link'] = link changes = data.get('changes') if cnum is None: changes = [] else: changes = [change for change in (changes or []) if change.get('cnum') == cnum] data['changes'] = changes context = Context.from_request(req, ticket.resource, absurls=True) alist = attmod.attachment_data(context) alist['can_create'] = False data.update({ 'can_append': False, 'show_editor': False, 'start_time': ticket['changetime'], 'context': context, 'alist': alist, 'styles': self._get_styles(chrome), 'link': tag.a(link, href=link), 'tag_': tag_, }) template = 'htmlnotification_ticket.html' # use pretty_dateinfo in TimelineModule TimelineModule(self.env).post_process_request(req, template, data, None) rendered = chrome.render_template(req, template, data, fragment=True) return unicode(rendered)
def test_daysback_less_than_min(self): """Daysback minimum value is 1.""" req = MockRequest(self.env, args={'daysback': '-1'}) data = TimelineModule(self.env).process_request(req)[1] self.assertEqual(1, data['daysback'])
def test_daysback_greater_than_max(self): """Daysback is limited to [timeline] max_daysback.""" req = MockRequest(self.env, args={'daysback': '100'}) data = TimelineModule(self.env).process_request(req)[1] self.assertEqual(90, data['daysback'])
def test_daysback_invalid_default_is_used(self): """Daysback request value is invalid: default value is used.""" req = MockRequest(self.env, args={'daysback': '--'}) data = TimelineModule(self.env).process_request(req)[1] self.assertEqual(30, data['daysback'])
def test_daysback_from_session(self): """Daysback value is retrieved from session attributes.""" PermissionSystem(self.env).grant_permission('user1', 'TIMELINE_VIEW') req = MockRequest(self.env, authname='user1') req.session.set('timeline.daysback', '45') data = TimelineModule(self.env).process_request(req)[1] self.assertEqual(45, data['daysback'])
def test_daysback_default_is_90_for_rss_format(self): """Daysback default is 90 for RSS format request.""" PermissionSystem(self.env).grant_permission('user1', 'TIMELINE_VIEW') req = MockRequest(self.env, authname='user1', args={'format': 'rss'}) req.session.set('timeline.daysback', '45') data = TimelineModule(self.env).process_request(req)[1] self.assertEqual(90, data['daysback'])
def test_daysback_invalid_session_value_default_is_used(self): """Daysback session value is invalid: default value is used.""" PermissionSystem(self.env).grant_permission('user1', 'TIMELINE_VIEW') req = MockRequest(self.env, authname='user1', args={'daysback': '--'}) req.session.set('timeline.daysback', '45') data = TimelineModule(self.env).process_request(req)[1] self.assertEqual(45, data['daysback'])
def test_no_exception_when_from_year_before_1900(self): """Exception not raised when 'from' year before 1900 (#12489).""" req = MockRequest(self.env, args={ 'from': '1899-12-23', 'daysback': 7, }) TimelineModule(self.env).process_request(req) self.assertIn('prev', req.chrome['links'])
def __getattr__(self, attrnm): """Forward attribute access request to TimelineModule """ try: value = getattr(TimelineModule(self.env), attrnm) if isinstance(value, MethodType): raise AttributeError() except AttributeError: raise AttributeError("'%s' object has no attribute '%s'" % (self.__class__.__name__, attrnm)) else: return value
def test_invalid_date_format_add_warning(self): """Warning is added when date format is invalid.""" req = MockRequest(self.env, args={ 'from': '2011-02-02T11:38:50 01:00', }) TimelineModule(self.env).process_request(req) self.assertIn(u'"2011-02-02T11:38:50 01:00" is an invalid date, ' u'or the date format is not known. Try "%s" or "%s" ' u'instead.' % (get_date_format_hint(locale_en), get_date_format_hint('iso8601')), req.chrome['warnings'])
def test_tag_query_save(self): """Save timeline tag query string in session.""" self.assertEqual('tag_query', self.tef.key) from trac.timeline.web_ui import TimelineModule TimelineModule(self.env) perms = PermissionSystem(self.env) perms.grant_permission('anonymous', 'TAGS_VIEW') perms.grant_permission('anonymous', 'TIMELINE_VIEW') req = MockRequest(self.env, path_info='/timeline', args={'tag_query': 'query_str'}) dispatcher = RequestDispatcher(self.env) self.assertRaises(RequestDone, dispatcher.dispatch, req) self.assertEqual('query_str', req.session['timeline.tag_query'])
def environment_needs_upgrade(self, db): if not self.has_pretty_dateinfo: return False def find(list, val): try: return list.index(val) except ValueError: return len(list) filters = RequestDispatcher(self.env).filters if find(filters, self) < find(filters, TimelineModule(self.env)): return False name = self.__class__.__name__ self.log.warn( 'Prepend %s to request_filters in [trac] section\n' '[trac]\n' 'request_filters = %s', name, ','.join([name] + self.config.getlist('trac', 'request_filters'))) return False
def event_providers(self): """Introduce wrappers around timeline event providers in order to filter event streams. """ for p in TimelineModule(self.env).event_providers: yield TimelineFilterAdapter(p, self.context, self.keep_mismatched)
def expand_macro(self, formatter, name, text, args=None): assert text.isdigit(), "Argument must be a number" out = "<dl class='lastevents'>" add_stylesheet(formatter.req, 'tracprojectmanager/css/lastevents.css') all_events = [] if hasattr(self.env, 'cached_lastevents'): expiration = self.env.cached_lastevents[1] + timedelta( seconds=EVENT_CACHE_INTERVAL) if datetime.now() < expiration: all_events = self.env.cached_lastevents[0] if not all_events: stop = datetime.now(formatter.req.tz) start = stop - timedelta(days=EVENT_MAX_DAYS) projects = get_project_list(self.env, formatter.req) user = formatter.req.authname for project, project_path, project_url, env in projects: env_timeline = TimelineModule(env) for provider in env_timeline.event_providers: filters = [ x[0] for x in provider.get_timeline_filters(formatter.req) ] self.env.log.info("Project %s - Filters: %s", project, filters) try: events = provider.get_timeline_events( formatter.req, start, stop, filters) #self.env.log.info("Event count: %d", len([x for x in events])) for event in events: context = Context(formatter.resource, Href(project_url), formatter.req.perm) context.req = formatter.req #context = Context.from_request(formatter.req) if len(event) == 6: # 0.10 events kind, url, title, date, author, desc = event else: # 0.11 events if len(event) == 5: # with special provider kind, date, author, data, provider = event else: kind, date, author, data = event title = to_unicode( provider.render_timeline_event( context, 'title', event)) url = provider.render_timeline_event( context, 'url', event) desc = to_unicode( provider.render_timeline_event( context, 'description', event)) all_events.append((project, kind, date, title, url, author, desc)) except Exception, ex: #import sys self.env.log.warning("Exception: %s" % traceback.format_exc()) #out = out + "%s<br/>" % traceback.format_exc() all_events.sort(cmp=lambda x, y: x[2] < y[2] and 1 or -1) self.env.cached_lastevents = [all_events, datetime.now()]
def _save_attachement(self, req, attachment): from trac.web import RequestDone from trac.attachment import AttachmentModule, InvalidAttachment from trac.resource import get_resource_url from trac.timeline.web_ui import TimelineModule import os import unicodedata from trac.util.datefmt import pretty_timedelta response = None try: upload = req.args["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 = AttachmentModule(self.env).max_size if max_size >= 0 and size > max_size: raise TracError(_("Maximum attachment size: %(num)s bytes", num=max_size), _("Upload failed")) # We try to normalize the filename to unicode NFC if we can. # Files uploaded from OS X might be in NFD. filename = unicodedata.normalize("NFC", unicode(upload.filename, "utf-8")) filename = filename.replace("\\", "/").replace(":", "/") filename = os.path.basename(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 for manipulator in AttachmentModule(self.env).manipulators: for field, message in manipulator.validate_attachment(req, attachment): if field: raise InvalidAttachment( _("Attachment field %(field)s is " "invalid: %(message)s", field=field, message=message) ) else: raise InvalidAttachment(_("Invalid attachment: %(message)s", message=message)) if req.args.get("replace"): try: old_attachment = Attachment(self.env, attachment.resource(id=filename)) if not (old_attachment.author and req.authname and old_attachment.author == req.authname): req.perm(attachment.resource).require("ATTACHMENT_DELETE") 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.filename = None attachment.insert(filename, upload.file, size) timeline = TimelineModule(self.env).get_timeline_link( req, attachment.date, pretty_timedelta(attachment.date), precision="second" ) response = { "attachment": { "href": get_resource_url(self.env, attachment.resource, req.href), "realm": attachment.resource.parent.realm, "objid": attachment.resource.parent.id, "filename": filename, "size": size, "author": attachment.author, "description": attachment.description, "timeline": timeline.generate().render().replace("<", "<").replace(">", ">"), } } except (TracError, InvalidAttachment), e: response = {"error": e.message}
def _save_attachement(self, req, attachment): from trac.web import RequestDone from trac.attachment import AttachmentModule, InvalidAttachment from trac.resource import get_resource_url from trac.timeline.web_ui import TimelineModule import os import unicodedata from trac.util.datefmt import pretty_timedelta response = None try: upload = req.args['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 = AttachmentModule(self.env).max_size if max_size >= 0 and size > max_size: raise TracError( _('Maximum attachment size: %(num)s bytes', num=max_size), _('Upload failed')) # We try to normalize the filename to unicode NFC if we can. # Files uploaded from OS X might be in NFD. filename = unicodedata.normalize('NFC', unicode(upload.filename, 'utf-8')) filename = filename.replace('\\', '/').replace(':', '/') filename = os.path.basename(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 for manipulator in AttachmentModule(self.env).manipulators: for field, message in manipulator.validate_attachment( req, attachment): if field: raise InvalidAttachment( _( 'Attachment field %(field)s is ' 'invalid: %(message)s', field=field, message=message)) else: raise InvalidAttachment( _('Invalid attachment: %(message)s', message=message)) if req.args.get('replace'): try: old_attachment = Attachment( self.env, attachment.resource(id=filename)) if not (old_attachment.author and req.authname \ and old_attachment.author == req.authname): req.perm( attachment.resource).require('ATTACHMENT_DELETE') 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.filename = None attachment.insert(filename, upload.file, size) timeline = TimelineModule(self.env).get_timeline_link( req, attachment.date, pretty_timedelta(attachment.date), precision='second') response = { 'attachment': { 'href': get_resource_url(self.env, attachment.resource, req.href), 'realm': attachment.resource.parent.realm, 'objid': attachment.resource.parent.id, 'filename': filename, 'size': size, 'author': attachment.author, 'description': attachment.description, 'timeline': timeline.generate().render().replace('<', '<').replace( '>', '>') } } except (TracError, InvalidAttachment), e: response = {'error': e.message}
def get_timeline_markup(self, req, call, maxrows=10): """ Generates the markup needed when this component is called both explicitly inside wiki pages and implicitly by the ISideBarBoxProvider. Note this code uses methods from the trac TimelineModule module. """ chrome = Chrome(self.env) # last 14 days should be enough stop = datetime.now(req.tz) start = stop - timedelta(days=50) # use code from trac/timeline to generate event data timeline = TimelineModule(self.env) available_filters, filters = timeline.get_filters(req) include_authors, exclude_authors = timeline.authors() events = timeline.get_events(req, start, stop, filters, available_filters, include_authors, exclude_authors, maxrows) show_gravatar = self.config.get('avatar','mode').lower() != 'off' # create the mark up context = Context.from_request(req) event_list = tag.ul(class_="timeline-list no-style") for event in events: event_title = event['render']('title', context) event_url = event['render']('url', context) event_list.append(tag.li( show_gravatar and tag.img( src=req.href.avatar(event['author'] if event['author'] else 'anonymous'), class_="avatar", ) or "", tag.span( chrome.authorinfo(req, event['author']), class_="author" ), tag.span( pretty_age(event['date']), class_="date", ), tag.div( tag.i(class_="event-type fa fa-" + event['kind']), tag.a( event_title, href=event_url, ), class_="event-summary" ), class_="cf" )) # if the markup is being generated via ISideBarBoxProvider we don't # need to add a span3 class div_classes = "box-sidebar" if call == "macro": div_classes += " span3 right" return tag.div( tag.h3( tag.i( class_="fa fa-calendar" ), " Recent Project Activity" ), event_list, class_=div_classes, )
def test_implements_itimelineeventsprovider(self): from trac.timeline.web_ui import TimelineModule self.assertTrue(self.tep in TimelineModule(self.env).event_providers)