Exemple #1
0
def newSprintPost(handler, p_project, p_name, p_start, p_end, p_members = None, p_private = False, p_hidden = False):
	def die(msg):
		print msg
		done()

	handler.wrappers = False
	p_project = int(p_project)
	if not handler.session['user']:
		die("You must be logged in to create a new sprint")

	project = Project.load(p_project)
	if not project:
		die("Unknown project ID: %d" % p_project)

	if p_name == '':
		die("The sprint name cannot be empty")

	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)
	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)
	if msg:
		die(msg)

	members = set(User.load(int(memberid)) for memberid in p_members)
	if None in members:
		die("Unknown username")
	if handler.session['user'] not in members:
		die("The scrummaster (%s) must be a sprint member" % handler.session['user'])

	sprint = Sprint(project.id, p_name, handler.session['user'].id, dateToTs(start), dateToTs(end))
	sprint.members |= members
	if p_private or p_hidden:
		sprint.flags.add('private')
	if p_hidden:
		sprint.flags.add('hidden')
	sprint.save()
	# Make a default 'Miscellaneous' group so there's something to add tasks to
	Group(sprint.id, 'Miscellaneous', 1, False).save()
	# Make the standard set of sprint goals
	Goal.newSet(sprint)

	handler.responseCode = 299
	print "/sprints/%d" % sprint.id
	Event.newSprint(handler, sprint)
Exemple #2
0
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)
Exemple #3
0
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
Exemple #4
0
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>"
Exemple #5
0
def sprintByID(context, id):
	id = to_int(id, 'id', fail)
	context['sprint'] = Sprint.load(id)
	if not context['sprint']:
		fail("No sprint with ID %d" % id)
	print context['sprint'].safe.name
	return ('sprint', "[sprint %d] $" % id)
Exemple #6
0
    def GET(self):
        url = web.input(sprint=None, command=None)

        if (url.sprint == None or url.sprint == "" or url.command == None):
            return render.index(
                None, None, """<br>
			    Howdy!
	    		In the input fields above please enter the sprint ID you are interested in, use YWW (year + week number) format. <br>
	    		For example '211' will show the sprint that ends in week 11 year 2012.<br>
	    		<br>""")
        else:
            sprint = Sprint(url.sprint)
            if sprint.okToGo == False:
                return render.index(
                    url.sprint, None, """<br>
	    			There was a problem getting information on the sprint, there could be db connection issue<br>
	    			<br>""")

            # do the requested work
            if url.command == 'plotburn':
                res = sprint.plotBurnDownChart()
                return render.burndown(url.sprint)
            elif url.command == 'ploteffort':
                res = sprint.plotEffortBarsChart()
                return render.burndown(url.sprint)
            elif url.command == 'plotbar':
                res = sprint.plotEffortStackedBarChart()
                return render.burndown(url.sprint)
            elif url.command == 'issuesstatus':
                res = sprint.printIssuesPerStatus()
            elif url.command == 'allissues' or url.command == 'issues':
                rows = sprint.getIssues("All")
                return render.issueslist(url.sprint, "All", rows)
            elif url.command == 'doneissues':
                rows = sprint.getIssues("Done")
                return render.issueslist(url.sprint, "Done", rows)
            elif url.command == 'openissues':
                rows = sprint.getIssues("Open")
                return render.issueslist(url.sprint, "Open", rows)
            elif url.command == 'effortsummary':
                res = sprint.printEffortSummary()

            return render.index(url.sprint, url.command,
                                """<br>Job done!<br>""")
Exemple #7
0
def distribute(handler, sprint):
    handler.title("Distribute Tasks")
    sprintid = int(sprint)
    sprint = Sprint.load(sprintid)
    if not sprint or sprint.isHidden(handler.session["user"]):
        ErrorBox.die("Invalid Sprint", "No sprint with ID <b>%d</b>" % id)

    handler.title(sprint.safe.name)
    print sprintTabs(sprint, "distribute")
    requirePriv(handler, "Write")
    if not (sprint.isActive() or sprint.isPlanning()):
        ErrorBox.die("Sprint Closed", "Unable to modify inactive sprint")
    if not sprint.canEdit(handler.session["user"]):
        ErrorBox.die("Permission Denied", "You don't have permission to modify this sprint")

    print '<script type="text/javascript" src="/static/highcharts/js/highcharts.js"></script>'
    print '<script type="text/javascript" src="/static/highcharts/js/highcharts-more.js"></script>'
    print '<script type="text/javascript">'
    print "var sprintid = %d;" % sprint.id
    print "</script>"

    print InfoBox("Loading...", id="post-status", close=True)

    print '<div id="distribution-range">'
    print "Acceptable commitment: <span></span>"
    print "</div>"
    print '<div id="distribution-range-slider"></div>'
    print '<div class="clear"></div>'

    print '<div id="distribution-chart"></div>'

    for col in ("left", "right"):
        print '<div class="distribution %s">' % col
        for user in sorted(sprint.members):
            print '<img class="user-gravatar" src="%s" userid="%d" title="%s">' % (
                user.getAvatar(64),
                user.id,
                user.safe.username,
            )
        if col == "right":
            print '<img class="user-gravatar" src="/static/images/revision-deferred.svg" userid="deferred" title="Deferred tasks">'
        print "<br><br>"

        print '<div class="selected">'
        print '<img style="visibility: hidden" class="user-gravatar" src="%s">' % User.getBlankAvatar(64)
        print '<div class="info">'
        print '<div class="username"></div>'
        print '<div class="hours"></div>'
        print '<div class="task-progress-total"><div class="progress-current" style="visibility: hidden;"></div></div>'
        print "</div>"
        print "</div>"
        print '<div class="clear"></div>'
        print '<div class="tasks"></div>'
        print "</div>"

    print '<div class="clear"></div><br><br>'
