def _go_graph(self, req):
     #search all artifacts.
     artifacts = ArtifactManager.find_all(self.env)
     
     milestones = [m for m in Milestone.select(self.env)
                   if 'MILESTONE_VIEW' in req.perm(m.resource)]
     
     #add artifacts to milestone.
     for m in milestones:
         artifacts_of_m = [artifact for artifact in artifacts
                if artifact.milestone == m.name]
         setattr(m, 'artifacts', artifacts_of_m)
     
     manager = SCMManager(self.env, req)
     nodes = manager.artifacts_to_nodes(artifacts)
     
     roots = manager.get_roots(nodes)
     
     gm = GraphManager(self.env)
     nodes = gm.to_graphnode(roots)
     
     gm.to_s(nodes)
     
     graph = gm.to_graphviz(nodes, milestones)
     
     self.env.log.info('graphviz graph:\n%s' % graph)
     
     data = {'milestones':milestones, 'artifacts':artifacts, 'graph':graph}
     return 'releaseartifactgraph.html', data, None
Example #2
0
    def _render_editor(self, req, milestone):
        # Suggest a default due time of 18:00 in the user's timezone
        now = datetime.now(req.tz)
        default_due = datetime(now.year, now.month, now.day, 18)
        if now.hour > 18:
            default_due += timedelta(days=1)
        default_due = to_datetime(default_due, req.tz)

        data = {
            'milestone': milestone,
            'datetime_hint': get_datetime_format_hint(req.lc_time),
            'default_due': default_due,
            'milestone_groups': [],
        }

        if milestone.exists:
            req.perm(milestone.resource).require('MILESTONE_MODIFY')
            milestones = [m for m in Milestone.select(self.env)
                          if m.name != milestone.name
                          and 'MILESTONE_VIEW' in req.perm(m.resource)]
            data['milestone_groups'] = group_milestones(milestones,
                'TICKET_ADMIN' in req.perm)
        else:
            req.perm(milestone.resource).require('MILESTONE_CREATE')
            if milestone.name:
                add_notice(req, _("Milestone %(name)s does not exist. You can"
                                  " create it here.", name=milestone.name))

        chrome = Chrome(self.env)
        chrome.add_jquery_ui(req)
        chrome.add_wiki_toolbars(req)
        return 'milestone_edit.html', data, None
Example #3
0
    def _render_editor(self, req, milestone):
        data = {
            'milestone': milestone,
            'datetime_hint': get_datetime_format_hint(req.lc_time),
            'default_due': self.get_default_due(req),
            'milestone_groups': [],
        }

        if milestone.exists:
            req.perm(milestone.resource).require('MILESTONE_MODIFY')
            milestones = [m for m in Milestone.select(self.env)
                          if m.name != milestone.name
                          and 'MILESTONE_VIEW' in req.perm(m.resource)]
            data['milestone_groups'] = group_milestones(milestones,
                'TICKET_ADMIN' in req.perm)
            data['num_open_tickets'] = milestone \
                                       .get_num_tickets(exclude_closed=True)
            data['retarget_to'] = self.default_retarget_to
        else:
            req.perm(milestone.resource).require('MILESTONE_CREATE')
            if milestone.name:
                add_notice(req, _("Milestone %(name)s does not exist. You can"
                                  " create it here.", name=milestone.name))

        chrome = Chrome(self.env)
        chrome.add_jquery_ui(req)
        chrome.add_wiki_toolbars(req)
        add_stylesheet(req, 'common/css/roadmap.css')
        return 'milestone_edit.html', data, None
Example #4
0
 def select(env, include_completed=True, db=None):
     if not db:
         db = env.get_db_cnx()
         
     milestones = [ StructuredMilestone(env, milestone) \
                     for milestone in Milestone.select(env, include_completed, db)]
     return StructuredMilestone.reorganize(milestones)
    def _reindex_milestone(self, realm, feedback, finish_fb):
        resources = Milestone.select(self.env)

        def check(milestone, check):
            return True

        index = self._index_milestone
        return self._index(realm, resources, check, index, feedback, finish_fb)
