def taskEditPost(handler, ids, p_hours, p_status, p_goal, p_assigned=[], p_include={}): handler.title("Edit tasks") requirePriv(handler, "Write") allIDs = map(int, uniq(ids.split(","))) ids = map(lambda i: to_int(i, "include", ErrorBox.die), p_include.keys()) if not set(ids) <= set(allIDs): ErrorBox.die("Included tasks don't match query arguments") tasks = dict((id, Task.load(id)) for id in ids) if not all(tasks.values()): ids = [str(id) for (id, task) in tasks.iteritems() if not task] ErrorBox.die( "No %s with %s %s" % ("task" if len(ids) == 1 else "tasks", "ID" if len(ids) == 1 else "IDs", ", ".join(ids)) ) tasks = [tasks[id] for id in ids] if len(set(task.sprint for task in tasks)) > 1: ErrorBox.die("All tasks must be in the same sprint") sprint = (tasks[0] if len(tasks) > 0 else Task.load(allIDs[0])).sprint if sprint.isHidden(handler.session["user"]): ErrorBox.die( "No %s with %s %s" % ("task" if len(ids) == 1 else "tasks", "ID" if len(ids) == 1 else "IDs", ", ".join(ids)) ) if not sprint.canEdit(handler.session["user"]): ErrorBox.die("You don't have permission to modify this sprint") assignedids = set(to_int(i, "assigned", ErrorBox.die) for i in p_assigned) changes = { "assigned": False if assignedids == set() else {User.load(assignedid) for assignedid in assignedids}, "hours": False if p_hours == "" else int(p_hours), "status": False if p_status == "" else p_status, "goal": False if p_goal == "" else Goal.load(int(p_goal)), } if changes["assigned"] and not all(changes["assigned"]): ErrorBox.die("Invalid assignee") if changes["assigned"] and not set(changes["assigned"]).issubset(sprint.members): ErrorBox.die("Unable to assign tasks to non-sprint members") if changes["goal"] and changes["goal"].sprint != sprint: ErrorBox.die("Unable to set goal to a goal outside the sprint") changed = set() for task in tasks: for field, value in changes.iteritems(): if value is not False and getattr(task, field) != value: setattr(task, field, value) changed.add(task) Event.taskUpdate(handler, task, field, value) if len(changed) == 0: delay(handler, WarningBox("No changes necessary", close=3, fixed=True)) else: for task in changed: task.saveRevision(handler.session["user"]) delay(handler, SuccessBox("Updated %d %s" % (len(changed), "task" if len(changed) == 1 else "tasks"))) redirect("/sprints/%d" % sprint.id)
def newNoteModify(handler, taskid, id, p_action): handler.title("New Note") requirePriv(handler, "User") if p_action != "delete": ErrorBox.die("Invalid Action", "Unrecognized action <b>%s</b>" % p_action) taskid = int(taskid) task = Task.load(taskid) if not task or task.sprint.isHidden(handler.session["user"]): ErrorBox.die("Invalid Task", "No task with ID <b>%d</b>" % taskid) id = int(id) note = Note.load(id) if not note: ErrorBox.die("Invalid Note", "No note with ID <b>%d</b>" % noteid) elif note.task != task: # Doesn't really matter, but shouldn't happen ErrorBox.die("Task mismatch", "Note/task mismatch") elif note.user != handler.session["user"]: ErrorBox.die("Permission denied", "Notes can only be deleted by their creators") note.delete() delay(handler, SuccessBox("Deleted note", close=3)) Event.deleteNote(handler, note) redirect("/tasks/%d" % task.id)
def taskByID(context, id): id = to_int(id, 'id', fail) context['task'] = Task.load(id) if not context['task']: fail("No task with ID %d" % id) print context['task'].safe.name return ('task', "[task %d] $" % id)
def newNotePost(handler, taskid, p_body, dryrun=False): handler.title("New Note") if dryrun: handler.wrappers = False requirePriv(handler, "User") taskid = int(taskid) task = Task.load(taskid) if not task or task.sprint.isHidden(handler.session["user"]): ErrorBox.die("Invalid Task", "No task with ID <b>%d</b>" % taskid) note = Note(task.id, handler.session["user"].id, p_body) if dryrun: print note.render() else: if p_body == "": ErrorBox.die("Empty Body", "No note provided") note.save() Event.newNote(handler, note) redirect("/tasks/%d#note%d" % (task.id, note.id))
def sprintPost(handler, sprintid, p_id, p_rev_id, p_field, p_value): def die(msg): print msg done() handler.wrappers = False sprintid = to_int(sprintid, 'sprintid', die) p_id = to_int(p_id, 'id', die) p_rev_id = to_int(p_rev_id, 'rev_id', die) if not handler.session['user']: die("You must be logged in to modify tasks") sprint = Sprint.load(sprintid) if not sprint or sprint.isHidden(handler.session['user']): die("There is no sprint with ID %d" % sprintid) elif not (sprint.isActive() or sprint.isPlanning()): die("Unable to modify inactive sprint") elif not sprint.canEdit(handler.session['user']): die("You don't have permission to modify this sprint") # Special case group moves; p_id is the group ID, not task task = None if p_field != 'groupmove': task = Task.load(p_id) if not task: die("Task %d does not exist" % p_id) if task.sprint != sprint: die("Attempting to modify task outside the specified sprint") if task.revision != p_rev_id: #TODO Implement collision support die("Collision with %s detected. Changes not saved" % task.creator) if p_value.strip() == '': die("Value cannot be empty") if p_field in ['status', 'name', 'goal', 'assigned', 'hours', 'deleted']: for case in switch(p_field): if case('status') or case('name'): parsedValue = p_value break elif case('goal'): parsedValue = None if p_value != '0': parsedValue = Goal.load(to_int(p_value, 'goal', die)) if not parsedValue: die("Unknown goal: <b>%s</b>" % stripTags(p_value)) if parsedValue.sprint != sprint: die("Attempting to use goal outside the specified sprint") break elif case('assigned'): parsedValue = set(User.load(username = username) for username in p_value.split(' ')) if not all(parsedValue): die("Unknown user(s): <b>%s</b>" % stripTags(p_value)) break elif case('hours'): parsedValue = int(p_value) break elif case('deleted'): parsedValue = True if p_value == 'true' else False if p_value == 'false' else die("Bad value for field 'deleted'") break if task.__getattribute__(p_field) != parsedValue: # Only save if the field has changed task.__setattr__(p_field, parsedValue) # Is this within the 5-minute window, by the same user? # If we're in pre-planning, the task's timestamp will be in the future, so (ts - task.timestamp) will be negative, which satisfies the check if task.creator == handler.session['user'] and (dateToTs(getNow()) - task.timestamp) < 5*60: task.save() else: task.saveRevision(handler.session['user']) Event.taskUpdate(handler, task, p_field, parsedValue) elif p_field == 'taskmove': if ':' not in p_value: die("Malformed value") newGroupID, newSeq = map(lambda i: to_int(i, 'value', die), p_value.split(':', 1)) newGroup = Group.load(newGroupID) if not newGroup: die("No group with ID %d" % newGroupID) maxSeq = len(Task.loadAll(groupid = newGroup.id)) if task.group != newGroup: maxSeq += 1 if not 1 <= newSeq <= maxSeq: die("Bad sequence number") task.move(newSeq, newGroup) elif p_field == 'groupmove': group = Group.load(p_id) if not group: die("Group %d does not exist" % p_id) if group.sprint != sprint: die("Attempting to modify group outside the specified sprint") newSeq = to_int(p_value, 'value', die) if not 1 <= newSeq <= len(sprint.getGroups()): die("Bad sequence number") group.move(newSeq) else: die("Unexpected field name: %s" % stripTags(p_field)) handler.responseCode = 299 if task is not None: print task.revision
def distributeUpdate(handler, p_sprint, p_targetUser=None, p_task=None): def die(msg): print toJS({"error": msg}) done() handler.title("Distribute Tasks") requirePriv(handler, "Write") handler.wrappers = False handler.contentType = "application/json" sprintid = int(p_sprint) sprint = Sprint.load(sprintid) if not sprint or sprint.isHidden(handler.session["user"]): die("No sprint with ID %d" % sprintid) if not sprint.canEdit(handler.session["user"]): die("Unable to edit sprint") # Make changes if p_targetUser != None and p_task != None: task = Task.load(int(p_task)) if not task: die("Invalid task ID") if p_targetUser == "deferred": task.status = "deferred" task.hours = 0 if task.creator == handler.session["user"] and (dateToTs(getNow()) - task.timestamp) < 5 * 60: task.save() else: task.saveRevision(handler.session["user"]) Event.taskUpdate(handler, task, "status", task.status) else: userid = to_int(p_targetUser, "targetUser", die) user = User.load(userid) if not user: die("No user with ID %d" % userid) task.assigned = {user} if task.creator == handler.session["user"] and (dateToTs(getNow()) - task.timestamp) < 5 * 60: task.save() else: task.saveRevision(handler.session["user"]) Event.taskUpdate(handler, task, "assigned", task.assigned) def makeTaskMap(task): return { "id": task.id, "groupid": task.group.id, "hours": task.hours, "name": task.name, "important": task.hours > 8, "team": len(task.assigned) > 1, } # Return current info tasks = filter(lambda task: task.stillOpen(), sprint.getTasks()) avail = Availability(sprint) deferredTasks = filter(lambda task: task.status == "deferred", sprint.getTasks()) m = { "deferred": { "username": "******", "groups": [ {"id": group.id, "name": group.name} for group in sorted( (group for group in set(task.group for task in deferredTasks)), key=lambda group: group.seq ) ], "tasks": [makeTaskMap(task) for task in deferredTasks], } } for user in sprint.members: userTasks = filter(lambda task: user in task.assigned, tasks) m[user.id] = { "username": user.username, "hours": sum(task.hours for task in userTasks), "availability": avail.getAllForward(getNow().date(), user), "groups": [ {"id": group.id, "name": group.name} for group in sorted( (group for group in set(task.group for task in userTasks)), key=lambda group: group.seq ) ], "tasks": [makeTaskMap(task) for task in userTasks], } print toJS(m)
def task(handler, ids): requirePriv(handler, "User") Chart.include() Markdown.head(".note .text .body pre code") print '<script src="/static/jquery.typing-0.2.0.min.js" type="text/javascript"></script>' undelay(handler) tasks = {} if "," not in ids: # Single ID ids = [int(ids)] tasks[ids[0]] = Task.load(ids[0]) def header(task, text, level): if level == 1: handler.title(text) else: print "<h%d>%s</h%d>" % (level, text, level) else: # Many IDs ids = map(int, uniq(ids.split(","))) tasks = {id: Task.load(id) for id in ids} handler.title("Task Information") if not all(tasks.values()): ids = [str(id) for (id, task) in tasks.iteritems() if not task] ErrorBox.die( "No %s with %s %s" % ("task" if len(ids) == 1 else "tasks", "ID" if len(ids) == 1 else "IDs", ", ".join(ids)) ) if len(set(task.sprint for task in tasks.values())) == 1: # All in the same sprint print '<small>(<a href="/sprints/%d?search=highlight:%s">Show in backlog view</a>)</small><br><br>' % ( tasks.values()[0].sprint.id, ",".join(map(str, ids)), ) for id in ids: print '<a href="#task%d">%s</a><br>' % (id, tasks[id].safe.name) def header(task, text, level): if level == 1: print "<hr>" print '<a name="task%d"></a>' % task.id print '<a href="#task%d"><h2>%s</h2></a>' % (task.id, text) else: print "<h%d>%s</h%d>" % (level + 1, text, level + 1) for id in ids: task = tasks[id] if not task or task.sprint.isHidden(handler.session["user"]): ErrorBox.die("Tasks", "No task with ID <b>%d</b>" % id) elif not task.sprint.canView(handler.session["user"]): ErrorBox.die("Private", "You must be a sprint member to view this sprint's tasks") revs = task.getRevisions() startRev = task.getStartRevision() header(task, task.safe.name, 1) header(task, "Info", 2) print 'Part of <a href="/sprints/%d">%s</a>, <a href="/sprints/%d#group%d">%s</a>' % ( task.sprintid, task.sprint, task.sprintid, task.groupid, task.group, ), if task.goal: print 'to meet the goal <img class="bumpdown" src="/static/images/tag-%s.png"> <a href="/sprints/%d?search=goal:%s">%s</a>' % ( task.goal.color, task.sprintid, task.goal.color, task.goal.safe.name, ), print "<br>" print "Assigned to %s<br>" % ", ".join(map(str, task.assigned)) print "Last changed %s ago<br><br>" % timesince(tsToDate(task.timestamp)) hours, total, lbl = task.hours, startRev.hours, "<b>%s</b>" % statuses[task.status].text if task.deleted: if task.sprint.canEdit(handler.session["user"]): print '<form method="post" action="/sprints/%d">' % task.sprint.id print '<input type="hidden" name="id" value="%d">' % task.id print '<input type="hidden" name="rev_id" value="%d">' % task.revision print '<input type="hidden" name="field" value="deleted">' print '<input type="hidden" name="value" value="false">' print "Deleted (%s)" % Button("undelete", id="undelete").mini().positive() print "</form>" else: print "Deleted" print "<br>" elif task.status == "complete": print ProgressBar(lbl, total - hours, total, zeroDivZero=True, style="progress-current-green") elif task.status in ("blocked", "canceled", "deferred", "split"): hours = filter(lambda rev: rev.hours > 0, revs) hours = hours[-1].hours if len(hours) > 0 else 0 print ProgressBar(lbl, total - hours, total, zeroDivZero=True, style="progress-current-red") else: print ProgressBar(lbl, total - hours, total, zeroDivZero=True) header(task, "Notes", 2) for note in task.getNotes(): print '<div id="note%d" class="note">' % note.id print '<form method="post" action="/tasks/%d/notes/%d/modify">' % (id, note.id) print '<div class="avatar"><img src="%s"></div>' % note.user.getAvatar() print '<div class="text">' print '<div class="title"><a class="timestamp" href="#note%d">%s</a> by <span class="author">%s</span>' % ( note.id, tsToDate(note.timestamp).replace(microsecond=0), note.user.safe.username, ) if note.user == handler.session["user"]: print '<button name="action" value="delete" class="fancy mini danger">delete</button>' print "</div>" print '<div class="body markdown">%s</div>' % note.render() print "</div>" print "</form>" print "</div>" print '<div class="note new-note">' print '<form method="post" action="/tasks/%d/notes/new">' % id print '<div class="avatar"><div><img src="%s"></div></div>' % handler.session["user"].getAvatar() print '<div class="text">' print '<div class="title">' print "<b>New note</b>" print '<a target="_blank" href="/help/markdown" class="fancy mini">help</a>' print "</div>" print '<div class="body"><textarea name="body" class="large"></textarea></div>' print Button("Post").post().positive() print "<hr>" print '<div class="body markdown"><div id="preview"></div></div>' print "</div>" print "</form>" print "</div>" print '<button class="btn start-new-note">Add Note</button>' print '<div class="clear"></div>' header(task, "History", 2) chart = TaskChart("chart%d" % id, task) chart.js() chart.placeholder() showHistory(task, False) print "<br>"
def taskEdit(handler, ids): handler.title("Edit tasks") requirePriv(handler, "Write") ids = map(int, uniq(ids.split(","))) tasks = dict((id, Task.load(id)) for id in ids) if not all(tasks.values()): ids = [str(id) for (id, task) in tasks.iteritems() if not task] ErrorBox.die( "No %s with %s %s" % ("task" if len(ids) == 1 else "tasks", "ID" if len(ids) == 1 else "IDs", ", ".join(ids)) ) tasks = [tasks[id] for id in ids] if len(set(task.sprint for task in tasks)) > 1: ErrorBox.die("All tasks must be in the same sprint") sprint = tasks[0].sprint if sprint.isHidden(handler.session["user"]): ErrorBox.die( "No %s with %s %s" % ("task" if len(ids) == 1 else "tasks", "ID" if len(ids) == 1 else "IDs", ", ".join(ids)) ) if not (sprint.isActive() or sprint.isPlanning()): ErrorBox.die("You can't mass-edit tasks from an inactive sprint") elif not sprint.canEdit(handler.session["user"]): ErrorBox.die("You don't have permission to modify this sprint") print "<h3>New values</h3>" print '<form method="post" action="/tasks/%s/edit">' % ",".join(map(str, ids)) print '<table id="task-edit-values" class="list">' print '<tr><td class="left">Assigned:</td><td class="right">' print '<select id="select-assigned" name="assigned[]" data-placeholder="(unchanged)" multiple>' for user in sorted(sprint.members): print '<option value="%d">%s</option>' % (user.id, user.safe.username) print "</select>" print "</td></tr>" print '<tr><td class="left">Hours:</td><td class="right"><input type="text" name="hours" class="hours"></td></tr>' print '<tr><td class="left">Status:</td><td class="right"><select name="status">' print '<option value="">(unchanged)</option>' for statusSet in statusMenu: for name in statusSet: print '<option value="%s">%s</option>' % (name, statuses[name].text) print "</select></td></tr>" print '<tr><td class="left">Sprint Goal:</td><td class="right"><select name="goal">' print '<option value="">(unchanged)</option>' print '<option value="0">None</option>' for goal in sprint.getGoals(): print '<option value="%d">%s</option>' % (goal.id, goal.safe.name) print "</select></td></tr>" print '<tr><td class="left"> </td><td class="right">' print Button("Save", type="submit").positive() print Button("Cancel", url="/sprints/%d" % sprint.id, type="button").negative() print "</td></tr>" print "</table>" print "<br>" print "<h3>Current values</h3>" print '<table border=0 cellspacing=0 cellpadding=2 class="task-edit">' for task in tasks: print '<tr><td class="task-name" colspan="4"><input type="checkbox" id="task%d" name="include[%d]" checked="true"> <label for="task%d">%s</label></td></tr>' % ( task.id, task.id, task.id, task.safe.name, ) print '<tr class="task-fields">' print '<td class="task-assigned">%s</td>' % ", ".join(map(str, task.assigned)) print '<td class="task-hours"><img src="/static/images/time-icon.png"> %d %s</td>' % ( task.hours, "hour" if task.hours == 1 else "hours", ) print '<td class="task-status"><img class="status" src="%s"> %s</td>' % (task.stat.icon, task.stat.text) print '<td class="task-goal"><img class="goal" src="/static/images/tag-%s.png"> %s</td>' % ( (task.goal.color, task.goal.safe.name) if task.goal else ("none", "None") ) print "</tr>" print "</table>" print "</form>"