Exemple #8
0
def showSprintRetrospective(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")
	editing = (sprint.owner == handler.session['user'])

	handler.title(sprint.safe.name)
	drawNavArrows(sprint, handler.session['user'], 'retrospective')
	print tabs(sprint, ('wrapup', 'retrospective'))

	Markdown.head()
	print "<script type=\"text/javascript\">"
	print "var sprint_id = %d;" % sprint.id
	print "var editing = %s;" % toJS(editing)
	print "</script>"

	if not (sprint.isReview() or sprint.isOver()):
		ErrorBox.die('Sprint Open', "The retrospective isn't available until the sprint is in review")

	retro = Retrospective.load(sprint)
	if retro is None:
		if editing:
			print "<form method=\"post\" action=\"/sprints/%d/retrospective/start\">" % sprint.id
			print Button("Start Retrospective").post().positive()
			print "</form>"
		else:
			print ErrorBox("This sprint has no retrospective")
		done()

	print "The sprint retrospective asks two main questions:<br>"
	print "<ul class=\"retrospective-list\">"
	print "<li class=\"good\">What went well during the sprint?</li>"
	print "<li class=\"bad\">What could be improved in the next sprint?</li>"
	print "</ul>"
	if editing:
		print "For now the list of categories is fixed; eventually you'll be able to modify them per-sprint. Click an existing item to edit it, or the margin icon to toggle it. Fill in the text area at the end of the category to add a new item; clear a text area to delete it. <a href=\"/help/markdown\">Markdown</a> is available if necessary"

	print "<div class=\"retrospective markdown\">"
	for category, entries in retro.iteritems():
		print "<div class=\"category\" data-id=\"%d\">" % category.id
		print "<div class=\"name\">%s</div>" % stripTags(category.name)
		for entry in sorted(entries, key = lambda entry: 0 if entry.good else 1):
			print "<div class=\"entry %s\" data-id=\"%d\"><textarea>%s</textarea></div>" % ('good' if entry.good else 'bad', entry.id, stripTags(entry.body))
		if not editing and len(entries) == 0:
			print "<div class=\"none\">No entries</div>"
		print "</div>"
	print "</div>"
Exemple #9
0
	def GET(self):
		url = web.input(sprint=None, command=None)

		if(url.sprint == None or url.sprint == "" or url.command == None):
			return render.index(None, None,
			    """<br>
			    Howdy!
	    		In the input fields above please enter the sprint ID you are interested in, use YWW (year + week number) format. <br>
	    		For example '211' will show the sprint that ends in week 11 year 2012.<br>
	    		<br>""")
		else:
			sprint = Sprint(url.sprint)
			if sprint.okToGo == False:
				return render.index(url.sprint,  None, 
					"""<br>
	    			There was a problem getting information on the sprint, there could be db connection issue<br>
	    			<br>""")
			   
			# do the requested work
			if url.command == 'plotburn':
				res = sprint.plotBurnDownChart()
				return render.burndown(url.sprint)
			elif url.command == 'ploteffort':
				res = sprint.plotEffortBarsChart()
				return render.burndown(url.sprint)
			elif url.command == 'plotbar':
				res = sprint.plotEffortStackedBarChart()
				return render.burndown(url.sprint)
			elif url.command == 'issuesstatus':
				res = sprint.printIssuesPerStatus()
			elif url.command == 'allissues' or url.command == 'issues':
				rows = sprint.getIssues("All")
				return render.issueslist(url.sprint, "All", rows)
			elif url.command == 'doneissues':
				rows = sprint.getIssues("Done")
				return render.issueslist(url.sprint, "Done", rows)
			elif url.command == 'openissues':
				rows = sprint.getIssues("Open")
				return render.issueslist(url.sprint, "Open", rows)
			elif url.command == 'effortsummary':
				res = sprint.printEffortSummary()

			return render.index(url.sprint, url.command, """<br>Job done!<br>""")
Exemple #10
0
def finger(context, username):
	user = nameLookup(User, username, key = 'username')
	if not user:
		fail("No user named %s" % username)
	print clr(user.username, bold = True)
	print "Last seen: %s" % clr(tsToDate(user.lastseen) if user.lastseen else 'Never')
	sprints = Sprint.loadAllActive(user)
	if sprints:
		print "Member of: %s" % ', '.join(link(sprint.name, "/sprints/%d" % sprint.id) for sprint in sprints)
	if context['handler'].session['user'].hasPrivilege('Admin'):
		if user.hotpKey:
			print "HOTP key: %s" % clr(user.hotpKey)
		if user.resetkey:
			print "Reset key: %s" % clr(user.resetkey)
Exemple #11
0
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)
Exemple #12
0
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>"
Exemple #13
0
def sprintRetrospectiveRender(handler, sprintid, p_id, p_catid, p_body = None, p_good = None):
	def die(msg):
		print msg
		done()

	handler.wrappers = False
	if p_id != 'new':
		p_id = to_int(p_id, 'id', die)
	p_catid = to_int(p_catid, 'catid', die)
	if p_good is not None:
		p_good = to_bool(p_good)
	if not handler.session['user']:
		die("You must be logged in to modify sprint info")

	sprintid = int(sprintid)
	sprint = Sprint.load(sprintid)
	if not sprint or sprint.isHidden(handler.session['user']):
		die("There is no sprint with ID %d" % sprintid)
	elif sprint.owner != handler.session['user'] and (p_body is not None or p_good is not None):
		die("Only the scrummaster can edit the retrospective")
	elif not (sprint.isReview() or sprint.isOver()):
		die("The retrospective isn't available until the sprint is in review")

	if p_id == 'new':
		if p_body is None or p_good is None:
			die("Missing body/good")
		entry = RetroEntry(p_catid, p_body, p_good)
	else:
		entry = RetroEntry.load(p_id)
		if p_body is not None:
			entry.body = p_body
		if p_good is not None:
			entry.good = p_good

		if entry.body == '':
			entry.delete()
			handler.responseCode = 299
			print toJS({'id': entry.id, 'deleted': True});
			return

	entry.save()
	handler.responseCode = 299
	print toJS({'id': entry.id, 'body': Markdown.render(entry.body), 'good': entry.good})