Example #6
0
 def default_retarget_to(self):
     if self._default_retarget_to and \
        not any(self._default_retarget_to == m.name
                for m in Milestone.select(self.env)):
         self.log.warn('Milestone "%s" does not exist. Update the '
                       '"default_retarget_to" option in the [milestone] '
                       'section of trac.ini', self._default_retarget_to)
     return self._default_retarget_to
Example #7
0
 def _modify_roadmap_page(self, req, template, data, content_type,
                          is_active):
     """Insert roadmap.css + products breadcrumb
     """
     add_stylesheet(req, 'dashboard/css/roadmap.css')
     self._add_products_general_breadcrumb(req, template, data,
                                           content_type, is_active)
     data['milestone_list'] = [m.name for m in Milestone.select(self.env)]
     req.chrome['ctxtnav'] = []
Example #8
0
    def test_select_milestones(self):
        cursor = self.db.cursor()
        cursor.executemany("INSERT INTO milestone (name) VALUES (%s)", [("1.0",), ("2.0",)])
        cursor.close()

        milestones = list(Milestone.select(self.env))
        self.assertEqual("1.0", milestones[0].name)
        assert milestones[0].exists
        self.assertEqual("2.0", milestones[1].name)
        assert milestones[1].exists
Example #9
0
    def test_select_milestones(self):
        self.env.db_transaction.executemany(
            "INSERT INTO milestone (name) VALUES (%s)",
            [('1.0',), ('2.0',)])

        milestones = list(Milestone.select(self.env))
        self.assertEqual('1.0', milestones[0].name)
        assert milestones[0].exists
        self.assertEqual('2.0', milestones[1].name)
        assert milestones[1].exists
Example #10
0
 def list_view(self, req, cat, page):
     data = {
         'view': 'list',
         'sprints': self.sm.select(),
         'format_datetime' : datefmt.format_datetime,
         'date_hint' : datefmt.get_date_format_hint(),
         'datetime_hint' : datefmt.get_datetime_format_hint(),
         'milestones' : [m.name for m in Milestone.select(self.env)],
     }
     data.update(req.args)
     return 'agilo_admin_sprint.html', data
Example #11
0
    def _render_confirm(self, req, milestone):
        req.perm(milestone.resource).require('MILESTONE_DELETE')

        milestones = [m for m in Milestone.select(self.env)
                      if m.name != milestone.name
                      and 'MILESTONE_VIEW' in req.perm(m.resource)]
        data = {
            'milestone': milestone,
            'milestone_groups': group_milestones(milestones,
                'TICKET_ADMIN' in req.perm)
        }
        return 'milestone_delete.html', data, None
 def _go_list(self, req):
     #search all artifacts.
     artifacts = ArtifactManager.find_all(self.env)
     
     milestones = [m for m in Milestone.select(self.env)
                   if 'MILESTONE_VIEW' in req.perm(m.resource)]
     
     #add artifacts to milestone.
     for m in milestones:
         artifacts_of_m = [artifact for artifact in artifacts
                if artifact.milestone == m.name]
         setattr(m, 'artifacts', artifacts_of_m)
     
     data = {'milestones':milestones, 'artifacts':artifacts}        
     return 'releaseartifactgraph.html', data, None
