Пример #1
0
def adminReplPost(handler, p_code):
	def makeStr(v):
		if isinstance(v, list):
			return "<ul>%s</ul>" % ''.join("<li>%s</li>" % item for item in v)
		return str(v)

	handler.wrappers = False

	p_code = re.sub('^!([A-Za-z]+)$', 'from \\1 import \\1', p_code)
	match = re.match('!([A-Za-z ]+)$', p_code)
	if match:
		parts = match.group(1).split(' ')
		res = []
		for part in parts:
			w = ResponseWriter()
			adminReplPost(handler, "!%s" % part)
			res.append(fromJS(w.done()))
		print toJS(res)
		done()

	if 'admin-repl' not in handler.session:
		print toJS({'code': highlightCode(p_code), 'stdout': '', 'stderr': ['Session Expired', 'REPL session no longer exists'], 'vars': {}})
		done()

	writer = ResponseWriter()
	try:
		Event.repl(handler, p_code)
		exec compile(p_code, '<admin repl>', 'single') in handler.session['admin-repl']
		stderr = ''
	except:
		stderr = map(str, [sys.exc_info()[0].__name__, sys.exc_info()[1]])
	stdout = writer.done()

	vars = sorted([(k, pformat(v), makeStr(v)) for (k, v) in handler.session['admin-repl'].items() if k != '__builtins__'], lambda (k1, v1, vs1), (k2, v2, vs2): cmp(k1, k2))
	print toJS({'code': highlightCode(p_code), 'stdout': stdout, 'stderr': stderr, 'vars': vars})
Пример #2
0
	def __str__(self):
		writer = ResponseWriter()
		try:
			self.out()
		finally:
			rtn = writer.done()
		return rtn
Пример #3
0
def entry(rev, oldRev, describeTask, icon, color, text):
	useRev = oldRev or rev
	print "<div class=\"revision-entry\" style=\"border-color: %s\" assigned=\"%s\">" % (color, ' '.join(user.username for user in rev.assigned))
	print "<div class=\"type\"><img class=\"bullet\" src=\"/static/images/%s\">&nbsp;%s</div>" % (icon, text)
	print "<div class=\"timestamp\">%s by %s</div>" % (tsToDate(rev.timestamp).strftime('%H:%M:%S'), userStr(rev))
	print "<div class=\"body\">"
	if describeTask:
		print "<a href=\"/tasks/%d\">%s</a>, assigned to %s<br>" % (rev.id, useRev.safe.name, ', '.join(userStr(useRev, user) for user in sorted(useRev.assigned)))

	w = ResponseWriter()
	yield
	text = w.done().strip()

	# Show hours change
	if oldRev:
		hoursDiff = rev.hours - oldRev.hours
		if hoursDiff != 0:
			# Skip it if we moved to an end state and zeroed the hours
			if oldRev.status == rev.status or rev.status not in ('canceled', 'complete', 'deferred', 'split') or rev.hours != 0:
				if text != '':
					text += '. '
				text += "Hours %s by <span class=\"hours-%s\">%d</span> to %d" % ('increased' if hoursDiff > 0 else 'decreased', 'up' if hoursDiff > 0 else 'down', abs(hoursDiff), rev.hours)

	print text
	print "</div>"
	print "</div>"