Exemple #14
0
def sprint(context):
	match = re.search('^/sprints/([0-9]+)/?', context['path'])
	if match:
		return sprintByID(context, int(match.group(1)))

	match = re.search('\\?sprint=([0-9]+)$', context['path'])
	if match:
		return sprintByID(context, int(match.group(1)))

	sprints = Sprint.loadAllActive(context['handler'].session['user'])
	for case in switch(len(sprints)):
		if case(0):
			fail("No active sprints")
			break
		if case(1):
			return sprintByID(context, sprints[0].id)
		if case():
			fail("%d active sprints" % len(sprints))
			break
Exemple #15
0
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\">&nbsp;None</a>"
	for member in sorted(sprint.members):
		print "<a class=\"fancy\" assigned=\"%s\" href=\"/sprints/%d/history?assigned=%s\"><img src=\"%s\">&nbsp;%s</a>" % (member.username, id, member.username, member.getAvatar(16), member.username)
	print "</div><br>"

	chart.placeholder()
	showHistory(tasks, True)
	print "<br>"
Exemple #16
0
def sprintCleanup():
	for sprint in Sprint.loadAll():
		if 'deleted' in sprint.flags:
			sprint.delete(deep = True)
			print "Deleted sprint %d (%s: %s)<br>" % (sprint.id, sprint.project.safe.name, sprint.safe.name)
			continue

		if (not sprint.isOver()) or 'cleaned-up' in sprint.flags:
			continue
		tasks = Task.loadAll(sprintid = sprint.id, deleted = False)
		tasks = filter(lambda task: task.stillOpen(), tasks)

		for task in tasks:
			task.status = 'deferred'
			task.creator = sprint.owner
			task.timestamp = sprint.end
			task.revise()

		sprint.flags.add('cleaned-up')
		sprint.save()
		print "Cleaned up sprint %d (%s: %s): %s deferred<br>" % (sprint.id, sprint.project.safe.name, sprint.safe.name, pluralize(len(tasks), 'task', 'tasks'))
Exemple #17
0
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)
Exemple #18
0
def apiSprintInfo(handler, id):
	def die(msg):
		print toJS({'error': msg})
		done()

	id = int(id)
	handler.wrappers = False

	sprint = Sprint.load(id)
	if not sprint or sprint.isHidden(handler.session['user']):
		die("No sprint with ID %d" % id)

	abbrName = sprint.name.strip()
	if abbrName.startswith(sprint.project.name):
		abbrName = abbrName[len(sprint.project.name):].strip()

	print toJS({
			'id': sprint.id,
			'name': sprint.name,
			'abbreviated name': abbrName,
			'start': sprint.start,
			'end': sprint.end
			})
Exemple #19
0
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)
Exemple #20
0
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)
Exemple #21
0
def apiSprintsList(handler, calendar = False, start = None, end = None, _ = None):
	def die(msg):
		print toJS({'error': msg})
		done()

	handler.wrappers = False
	start = int(start) if start else None
	end = int(end) if end else None

	sprints = filter(lambda sprint: not sprint.isHidden(handler.session['user']), Sprint.loadAll())
	if start and end:
		sprints = filter(lambda sprint: (start <= sprint.start <= end) or (start <= sprint.end <= end), sprints)

	rtn = [{'id': sprint.id,
	        'title': "%s - %s" % (sprint.project.name, sprint.name) if calendar else sprint.name,
	        'start': tsToDate(sprint.start).strftime('%Y-%m-%d'),
	        'end': tsToDate(sprint.end).strftime('%Y-%m-%d'),
	        'active': sprint.isActive(),
	        'member': handler.session['user'] in sprint.members
	       } for sprint in sprints]
	if calendar:
		for entry in rtn:
			entry.update({'color': 'green' if entry['member'] and entry['active'] else '#36c;'})
	print toJS(rtn)