Example #13
0
    def users(self, req):
        """hours for all users"""

        data = { 'hours_format' :  hours_format }

        ### date data
        self.date_data(req, data)

        ### milestone data
        milestone = req.args.get('milestone')
        milestones = Milestone.select(self.env)
        data['milestones'] = milestones

        ### get the hours
        #trachours = TracHoursPlugin(self.env)
        #tickets = trachours.tickets_with_hours()
        hours = get_all_dict(self.env, 
                             "SELECT * FROM ticket_time WHERE time_started >= %s AND time_started < %s",
                             *[int(time.mktime(i.timetuple()))
                               for i in (data['from_date'], data['to_date'])])
        worker_hours = {}
        for entry in hours:
            worker = entry['worker']
            if  worker not in worker_hours:
                worker_hours[worker] = 0

            if milestone:
                if milestone != Ticket(self.env, entry['ticket']).values.get('milestone'):
                    continue

            worker_hours[worker] += entry['seconds_worked']

        worker_hours = [(worker, seconds/3600.)
                        for worker, seconds in 
                        sorted(worker_hours.items())]
        data['worker_hours'] = worker_hours

        if req.args.get('format') == 'csv':
            buffer = StringIO()
            writer = csv.writer(buffer)
            format = '%B %d, %Y'
            title = "Hours for %s" % self.env.project_name
            writer.writerow([title, req.abs_href()])
            writer.writerow([])
            writer.writerow(['From', 'To'])
            writer.writerow([data[i].strftime(format) 
                             for i in 'from_date', 'to_date'])
            if milestone:
Example #14
0
    def users(self, req):
        """hours for all users"""

        data = {'hours_format':  hours_format}

        ### date data
        self.date_data(req, data)

        ### milestone data
        milestone = req.args.get('milestone')
        milestones = Milestone.select(self.env)
        data['milestones'] = milestones

        ### get the hours
        #trachours = TracHoursPlugin(self.env)
        #tickets = trachours.tickets_with_hours()
        hours = get_all_dict(self.env, """
            SELECT * FROM ticket_time
            WHERE time_started >= %s AND time_started < %s
            """, *[int(time.mktime(i.timetuple()))
                   for i in (data['from_date'], data['to_date'])])
        worker_hours = {}
        for entry in hours:
            worker = entry['worker']
            if worker not in worker_hours:
                worker_hours[worker] = 0

            if milestone and milestone != \
                    Ticket(self.env, entry['ticket']).values.get('milestone'):
                continue

            worker_hours[worker] += entry['seconds_worked']

        worker_hours = [(worker, seconds/3600.)
                        for worker, seconds in 
                        sorted(worker_hours.items())]
        data['worker_hours'] = worker_hours

        if req.args.get('format') == 'csv':
            req.send(self.export_csv(req, data))

        #add_link(req, 'prev', self.get_href(query, args, context.href),
        #         _('Prev Week'))
        #add_link(req, 'next', self.get_href(query, args, context.href),
        #         _('Next Week'))
        #prevnext_nav(req, _('Prev Week'), _('Next Week'))

        return 'hours_users.html', data, "text/html"
Example #15
0
    def process_request(self, req):
        req.perm.require('ROADMAP_VIEW')

        show = req.args.getlist('show')
        if 'all' in show:
            show = ['completed']

        milestones = Milestone.select(self.env, 'completed' in show)
        if 'noduedate' in show:
            milestones = [m for m in milestones
                          if m.due is not None or m.completed]
        milestones = [m for m in milestones
                      if 'MILESTONE_VIEW' in req.perm(m.resource)]

        stats = []
        queries = []

        for milestone in milestones:
            tickets = get_tickets_for_milestone(
                    self.env, milestone=milestone.name, field='owner')
            tickets = apply_ticket_permissions(self.env, req, tickets)
            stat = get_ticket_stats(self.stats_provider, tickets)
            stats.append(milestone_stats_data(self.env, req, stat,
                                              milestone.name))
            #milestone['tickets'] = tickets # for the iCalendar view

        if req.args.get('format') == 'ics':
            self._render_ics(req, milestones)
            return

        # FIXME should use the 'webcal:' scheme, probably
        username = None
        if req.authname and req.authname != 'anonymous':
            username = req.authname
        icshref = req.href.roadmap(show=show, user=username, format='ics')
        add_link(req, 'alternate', auth_link(req, icshref), _('iCalendar'),
                 'text/calendar', 'ics')

        data = {
            'milestones': milestones,
            'milestone_stats': stats,
            'queries': queries,
            'show': show,
        }
        add_stylesheet(req, 'common/css/roadmap.css')
        return 'roadmap.html', data, None