Пример #4
0
def adminInfo(handler):
	handler.title('Information')
	requirePriv(handler, 'Admin')

	print "<div class=\"info\">"

	print "<h3>Uptime</h3>"
	loadTime = getLoadtime()
	print "Started %s<br>" % loadTime
	print "Up for %s<br>" % timesince(loadTime)
	print "Total requests: %d<br>" % server().getTotalRequests()
	print "<form method=\"post\" action=\"/admin/restart\">"
	print Button('Restart', type = 'submit').negative()
	print "</form>"

	print "<h3>Threads</h3>"
	print "<table border=\"1\" cellspacing=\"0\" cellpadding=\"4\">"
	print "<tr><th>ID</th><th class=\"main\">Name</th><th>Alive</th><th>Daemon</th></tr>"
	for thread in sorted(threads(), key = lambda thread: thread.name):
		print "<tr><td>%s</td><td>" % ('None' if thread.ident is None else "%x" % abs(thread.ident))
		print thread.name
		print "<br>"
		try:
			print CollapsibleBox('Traceback', formatTrace(traceback.extract_stack(sys._current_frames()[thread.ident])))
		except Exception:
			pass
		print "</td><td class=\"%s\">&nbsp;</td><td class=\"%s\">&nbsp;</td></tr>" % ('yes' if thread.isAlive() else 'no', 'yes' if thread.daemon else 'no')
	print "</table>"

	print "<h3>Locks</h3>"
	print "<table border=\"1\" cellspacing=\"0\" cellpadding=\"4\">"
	print "<tr><th class=\"main\">Name</th><th>Available</th><th>Reentrant</th></tr>"
	for (name, lock) in sorted(locks.iteritems()):
		print "<tr><td>"
		print name
		avail = lock.avail()
		if not avail:
			print "<br>"
			writer = ResponseWriter()
			try:
				owner, tb = lock.owner, lock.tb
				name = ("%x" % abs(owner)) if owner else 'None'
				#TODO Is there no O(1) way to do this?
				for thread in threads():
					if thread.ident == owner:
						name = "%s (%x)" % (thread.name, abs(owner))
						break
				print "Owned by: <b>%s</b><br><br>" % name
				if tb:
					print "Acquisition traceback:<br>"
					print formatTrace(tb)
					print "<br>"
				print "Current traceback:<br>"
				print formatTrace(traceback.extract_stack(sys._current_frames()[owner]))
			except Exception, e:
				writer.clear()
				print "<i>(Unable to retrieve stack trace)</i>"
			print CollapsibleBox('Ownership', writer.done())
		print "</td><td class=\"%s\">%s</td><td class=\"%s\">&nbsp;</td></tr>" % ('yes' if avail else 'no', '&nbsp;' if avail else (lock.owner or '???'), 'yes' if lock.reentrant() else 'no')
Пример #5
0
	def run(self):
		writer = ResponseWriter()
		lock = server().block_requests()
		try:
			self.fn()
			self.log = writer.done()
		except Exception, e:
			writer.done()
			self.log = "<div style=\"font-weight: bold; color: #f00\">%s</div>" % stripTags(str(e))
Пример #6
0
	def __str__(self):
		w = ResponseWriter()
		print "<table class=\"list\">"
		if self.headers:
			print "<tr><th class=\"left\">%s</th><th class=\"right\">%s</th></tr>" % self.headers
		for element in self.elements:
			print "<tr><td class=\"left\">%s</td><td class=\"right\">%s</td></tr>" % element
		print "</table>"
		return w.done()
Пример #7
0
	def getLogString(self):
		writer = ResponseWriter()
		print "default sprint tab: %s" % self.defaultSprintTab
		print "default tasks tab: %s" % self.defaultTasksTab
		print "backlog styles:"
		for k, v in self.backlogStyles.iteritems():
			print "  %s: %s" % (k, v)
		print "messages:"
		for k, v in self.messages.iteritems():
			print "  %s: %s" % (k, v)
		return writer.done()
Пример #8
0
	def __str__(self):
		w = ResponseWriter()
		if self.label:
			print "%s " % self.label
		print "<div class=\"task-progress-total\" style=\"position: relative; top: 5px\">"
		if self.pcnt > 0:
			print "<div class=\"progress-current%s\" style=\"width: %d%%;\">" % (" %s" % self.cls, min(self.pcnt, 100))
		print "<span class=\"progress-percentage\">%d/%d hours (%s%%)</span>" % (self.amt, self.total, '&#8734;' if self.pcnt == inf else "%d" % self.pcnt)
		if self.pcnt > 0:
			print "</div>"
		print "</div>"
		return w.done().replace("\n", "")