Exemple #22
0
def showBacklog(handler, id, search = None, devEdit = False):
	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")

	# Redirect to search help page if searched for empty string
	if search == '':
		redirect('/help/search')

	handler.title(sprint.safe.name)
	drawNavArrows(sprint, handler.session['user'], '')

	tasks = sprint.getTasks()
	editable = sprint.canEdit(handler.session['user']) or (devEdit and isDevMode(handler))
	search = Search(sprint, search)

	print "<script src=\"/settings/sprints.js\" type=\"text/javascript\"></script>"

	print "<script type=\"text/javascript\">"
	print "var sprintid = %d;" % id
	print "var currentUser = %s;" % toJS(handler.session['user'].username if handler.session['user'] else None)
	print "var totalTasks = %d;" % len(tasks)

	# True is a placeholder for the dynamic tokens (status, assigned)
	print "var searchTokens = %s;" % toJS(filter(None, [search.getBaseString() if search.hasBaseString() else None] + [True] + ["%s:%s" % (filt.getKey(), filt.value) for filt in search.getAll() if filt.getKey() not in ('status', 'assigned')]))
	print "var searchDescriptions = %s;" % toJS(filter(None, ["matching %s" % search.getBaseString() if search.hasBaseString() else None] + [True] + [filt.description() for filt in search.getAll()]))

	print "TaskTable.init({link_hours_status: %s});" % toJS(not sprint.isPlanning())
	print "$('document').ready(function() {"
	if search.has('assigned'):
		print "    $('%s').addClass('selected');" % ', '.join("#filter-assigned a[assigned=\"%s\"]" % user.username for user in search.get('assigned').users + ([handler.session['user']] if search.get('assigned').currentUser else []))
	if search.has('status'):
		print "    $('%s').addClass('selected');" % ', '.join("#filter-status a[status=\"%s\"]" % status.name for status in search.get('status').statuses)
	print "    apply_filters();"
	print "});"
	print "</script>"

	print "<div id=\"selected-task-box\">"
	print "<span></span>"
	print Button('history', id = 'selected-history').positive()
	print Button('highlight', id = 'selected-highlight').positive()
	print Button('mass edit', id = 'selected-edit').positive()
	print Button('cancel', id = 'selected-cancel') #.negative()
	print "</div>"

	print "<div class=\"backlog-tabs\">"
	print tabs(sprint, 'backlog')
	print "<input type=\"text\" id=\"search\" value=\"%s\">" % search.getFullString().replace('"', '&quot;')
	print "</div>"

	undelay(handler)

	print InfoBox('Loading...', id = 'post-status', close = True)

	avail = Availability(sprint) if sprint.isActive() else None
	dayStart = Weekday.today().date()
	print "<div id=\"filter-assigned\">"
	print "<a class=\"fancy danger\" href=\"#\"><img src=\"/static/images/cross.png\">&nbsp;None</a>"
	for member in sorted(sprint.members):
		cls = ['fancy']
		if not sprint.isPlanning() and avail and avail.get(member, dayStart) == 0:
			cls.append('away')
		print "<a class=\"%s\" assigned=\"%s\" href=\"/sprints/%d?search=assigned:%s\"><img src=\"%s\">&nbsp;%s</a>" % (' '.join(cls), member.username, id, member.username, member.getAvatar(16), member.username)
	print "</div><br>"

	print "<div id=\"filter-status\">"
	print "<a class=\"fancy danger\" href=\"#\"><img src=\"/static/images/cross.png\">&nbsp;None</a>"
	for status in sorted(statuses.values()):
		print "<a class=\"fancy\" status=\"%s\" href=\"/sprints/%d?search=status:%s\"><img src=\"%s\">%s</a>" % (status.name, id, status.name.replace(' ', '-'), status.getIcon(), status.text)
	print "</div><br>"

	if handler.session['user'].hasPrivilege('Admin') and 'deleted' in sprint.flags:
		print "<form method=\"post\" action=\"/admin/projects/%d/cancel-deletion/%d\">" % (sprint.project.id, sprint.id)
		print WarningBox("This sprint is flagged for deletion during nightly cleanup. %s" % Button('Cancel').mini().post())
		print "</form>"
	if sprint.isPlanning():
		if sprint.isActive():
			print InfoBox("Today is <b>sprint planning</b> &mdash; tasks aren't finalized until the end of the day")
		else:
			daysTillPlanning = (tsToDate(sprint.start) - getNow()).days + 1
			print InfoBox("The sprint has <b>not begun</b> &mdash; planning is %s. All changes are considered to have been made midnight of plan day" % ('tomorrow' if daysTillPlanning == 1 else "in %d days" % daysTillPlanning))
	elif sprint.isReview():
		print InfoBox("Today is <b>sprint review</b> &mdash; this is the last day to make changes to the backlog. All open tasks will be deferred at the end of the day")
	elif 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>" % sprint.id)

	tasks = search.filter(tasks)

	if isDevMode(handler):
		print Button('#all-tasks borders', "javascript:$('#all-tasks, #all-tasks tr td').css('border', '1px solid #f00').css('border-collapse', 'collapse');").negative()
		if not editable:
			print Button('make editable', "/sprints/%d?devEdit" % id).negative()
		elif devEdit:
			print Button('make uneditable', "/sprints/%d" % id).negative()

		print "<div class=\"debugtext\">"
		print "start: %d (%s)<br>" % (sprint.start, tsToDate(sprint.start))
		print "end: %d (%s)<br>" % (sprint.end, tsToDate(sprint.end))
		print "</div>"

	showing = ResponseWriter()
	print "<span id=\"task-count\"></span>"
	# save-search href set by update_task_count()
	print "<a class=\"save-search\"><img src=\"/static/images/save.png\" title=\"Save search\"></a>"
	print "<a class=\"cancel-search\" href=\"/sprints/%d\"><img src=\"/static/images/cross.png\" title=\"Clear search\"></a>" % id
	showing = showing.done()

	print TaskTable(sprint, editable = editable, tasks = tasks, tableID = 'all-tasks', dateline = showing, taskClasses = {task: ['highlight'] for task in (search.get('highlight').tasks if search.has('highlight') else [])}, debug = isDevMode(handler), groupActions = True, taskModActions = True, index = True, goal = True, status = True, name = True, assigned = True, historicalHours = True, hours = True, devEdit = devEdit)