Example #16
0
    def _render_confirm(self, req, milestone):
        req.perm(milestone.resource).require('MILESTONE_DELETE')

        milestones = [m for m in Milestone.select(self.env)
                      if m.name != milestone.name
                      and 'MILESTONE_VIEW' in req.perm(m.resource)]
        attachments = Attachment.select(self.env, self.realm, milestone.name)
        data = {
            'milestone': milestone,
            'milestone_groups': group_milestones(milestones,
                'TICKET_ADMIN' in req.perm),
            'num_tickets': milestone.get_num_tickets(),
            'retarget_to': self.default_retarget_to,
            'attachments': list(attachments)
        }
        add_stylesheet(req, 'common/css/roadmap.css')
        return 'milestone_delete.html', data, None
Example #17
0
    def detail_view(self, req, cat, page, name):
        sprint = self.sm.get(name=name)
        if not sprint or not sprint.exists:
            return req.redirect(req.href.admin(cat, page))

        data = {
            'view': 'detail',
            'sprint': sprint,
            'teams': self.tm.select(),
            'format_datetime': datefmt.format_datetime,
            'date_hint': datefmt.get_date_format_hint(),
            'datetime_hint': datefmt.get_datetime_format_hint(),
            'milestones': [m.name for m in Milestone.select(self.env)],
        }
        data.update(req.args)
        add_script(req, 'common/js/wikitoolbar.js')
        return 'agilo_admin_sprint.html', data
Example #18
0
    def _modify_resource_breadcrumb(self, req, template, data, content_type,
                                    is_active):
        """Provides logic for breadcrumb resource permissions
        """
        if data and ('ticket' in data.keys()) and data['ticket'].exists:
            data['resourcepath_template'] = 'bh_path_ticket.html'
            # determine path permissions
            for resname, permname in [('milestone', 'MILESTONE_VIEW'),
                                      ('product', 'PRODUCT_VIEW')]:
                res = Resource(resname, data['ticket'][resname])
                data['path_show_' + resname] = permname in req.perm(res)

            # add milestone list + current milestone to the breadcrumb
            data['milestone_list'] = [m.name
                                      for m in Milestone.select(self.env)]
            mname = data['ticket']['milestone']
            if mname:
                data['milestone'] = Milestone(self.env, mname)
Example #19
0
 def task(add):
     """the thread task - either we are discovering or adding events"""
     with lock:
         env = ProductEnvironment(self.global_env,
                                  self.default_product)
         if add:
             name = 'milestone_from_' + threading.current_thread().name
             milestone = Milestone(env)
             milestone.name = name
             milestone.insert()
         else:
             # collect the names of milestones reported by Milestone and
             # directly from the db - as sets to ease comparison later
             results.append({
                 'from_t': set([m.name for m in Milestone.select(env)]),
                 'from_db': set(
                     [v[0] for v in self.env.db_query(
                         "SELECT name FROM milestone")])})
