def _get_ticket_data(self, req, results):
        ats = AgileToolsSystem(self.env)
        loc = LogicaOrderController(self.env)
        closed_statuses = loc.type_and_statuses_for_closed_statusgroups()

        # TODO calculate which statuses are closed using the query system
        # when it is able to handle this
        tickets = []
        for result in results:
            if result['status'] not in closed_statuses[result['type']]:
                filtered_result = dict((k, v)
                                   for k, v in result.iteritems()
                                   if k in self.fields)

                if "remaininghours" in filtered_result:
                    try:
                        hours = float(filtered_result["remaininghours"])
                    except (ValueError, TypeError):
                        hours = 0
                    del filtered_result["remaininghours"]
                else:
                    hours = 0

                if "effort" in filtered_result:
                    try:
                        storypoints = float(filtered_result['effort'])
                    except (ValueError, TypeError):
                        storypoints = 0
                else:
                    storypoints = 0

                reporter = filtered_result["reporter"]
                session = DetachedSession(self.env, reporter)

                filtered_result.update({
                    'id': result['id'],
                    'position': ats.position(result['id']),
                    'hours': hours,
                    'effort': storypoints,
                    'reporter': session.get('name', reporter),
                    'changetime': to_utimestamp(filtered_result['changetime'])
                    })

                tickets.append(filtered_result)

        return tickets
    def _get_user_data_(self, req, milestone, field, results, fields):
        """Get data grouped by users. Includes extra user info."""
        ats = AgileToolsSystem(self.env)
        sp = SimplifiedPermissions(self.env)

        tickets_json = defaultdict(lambda: defaultdict(dict))

        all_users = []
        user_data = {}
        use_avatar = self.config.get('avatar','mode').lower() != 'off'

        # TODO: allow the task board to respect user groups
        for group, data in sp.group_memberships().items():
            for member in data['members']:
                if member.sid not in user_data:
                    all_users.append(member.sid);
                    user_data[member.sid] = {
                        'name': member.get("name", member.sid),
                        'avatar': use_avatar and req.href.avatar(member.sid) or None
                    }

        def name_for_sid(sid):
            return user_data[sid]["name"] if sid in user_data else sid

        options = [""] + sorted(all_users, key=name_for_sid)

        for result in results:
            ticket = Ticket(self.env, result['id'])
            filtered_result = dict((k, v)
                                   for k, v in result.iteritems()
                                   if k in fields)
            filtered_result['position'] = ats.position(result['id'])
            filtered_result['_changetime'] = to_utimestamp(result['changetime'])
            # we use Trac's to_json() (through add_script_data), so
            # we'll replace any types which can't be json serialised
            for k, v in filtered_result.items():
                if isinstance(v, datetime): filtered_result[k] = pretty_age(v)
            group_field_val = ticket.get_value_or_default(field["name"]) or ""
            tickets_json[group_field_val][result["id"]] = filtered_result

        return (field["name"], tickets_json, options, user_data)
    def _get_standard_data_(self, req, milestone, field, results, fields):
        """Get ticket information when no custom grouped-by method present."""
        ats = AgileToolsSystem(self.env)
        tickets_json = defaultdict(lambda: defaultdict(dict))

        # Allow for the unset option
        options = [""] + [option for option in field["options"]]

        for result in results:
            ticket = Ticket(self.env, result['id'])
            filtered_result = dict((k, v)
                                   for k, v in result.iteritems()
                                   if k in fields)
            filtered_result['position'] = ats.position(result['id'])
            filtered_result['_changetime'] = to_utimestamp(result['changetime'])
            # we use Trac's to_json() (through add_script_data), so
            # we'll replace any types which can't be json serialised
            for k, v in filtered_result.items():
                if isinstance(v, datetime): filtered_result[k] = pretty_age(v)
            group_field_val = ticket.get_value_or_default(field["name"]) or ""
            tickets_json[group_field_val][result["id"]] = filtered_result

        return (field["name"], tickets_json, options)
    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 _get_status_data(self, req, milestone, field, results, fields):
        """Get data grouped by WORKFLOW and status.

        It's not possible to show tickets in different workflows on the same
        taskboard, so we create an additional outer group for workflows.
        We then get the workflow with the most tickets, and show that first"""
        ats = AgileToolsSystem(self.env)
        loc = LogicaOrderController(self.env)

        # Data for status much more complex as we need to track the workflow
        tickets_json = defaultdict(lambda: defaultdict(dict))
        by_type = defaultdict(int)
        by_wf = defaultdict(int)
        wf_for_type = {}

        # Store the options required in order to complete an action
        # E.g. closing a ticket requires a resolution
        act_controls = {}

        for r in results:
            # Increment type statistics
            by_type[r['type']] += 1
            tkt = Ticket(self.env, r['id'])
            if r['type'] not in wf_for_type:
                wf_for_type[r['type']] = \
                    loc._get_workflow_for_typename(r['type'])
            wf = wf_for_type[r['type']]

            state = loc._determine_workflow_state(tkt, req=req)
            op = Operation(self.env, wf, state)
            filtered = dict((k, v)
                            for k, v in r.iteritems()
                            if k in fields)
            filtered['position'] = ats.position(r['id'])
            filtered['_changetime'] = to_utimestamp(r['changetime'])
            # we use Trac's to_json() (through add_script_data), so
            # we'll replace any types which can't be json serialised
            for k, v in filtered.items():
                if isinstance(v, datetime): filtered[k] = pretty_age(v)
            filtered['actions'] = self._get_status_actions(req, op, wf, state)
            # Collect all actions requiring further input
            self._update_controls(req, act_controls, filtered['actions'], tkt)

            tickets_json[wf.name][r["status"]][r["id"]] = filtered

        # Calculate number of tickets per workflow
        for ty in by_type:
            by_wf[wf_for_type[ty]] += by_type[ty]

        wf_statuses = dict((wf.name, wf.ordered_statuses) for wf in by_wf)

        # Retrieve Kanban-style status limits
        db = self.env.get_read_db()
        cursor = db.cursor()
        cursor.execute("""
            SELECT status, hardlimit FROM kanban_limits
            WHERE milestone = %s""", (milestone,))
        status_limits = dict((limit[0], limit[1]) for limit in cursor)

        # Initially show the most used workflow
        show_first = max(by_wf, key=lambda n: by_wf[n]).name
        return ("status", tickets_json, wf_statuses, status_limits, show_first, act_controls)