Exemple #23
0
def showSprintChecklist(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")
	tasks = sprint.getTasks()

	handler.title(sprint.safe.name)
	drawNavArrows(sprint, handler.session['user'], 'checklist')
	print tabs(sprint, ('planning', 'checklist'))

	warnings = sprint.getWarnings()

	def good(text): print "<div class=\"good\">%s</div>" % text
	def warn(text): print "<div class=\"warn\">%s</div>" % text
	def bad(text): print "<div class=\"bad\">%s</div>" % text
	def userStr(l):
		l = map(str, sorted(l))
		for case in switch(len(l)):
			if case(0):
				return ''
			if case(1):
				return l[0]
			if case(2):
				return "%s and %s" % tuple(l)
			if case():
				front, last = l[:-1], l[-1]
				return "%s, and %s" % (', '.join(front), last)

	print "<div class=\"sprint-warnings\">"
	print "<h1>Members</h1>"

	if 'no-availability' in warnings:
		bad("%s %s no <a href=\"/sprints/%d/availability\">availability</a>" % (userStr(warnings['no-availability']), 'has' if len(warnings['no-availability']) == 1 else 'have', sprint.id))
	else:
		good("All members have some availability during the sprint")

	if 'overcommitted' in warnings:
		bad("%s %s <a href=\"/sprints/%d/metrics#commitment-by-user\">overcommitted</a>" % (userStr(warnings['overcommitted']), 'is' if len(warnings['overcommitted']) == 1 else 'are', sprint.id))
	else:
		good("All members have enough availability for their tasks")

	if 'users-no-tasks' in warnings:
		bad("%s %s <a href=\"/sprints/%d/metrics#commitment-by-user\">no tasks</a>" % (userStr(warnings['users-no-tasks']), 'has' if len(warnings['users-no-tasks']) == 1 else 'have', sprint.id))
	else:
		good("All members have tasks assigned")

	print "<br><h1>Goals</h1>"

	if 'no-sprint-goals' in warnings:
		bad("There are no <a href=\"/sprints/%d/info\">sprint goals</a>" % sprint.id)
	else:
		goals = filter(lambda goal: goal.name != '', sprint.getGoals())
		good(pluralize(len(goals), 'sprint goal is defined', 'sprint goals are defined'))

	if 'tasks-without-goals' in warnings:
		bad("There are many <a href=\"/sprints/%d?search=goal:none\">tasks unrelated to the sprint goals</a>" % sprint.id)
	elif 'no-sprint-goals' in warnings and tasks != []:
		warn("There are %s waiting for goals" % pluralize(len(tasks), 'task', 'tasks'))
	else:
		unaffiliated = filter(lambda task: not task.goal, tasks)
		if unaffiliated == []:
			good("All tasks contribute to the sprint goals")
		else:
			good("Most tasks contribute to the sprint goals (<a href=\"/sprints/%d?search=goal:none\">%d left</a>)" % (sprint.id, len(unaffiliated)))

	if 'goals-no-tasks' in warnings:
		bad("There are <a href=\"/sprints/%d/metrics#goals\">goals with no tasks assigned</a> (%s)" % (sprint.id, '&nbsp;'.join("<img class=\"bumpdown\" src=\"/static/images/tag-%s.png\">" % goal.color for goal in warnings['goals-no-tasks'])))

	print "<br><h1>Tasks</h1>"

	if 'open-without-hours' in warnings:
		bad("There are <a href=\"/sprints/%d?search=status:not-started,in-progress,blocked hours:0\">open tasks with no hour estimate</a>" % sprint.id)
	else:
		good("All open tasks have hours estimated")

	if 'closed-with-hours' in warnings:
		bad("There are <a href=\"/sprints/%d?search=status:complete,canceled,split hours:>0\">closed tasks with hours</a>. These hours are counted as part of the sprint commitment" % sprint.id)
	else:
		good("All closed tasks have 0 hours")

	if 'too-many-hours' in warnings:
		bad("There are <a href=\"/sprints/%d?search=hours:>24\">tasks with too many hours</a>" % sprint.id)
	else:
		good("All tasks have at most 24 hours estimated")

	print "</div>"
Exemple #24
0
def showAvailability(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")
	tasks = sprint.getTasks()

	handler.title(sprint.safe.name)
	drawNavArrows(sprint, handler.session['user'], 'availability')
	print tabs(sprint, 'availability')

	print "<script type=\"text/javascript\">"
	print "var sprintid = %d;" % id
	print "</script>"

	print InfoBox('Loading...', id = 'post-status', close = True)

	avail = Availability(sprint)
	oneday = timedelta(1)
	start, end = tsToDate(sprint.start), tsToDate(sprint.end)
	editable = sprint.canEdit(handler.session['user'])

	print "<form method=\"post\" action=\"/sprints/%d/availability\">" % sprint.id

	# This is in case the user presses enter
	print Button('', id = 'save-button2', type = 'button')

	print "<table class=\"availability\">"
	print "<tr class=\"dateline\">"
	print "<td>&nbsp;</td>"
	for day in sprint.getDays():
		print "<td>%s<br>%s</td>" % (day.strftime('%d'), day.strftime('%a'))
		if day.weekday() == 4:
			print "<td class=\"spacer\">&nbsp;</td>"
	print "<td class=\"buttons\"></td>" # The stylesheet counts cells from the right, so it's important that this be here to match the other rows
	print "</tr>"
	if editable:
		print "<tr class=\"dateline\">"
		print "<td class=\"buttons\">"
		if editable:
			print Button('set all 8', id = 'set-all-8', type = 'button').info()
		print "</td>"
		for day in sprint.getDays():
			print "<td class=\"buttons\"><img src=\"/static/images/clipboard.png\" title=\"Copy first down\"></td>"
			if day.weekday() == 4:
				print "<td class=\"spacer\">&nbsp;</td>"
		print "<td class=\"buttons\"></td>"
		print "</tr>"

	for user in sorted(sprint.members):
		print "<tr class=\"userline\">"
		print "<td class=\"username\">%s&nbsp;<img src=\"%s\"></td>" % (user.safe.username, user.getAvatar(16))
		for day in sprint.getDays():
			if editable:
				print "<td><input type=\"text\" name=\"hours[%d,%d]\" value=\"%d\"></td>" % (user.id, dateToTs(day), avail.get(user, day))
			else:
				print "<td style=\"text-align: center\">%d</td>" % avail.get(user, day)

			if day.weekday() == 4:
				print "<td class=\"spacer\">&nbsp;</td>"
		print "<td class=\"buttons\">"
		if editable:
			print "<img src=\"/static/images/clipboard.png\" title=\"Copy first right\">"
		print "</td>"
		print "</tr>"

	if editable:
		print "<tr><td>%s</td></tr>" % Button('Save', id = 'save-button', type = 'button').positive()
	print "</table>"
	print "</form>"
Exemple #25
0
def newTaskImport(handler, group, source=None, assigned=None):
    # 'assigned' is ignored, it's just in case the user gets here from a filtered backlog
    handler.title("New Tasks")
    requirePriv(handler, "User")
    id = int(group)

    print tabs.format(id).where("import")

    group = Group.load(id)
    if not group or group.sprint.isHidden(handler.session["user"]):
        ErrorBox.die("Invalid Group", "No group with ID <b>%d</b>" % id)

    sprint = group.sprint
    if not (sprint.isActive() or sprint.isPlanning()):
        ErrorBox.die("Sprint Closed", "Unable to modify inactive sprint")
    elif not sprint.canEdit(handler.session["user"]):
        ErrorBox.die("Permission Denied", "You don't have permission to modify this sprint")

    sprints = sprint.project.getSprints()
    sprintIdx = sprints.index(sprint)
    prevSprint = sprints[sprintIdx - 1] if sprintIdx > 0 else None

    if not source:
        print "Select a sprint to import from:<br><br>"
        print '<form method="get" action="/tasks/new/import">'
        print '<input type="hidden" name="group" value="%d">' % group.id
        print '<select name="source" id="import-source">'
        for projectIter in Project.getAllSorted(handler.session["user"], sprint.project):
            print '<optgroup label="%s">' % projectIter.safe.name
            for sprintIter in projectIter.getSprints():
                print '<option value="%d"%s>%s</option>' % (
                    sprintIter.id,
                    " selected" if sprintIter == prevSprint else "",
                    sprintIter.safe.name,
                )
            print "</optgroup>"
        print "</select>"
        print "<br><br>"
        print Button("Next").positive().post()
        print "</form>"
    else:
        id = int(source)
        source = Sprint.load(id)
        if not source:
            ErrorBox.die("Invalid Sprint", "No sprint with ID <b>%d</b>" % id)

        print '<script type="text/javascript">'
        nextURL = "/sprints/%d" % sprint.id
        if assigned:
            nextURL += "?search=assigned:%s" % stripTags(assigned.replace(" ", ","))
        print "next_url = %s;" % toJS(nextURL)
        print 'post_url = "/tasks/new/import?group=%d&source=%d";' % (group.id, source.id)
        print "scrummaster = %s;" % toJS(sprint.owner.username)
        print "TaskTable.init();"
        print "</script>"

        print '<b>Source sprint</b>: <a href="/sprints/%d">%s</a><br>' % (source.id, source.name)
        print '<b>Target sprint</b>: <a href="/sprints/%d">%s</a><br><br>' % (sprint.id, sprint.name)
        print "All incomplete tasks are listed here, with their current values from the source sprint. You can change any of the fields before importing. Only checked tasks will be imported<br><br>"

        assignedList = [sprint.owner] + list(sprint.members - {sprint.owner})
        print TaskTable(
            source,
            editable=True,
            assignedList=assignedList,
            checkbox=True,
            status=True,
            name=True,
            assigned=True,
            hours=True,
            debug=isDevMode(handler),
        )

        print InfoBox("Loading...", id="post-status", close=True)
        print Button("Import", id="save-button", type="button").positive()
        print Button("Cancel", id="cancel-button", type="button").negative()
        print "</form><br><br>"
Exemple #26
0
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
Exemple #27
0
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)
Exemple #28
0
def showInfo(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")
	tasks = sprint.getTasks()
	editable = sprint.owner == handler.session['user'] # Info can be edited even after the sprint closes

	handler.title(sprint.safe.name)
	drawNavArrows(sprint, handler.session['user'], 'info')

	print "<script type=\"text/javascript\">"
	print "var sprintid = %d;" % id
	print "var startMin = '%s';" % min(tsToDate(sprint.start), getNow()).strftime('%m/%d/%Y')
	print "var endMin = '%s';" % min(tsToDate(sprint.end), getNow()).strftime('%m/%d/%Y')
	print "</script>"

	print InfoBox('Loading...', id = 'post-status', close = True)

	print tabs(sprint, 'info')

	print "<form method=\"post\" action=\"/sprints/info?id=%d\">" % sprint.id
	print "<b>Name</b><br>"
	if editable:
		print "<input type=\"text\" name=\"name\" class=\"name\" value=\"%s\"><br><br>" % sprint.safe.name
	else:
		print "%s<br><br>" % sprint.safe.name
	print "<b>Duration</b><br>"
	if editable:
		print "<input type=\"text\" name=\"start\" class=\"date\" value=\"%s\">" % (tsToDate(sprint.start).strftime('%m/%d/%Y')),
	else:
		print tsToDate(sprint.start).strftime('%m/%d/%Y'),
	print '-',
	if editable:
		print "<input type=\"text\" name=\"end\" class=\"date\" value=\"%s\">" % (tsToDate(sprint.end).strftime('%m/%d/%Y')),
	else:
		print tsToDate(sprint.end).strftime('%m/%d/%Y'),
	print "<br><br>"
	print "<b>Sprint goals</b><br>"
	for goal in sprint.getGoals():
		if editable:
			print "<input type=\"text\" class=\"goal\" style=\"background-image: url(/static/images/tag-%s.png)\" name=\"goals[%d]\" goalid=\"%d\" value=\"%s\"><br>" % (goal.color, goal.id, goal.id, goal.safe.name)
			numTasks = len(filter(lambda task: task.goal == goal, tasks))
			if numTasks > 0:
				print "<div class=\"clear-goal-tasks\" id=\"clear-tasks-%d\"><input type=\"checkbox\" id=\"clear-tasks-check-%d\" name=\"clear[]\" value=\"%d\"><label for=\"clear-tasks-check-%d\">Clear the %s currently assigned to this goal (%s will no longer contribute to a sprint goal)</label></div>" % (goal.id, goal.id, goal.id, goal.id, pluralize(numTasks, 'task', 'tasks'), 'it' if numTasks == 1 else 'they')
		elif goal.name:
			print "<img class=\"bumpdown\" src=\"/static/images/tag-%s.png\">&nbsp;%s<br>" % (goal.color, goal.safe.name)
	print "</table>"
	print "<br>"
	print "<b>Members</b><br>"
	if editable:
		print "<select name=\"members[]\" id=\"select-members\" multiple>"
		for user in sorted(User.loadAll()):
			if user.hasPrivilege('User') or user in sprint.members:
				print "<option value=\"%d\"%s>%s</option>" % (user.id, ' selected' if user in sprint.members else '', user.safe.username)
		print "</select>"
	else:
		print ', '.join(member.str('scrummaster' if member == sprint.owner else 'member') for member in sorted(sprint.members))
	print "<br><br>"
	print "<b>Options</b><br>"
	if editable:
		print "<div><input type=\"checkbox\" name=\"private\" id=\"flag-private\"%s%s><label for=\"flag-private\">Private &ndash; Only sprint members can view tasks</label></div>" % (' checked' if 'private' in sprint.flags else '', ' disabled' if 'hidden' in sprint.flags else '')
		print "<div><input type=\"checkbox\" name=\"hidden\" id=\"flag-hidden\"%s><label for=\"flag-hidden\">Hidden &ndash; Only sprint members can see the sprint</label></div>" % (' checked' if 'hidden' in sprint.flags else '')
	else:
		if 'hidden' in sprint.flags:
			print "<img src=\"/static/images/shield.png\" class=\"option\">&nbsp;Hidden"
		elif 'private' in sprint.flags:
			print "<img src=\"/static/images/lock.png\" class=\"option\">&nbsp;Private"
	print "<br>"

	if editable:
		print Button('Save', id = 'save-button', type = 'button').positive()
	print "</form>"
Exemple #29
0
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)
Exemple #30
0
def user(handler, username):
	user = User.load(username = username)
	if not user:
		ErrorBox.die('User', "No user named <b>%s</b>" % stripTags(username))

	Markdown.head('form#message-form .body pre code')
	print "<script src=\"/static/jquery.typing-0.2.0.min.js\" type=\"text/javascript\"></script>"
	print "<script src=\"/static/users.js\" type=\"text/javascript\"></script>"
	Chart.include()
	undelay(handler)

	handler.title(user.safe.username)
	handler.replace('$bodytitle$', '', 1)
	print "<img src=\"%s\" class=\"gravatar\">" % user.getAvatar(64)
	print "<h1>%s</h1>" % user.safe.username
	if isDevMode(handler):
		print "<div class=\"debugtext\">User ID: %d</div>" % user.id
	print "<div class=\"clear\"></div>"

	if handler.session['user'] and handler.session['user'].hasPrivilege('Admin'):
		print "<h3>Admin</h3>"
		print "<form method=\"post\" action=\"/admin/users\">"
		print "<input type=\"hidden\" name=\"username\" value=\"%s\">" % user.username
		print "<button type=\"submit\" class=\"btn\" name=\"action\" value=\"resetpw\">Reset password</button>"
		print "<button type=\"submit\" class=\"btn\" name=\"action\" value=\"impersonate\">Impersonate</button>"
		print "<button type=\"submit\" class=\"btn\" name=\"action\" value=\"sessions\">Manage sessions</button>"
		print "<button type=\"submit\" class=\"btn\" name=\"action\" value=\"privileges\">Manage privileges</button>"
		print "</form>"

	if user == handler.session['user']:
		print "<h3>Avatar</h3>"
		if user.hasLocalAvatar():
			print "Your avatar is currently <a href=\"/users/%s/avatar/set\">locally hosted</a>" % user.username
		else:
			print "Your avatar can be changed at <a href=\"http://gravatar.com/\" target=\"_new\">http://gravatar.com/</a>. It must be associated with the e-mail <b>%s</b>, and be rated PG. You can also host an avatar <a href=\"/users/%s/avatar/set\">locally</a>, if necessary" % (user.getEmail(), user.username)

		print "<h3>Authentication</h3>"
		print "Your sprint tool password can be changed <a href=\"/resetpw\">here</a>.",
		if settings.kerberosRealm:
			print "You can also use your %s kerberos password to login" % settings.kerberosRealm,
		print "<br><br>"
		if user.hotpKey == '':
			print "You also have the option to use two-factor authentication via <a href=\"http://en.wikipedia.org/wiki/HOTP\">HOTP</a>. You can use <a href=\"http://support.google.com/a/bin/answer.py?hl=en&answer=1037451\">Google Authenticator</a> to generate verification codes<br><br>"
			print "<form method=\"post\" action=\"/security/two-factor\">"
			print "<button type=\"submit\" class=\"btn danger\" name=\"action\" value=\"enable\">Enable two-factor authentication</button>"
			print "</form>"
		else:
			print "You are currently using two-factor authentication<br><br>"
			print "<form method=\"post\" action=\"/security/two-factor\">"
			print "<button type=\"submit\" class=\"btn danger\" name=\"action\" value=\"enable\">Reset HOTP key</button>"
			print "<button type=\"submit\" class=\"btn danger\" name=\"action\" value=\"disable\">Disable two-factor authentication</button>"
			print "</form>"

		print "<h3>Messages</h3>"
		print "Your inbox and sent messages can be viewed <a href=\"/messages/inbox\">here</a><br>"

	print "<h3>Last seen</h3>"
	if not user.lastseen:
		print "Never"
	elif dateToTs(getNow()) - user.lastseen < 60:
		print "Just now"
	else:
		print "%s ago" % timesince(tsToDate(user.lastseen))

	if handler.session['user'] and handler.session['user'] != user:
		print "<h3>Message</h3>"
		print "<small>(Messages are formatted in <a target=\"_blank\" href=\"/help/markdown\">markdown</a>)</small>"
		print "<form id=\"message-form\" method=\"post\" action=\"/messages/send\">"
		print "<input type=\"hidden\" name=\"userid\" value=\"%d\">" % user.id
		print "<textarea name=\"body\" class=\"large\"></textarea>"
		print "<div class=\"body markdown\"><div id=\"preview\"></div></div>"
		print Button('Send').post().positive()
		print "</form>"

	print "<h3>Project distribution</h3>"
	sprints = filter(lambda s: user in s.members, Sprint.loadAllActive())
	sprintHours = map(lambda s: (s, Availability(s).getAllForward(getNow(), user)), sprints)
	projectHours = map(lambda (p, g): (p, sum(hours for sprint, hours in g)), groupby(sprintHours, lambda (s, a): s.project))

	# For now at least, don't show projects with no hours
	projectHours = filter(lambda (p, h): h > 0, projectHours)
	if len(projectHours) > 0:
		chart = Chart('chart')
		chart.title.text = ''
		chart.tooltip.formatter = "function() {return '<b>' + this.point.name + '</b>: ' + this.point.y + '%';}"
		chart.plotOptions.pie.allowPointSelect = True
		chart.plotOptions.pie.cursor = 'pointer'
		chart.plotOptions.pie.dataLabels.enabled = False
		chart.plotOptions.pie.showInLegend = True
		chart.credits.enabled = False
		chart.series = seriesList = []

		series = {
			'type': 'pie',
			'name': '',
			'data': []
		}
		seriesList.append(series)

		total = sum(hours for project, hours in projectHours)
		for project, hours in projectHours:
			series['data'].append([project.name, float("%2.2f" % (100 * hours / total))])

		chart.js()
		chart.placeholder()
	else:
		print "Not a member of any active sprints"
Exemple #31
0
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