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 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 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 newGroupPost(handler, p_group, p_name): def die(msg): print msg done() handler.title('New Group') requirePriv(handler, 'User') handler.wrappers = False predid = to_int(p_group, 'group', die) pred = Group.load(predid) if not pred: die("No group with ID <b>%d</b>" % predid) elif p_name.strip() == '': die("Group must have a non-empty name") group = Group(pred.sprint.id, p_name, pred.seq + 1) group.save() handler.responseCode = 299 delay(handler, """ <script type=\"text/javascript\"> $(document).ready(function() { $('#group%d').effect('highlight', {}, 3000); }); </script>""" % group.id) print "/sprints/%d#group%d" % (group.sprint.id, group.id) Event.newGroup(handler, group)
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 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 newTaskPost(handler, p_group, p_name, p_goal, p_status, p_hours, p_assigned=[]): def die(msg): print msg done() requirePriv(handler, "User") handler.wrappers = False groupid = to_int(p_group, "group", die) group = Group.load(groupid) if not group or group.sprint.isHidden(handler.session["user"]): die("No group with ID <b>%d</b>" % groupid) sprint = group.sprint if 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") if p_name.strip() == "": die("Task must have a non-empty name") assignedids = set(to_int(i, "assigned", die) for i in p_assigned) assigned = set(User.load(assignedid) for assignedid in assignedids) if assigned == set(): assigned.add(handler.session["user"] if handler.session["user"] in sprint.members else sprint.owner) if not all(assigned): die("Invalid assignee") goalid = to_int(p_goal, "goal", die) if goalid != 0: goal = Goal.load(goalid) if not goal: die("No goal with ID <b>%d</b>" % goalid) if goal.sprint != group.sprint: die("Goal does not belong to the correct sprint") hours = to_int(p_hours, "hours", die) task = Task(groupid, group.sprintid, handler.session["user"].id, goalid, p_name, p_status, hours) task.assigned |= assigned task.save() handler.responseCode = 299 delay( handler, """ <script type=\"text/javascript\"> $(document).ready(function() { $('#task%d').effect('highlight', {}, 3000); }); </script>""" % task.id, ) delay(handler, SuccessBox("Added task <b>%s</b>" % task.safe.name, close=3, fixed=True)) Event.newTask(handler, task)
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 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 newSavedSearchPost(handler, p_name, p_query, p_public = False, p_sprintid = None): # def die(msg): # print msg # done() handler.title('New Search') requirePriv(handler, 'User') handler.wrappers = False search = SavedSearch(handler.session['user'].id, p_name, p_query, bool(p_public)) search.save() handler.responseCode = 299 delay(handler, SuccessBox("Saved search <b>%s</b>" % search.safe.name, close = 3, fixed = True)) if p_sprintid: print "/search/saved/%d/run/%s" % (search.id, p_sprintid) else: print "/search/saved"
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 userAvatarSet(handler, username, p_data): 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 len(p_data) > AVATAR_MAX_SIZE: ErrorBox.die("Set avatar", "Avatar too large (%s)" % pluralize(len(p_data), 'byte', 'bytes')) type = imgtype(None, p_data) if type is None: ErrorBox.die("Set avatar", "Image type unrecognized") elif type not in AVATAR_TYPES: ErrorBox.die("Set avatar", "Unsupported image type %s" % type.upper()) user.avatar = b64encode(p_data) user.save() delay(handler, SuccessBox("Avatar uploaded")) redirect("/users/%s" % user.username)
def adminProjectsCancelSprintDeletionPost(handler, projectid, id): handler.title('Undelete Sprint') requirePriv(handler, 'Admin') project = Project.load(int(projectid)) if not project: ErrorBox.die('Invalid Project', "No project with ID <b>%d</b>" % int(projectid)) sprint = Sprint.load(int(id)) if not sprint: ErrorBox.die('Invalid Sprint', "No sprint with ID <b>%d</b>" % int(id)) if sprint.project != project: # We really don't use the project at all, but it's the principle of the thing ErrorBox.die('Invalid Sprint', "Project/sprint mismatch") if 'deleted' not in sprint.flags: ErrorBox.die('Invalid Sprint', "Sprint is not deleted") sprint.flags.remove('deleted') sprint.save() Event.undeleteSprint(handler, sprint) delay(handler, SuccessBox("Deletion canceled for %s" % sprint.link(handler.session['user']), close = True)) redirect("/admin/projects/%d" % project.id)
def adminProjectsDeletePost(handler, id, p_newproject = None): handler.title('Delete 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)) sprints = project.getSprints() if len(sprints) > 0: if p_newproject is None: ErrorBox.die('Missing Parameter', "No new project specified") p_newproject = to_int(p_newproject, 'newproject', ErrorBox.die) target = Project.load(p_newproject) if not target: ErrorBox.die('Invalid Project', "No target project with ID <b>%d</b>" % p_newproject) for sprint in sprints: sprint.project = target sprint.save() project.delete() delay(handler, SuccessBox("Project deleted", close = True)) redirect("/admin/projects")
def deleteMessage(handler, id, p_x = None, p_y = None): handler.title('Delete message') requirePriv(handler, 'User') id = int(id) message = Message.load(id) if not message: ErrorBox.die("Message %d does not exist" % id) if message.user == handler.session['user']: verb = 'deleted' redir = '/messages/inbox' elif message.sender == handler.session['user']: verb = 'retracted' redir = '/messages/sent' if message.read: ErrorBox.die("Unable to retract read messages") else: ErrorBox.die("You don't have permission to delete this message") message.delete() delay(handler, SuccessBox("Message %s" % verb, close = 3)) redirect(redir)
def sprintAvailabilityPost(handler, id, p_hours): def die(msg): print msg done() handler.wrappers = False id = int(id) if not handler.session['user']: die("You must be logged in to modify sprint info") sprint = Sprint.load(id) if not sprint or sprint.isHidden(handler.session['user']): die("There is no sprint with ID %d" % id) if 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") avail = Availability(sprint) for k, hours in p_hours.items(): userid, timestamp = map(int, k.split(',', 1)) hours = int(hours) user = User.load(userid) if not user in sprint.members: die("Trying to set availability of non-member %s" % user.safe.username) time = tsToDate(timestamp) if not sprint.start <= timestamp <= sprint.end: die("Trying to set availability outside of sprint window") avail.set(user, time, hours) handler.responseCode = 299 delay(handler, SuccessBox("Updated availability", close = 3, fixed = True)) Event.sprintAvailUpdate(handler, sprint)
def adminProjectsDeleteSprintsPost(handler, id, p_newproject = None, p_sprintid = None): # p_newproject is part of the form because of adminProjectsMoveSprintsPost; it's unused here handler.title('Delete 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)) if not p_sprintid: delay(handler, WarningBox("No sprints to delete", 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.flags.add('deleted') sprint.save() Event.deleteSprint(handler, sprint) delay(handler, SuccessBox("%s queued for deletion. To delete now, run the <a href=\"/admin/cron\">Sprint Cleanup cron job</a>" % pluralize(len(sprints), 'sprint', 'sprints'), close = True)) redirect("/admin/projects/%d" % project.id)
def updateSavedSearch(handler, id, action): 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 follow your own search") elif action == 'follow' and search.following(handler.session['user']): ErrorBox.die('Invalid State', "Already following this search") elif action == 'unfollow' and not search.following(handler.session['user']): ErrorBox.die('Invalid State', "Not following this search") if action == 'follow': search.follow(handler.session['user']) delay(handler, SuccessBox("Search followed")) elif action == 'unfollow': search.unfollow(handler.session['user']) delay(handler, SuccessBox("Search unfollowed")) else: ErrorBox.die('Invalid Action', "Unknown action %s" % stripTags(action)) redirect('/search/saved/others')
del settings['kerberosRealm'] else: settings.kerberosRealm = p_kerberosRealm if p_smtpServer == '': if settings.smtpServer: del settings['smtpServer'] if settings.smtpFrom: del settings['smtpFrom'] else: settings.smtpServer = p_smtpServer settings.smtpFrom = p_smtpFrom or "sprint@%s" % p_smtpServer settings.autolink = zip(*autolinks) delay(handler, SuccessBox("Updated settings", close = True)) Event.adminSettings(handler, settings) redirect('/admin/settings') @admin('admin/users', 'Users', 'users') def adminUsers(handler): handler.title('User Management') requirePriv(handler, 'Admin') 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 "</style>" undelay(handler) print "<h3>New User</h3>"
def sprintInfoPost(handler, id, p_name, p_start, p_end, p_goals, p_members = None, p_clear = [], p_private = False, p_hidden = False): def die(msg): print msg done() handler.wrappers = False if not handler.session['user']: die("You must be logged in to modify sprint info") id = to_int(id, 'id', die) p_members = to_int(p_members, 'members', die) sprint = Sprint.load(id) if not sprint or sprint.isHidden(handler.session['user']): die("There is no sprint with ID %d" % id) if sprint.owner != handler.session['user']: die("You must be the scrummaster to modify sprint information") try: start = re.match("^(\d{1,2})/(\d{1,2})/(\d{4})$", p_start) if not start: raise ValueError month, day, year = map(int, start.groups()) start = datetime(year, month, day, 0, 0, 0) except ValueError: die("Malformed start date: %s" % stripTags(p_start)) try: end = re.match("^(\d{1,2})/(\d{1,2})/(\d{4})$", p_end) if not end: raise ValueError month, day, year = map(int, end.groups()) end = datetime(year, month, day, 23, 59, 59) except ValueError: die("Malformed end date: %s" % stripTags(p_end)) msg = Sprint.validateDates(start, end, tsToDate(sprint.start), tsToDate(sprint.end)) if msg: die(msg) goals = map(Goal.load, to_int(p_goals.keys(), 'goals', die)) if not all(goals): die("One or more goals do not exist") members = set(map(User.load, p_members)) if p_members else set() if not all(members): die("One or more members do not exist") if sprint.owner not in members: die("The scrummaster (%s) must be a sprint member" % sprint.owner) tasks = sprint.getTasks() changedTasks = set() avail = Availability(sprint) addMembers = set(members) - set(sprint.members) delMembers = set(sprint.members) - set(members) for user in delMembers: for task in filter(lambda task: user in task.assigned, tasks): print "Removing %s from %d<br>" % (user, task.id) task.assigned -= {user} if len(task.assigned) == 0: print "Adding %s to %d<br>" % (sprint.owner, task.id) task.assigned = {sprint.owner} changedTasks.add(task) avail.delete(user) sprint.members -= {user} # For event dispatching changes = OrderedDict([ ('name', None if sprint.name == p_name else p_name), ('start', None if tsToDate(sprint.start) == start else start), ('end', None if tsToDate(sprint.end) == end else end), ('addMembers', addMembers), ('delMembers', delMembers), # Updated later ('addGoals', []), ('removeGoals', []), ('addFlags', []), ('removeFlags', []), ]) sprint.members |= addMembers sprint.name = p_name p_private = (p_private or p_hidden) # Hidden implies Private for flagName, flagValue in (('private', p_private), ('hidden', p_hidden)): if flagValue and flagName not in sprint.flags: sprint.flags.add(flagName) changes['addFlags'].append(flagName) elif not flagValue and flagName in sprint.flags: sprint.flags.remove(flagName) changes['removeFlags'].append(flagName) if dateToTs(start) != sprint.start or dateToTs(end) != sprint.end: sprint.start = dateToTs(start) sprint.end = dateToTs(end) avail.trim() sprint.save() for id in p_goals: goal = Goal.load(int(id)) if goal.name != p_goals[id]: if goal.name: changes['removeGoals'].append(goal.name) if p_goals[id]: changes['addGoals'].append(p_goals[id]) goal.name = p_goals[id] goal.save() if start: for task in sprint.getTasks(includeDeleted = True): for rev in task.getRevisions(): if rev.timestamp < sprint.start: rev.timestamp = sprint.start rev.save() else: break for task in changedTasks: if task.timestamp < sprint.start: task.timestamp = sprint.start if p_clear: ids = [to_int(goalid, 'p_clear', die) for goalid in p_clear] for task in tasks: if task.goal and task.goal.id in ids: task.goal = None changedTasks.add(task) for task in changedTasks: print "Saving new revision for %d<br>" % task.id task.saveRevision(handler.session['user']) handler.responseCode = 299 delay(handler, SuccessBox("Updated info", close = 3, fixed = True)) Event.sprintInfoUpdate(handler, sprint, changes)
def newTaskImportPost(handler, group, source, p_data): def die(msg): print msg done() handler.title("Import Tasks") requirePriv(handler, "User") handler.wrappers = False id = int(group) group = Group.load(id) if not group or group.sprint.isHidden(handler.session["user"]): die("No group with ID <b>%d</b>" % id) sprint = group.sprint if 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") id = int(source) source = Sprint.load(id) if not source: die("No sprint with ID <b>%d</b>" % id) try: data = fromJS(p_data) except ValueError: die("Improperly encoded data") if not isinstance(data, list) or not all( set(task.keys()) == {"name", "assigned", "status", "groupid", "hours"} for task in data ): die("Improperly encoded data") usernames = {user.username for user in sprint.members} if not all( set(task["assigned"].split(" ")) <= usernames and task["status"] in statuses and isinstance(task["groupid"], int) and Group.load(task["groupid"]) is not None and isinstance(task["hours"], int) and task["hours"] >= 0 for task in data ): die("Invalid data") dataByGroup = {} for task in data: if task["groupid"] not in dataByGroup: dataByGroup[task["groupid"]] = [] dataByGroup[task["groupid"]].append(task) newGroups = {} # old sprint's group ID -> new sprint's new Group object for groupid in dataByGroup: oldGroup = Group.load(groupid) group = Group.load(sprintid=sprint.id, name=oldGroup.name) if not group: # No group in this sprint with the right name if groupid in newGroups: # Already made a new group group = newGroups[groupid] else: # Need a new group group = newGroups[groupid] = Group(sprint.id, oldGroup.name) group.save() for taskData in dataByGroup[groupid]: task = Task( group.id, sprint.id, handler.session["user"].id, 0, taskData["name"], taskData["status"], taskData["hours"], {User.load(username=username).id for username in taskData["assigned"].split(" ")}, ) task.save() Event.newTask(handler, task) numGroups, numTasks = len(newGroups), len(data) if numGroups > 0 and numGroups > 0: delay( handler, SuccessBox( "Added %d %s, %d %s" % (numGroups, "group" if numGroups == 1 else "groups", numTasks, "task" if numTasks == 1 else "tasks"), close=3, fixed=True, ), ) elif numGroups > 0: delay( handler, SuccessBox("Added %d %s" % (numGroups, "group" if numGroups == 1 else "groups"), close=3, fixed=True), ) elif numTasks > 0: delay( handler, SuccessBox("Added %d %s" % (numTasks, "task" if numTasks == 1 else "tasks"), close=3, fixed=True) ) else: delay(handler, WarningBox("No changes", close=3, fixed=True)) handler.responseCode = 299
def newTaskMany(handler, group, p_body, dryrun=False): def die(msg): print msg done() handler.wrappers = False requirePriv(handler, "User") id = int(group) defaultGroup = Group.load(id) if not defaultGroup or defaultGroup.sprint.isHidden(handler.session["user"]): die("No group with ID <b>%d</b>" % id) sprint = defaultGroup.sprint if 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") group = defaultGroup groups = [group] newGroups = [] tasks = {group: []} sep = "|" lines = map(lambda x: x.strip(" \r\n"), p_body.split("\n")) errors = [] defaultAssigned = {handler.session["user"] if handler.session["user"] in sprint.members else sprint.owner} for line in lines: if line == "": group = defaultGroup elif line[0] == "#": # Comment continue elif len(line) == 1: # Separator sep = line[0] elif ( line[0] == "[" and line[-1] == "]" and set(line[1:-1].split(" ")) <= set(u.username for u in sprint.members) ): # Default assigned defaultAssigned = {User.load(username=username) for username in line[1:-1].split(" ")} elif line[-1] == ":": # Group line = line[:-1] group = Group.load(sprintid=sprint.id, name=line) if not group: # Check if new group already defined for newGroup in newGroups: if newGroup.name == line: group = newGroup break if not group: # Make new group group = Group(sprint.id, line) newGroups.append(group) group.id = -len(newGroups) if not group in groups: # First time this group has been used in the script groups.append(group) tasks[group] = [] else: parts = line.split(sep) name, assigned, status, hours = None, None, None, None if not 2 <= len(parts) <= 4: errors.append("Unable to parse (field count mismatch): %s" % stripTags(line)) continue for part in parts: part = part.strip() if part == "": errors.append("Unable to parse (empty field): %s" % stripTags(line)) continue # Hours if hours is None: try: hours = int(part) continue except ValueError: pass # Status if status is None and part.lower() in statuses: status = part.lower() continue # Assigned if assigned is None and set(part.split(" ")) <= set(u.username for u in sprint.members): assigned = set(User.load(username=username) for username in part.split(" ")) continue # Name if name is None: name = part continue errors.append("Unable to parse (no field match on '%s'): %s" % (stripTags(part), stripTags(line))) if assigned is None: assigned = defaultAssigned if status is None: status = "not started" if name is None or hours is None: errors.append("Unable to parse (missing required fields): %s" % stripTags(line)) if not any(v is None for v in (name, assigned, status, hours)): tasks[group].append((name, assigned, status, hours)) if dryrun: handler.log = False numTasks = sum(len(taskSet) for taskSet in tasks.values()) taskHours = sum( hours for taskSet in tasks.values() for name, assigned, status, hours in taskSet if status != "deferred" ) ownTaskHours = sum( hours for taskSet in tasks.values() for name, assigned, status, hours in taskSet if status != "deferred" and handler.session["user"] in assigned ) avail = Availability(sprint) availHours = avail.getAllForward(getNow().date(), handler.session["user"]) usedHours = sum(task.effectiveHours() for task in sprint.getTasks() if handler.session["user"] in task.assigned) availHours -= usedHours if errors: print ErrorBox("<br>".join(errors)) if numTasks: box = InfoBox stats = "Adding %s " % pluralize(numTasks, "task", "tasks") if newGroups: stats += "and %s " % pluralize(len(newGroups), "group", "groups") stats += "for a total of %s" % pluralize(taskHours, "hour", "hours") if ownTaskHours != taskHours: stats += ", %s yours" % pluralize(ownTaskHours, "hour", "hours") if ownTaskHours: if availHours == 0: stats += ". You have no future availability for these tasks" box = WarningBox elif availHours < 0: stats += ". You are already overcommitted by %s" % pluralize(-availHours, "hour", "hours") box = WarningBox else: stats += ", %d%% of your future availability" % (100 * ownTaskHours / availHours) box = WarningBox if ownTaskHours > availHours else InfoBox print box(stats) elif not errors: print InfoBox('Waiting for tasks. Click "Help" above if needed') groupedTasks = OrderedDict( ( group, [ Task( group.id, sprint.id, handler.session["user"].id, 0, name, status, hours, {user.id for user in assigned}, 1, id=0, ) for name, assigned, status, hours in tasks[group] ], ) for group in groups ) print TaskTable(sprint, False, tasks=groupedTasks, status=True, name=True, assigned=True, hours=True) elif errors: die("There are unparseable lines in the task script. See the preview for more information") else: # There's some weirdness in the way groups auto-sequence that breaks when multiple groups are made without saving seq = maxOr(group.seq for group in sprint.getGroups()) + 1 for group in newGroups: group.seq = seq seq += 1 for group in groups: # Changing a group's ID will change its hash, so this pulls from tasks before saving the group in case it's new groupTasks = tasks[group] if group in newGroups: group.id = 0 group.save() for name, assigned, status, hours in groupTasks: task = Task(group.id, group.sprint.id, handler.session["user"].id, 0, name, status, hours) task.assigned |= assigned task.save() Event.newTask(handler, task) numGroups = len(newGroups) numTasks = sum(map(lambda g: len(g), tasks.values())) if numGroups > 0 and numGroups > 0: delay( handler, SuccessBox( "Added %d %s, %d %s" % ( numGroups, "group" if numGroups == 1 else "groups", numTasks, "task" if numTasks == 1 else "tasks", ), close=3, fixed=True, ), ) elif numGroups > 0: delay( handler, SuccessBox("Added %d %s" % (numGroups, "group" if numGroups == 1 else "groups"), close=3, fixed=True), ) elif numTasks > 0: delay( handler, SuccessBox("Added %d %s" % (numTasks, "task" if numTasks == 1 else "tasks"), close=3, fixed=True), ) else: delay(handler, WarningBox("No changes", close=3, fixed=True)) handler.responseCode = 299
def adminUsersPost(handler, p_action, p_username, p_privileges = [], p_send_welcome = False): handler.title('User Management') requirePriv(handler, 'Admin') for case in switch(p_action): if case('resetpw'): handler.title('Reset password') user = User.load(username = p_username) if not user: ErrorBox.die('Reset Password', "No user named <b>%s</b>" % stripTags(p_username)) hadPreviousKey = (user.resetkey != None and user.resetkey != '0') user.resetkey = "%x" % random.randint(0x10000000, 0xffffffff) user.save() Event.genResetKey(handler, user) print "Reset key for %s: <a href=\"/resetpw/%s?key=%s\">%s</a><br>" % (user, user.safe.username, user.resetkey, user.resetkey) if hadPreviousKey: print "<b>Warning</b>: This invalides the previous reset key for this user<br>" break if case('impersonate'): user = User.load(username = p_username) if not user: ErrorBox.die('Impersonate User', "No user named <b>%s</b>" % stripTags(p_username)) if not 'impersonator' in handler.session: handler.session['impersonator'] = handler.session['user'] handler.session.remember('impersonator') Event.impersonate(handler, user) handler.session['user'] = user redirect('/') break if case('sessions'): redirect("/admin/sessions?username=%s" % p_username) break if case('privileges'): redirect("/admin/privileges?username=%s" % p_username) break if case('new'): if User.load(username = p_username): ErrorBox.die('Add User', "There is already a user named <b>%s</b>" % stripTags(p_username)) if not re.match("^%s$" % USERNAME_PATTERN, p_username): ErrorBox.die('Add User', "Username <b>%s</b> is illegal" % stripTags(p_username)) if not all(name in privList for name in p_privileges): ErrorBox.die('Add User', "Unrecognized privilege name") user = User(p_username, '', privileges = set(p_privileges)) Event.newUser(handler, user) for priv in p_privileges: Event.grantPrivilege(handler, user, priv, True) if p_send_welcome: user.resetkey = "%x" % random.randint(0x10000000, 0xffffffff) msg = "A Sprint account has been created for you with the username `%s'. " % user.username if settings.kerberosRealm: msg += "You can use your %s password to login, or set a dedicated Sprint password here: " % settings.kerberosRealm else: msg += "You can set a password here: " msg += "http://%s:%d/resetpw/%s?key=%s" % (gethostname(), PORT, user.safe.username, user.resetkey) sendmail(user.getEmail(), "Sprint - New Account", msg) user.save() delay(handler, SuccessBox("Added user <b>%s</b>" % stripTags(p_username), close = True)) redirect("/users/%s" % user.username) break