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 exportSprints(handler, project): id = int(project) handler.title('Export Sprint') requirePriv(handler, 'User') project = Project.load(id) if not project: ErrorBox.die('Invalid project', "No project with ID <b>%d</b>" % id) print "<link href=\"/static/jquery.multiselect.css\" rel=\"stylesheet\" type=\"text/css\" />" print "<script src=\"/static/jquery.multiselect.js\" type=\"text/javascript\"></script>" print "<script src=\"/static/sprints-export.js\" type=\"text/javascript\"></script>" print "<style type=\"text/css\">" print "select {width: 50%;}" print "img.format {" print " width: 64px;" print " cursor: pointer;" print " padding: 5px;" print " border: 3px solid #fff;" print "}" print "img.format.selected, .ui-effects-transfer {border: 3px solid #f00;}" print "</style>" print "<h2>Sprints</h2>" print "<select name=\"sprints\" multiple>" for sprint in project.getSprints(): if sprint.canView(handler.session['user']): print "<option value=\"%d\"%s>%s</option>" % (sprint.id, ' selected' if sprint.isActive() else '', sprint.safe.name) print "</select>" print "<h2>Format</h2>" for export in exports.values(): print "<img class=\"format\" src=\"/static/images/%s.png\" title=\"%s\" export-name=\"%s\">" % (export.getIcon(), export.getFriendlyName(), export.getName()) print "<br><br>" print Button('Export', id = 'export-button').positive()
def twoFactorAuthentication(handler, p_action): handler.title("Two-Factor Authentication") requirePriv(handler, "User") if p_action == "enable": handler.session["user"].hotpKey = b32encode("".join(chr(randrange(256)) for _ in range(10))) handler.session["user"].save() Event.tfa(handler, handler.session["user"]) print SuccessBox("Two-factor authentication is <b>enabled</b>") print "Your HOTP key is <b>%s</b>:<br><br>" % handler.session["user"].hotpKey print '<div style="text-align: center"><img src="https://chart.googleapis.com/chart?chs=200x200&chld=M|0&cht=qr&chl=otpauth://totp/Sprint%%3Fsecret%%3D%s"></div><br>' % handler.session[ "user" ].hotpKey print "This key will not be displayed again — be sure to write it down, or add it to your Google Authenticator list now" elif p_action == "disable": handler.session["user"].hotpKey = "" handler.session["user"].save() Event.tfa(handler, handler.session["user"]) delay(handler, SuccessBox("Two-factor authentication is <b>disabled</b>")) redirect("/users/%s" % handler.session["user"].username) else: ErrorBox.die("Unexpected action: <b>%s</b>" % stripTags(p_action))
def adminPrivilegesPost(handler, p_grant): handler.title("Privileges") requirePriv(handler, 'Admin') p_grant = {name: privs.keys() for name, privs in p_grant.iteritems()} privNames = set() for privs in p_grant.values(): privNames |= set(privs) if not privNames <= set(privList.keys()): ErrorBox.die("Update privileges", "Unrecognized privilege name") for user in User.loadAll(): for priv in privList: privs = p_grant.get(user.username, []) has = user.hasPrivilege(priv) if has and priv not in privs: print "Revoking %s from %s<br>" % (priv, user.username) user.privileges.remove(priv) Event.revokePrivilege(handler, user, priv) elif not has and priv in privs: print "Granting %s to %s<br>" % (priv, user.username) user.privileges.add(priv) Event.grantPrivilege(handler, user, priv, False) user.save() print "Done"
def findActiveSprint(handler, project = None, search = None): handler.title('Active Sprint') requirePriv(handler, 'User') if project: projectid = int(project) project = Project.load(projectid) if not project: ErrorBox.die('Load project', "No project with ID <b>%d</b>" % projectid) url = "/sprints/%d" if search: url += "?search=%s" % search sprints = Sprint.loadAllActive(handler.session['user'], project) sprints = filter(lambda sprint: sprint.canView(handler.session['user']), sprints) for case in switch(len(sprints)): if case(0): ErrorBox.die('Active sprint', 'No active sprints found') break if case(1): redirect(url % sprints[0].id) break if case(): print "You are active in multiple sprints%s:<br><br>" % (" in the %s project" % project.safe.name if project else '') for sprint in sprints: print "<a href=\"%s\">%s</a><br>" % (url % sprint.id, sprint.safe.name) break
def userAvatarSet(handler, username): handler.title('Set avatar') requirePriv(handler, 'User') user = User.load(username = username) if not user: ErrorBox.die("Set avatar", "No user named <b>%s</b>" % stripTags(username)) if user != handler.session['user']: #TODO Allow devs redirect("/users/%s/avatar/set" % handler.session['user'].username) if not Image: ErrorBox.die("Set avatar", "This sprint install does not have the Python Imaging Library (PIL), so local avatars are not supported") print "Restrictions on a locally hosted avatar:" print "<ul>" print "<li>Type: %s</li>" % ", ".join(AVATAR_TYPES).upper() print "<li>Size: %s bytes</li>" % AVATAR_MAX_SIZE print "</ul>" print "<form method=\"post\" enctype=\"multipart/form-data\" action=\"/users/%s/avatar/set\">" % user.username print "<input type=\"file\" name=\"data\"><br>" # Using a plain button here because the file field isn't styled print "<button>Upload</button>" print "</form>" if user.hasLocalAvatar(): print "<br>" print "You can also remove your existing local avatar. Your account will switch back to using your gravatar image<br>" print "<form method=\"post\" action=\"/users/%s/avatar/remove\">" % user.username print "<button>Remove avatar</button>" print "</form>"
def adminSessionsPost(handler, p_key, p_action, p_value = None): handler.title('Sessions') requirePriv(handler, 'Admin') print "<script src=\"/static/admin-sessions.js\" type=\"text/javascript\"></script>" if not p_key in Session.getIDs(): ErrorBox.die("Retrieve session", "No session exists with key <b>%s</b>" % stripTags(p_key)) session = Session.load(p_key) for case in switch(p_action): if case('reassign'): handler.title('Reassign Session') if p_value: user = User.load(int(p_value)) if not user: ErrorBox.die("Load user", "No user exists with ID <b>%s</b>" % stripTags(p_value)) session['user'] = user redirect('/admin/sessions') else: print "<form method=\"post\" action=\"/admin/sessions\">" print "<input type=\"hidden\" name=\"action\" value=\"reassign\">" print "<input type=\"hidden\" name=\"key\" value=\"%s\">" % p_key print "<select id=\"selectUser\" name=\"value\">" for user in sorted(User.loadAll()): print "<option value=\"%d\">%s</option>" % (user.id, user.safe.username) print "</select><br>" print Button('Reassign', type = 'submit').positive() print Button('Cancel', id = 'cancel-button', type = 'button', url = '/admin/sessions').negative() print "</form>" break if case('destroy'): Session.destroy(p_key) redirect('/admin/sessions') break break
def adminProjectsMoveSprintsPost(handler, id, p_newproject, p_sprintid = None): handler.title('Move Sprints') requirePriv(handler, 'Admin') project = Project.load(int(id)) if not project: ErrorBox.die('Invalid Project', "No project with ID <b>%d</b>" % int(id)) p_newproject = to_int(p_newproject, 'newproject', ErrorBox.die) target = Project.load(p_newproject) if not p_sprintid: delay(handler, WarningBox("No sprints to move", close = True)) redirect("/admin/projects/%d" % project.id) sprintids = [to_int(id, 'sprintid', ErrorBox.die) for id in p_sprintid] sprints = [Sprint.load(id) for id in sprintids] if not all(sprints): ErrorBox.die("Invalid sprint ID(s)") for sprint in sprints: sprint.project = target sprint.save() delay(handler, SuccessBox("%s moved" % pluralize(len(sprints), 'sprint', 'sprints'), close = True)) redirect("/admin/projects/%d" % project.id)
def adminProjectsManage(handler, id): handler.title('Manage Project') requirePriv(handler, 'Admin') project = Project.load(int(id)) if not project: ErrorBox.die('Invalid Project', "No project with ID <b>%d</b>" % int(id)) undelay(handler) sprints = project.getSprints() otherProjects = sorted((p for p in Project.loadAll() if p != project), key = lambda p: p.name) for sprint in sprints: if 'deleted' in sprint.flags: print "<form method=\"post\" action=\"/admin/projects/%d/cancel-deletion/%d\">" % (project.id, sprint.id) print WarningBox("%s is flagged for deletion during nightly cleanup. %s" % (sprint.link(handler.session['user']), Button('Cancel').mini().post())) print "</form>" print "<a name=\"sprints\"></a>" print "<h3>Sprints</h3>" if len(sprints) > 0: print "<form method=\"post\" action=\"/admin/projects/%d/sprints\">" % project.id for sprint in sprints: print "<input type=\"checkbox\" name=\"sprintid[]\" value=\"%d\"> %s<br>" % (sprint.id, sprint.link(handler.session['user'])) print "<br>" print "Move to project: <select name=\"newproject\">" for p in otherProjects: print "<option value=\"%d\">%s</option>" % (p.id, p.safe.name) print "</select>" print Button('Move').post('move').positive() print "<br><br>" print "Delete sprints (irreversible once finalized):" print Button('Delete').post('delete').negative() print "</form>" else: print "No sprints" print "<a name=\"rename\"></a>" print "<h3>Rename</h3>" print "<form method=\"post\" action=\"/admin/projects/%d/edit\">" % project.id print "Name: <input type=\"text\" name=\"name\" value=\"%s\">" % project.safe.name print Button('Rename', type = 'submit').positive() print "</form>" print "<a name=\"delete\"></a>" print "<h3>Delete</h3>" print "<form method=\"post\" action=\"/admin/projects/%d/delete\">" % project.id if len(project.getSprints()) > 0: if len(otherProjects) > 0: print "Delete <b>%s</b> and move all sprints to <select name=\"newproject\">" % project.safe.name for p in otherProjects: print "<option value=\"%d\">%s</option>" % (p.id, p.safe.name) print "</select>" print Button('Delete', type = 'submit').negative() else: print "Unable to remove the only project if it has sprints" else: print Button("Delete %s" % project.safe.name, type = 'submit').negative() print "</form><br>"
def adminProjectsEditPost(handler, id, p_name): handler.title('Edit Project') requirePriv(handler, 'Admin') project = Project.load(int(id)) if not project: ErrorBox.die('Invalid Project', "No project with ID <b>%d</b>" % int(id)) project.name = p_name project.save() delay(handler, SuccessBox("Saved project changes", close = True)) redirect("/admin/projects/%d" % project.id)
def userTasks(handler, username): handler.title('User tasks') user = User.load(username = username) if not user: ErrorBox.die("User tasks", "No user named <b>%s</b>" % stripTags(username)) tasks = [task.id for task in Task.loadAll() if user in task.assigned and task.stillOpen() and task.sprint.isActive()] if len(tasks) == 0: ErrorBox.die("User tasks", "%s has no open tasks in active sprints" % user) redirect("/tasks/%s" % ','.join(map(str, tasks)))
def searchRunSprint(handler, id, sprintid): handler.title('Run Search') requirePriv(handler, 'User') id = int(id) search = SavedSearch.load(id) if not search: ErrorBox.die('Invalid Search', "Search <b>%d</b> does not exist" % int(id)) elif search.user != handler.session['user'] and not search.public: ErrorBox.die('Invalid Search', "You cannot run search <b>%d</b>" % int(id)) redirect("/sprints/%s?search=%s" % (sprintid, search.query))
def adminProjectsPost(handler, p_name): handler.title('Project Management') requirePriv(handler, 'Admin') if Project.load(name = p_name): ErrorBox.die('Add Project', "There is already a project named <b>%s</b>" % stripTags(p_name)) project = Project(p_name) project.save() delay(handler, SuccessBox("Added project <b>%s</b>" % stripTags(p_name), close = True)) Event.newProject(handler, project) redirect('/')
def userAvatarRemove(handler, username): handler.title('Remove avatar') requirePriv(handler, 'User') user = User.load(username = username) if not user: ErrorBox.die("Remove avatar", "No user named <b>%s</b>" % stripTags(username)) if user != handler.session['user']: #TODO Allow devs redirect("/users/%s/avatar/set" % handler.session['user'].username) user.avatar = None user.save() delay(handler, SuccessBox("Avatar removed")) redirect("/users/%s" % user.username)
def newTaskManyUpload(handler, group, p_data): requirePriv(handler, "User") # Vague sanity check that this is actually text, from http://stackoverflow.com/a/7392391/309308 textchars = "".join(map(chr, [7, 8, 9, 10, 12, 13, 27] + range(0x20, 0x100))) is_binary_string = lambda bytes: bool(bytes.translate(None, textchars)) if is_binary_string(p_data): ErrorBox.die( "The uploaded file appears to be binary -- it should be a text file matching the normal add tasks format" ) handler.session["many-upload"] = p_data redirect("/tasks/new/many?group=%s" % group)
def newSprint(handler, project): id = int(project) handler.title('New Sprint') requirePriv(handler, 'User') project = Project.load(id) if not project: ErrorBox.die('Invalid project', "No project with ID <b>%d</b>" % id) sprints = project.getSprints() print "<style type=\"text/css\">" print "table.list td.right > * {width: 400px;}" print "table.list td.right button {width: 200px;}" # Half of the above value print "#select-members {padding-right: 5px;}" print "</style>" print "<script src=\"/static/sprints-new.js\" type=\"text/javascript\"></script>" print InfoBox('', id = 'post-status') print "<form method=\"post\" action=\"/sprints/new\">" print "<table class=\"list\">" print "<tr><td class=\"left\">Project:</td><td class=\"right\">" print "<select id=\"select-project\" name=\"project\">" for thisProject in Project.loadAll(): print "<option value=\"%d\"%s>%s</option>" % (thisProject.id, ' selected' if thisProject == project else '', thisProject.safe.name) print "</select>" print "</td></tr>" print "<tr><td class=\"left\">Name:</td><td class=\"right\"><input type=\"text\" name=\"name\" class=\"defaultfocus\"></td></tr>" print "<tr><td class=\"left\">Planning:</td><td class=\"right\"><input type=\"text\" name=\"start\" class=\"date\" value=\"%s\"></td></tr>" % Weekday.today().strftime('%m/%d/%Y') print "<tr><td class=\"left\">Wrapup:</td><td class=\"right\"><input type=\"text\" name=\"end\" class=\"date\"></td></tr>" print "<tr><td class=\"left no-bump\">Members:</td><td class=\"right\">" print "<select name=\"members[]\" id=\"select-members\" multiple>" # Default to last sprint's members members = {handler.session['user']} if sprints: members |= sprints[-1].members for user in sorted(User.loadAll()): if user.hasPrivilege('User'): print "<option value=\"%d\"%s>%s</option>" % (user.id, ' selected' if user in members else '', user.safe.username) print "</select>" print "</td></tr>" print "<tr><td class=\"left no-bump\">Options:</td><td class=\"right\">" print "<div><input type=\"checkbox\" name=\"private\" id=\"flag-private\"><label for=\"flag-private\">Private – Only sprint members can view tasks</label></div>" print "<div><input type=\"checkbox\" name=\"hidden\" id=\"flag-hidden\"><label for=\"flag-hidden\">Hidden – Only sprint members can see the sprint</label></div>" print "</td></tr>" print "<tr><td class=\"left\"> </td><td class=\"right\">" print Button('Save', id = 'save-button', type = 'button').positive() print Button('Cancel', id = 'cancel-button', type = 'button').negative() print "</td></tr>" print "</table>" print "</form>"
def sendResetEmailPost(handler, p_username): handler.title('Reset password') user = User.load(username = p_username) if not user: ErrorBox.die("Invalid User", "No user named <b>%s</b>" % stripTags(p_username)) user.resetkey = "%x" % random.randint(0x10000000, 0xffffffff) try: sendmail(user.getEmail(), "Sprint - Password Reset", "Someone (hopefully you) requested a Sprint password reset. You can follow this link to reset your password: http://%s:%d/resetpw/%s?key=%s. If you didn't request this or no longer need it, it will expire in a day or two." % (gethostname(), PORT, user.safe.username, user.resetkey)) except Exception: Event.error(handler, "<div style=\"white-space: normal\">%s</div>" % formatException()) ErrorBox.die("Reset failed", "Unable to send password reset e-mail") user.save() print SuccessBox("A password reset e-mail has been sent")
def showMetrics(handler, id): requirePriv(handler, 'User') id = int(id) sprint = Sprint.load(id) if not sprint or sprint.isHidden(handler.session['user']): ErrorBox.die('Sprints', "No sprint with ID <b>%d</b>" % id) elif not sprint.canView(handler.session['user']): ErrorBox.die('Private', "You must be a sprint member to view this sprint") context = {} context['sprint'] = sprint context['allTasks'] = sprint.getTasks(includeDeleted = True) context['tasks'] = tasks = filter(lambda task: not task.deleted, context['allTasks']) context['revisions'] = {(task.id, day): task.getRevisionAt(day) for task, day in product(context['allTasks'], sprint.getDays())} handler.title(sprint.safe.name) drawNavArrows(sprint, handler.session['user'], 'metrics') print "<style type=\"text/css\">" print "h2 a {color: #000;}" print "</style>" charts = [ ('general', 'Hours (general)', HoursChart('chart-general', **context)), ('commitment-by-user', 'Commitment (by user)', CommitmentByUserChart(**context)), ('earned-value', 'Earned Value', EarnedValueChart('chart-earned-value', **context)), ('by-user', 'Hours (by user)', HoursByUserChart('chart-by-user', **context)), ('status', 'Task status', StatusChart('chart-status', **context)), ('commitment', 'Total commitment', CommitmentChart('chart-commitment', **context)), ('goals', 'Sprint goals', GoalsChart(**context)), ] Chart.include() map(lambda (anchor, title, chart): chart.js(), charts) print tabs(sprint, 'metrics') if not sprint.isOver(): noHours = filter(lambda task: task.stillOpen() and task.hours == 0, tasks) if noHours != []: print WarningBox("There are <a href=\"/sprints/%d?search=status:not-started,in-progress,blocked hours:0\">open tasks with no hour estimate</a>. These unestimated tasks can artifically lower the tasking lines in the following charts" % sprint.id) for anchor, title, chart in charts: print "<a name=\"%s\">" % anchor print "<h2><a href=\"#%s\">%s</a></h2>" % (anchor, title) chart.placeholder() print "<br><br>"
def exportRender(handler, sprints, format): if not handler.session['user']: ErrorBox.die('Forbidden', "You must be logged in to create a new sprint") ids = map(int, sprints.split(',')) sprints = map(Sprint.load, ids) try: export = exports[format] except KeyError: ErrorBox.die('Export format', "No format named <b>%s</b>" % stripTags(format)) for sprint in sprints: if sprint and sprint.canView(handler.session['user']): export.process(sprint) handler.wrappers = False handler.forceDownload = "%s.%s" % (sprints[0].name if len(sprints) == 1 else 'sprints', export.getExtension())
def resetUserPassword(handler, username, key = None): handler.title('Reset password') user = User.load(username = username) if not user: ErrorBox.die('User', "No user named <b>%s</b>" % stripTags(username)) print "<style type=\"text/css\">" print "input {" print " position: relative;" print " top: -2px;" print "}" print "</style>" if user == handler.session['user'] or (user.resetkey and user.resetkey == key): printResetForm(handler, user, user.resetkey) else: ErrorBox.die('Reset Password', 'Invalid reset key')
def sendResetEmail(handler): handler.title('Reset password') if handler.session['user']: redirect('/resetpw') if not settings.smtpServer: ErrorBox.die("Sprint is not configured for sending e-mail. You will need to contact an administrator to reset your password") print "A reset link will be send to your e-mail address. You can also contact an administrator to reset your password.<br><br>" print "<form method=\"post\" action=\"/resetpw/:mail\">" print "<table class=\"list\">" print "<tr><td class=\"left\">Username:</td><td><select name=\"username\">" for user in User.loadAll(orderby = 'username'): print "<option value=\"%s\">%s</option>" % (user.safe.username, user.safe.username) print "</select></td></tr>" print "<tr><td> </td><td>" print Button('Send e-mail', type = 'submit').positive() print "</td></tr>" print "</table>"
def deleteGroupPost(handler, id, p_newGroup = None): def die(msg): print msg done() handler.title('Manage Group') requirePriv(handler, 'User') handler.wrappers = False id = to_int(id, 'id', die) if p_newGroup is not None: p_newGroup = to_int(p_newGroup, 'newGroup', die) group = Group.load(id) if not group: ErrorBox.die('Invalid Group', "No group with ID <b>%d</b>" % id) if not group.deletable: ErrorBox.die('Invalid Group', "Group is not deletable") if len(group.sprint.getGroups()) == 1: ErrorBox.die('Invalid Group', "Cannot delete the last group in a sprint") tasks = group.getTasks() newGroup = None if p_newGroup == 0: # Delete tasks for task in tasks: task.deleted = True task.revise() elif p_newGroup is not None: # Move tasks newGroup = Group.load(p_newGroup) if not newGroup: ErrorBox.die('Invalid Group', "No group with ID <b>%d</b>" % p_newGroup) # Move all the tasks to the end of the new group seq = len(newGroup.getTasks()) for task in tasks: seq += 1 task.move(seq, newGroup) elif len(tasks): ErrorBox.die('Invalid Request', "Missing new group request argument") sprintid = group.sprintid group.delete() Event.deleteGroup(handler, group) redirect("/sprints/%d%s" % (sprintid, "#group%d" % newGroup.id if newGroup else ''))
def showSprintHistory(handler, id, assigned = None): requirePriv(handler, 'User') id = int(id) sprint = Sprint.load(id) if not sprint or sprint.isHidden(handler.session['user']): ErrorBox.die('Sprints', "No sprint with ID <b>%d</b>" % id) elif not sprint.canView(handler.session['user']): ErrorBox.die('Private', "You must be a sprint member to view this sprint") tasks = sprint.getTasks(includeDeleted = True) handler.title(sprint.safe.name) drawNavArrows(sprint, handler.session['user'], 'history') Chart.include() chart = TaskChart('chart', sprint.getTasks()) chart.js() print "<script type=\"text/javascript\">" tasksByAssigned = {member.username: [task.id for task in tasks if member in task.assigned] for member in sprint.members} print "var tasks_by_assigned = %s;" % toJS(tasksByAssigned) print "$(document).ready(function() {" if assigned: print " $('%s').addClass('selected');" % ', '.join("#filter-assigned a[assigned=\"%s\"]" % username for username in assigned.split(',')) print " setup_filter_buttons();" print " apply_filters();" print "});" print "</script>" print tabs(sprint, 'history') if len(tasks) == 0: print ErrorBox("This sprint has no tasks") print "<br>" return print "<div id=\"filter-assigned\">" print "<a class=\"fancy danger\" href=\"#\"><img src=\"/static/images/cross.png\"> None</a>" for member in sorted(sprint.members): print "<a class=\"fancy\" assigned=\"%s\" href=\"/sprints/%d/history?assigned=%s\"><img src=\"%s\"> %s</a>" % (member.username, id, member.username, member.getAvatar(16), member.username) print "</div><br>" chart.placeholder() showHistory(tasks, True) print "<br>"
def send(handler, p_userid, p_body, dryrun = False): handler.title('Send message') if dryrun: handler.wrappers = False requirePriv(handler, 'User') targetID = int(p_userid) target = User.load(targetID) if not target: ErrorBox.die("No user with ID %d" % targetID) message = Message(target.id, handler.session['user'].id, "Direct message", p_body, 'markdown') if dryrun: print message.render() else: if p_body == '': ErrorBox.die('Empty Body', "No message provided") message.save() delay(handler, SuccessBox("Message dispatched to %s" % target, close = 3)) redirect('/messages/sent')
def renameGroupPost(handler, id, p_name): def die(msg): print msg done() handler.title('Manage Group') requirePriv(handler, 'User') id = int(id) group = Group.load(id) if not group: ErrorBox.die('Invalid Group', "No group with ID <b>%d</b>" % id) elif p_name.strip() == '': ErrorBox.die('Invalid Name', "Group must have a non-empty name") oldName = group.name group.name = p_name group.save() Event.renameGroup(handler, group, oldName) redirect("/sprints/%d#group%d" % (group.sprintid, group.id))
def adminTimePost(handler, p_date, p_time): handler.title('Mock time') requirePriv(handler, 'Admin') try: ts = re.match("^(\d{1,2})/(\d{1,2})/(\d{4})$", p_date) if not ts: raise ValueError("Malformed date: %s" % p_date) month, day, year = map(int, ts.groups()) ts2 = re.match("^(\d{1,2}):(\d{1,2})$", p_time) if not ts2: raise ValueError("Malformed time: %s" % p_time) hour, minute = map(int, ts2.groups()) effective = datetime(year, month, day, hour, minute, 0) delta = effective - datetime.now() setNowDelta(delta) Event.mockTime(handler, effective, delta) except ValueError, e: ErrorBox.die(e.message)
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 showSprintResults(handler, id): requirePriv(handler, 'User') id = int(id) sprint = Sprint.load(id) if not sprint or sprint.isHidden(handler.session['user']): ErrorBox.die('Sprints', "No sprint with ID <b>%d</b>" % id) elif not sprint.canView(handler.session['user']): ErrorBox.die('Private', "You must be a sprint member to view this sprint") tasksNow = sprint.getTasks() tasksStart = filter(None, (task.getRevisionAt(tsToDate(sprint.start)) for task in tasksNow)) tasksNow = {task.id: task for task in tasksNow} tasksStart = {task.id: task for task in tasksStart} handler.title(sprint.safe.name) drawNavArrows(sprint, handler.session['user'], 'results') print tabs(sprint, ('wrapup', 'results')) if not sprint.isOver(): ErrorBox.die('Sprint Open', "Results aren't available until the sprint has closed") from rorn.Box import WarningBox print WarningBox("This feature is still under development") print "<ul>" completed = filter(lambda task: task.status == 'complete', tasksNow.values()) print "<li><a href=\"/sprints/%d?search=status:complete\">%s completed</a> (%d%% of the original hourly commitment)</li>" % (sprint.id, pluralize(len(completed), 'task', 'tasks'), sum(tasksStart[task.id].hours if task.id in tasksStart else 0 for task in completed) * 100 / sum(task.hours for task in tasksStart.values())) planned = filter(lambda task: task.id in tasksStart, tasksNow.values()) print "<li>%d%% of tasks were planned from the start</li>" % (len(planned) * 100 / len(tasksNow)) print "</ul>"
def updateSavedSearch(handler, id, p_action, p_name, p_query, p_share = False): handler.title('Update Search') requirePriv(handler, 'User') search = SavedSearch.load(int(id)) if not search: ErrorBox.die('Invalid Search', "No search with ID <b>%d</b>" % int(id)) elif search.user != handler.session['user']: ErrorBox.die('Permission Denied', "You cannot modify another user's search") if p_action == 'update': search.name = p_name search.query = p_query search.public = p_share search.save() if not search.public: search.unfollow() delay(handler, SuccessBox("Updated search <b>%s</b>" % search.safe.name)) elif p_action == 'delete': search.delete() delay(handler, SuccessBox("Deleted search <b>%s</b>" % search.safe.name)) else: ErrorBox.die('Invalid Action', "Unknown action %s" % stripTags(p_action)) redirect('/search/saved')
def sprintRetrospectiveStart(handler, id): if not handler.session['user']: ErrorBox.die('Forbidden', "You must be logged in to modify sprint info") id = int(id) sprint = Sprint.load(id) if not sprint or sprint.isHidden(handler.session['user']): ErrorBox.die('Sprints', "There is no sprint with ID %d" % id) elif sprint.owner != handler.session['user']: ErrorBox.die('Forbidden', "Only the scrummaster can edit the retrospective") elif not (sprint.isReview() or sprint.isOver()): ErrorBox.die('Sprint Open', "The retrospective isn't available until the sprint is in review") Retrospective.init(sprint) redirect("/sprints/%d/retrospective" % sprint.id)