Пример #9
0
def prefs(handler):
	handler.title('Preferences')
	requirePriv(handler, 'User')

	prefs = handler.session['user'].getPrefs()
	print "<script src=\"/static/prefs.js\" type=\"text/javascript\"></script>"
	print InfoBox('', id = 'post-status', close = True)
	print "<form method=\"post\" action=\"/prefs\">"

	print InfoBox("Note", "Your password and avatar are controlled from <a href=\"/users/%s\">your profile</a>" % handler.session['user'].username)

	print "<a name=\"default-sprint-tab\"></a>"
	print "<h3>Default Sprint Tab</h3>"
	print "Which tab you're taken to when following links in the project list:<br><br>"
	print "<select name=\"default_sprint_tab\">"
	for tab in sprintTabs().group('').values():
		print "<option value=\"%s\"%s>%s</option>" % (tab.name, ' selected' if tab.name == prefs.defaultSprintTab else '', tab.getDisplayName())
	print "</select>"

	print "<a name=\"default-tasks-tab\"></a>"
	print "<h3>Default Tasks Tab</h3>"
	print "Which tab you're taken to when adding a new backlog task:<br><br>"
	print "<select name=\"default_tasks_tab\">"
	for tab in taskTabs.group('').values():
		print "<option value=\"%s\"%s>%s</option>" % (tab.name, ' selected' if tab.name == prefs.defaultTasksTab else '', tab.getDisplayName())
	print "</select>"

	print "<h3>Backlog Style</h3>"
	print "How each task on the backlog is styled, based on status:<br><br>"
	select = ResponseWriter()
	print "<select name=\"backlog_style[%s]\">"
	for name in backlogStyles:
		print "<option value=\"%s\">%s</option>" % (name, name.title())
	print "</select>"
	select = select.done()

	tbl = LRTable()
	for statusBlock in statusMenu:
		for name in statusBlock:
			val = prefs.backlogStyles[name]
			tbl[statuses[name].text] = (select % name).replace("<option value=\"%s\">" % val, "<option value=\"%s\" selected>" % val)
	print tbl

	print "<h3>Messages</h3>"
	print "Which events will automatically notify you via system message:<br><br>"
	for name, desc in messageTypes:
		print "<input type=\"checkbox\" name=\"messages[%s]\" id=\"messages[%s]\"%s><label for=\"messages[%s]\">%s</label><br>" % (name, name, ' checked' if prefs.messages[name] else '', name, desc)

	print "<br>"
	print Button('Save', id = 'save-button', type = 'button').positive()
	print "</form>"
Пример #10
0
	def __str__(self):
		w = ResponseWriter()
		try:
			print "<table%s>" % {True: "", False: " class=\"%s\"" % self.cls}[self.cls == None]
			widths = Table.extend(self.widths, self.cols, None)
			if self.headers:
				print "<tr>" + ''.join(["<th>%s</th>" % x for x in Table.extend(self.headers, self.cols, '&nbsp;')]) + "</tr>"
			for i in range(len(self.elements)):
				ext = Table.extend(self.elements[i], self.cols, '&nbsp;')
				print "<tr>" + ''.join(["<td%s>%s</td>" % (" style=\"width: %s\"" % widths[i] if widths[i] else "", ext[i]) for i in range(self.cols)]) + "</tr>"
			print "</table>"
			return w.done()
		except:
			w.done()
			raise
Пример #11
0
    def requestDone(self):
        if self.wrappers:
            types = ["less", "css", "js"]
            includes = {type: [] for type in types}
            handler = getattr(self, "handler", None)
            if handler and "statics" in handler:
                for key in ensureIter(handler["statics"]):
                    for type in types:
                        if isfile("static/%s.%s" % (key, type)):
                            includes[type].append("/static/%s.%s" % (key, type))

            writer = ResponseWriter()
            header(self, includes)
            sys.stdout.write(self.response)
            footer(self)
            self.response = writer.done()