Example #20
0
    def get_data_burndown(self,printer):
        self.log.debug("get_data_burndown") 
        database = self.env.get_db_cnx()
        cursor = database.cursor()
        
        data = {
                'milestones' : [],
                'date_hint': get_date_format_hint(),
                'printmilestone': printer,
                }
        
		# get all milestones
        milestones = [m for m in Milestone.select(self.env, True, database)]
        
		# prepare data for each milestone
        p = re.compile('[0-9]+')
        for milest in milestones:
            self.log.debug(milest.completed)
            tickets = _get_tickets_for_milestone(self.env, database, milest.name, 'history_size')
            self.log.debug(tickets)
            tasks = _get_tasks_for_milestone(self, database, milest.name)
            
            # calculate totalpoints of sprint
            totalpoints = 0
            for idx, ticket in enumerate(tickets):
                if ticket['history_size'] and p.match(ticket['history_size']):
                    totalpoints += int(ticket['history_size'])
            
			# prepare graph data
            graphdata = {'points': totalpoints, 'tasks': tasks, 'printer': printer}
            self.log.debug(graphdata)

			# if totalpoints and total tasks equal the zero not display burndown chart
            if totalpoints > 0 and len(tasks) > 0:
                graph_image_path = _get_graphflash_sprintburndown(self,milest.name,graphdata)
            else:
                graph_image_path = None
            
    		# data the all milestones
            d = {'milestone' : milest, 'tickets' : tickets, 'tasks': tasks, 'graph': graph_image_path, 'teste': totalpoints}
            data['milestones'].append(d);
            
        return data;
Example #21
0
 def process_admin_request(self, req, cat, page, path_info):
     envs = DatamoverSystem(self.env).all_environments()
     milestones = [m.name for m in Milestone.select(self.env)]
     
     if req.method == 'POST':
         source_type = req.args.get('source')
         if not source_type or source_type not in ('milestone', 'all'):
             raise TracError, "Source type not specified or invalid"
         source = req.args.get(source_type)
         dest = req.args.get('destination')
         action = None
         if 'copy' in req.args.keys():
             action = 'copy'
         elif 'move' in req.args.keys():
             action = 'move'
         else:
             raise TracError, 'Action not specified or invalid'
             
         action_verb = {'copy':'Copied', 'move':'Moved'}[action]
         
         milestone_filter = None
         if source_type == 'milestone':
             in_milestones = req.args.getlist('milestone')
             milestone_filter = lambda c: c in in_milestones
         elif source_type == 'all':
             milestone_filter = lambda c: True
         
         try:
             sel_milestones = [m for m in milestones if milestone_filter(m)]
             dest_db = _open_environment(dest).get_db_cnx()
             for milestone in sel_milestones:
                 copy_milestone(self.env, dest, milestone, dest_db)
             dest_db.commit()
                 
             if action == 'move':
                 for milestone in sel_milestones:
                     Milestone(self.env, milestone).delete()
                 
             req.hdf['datamover.message'] = '%s milestones %s'%(action_verb, ', '.join(sel_milestones))
         except TracError, e:
             req.hdf['datamover.message'] = "An error has occured: \n"+str(e)
             self.log.warn(req.hdf['datamover.message'], exc_info=True)
 def _get_milestones(self):
     return Milestone.select(self.env, include_completed=False)
