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)