Пример #12
0
	def __str__(self):
		writer = ResponseWriter()
		print "<div class=\"box blue rounded login\">"
		print "<div class=\"title\">Login</div>"
		print "<span class=\"boxBody\">"
		print "<form method=\"post\" action=\"/login\">"
		print "<input type=\"hidden\" name=\"redir\" value=\"%s\">" % (self.redir or '/{{path}}?{{get-args}}')
		print "<table style=\"margin-left: 30px;\" class=\"list\">"
		print "<tr><td class=\"left\">Username:</td><td class=\"right\"><input type=\"text\" name=\"username\" class=\"username defaultfocus\"></td></tr>"
		print "<tr><td class=\"left\">Password:</td><td class=\"right\"><input type=\"password\" name=\"password\" class=\"password\">&nbsp;<a class=\"resetpw\" href=\"/resetpw/:mail\">(Forgot password)</a></td></tr>"
		print "<tr><td class=\"left\">Verification Code:</td><td class=\"right\"><input type=\"text\" name=\"verification\" class=\"code\" maxlength=\"6\" size=\"6\"><br><small>(This is only necessary if you've enabled two-factor authentication)</small></td></tr>"
		print "<tr><td class=\"left\">&nbsp;</td><td class=\"right\"><button type=\"submit\">Login</button></td></tr>"
		print "</table>"
		print "</form>"
		print "</span>"
		print "</div>"
		return writer.done()
Пример #13
0
	def requestDone(self):
		if isinstance(self.log, LogEntry):
			self.log.save()

		if self.wrappers:
			types = ['less', 'css', 'js']
			includes = {type: [] for type in types}
			handler = getattr(self, 'handler', None)
			if handler and 'statics' in handler:
				for key in ensureList(handler['statics']):
					for type in types:
						if isfile("static/%s.%s" % (key, type)):
							includes[type].append("/static/%s.%s" % (key, type))

			writer = ResponseWriter()
			header(self, includes)
			print self.response
			footer(self)
			self.response = writer.done()
Пример #14
0
def run(handler, p_command, p_path, p_mode = ''):
	handler.wrappers = False

	if not handler.session['user']:
		print toJS({'error': 'You must be logged in to use the shell'})
		return

	# This is already checked in the "su" command, but could be spoofed by the user
	if p_mode == 'admin' and not (handler.session['user'].hasPrivilege('Admin') or ('impersonator' in handler.session and handler.session['impersonator'].hasPrivilege('Admin'))):
		print toJS({'error': "You need the Admin privilege"})
		return

	if 'shell' not in handler.session: # Shell context
		handler.session['shell'] = {}
	handler.session['shell']['handler'] = handler;
	handler.session['shell']['path'] = p_path
	handler.session['shell']['mode'] = p_mode

	parts = split(p_command)
	testCommands = filterCommands(parts, p_mode)

	if len(testCommands) == 0:
		print toJS({'error': 'Unrecognized command'})
	elif len(testCommands) > 1:
		print toJS({'error': 'Ambiguous command'})
	else:
		syntax, fn, doc = testCommands[0]
		args = [y for (x, y) in zip(syntax, parts) if x == '_']
		w = ResponseWriter()
		mode = None
		rtn = {}

		try:
			mode = fn(handler.session['shell'], *args)
		except CommandError, e:
			w.done()
			print toJS({'error': str(e)})
			return
		except Redirect, e:
			rtn['redirect'] = e.target
Пример #15
0
	def __str__(self):
		w = ResponseWriter()
		try:
			print "<div class=\"tabs-container\">"
			print "<ul class=\"nav nav-tabs\">"
			for tab in self.tabs.group('').values():
				if self.tabs.group(tab.name):
					tab.outGroup(self.fmt, self.whr, self.tabs.group(tab.name))
				else:
					tab.out(self.fmt, self.whr == tab)
			# The current tab isn't in the list
			if self.whr is not None and (self.whr.base, self.whr.name) not in self.tabs:
				self.whr.out(self.fmt, True)
			print "</ul>"
			print "<div class=\"clear\"></div>"
			if self.tabs.afterContent:
				print "<div class=\"after-content\">%s</div>" % self.tabs.afterContent
			print "</div>"
			return w.done()
		except:
			w.done()
			raise
