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 process_request(self, req):

        req.perm.assert_permission('TICKET_VIEW')

        # set the default user query
        if req.path_info == '/taskboard/set-default-query' and req.method == 'POST':
            self._set_default_query(req)

        # these headers are only needed when we update tickets via ajax
        req.send_header("Cache-Control", "no-cache, no-store, must-revalidate")
        req.send_header("Pragma", "no-cache")
        req.send_header("Expires", 0)

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

        group_by = req.args.get("group", "status")

        user_saved_query = False

        milestones = Milestone.select_names_select2(self.env, include_complete=False)

        # Try to find a user selected milestone in request - if not found 
        # check session_attribute for a user saved default, and if that is also
        # not found and fall back on the most upcoming milestone by due date
        milestone = req.args.get("milestone")
        milestone_not_found = False
        if milestone:
            try:
                Milestone(self.env, milestone)
            except ResourceNotFound:
                milestone_not_found = True
                milestone = None

        if not milestone:
            # try and find a user saved default
            default_milestone = req.session.get('taskboard_user_default_milestone')
            if default_milestone:
                milestone = default_milestone
                group_by = req.session.get('taskboard_user_default_group')
                user_saved_query = True

            # fall back to most imminent milestone by due date
            elif len(milestones["results"]):
                milestone = milestones["results"][0]["text"]

        # Ajax post
        if req.args.get("ticket") and xhr:
            result = self.save_change(req, milestone)
            req.send(to_json(result), 'text/json')
        else:
            data = {}
            constr = {}

            if milestone:
                constr['milestone'] = [milestone]

            # Ajax update: tickets changed between a period
            if xhr:
                from_iso = req.args.get("from", "")
                to_iso = req.args.get("to", "")
                if from_iso and to_iso:
                    constr['changetime'] = [from_iso + ".." + to_iso]

            # Get all tickets by milestone and specify ticket fields to retrieve
            cols = self._get_display_fields(req, user_saved_query)
            tickets = self._get_permitted_tickets(req, constraints=constr, 
                                                  columns=cols)
            sorted_cols = sorted([f for f in self.valid_display_fields
                    if f['name'] not in ('summary', 'type')],
                    key=lambda f: f.get('label'))

            if tickets:
                s_data = self.get_ticket_data(req, milestone, group_by, tickets)
                s_data['total_tickets'] = len(tickets)
                s_data['display_fields'] = cols
                data['cur_group'] = s_data['groupName']
            else:
                s_data = {}
                data['cur_group'] = group_by

            if xhr:
                if constr.get("changetime"):
                    s_data['otherChanges'] = \
                        self.all_other_changes(req, tickets, constr['changetime'])

                req.send(to_json(s_data), 'text/json')
            else:
                s_data.update({
                    'formToken': req.form_token,
                    'milestones': milestones,
                    'milestone': milestone,
                    'group': group_by,
                    'default_columns': self.default_display_fields
                })
                data.update({
                    'milestone_not_found': milestone_not_found,
                    'current_milestone': milestone,
                    'group_by_fields': self.valid_grouping_fields,
                    'fields': dict((f['name'], f) for f in self.valid_display_fields),
                    'all_columns': [f['name'] for f in sorted_cols],
                    'col': cols,
                    'condensed': self._show_condensed_view(req, user_saved_query)
                })

                add_script(req, 'agiletools/js/update_model.js')
                add_script(req, 'agiletools/js/taskboard.js')
                add_script(req, 'common/js/query.js')
                add_script_data(req, s_data)

                add_stylesheet(req, 'agiletools/css/taskboard.css')
                add_stylesheet(req, 'common/css/ticket.css')
                add_ctxtnav(req, tag.a(tag.i(class_='fa fa-bookmark'),
                                       _(" Set as default"),
                                       id_='set-default-query',
                                       title=_("Make this your default taskboard")))
                return "taskboard.html", data, None