Example #23
0
    def expand_macro(self, formatter, name, content):
        args, kwargs = parse_args(content, strict=False)

        milestones = filter(lambda m: not args or any(map(lambda a: m.name.startswith(a), args)), Milestone.select(self.env))

        req = formatter.req
        template = "listmilestones.html"
        data = {'milestones' : milestones}
        content_type = 'text/html'

        dispatcher = RequestDispatcher(self.env)
        response = dispatcher._post_process_request(req, template, data, content_type)

        # API < 1.1.2 does not return a method.
        if (len(response) == 3):
            template, data, content_type = response
            return Markup(Chrome(self.env).render_template(formatter.req, template, data, content_type=content_type, fragment=True))

        template, data, content_type, method = response
        return Markup(Chrome(self.env).render_template(formatter.req, template, data, content_type=content_type, fragment=True, method=method))
    def process_request(self, req):
        name = req.args.get('name')


        if not (name == 'query'):
            id = req.args.get('id')

        if name == 'ticket':
            ticket = Ticket(self.env, id)
            comm_num = 0
            attachment_num = len(self.get_attachments('ticket', id))
            ticket_log = self.changeLog(id)

            for log in ticket_log:
                if log[2] == 'comment' and log[4]:
                    comm_num += 1

            data = {'ticket': ticket,
                    'comm_num': comm_num,
                    'attachment_num': attachment_num}
            return 'bh_emb_ticket.html', data, None

        elif name == 'milestone':
            ticket_num = len(get_tickets_for_milestone(self.env, milestone=id))
            attachment_num = len(self.get_attachments('milestone', id))

            data = {'milestone': Milestone(self.env, id),
                    'product': self.env.product,
                    'ticket_number': ticket_num,
                    'attachment_number': attachment_num }
            return 'bh_emb_milestone.html', data, None

        elif name == 'products':
            product = Product(self.env, {'prefix': id})
            ticket_num = len(self.get_tickets_for_product(self.env, id))
            product_env = ProductEnvironment(self.env, product.prefix)
            milestone_num = len(Milestone.select(product_env))
            version_num = len(Version.select(product_env))
            components = component.select(product_env)
            component_num = 0

            for c in components:
                component_num += 1

            data = {'product': product,
                    'ticket_num': ticket_num,
                    'owner': product.owner,
                    'milestone_num': milestone_num,
                    'version_num': version_num,
                    'component_num': component_num}
            return 'bh_emb_product.html', data, None
        elif name == 'query':
            qstr = req.query_string
            qstr = urllib.unquote(qstr).decode('utf8')

            if qstr=='':
                qstr = 'status!=closed'

            qresults = self.query(req, qstr)
            filters = qresults[0]
            tickets = qresults[1]

            data={'tickets': tickets,
                  'query': qstr,
                  'filters': filters}
            return 'bh_emb_query.html', data, None
        else:
            msg = "It is not possible to embed this resource."
            raise ResourceNotFound((msg), ("Invalid resource"))
Example #25
0
    def _render_view(self, req, milestone):
        milestone_groups = []
        available_groups = []
        component_group_available = False
        ticket_fields = TicketSystem(self.env).get_ticket_fields()

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

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

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

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

        if by:
            def per_group_stats_data(gstat, group_name):
                return milestone_stats_data(self.env, req, gstat,
                                            milestone.name, by, group_name)
            milestone_groups.extend(
                grouped_stats_data(self.env, self.stats_provider, tickets, by,
                                   per_group_stats_data))

        add_stylesheet(req, 'common/css/roadmap.css')
        add_script(req, 'common/js/folding.js')

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

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

        return 'milestone_view.html', data, None