Пример #16
0
	def requestDone(self):
		if self.wrappers:
			types = ['less', 'css', 'js']
			includes = {type: [] for type in types}
			handler = getattr(self, 'handler', None)

			if handler and 'statics' in handler:
				for key in ensureList(handler['statics']):
					for type in types:
						if isfile(f"static/{key}.{type}"):
							includes[type].append(f"/static/{key}.{type}")

			if handler and 'view' in handler:
				includes['js'].append(f"/views/{handler['view']}.js")
				includes['less'].append(f"/views/{handler['view']}.less")

			with ResponseWriter(storageType = bytes) as writer:
				view = getattr(self, 'view', None)
				data = self.preprocessViewData(getattr(self, 'viewData', {}))

				header(self, includes, view)
				sys.stdout.write(self.response)
				footer(self, data, view)
				self.response = writer.done()
Пример #17
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)
Пример #18
0
def newTaskMany(handler, group, assigned=None):
    handler.title("New Tasks")
    requirePriv(handler, "User")
    id = int(group)

    body = ""
    if "many-upload" in handler.session:
        body = handler.session["many-upload"]
        del handler.session["many-upload"]
    elif assigned:
        body = "[%s]\n" % stripTags(assigned)

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

    print '<script src="/static/jquery.typing-0.2.0.min.js" type="text/javascript"></script>'
    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 "TaskTable.init();"
    print "</script>"

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

    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")

    help = ResponseWriter()
    print "Each line needs to match the following syntax. Unparseable lines generate an error message in the preview and must be resolved before saving"
    print "<ul>"
    print "<li><b>X</b> &mdash; A single character changes the field separator to that character. The exception is #, which starts a comment. The default field separator is |, so that's used in the examples here</li>"
    print '<li><b>X...X:</b> &mdash; A line ending in a colon is a group name. All tasks after that line will be added to that group. If no group of that name exists, it will be created (the preview will label that group as "(NEW)"). A blank line switches back to the default group, which is the group you clicked the new task button on, %s' % defaultGroup.safe.name
    print "<li><b>X...X|X...X[|X...X[|X...X]]</b> &mdash; 2-4 fields are a new task. The fields can appear in any order:<ul>"
    print "<li><b>name</b> &mdash; The name of the task</li>"
    print "<li><b>hours</b> &mdash; The number of hours this task will take</li>"
    print "<li><b>assignee</b> &mdash; The person assigned to this task. If multiple people, separate usernames with spaces. This field is optional as long as <b>status</b> is also omitted; it defaults to the current user if a sprint member, or the scrummaster otherwise, unless overridden (see below)</li>"
    print '<li><b>status</b> &mdash; The initial status of the task. This field is optional; it defaults to "not started"</li>'
    print "</ul></li>"
    print "<li><b>[X...X]</b> &mdash; A username (or space-separated list of usernames) wrapped in brackets makes that user or group the default assignee for all tasks that don't specify an assignee</li>"
    print "<li><b>#...</b> &mdash; A line starting with a hash character is a comment, and is ignored. You can only comment out entire lines; a hash within a line does not start a comment at that point</li>"
    print "</ul>"
    print "You can also use the form above the textarea to upload a text file. The file will be used to fill the textarea, so it should match the syntax described above"
    print CollapsibleBox("Help", help.done())

    print CollapsibleBox(
        "Groups",
        "<ul>%s</ul>"
        % "".join(
            "<li>%s</li>" % ("<b>%s</b> (default)" if group == defaultGroup else "%s") % group.safe.name
            for group in sprint.getGroups()
        ),
    )

    print '<form id="upload-tasks" method="post" enctype="multipart/form-data" action="/tasks/new/many/upload?group=%d">' % defaultGroup.id
    print '<input type="file" name="data"><br><br>'
    print "</form>"

    print '<form id="write-tasks" method="post" action="/tasks/new/many?group=%d">' % defaultGroup.id
    print '<textarea id="many-body" name="body" class="defaultfocus">%s</textarea>' % body

    print '<div id="preview"></div>'
    print InfoBox("Loading...", id="post-status", close=True)
    print '<div id="new-task-many-buttons">'
    print Button("Save All", id="save-button", type="button").positive()
    print Button("Cancel", id="cancel-button", type="button").negative()
    print "</div>"
    print "</form>"