Example #26
0
 def _sorted_milestones(self):
     """ Return a sorted list of active milestones. """
     milestones = Milestone.select(self.env, include_completed=False)
     return [m.name for m in milestones]
    def process_request(self, req):

        req.perm.assert_permission('BACKLOG_VIEW')

        ats = AgileToolsSystem(self.env)

        if req.get_header('X-Requested-With') == 'XMLHttpRequest':

            if req.method == "POST":

                if not req.perm.has_permission("BACKLOG_ADMIN"):
                    return self._json_errors(req, ["BACKLOG_ADMIN permission required"])

                str_ticket= req.args.get("ticket")
                str_relative = req.args.get("relative", 0)
                direction = req.args.get("relative_direction")
                milestone = req.args.get("milestone")

                # Moving a single ticket position (and milestone)
                if str_ticket:
                    try:
                        int_ticket = int(str_ticket)
                        int_relative = int(str_relative)
                    except (TypeError, ValueError):
                        return self._json_errors(req, ["Invalid arguments"])

                    try:
                        ticket = Ticket(self.env, int_ticket)
                    except ResourceNotFound:
                        return self._json_errors(req, ["Not a valid ticket"])

                    response = {}

                    # Change ticket's milestone
                    if milestone is not None:
                        try:
                            self._save_ticket(req, ticket, milestone)
                            ticket = self._get_permitted_tickets(req, constraints={'id': [str(int_ticket)]})
                            response['tickets'] = self._get_ticket_data(req, ticket)
                        except ValueError as e:
                            return self._json_errors(req, e.message)

                    # Reposition ticket
                    if int_relative:
                        position = ats.position(int_relative, generate=True)
                        if direction == "after":
                            position += 1

                        ats.move(int_ticket, position, author=req.authname)
                        response['success'] = True

                    return self._json_send(req, response)

                # Dropping multiple tickets into a milestone
                elif all (k in req.args for k in ("tickets", "milestone", "changetimes")):

                    changetimes = req.args["changetimes"].split(",")
                    milestone = req.args["milestone"]

                    try:
                        ids = [int(tkt_id) for tkt_id in req.args["tickets"].split(",")]
                    except (ValueError, TypeError):
                        return self._json_errors(req, ["Invalid arguments"])

                    unique_errors = 0
                    errors_by_ticket = []
                    # List of [<ticket_id>, [<error>, ...]] lists
                    if len(ids) == len(changetimes):
                        for i, int_ticket in enumerate(ids):
                            # Valid ticket
                            try:
                                ticket = Ticket(self.env, int_ticket)
                            except ResourceNotFound:
                                errors_by_ticket.append([int_ticket, ["Not a valid ticket"]])
                            # Can be saved
                            try:
                                self._save_ticket(req, ticket, milestone, ts=changetimes[i])
                            except ValueError as e:
                                # Quirk: all errors amalgomated into single
                                # we keep track of count at each time so that
                                # we can split the list up to errors by
                                # individual tickets
                                errors_by_ticket.append([int_ticket, e.message[unique_errors:]])
                                unique_errors = len(e.message)
                                if len(errors_by_ticket) > 5:
                                    errors_by_ticket.append("More than 5 tickets failed "
                                                            "validation, stopping.")
                                    break
                        if errors_by_ticket:
                            return self._json_errors(req, errors_by_ticket)
                        else:
                            # Client side makes additional request for all
                            # tickets after this
                            return self._json_send(req, {'success': True})
                    else:
                        return self._json_errors(req, ["Invalid arguments"])
                else:
                    return self._json_errors(req, ["Must provide a ticket"])
            else:
                # TODO make client side compatible with live updates
                milestone = req.args.get("milestone")
                from_iso = req.args.get("from")
                to_iso = req.args.get("to")

                if milestone is not None:
                    # Requesting an update
                    constr = { 'milestone': [milestone] }
                    if from_iso and to_iso:
                        constr['changetime'] = [from_iso + ".." + to_iso]

                    tickets = self._get_permitted_tickets(req, constraints=constr)
                    formatted = self._get_ticket_data(req, tickets)
                    self._json_send(req, {'tickets': formatted})
                else:
                    self._json_errors(req, ["Invalid arguments"])

        else:
            add_script(req, 'agiletools/js/jquery.history.js')
            add_script(req, "agiletools/js/update_model.js")
            add_script(req, "agiletools/js/backlog.js")
            add_stylesheet(req, "agiletools/css/backlog.css")

            milestones_select2 = Milestone.select_names_select2(self.env, include_complete=False)
            milestones_select2['results'].insert(0, {
                "children": [],
                "text": "Product Backlog",
                "id": "backlog",
                "is_backlog": True,
            })

            milestones_flat = [milestone.name for milestone in
                               Milestone.select(self.env, include_completed=False, include_children=True)]

            script_data = { 
                'milestones': milestones_select2,
                'milestonesFlat': milestones_flat,
                'backlogAdmin': req.perm.has_permission("BACKLOG_ADMIN"),
                }

            add_script_data(req, script_data)
            data = {'top_level_milestones': Milestone.select(self.env)}
            # Just post the basic template, with a list of milestones
            # The JS will then make a request for tickets in no milestone
            # and tickets in the most imminent milestone
            # The client will be able to make subsequent requests to pull
            # tickets from other milestones and drop tickets into them
            return "backlog.html", data, None
 def _add_milestones (self, req, ul):
     for milestone in Milestone.select(self.env, False):
         label = milestone.name
         href = '/milestone/' + label
         self._add_item(req, ul, label, href)