def renderBranchName(target): target.code("branch").text(review.branch.name, linkify=linkify.Context()) if repository.name != user.getPreference(db, "defaultRepository"): target.text(" in ") target.code("repository").text("%s:%s" % (configuration.base.HOSTNAME, repository.path)) cursor.execute("""SELECT id, remote, remote_name, disabled, previous FROM trackedbranches WHERE repository=%s AND local_name=%s""", (repository.id, review.branch.name)) row = cursor.fetchone() if row: trackedbranch_id, remote, remote_name, disabled, previous = row target.p("tracking disabled" if disabled else "tracking").text("tracking") target.code("branch").text(remote_name, linkify=linkify.Context(remote=remote)) target.text(" in ") target.code("repository").text(remote, linkify=linkify.Context()) if previous: target.span("lastupdate").script(type="text/javascript").text("document.write('(last fetched: ' + shortDate(new Date(%d)) + ')');" % (calendar.timegm(previous.utctimetuple()) * 1000)) if user in review.owners: buttons = target.div("buttons") if disabled: buttons.button("enabletracking", onclick="enableTracking(%d);" % trackedbranch_id).text("Enable Tracking") else: buttons.button("disabletracking", onclick="triggerUpdate(%d);" % trackedbranch_id).text("Update Now") buttons.button("disabletracking", onclick="disableTracking(%d);" % trackedbranch_id).text("Disable Tracking")
def renderShowComment(req, db, user): chain_id = req.getParameter("chain", filter=int) context_lines = req.getParameter("context", user.getPreference( db, "comment.diff.contextLines"), filter=int) default_compact = "yes" if user.getPreference( db, "commit.diff.compactMode") else "no" compact = req.getParameter("compact", default_compact) == "yes" default_tabify = "yes" if user.getPreference( db, "commit.diff.visualTabs") else "no" tabify = req.getParameter("tabify", default_tabify) == "yes" original = req.getParameter("original", "no") == "yes" chain = review_comment.CommentChain.fromId(db, chain_id, user) if chain is None or chain.state == "empty": raise page.utils.DisplayMessage("Invalid comment chain ID: %d" % chain_id) review = chain.review document = htmlutils.Document(req) html = document.html() head = html.head() body = html.body() document.setTitle("%s in review %s" % (chain.title(False), review.branch.name)) def renderHeaderItems(target): review_utils.renderDraftItems(db, user, review, target) target.div("buttons").span("buttonscope buttonscope-global") page.utils.generateHeader(body, db, user, renderHeaderItems, extra_links=[("r/%d" % review.id, "Back to Review")]) document.addExternalScript("resource/showcomment.js") document.addInternalScript(user.getJS(db)) document.addInternalScript(review.repository.getJS()) document.addInternalScript(review.getJS()) document.addInternalScript("var contextLines = %d;" % context_lines) document.addInternalScript( "var keyboardShortcuts = %s;" % (user.getPreference(db, "ui.keyboardShortcuts") and "true" or "false")) if not user.isAnonymous() and user.name == req.user: document.addInternalScript( "$(function () { markChainsAsRead([%d]); });" % chain_id) review_html.renderCommentChain(db, body.div("main"), user, review, chain, context_lines=context_lines, compact=compact, tabify=tabify, original=original, linkify=linkify.Context(db=db, request=req, review=review)) if user.getPreference(db, "ui.keyboardShortcuts"): page.utils.renderShortcuts(body, "showcomment", review=review) yield document.render(pretty=not compact)
def renderShowComments(req, db, user): context_lines = req.getParameter("context", user.getPreference( db, "comment.diff.contextLines"), filter=int) default_compact = "yes" if user.getPreference( db, "commit.diff.compactMode") else "no" compact = req.getParameter("compact", default_compact) == "yes" default_tabify = "yes" if user.getPreference( db, "commit.diff.visualTabs") else "no" tabify = req.getParameter("tabify", default_tabify) == "yes" original = req.getParameter("original", "no") == "yes" review_id = req.getParameter("review", filter=int) batch_id = req.getParameter("batch", None, filter=int) filter = req.getParameter("filter", "all") blame = req.getParameter("blame", None) profiler = profiling.Profiler() review = dbutils.Review.fromId(db, review_id) review.repository.enableBlobCache() cursor = db.cursor() profiler.check("create review") if blame is not None: blame_user = dbutils.User.fromName(db, blame) cursor.execute( """SELECT commentchains.id FROM commentchains JOIN commentchainlines ON (commentchainlines.chain=commentchains.id) JOIN fileversions ON (fileversions.new_sha1=commentchainlines.sha1) JOIN changesets ON (changesets.id=fileversions.changeset) JOIN commits ON (commits.id=changesets.child) JOIN gitusers ON (gitusers.id=commits.author_gituser) JOIN usergitemails USING (email) JOIN reviewchangesets ON (reviewchangesets.changeset=changesets.id AND reviewchangesets.review=commentchains.review) WHERE commentchains.review=%s AND usergitemails.uid=%s AND commentchains.state!='empty' AND (commentchains.state!='draft' OR commentchains.uid=%s) ORDER BY commentchains.file, commentchainlines.first_line""", (review.id, blame_user.id, user.id)) include_chain_ids = set([chain_id for (chain_id, ) in cursor]) profiler.check("initial blame filtering") else: include_chain_ids = None if filter == "toread": query = """SELECT commentchains.id FROM commentchains JOIN comments ON (comments.chain=commentchains.id) JOIN commentstoread ON (commentstoread.comment=comments.id) LEFT OUTER JOIN commentchainlines ON (commentchainlines.chain=commentchains.id) WHERE review=%s AND commentstoread.uid=%s ORDER BY file, first_line""" cursor.execute(query, (review.id, user.id)) else: query = """SELECT commentchains.id FROM commentchains LEFT OUTER JOIN commentchainlines ON (chain=id) WHERE review=%s AND commentchains.state!='empty'""" arguments = [review.id] if filter == "issues": query += " AND type='issue' AND (commentchains.state!='draft' OR commentchains.uid=%s)" arguments.append(user.id) elif filter == "draft-issues": query += " AND type='issue' AND commentchains.state='draft' AND commentchains.uid=%s" arguments.append(user.id) elif filter == "open-issues": query += " AND type='issue' AND commentchains.state='open'" elif filter == "addressed-issues": query += " AND type='issue' AND commentchains.state='addressed'" elif filter == "closed-issues": query += " AND type='issue' AND commentchains.state='closed'" elif filter == "notes": query += " AND type='note' AND (commentchains.state!='draft' OR commentchains.uid=%s)" arguments.append(user.id) elif filter == "draft-notes": query += " AND type='note' AND commentchains.state='draft' AND commentchains.uid=%s" arguments.append(user.id) elif filter == "open-notes": query += " AND type='note' AND commentchains.state='open'" else: query += " AND (commentchains.state!='draft' OR commentchains.uid=%s)" arguments.append(user.id) if batch_id is not None: query += " AND batch=%s" arguments.append(batch_id) # This ordering is inaccurate if comments apply to the same file but # different commits, but then, in that case there isn't really a # well-defined natural order either. Two comments that apply to the # same file and commit will at least be order by line number, and that's # better than nothing. query += " ORDER BY file, first_line" cursor.execute(query, arguments) profiler.check("main query") if include_chain_ids is None: chain_ids = [chain_id for (chain_id, ) in cursor] else: chain_ids = [ chain_id for (chain_id, ) in cursor if chain_id in include_chain_ids ] profiler.check("query result") document = htmlutils.Document(req) html = document.html() head = html.head() body = html.body() document.addInternalScript(user.getJS(db)) document.addInternalScript(review.getJS()) page.utils.generateHeader( body, db, user, lambda target: review_utils.renderDraftItems(db, user, review, target), extra_links=[("r/%d" % review.id, "Back to Review")]) profiler.check("page header") target = body.div("main") if chain_ids and not user.isAnonymous() and user.name == req.user: document.addInternalScript( "$(function () { markChainsAsRead([%s]); });" % ", ".join(map(str, chain_ids))) if chain_ids: processed = set() chains = [] file_ids = set() changesets_files = {} changesets = {} if blame is not None: annotators = {} review.branch.loadCommits(db) commits = log.commitset.CommitSet(review.branch.commits) for chain_id in chain_ids: if chain_id in processed: continue else: processed.add(chain_id) chain = review_comment.CommentChain.fromId(db, chain_id, user, review=review) chains.append(chain) if chain.file_id is not None: file_ids.add(chain.file_id) parent, child = review_html.getCodeCommentChainChangeset( db, chain, original) if parent and child: changesets_files.setdefault((parent, child), set()).add(chain.file_id) profiler.check("load chains") changeset_cache = {} for (from_commit, to_commit), filtered_file_ids in changesets_files.items(): changesets[(from_commit, to_commit)] = changeset_utils.createChangeset( db, user, review.repository, from_commit=from_commit, to_commit=to_commit, filtered_file_ids=filtered_file_ids)[0] profiler.check("create changesets") if blame is not None: annotators[(from_commit, to_commit)] = operation.blame.LineAnnotator( db, from_commit, to_commit, file_ids=file_ids, commits=commits, changeset_cache=changeset_cache) profiler.check("create annotators") for chain in chains: if blame is not None and chain.file_id is not None: try: changeset = changesets[(chain.first_commit, chain.last_commit)] annotator = annotators[(chain.first_commit, chain.last_commit)] except KeyError: # Most likely a comment created via /showfile. Such a # comment could be in code that 'blame_user' modified in the # review, but for now, let's skip the comment. continue else: file_in_changeset = changeset.getFile(chain.file_id) if not file_in_changeset: continue try: offset, count = chain.lines_by_sha1[ file_in_changeset.new_sha1] except KeyError: # Probably a chain raised against the "old" side of the diff. continue else: if not annotator.annotate(chain.file_id, offset, offset + count - 1, check_user=blame_user): continue profiler.check("detailed blame filtering") if chain.file_id is not None: from_commit, to_commit = review_html.getCodeCommentChainChangeset( db, chain, original) changeset = changesets.get((from_commit, to_commit)) else: changeset = None review_html.renderCommentChain(db, target, user, review, chain, context_lines=context_lines, compact=compact, tabify=tabify, original=original, changeset=changeset, linkify=linkify.Context( db=db, request=req, review=review)) profiler.check("rendering") yield document.render( stop=target, pretty=not compact ) + "<script>console.log((new Date).toString());</script>" profiler.check("transfer") page.utils.renderShortcuts(target, "showcomments", review=review) else: target.h1(align="center").text("No comments.") profiler.output(db, user, document) yield document.render(pretty=not compact)
def renderCreateReview(req, db, user): if user.isAnonymous(): raise page.utils.NeedLogin(req) repository = req.getParameter("repository", filter=gitutils.Repository.FromParameter(db), default=None) applyparentfilters = req.getParameter("applyparentfilters", "yes" if user.getPreference(db, 'review.applyUpstreamFilters') else "no") == "yes" cursor = db.cursor() if req.method == "POST": data = json_decode(req.read()) summary = data.get("summary") description = data.get("description") review_branch_name = data.get("review_branch_name") commit_ids = data.get("commit_ids") commit_sha1s = data.get("commit_sha1s") else: summary = req.getParameter("summary", None) description = req.getParameter("description", None) review_branch_name = req.getParameter("reviewbranchname", None) commit_ids = None commit_sha1s = None commits_arg = req.getParameter("commits", None) remote = req.getParameter("remote", None) upstream = req.getParameter("upstream", "master") branch_name = req.getParameter("branch", None) if commits_arg: try: commit_ids = map(int, commits_arg.split(",")) except: commit_sha1s = [repository.revparse(ref) for ref in commits_arg.split(",")] elif branch_name: cursor.execute("""SELECT commit FROM reachable JOIN branches ON (branch=id) WHERE repository=%s AND name=%s""", (repository.id, branch_name)) commit_ids = [commit_id for (commit_id,) in cursor] if len(commit_ids) > configuration.limits.MAXIMUM_REVIEW_COMMITS: raise page.utils.DisplayMessage( "Too many commits!", (("<p>The branch <code>%s</code> contains %d commits. Reviews can" "be created from branches that contain at most %d commits.</p>" "<p>This limit can be adjusted by modifying the system setting" "<code>configuration.limits.MAXIMUM_REVIEW_COMMITS</code>.</p>") % (htmlutils.htmlify(branch_name), len(commit_ids), configuration.limits.MAXIMUM_REVIEW_COMMITS)), html=True) else: return renderSelectSource(req, db, user) req.content_type = "text/html; charset=utf-8" if commit_ids: commits = [gitutils.Commit.fromId(db, repository, commit_id) for commit_id in commit_ids] elif commit_sha1s: commits = [gitutils.Commit.fromSHA1(db, repository, commit_sha1) for commit_sha1 in commit_sha1s] else: commits = [] if not commit_ids: commit_ids = [commit.getId(db) for commit in commits] if not commit_sha1s: commit_sha1s = [commit.sha1 for commit in commits] if summary is None: if len(commits) == 1: summary = commits[0].summary() else: summary = "" if review_branch_name: invalid_branch_name = "false" default_branch_name = review_branch_name else: invalid_branch_name = htmlutils.jsify(user.name + "/") default_branch_name = user.name + "/" match = re.search("(?:^|[Ff]ix(?:e[ds])?(?: +for)?(?: +bug)? +)([A-Z][A-Z0-9]+-[0-9]+)", summary) if match: invalid_branch_name = "false" default_branch_name = htmlutils.htmlify(match.group(1)) changesets = [] changeset_utils.createChangesets(db, repository, commits) for commit in commits: changesets.extend(changeset_utils.createChangeset(db, None, repository, commit, do_highlight=False)) changeset_ids = [changeset.id for changeset in changesets] all_reviewers, all_watchers = reviewing.utils.getReviewersAndWatchers( db, repository, changesets=changesets, applyparentfilters=applyparentfilters) document = htmlutils.Document(req) html = document.html() head = html.head() document.addInternalScript(user.getJS(db)) if branch_name: document.addInternalScript("var fromBranch = %s;" % htmlutils.jsify(branch_name)) if remote: document.addInternalScript("var trackedbranch = { remote: %s, name: %s };" % (htmlutils.jsify(remote), htmlutils.jsify(branch_name))) head.title().text("Create Review") body = html.body(onload="document.getElementById('branch_name').focus()") page.utils.generateHeader(body, db, user, lambda target: target.button(onclick="submitReview();").text("Submit Review")) document.addExternalStylesheet("resource/createreview.css") document.addExternalScript("resource/createreview.js") document.addExternalScript("resource/reviewfilters.js") document.addExternalScript("resource/autocomplete.js") document.addInternalScript(""" var invalid_branch_name = %s; var review_data = { commit_ids: %r, commit_sha1s: %r, changeset_ids: %r };""" % (invalid_branch_name, commit_ids, commit_sha1s, changeset_ids)) document.addInternalScript(repository.getJS()) main = body.div("main") table = main.table("basic paleyellow", align="center") table.tr().td("h1", colspan=3).h1().text("Create Review") row = table.tr("line") row.td("heading").text("Branch Name:") row.td("value").text("r/").input("value", id="branch_name", value=default_branch_name) row.td("status") row = table.tr() if not remote: row.td("help", colspan=3).div().text("""\ This is the main identifier of the review. It will be created in the review repository to contain the commits below. Reviewers can fetch it from there, and additional commits can be added to the review later by pushing them to this branch in the review repository.""") else: row.td("help", colspan=3).div().text("""\ This is the main identifier of the review. It will be created in the review repository to contain the commits below, and reviewers can fetch it from there.""") if remote: row = table.tr("line") row.td("heading").text("Tracked Branch:") value = row.td("value") value.code("branch inset").text(branch_name, linkify=linkify.Context(remote=remote)) value.text(" in ") value.code("remote inset").text(remote, linkify=linkify.Context()) row.td("status") row = table.tr() row.td("help", colspan=3).div().text("""\ Rather than pushing directly to the review branch in Critic's repository to add commits to the review, you will be pushing to this branch (in a separate repository,) from which Critic will fetch commits and add them to the review automatically.""") row = table.tr("line") row.td("heading").text("Summary:") row.td("value").input("value", id="summary", value=summary) row.td("status") row = table.tr() row.td("help", colspan=3).div().text("""\ The summary should be a short summary of the changes in the review. It will appear in the subject of all emails sent about the review. """) row = table.tr("line description") row.td("heading").text("Description:") textarea = row.td("value").textarea(id="description", rows=12) textarea.preformatted() if description: textarea.text(description) row.td("status") row = table.tr() row.td("help", colspan=3).div().text("""\ The description should describe the changes to be reviewed. It is usually fine to leave the description empty, since the commit messages are also available in the review. """) generateReviewersAndWatchersTable(db, repository, main, all_reviewers, all_watchers, applyparentfilters=applyparentfilters) row = table.tr("line recipients") row.td("heading").text("Recipient List:") cell = row.td("value", colspan=2).preformatted() cell.span("mode").text("Everyone") cell.span("users") cell.text(".") buttons = cell.div("buttons") buttons.button(onclick="editRecipientList();").text("Edit Recipient List") row = table.tr() row.td("help", colspan=3).div().text("""\ The basic recipient list for e-mails sent about the review. """) log.html.render(db, main, "Commits", commits=commits) return document
def renderShowReview(req, db, user): profiler = profiling.Profiler() cursor = db.cursor() if user.getPreference(db, "commit.diff.compactMode"): default_compact = "yes" else: default_compact = "no" compact = req.getParameter("compact", default_compact) == "yes" highlight = req.getParameter("highlight", None) review_id = req.getParameter("id", filter=int) review = dbutils.Review.fromId(db, review_id, load_commits=False, profiler=profiler) profiler.check("create review") if not review: raise page.utils.DisplayMessage, ("Invalid Review ID", "%d is not a valid review ID." % review_id) if review.getETag(db, user) == req.getRequestHeader("If-None-Match"): raise page.utils.NotModified profiler.check("ETag") # if usesExperimentalFeature(req, db, review): # def renderMessage(target): # url = "%s/r/%d" % (configuration.URL_PER_TYPE['development'], review.id) # p = target.p(style="padding-top: 1em") # p.text("Sorry, this review uses experimental features currently only available in the development version of Critic. Because of that, it can only be displayed there.") # p = target.p(style="padding-top: 1em") # p.b().a(href=url).text(url) # yield page.utils.displayMessage(db, req, user, "Experimental Feature Alert!", message=renderMessage) # return repository = review.repository prefetch_commits = {} cursor.execute("""SELECT sha1, child FROM changesets JOIN reviewchangesets ON (reviewchangesets.changeset=changesets.id) JOIN commits ON (commits.id=changesets.child) WHERE review=%s""", (review.id,)) prefetch_commits.update(dict(cursor)) profiler.check("commits (query)") cursor.execute("""SELECT old_head, commits1.sha1, new_head, commits2.sha1, new_upstream, commits3.sha1 FROM reviewrebases LEFT OUTER JOIN commits AS commits1 ON (commits1.id=old_head) LEFT OUTER JOIN commits AS commits2 ON (commits2.id=new_head) LEFT OUTER JOIN commits AS commits3 ON (commits3.id=new_upstream) WHERE review=%s""", (review.id,)) rebases = cursor.fetchall() if rebases: has_finished_rebases = False for old_head_id, old_head_sha1, new_head_id, new_head_sha1, new_upstream_id, new_upstream_sha1 in rebases: if old_head_id: prefetch_commits[old_head_sha1] = old_head_id if new_head_id: prefetch_commits[new_head_sha1] = new_head_id has_finished_rebases = True if new_upstream_id: prefetch_commits[new_upstream_sha1] = new_upstream_id profiler.check("auxiliary commits (query)") if has_finished_rebases: cursor.execute("""SELECT commits.sha1, commits.id FROM commits JOIN reachable ON (reachable.commit=commits.id) WHERE branch=%s""", (review.branch.id,)) prefetch_commits.update(dict(cursor)) profiler.check("actual commits (query)") prefetch_commits = gitutils.FetchCommits(repository, prefetch_commits) document = htmlutils.Document(req) html = document.html() head = html.head() body = html.body(onunload="void(0);") def flush(target=None): return document.render(stop=target, pretty=not compact) def renderHeaderItems(target): has_draft_items = review_utils.renderDraftItems(db, user, review, target) target = target.div("buttons") if not has_draft_items: if review.state == "open": if review.accepted(db): target.button(id="closeReview", onclick="closeReview();").text("Close Review") else: if user in review.owners or user.getPreference(db, "review.pingAnyReview"): target.button(id="pingReview", onclick="pingReview();").text("Ping Review") if user in review.owners or user.getPreference(db, "review.dropAnyReview"): target.button(id="dropReview", onclick="dropReview();").text("Drop Review") if user in review.owners and not review.description: target.button(id="writeDescription", onclick="editDescription();").text("Write Description") else: target.button(id="reopenReview", onclick="reopenReview();").text("Reopen Review") target.span("buttonscope buttonscope-global") profiler.check("prologue") page.utils.generateHeader(body, db, user, renderHeaderItems) cursor.execute("SELECT 1 FROM fullreviewuserfiles WHERE review=%s AND state='pending' AND assignee=%s", (review.id, user.id)) hasPendingChanges = bool(cursor.fetchone()) if hasPendingChanges: head.setLink("next", "showcommit?review=%d&filter=pending" % review.id) profiler.check("header") document.addExternalStylesheet("resource/showreview.css") document.addExternalStylesheet("resource/review.css") document.addExternalStylesheet("resource/comment.css") document.addExternalScript("resource/showreview.js") document.addExternalScript("resource/review.js") document.addExternalScript("resource/comment.js") document.addExternalScript("resource/autocomplete.js") document.addInternalScript(user.getJS()) document.addInternalScript("var owners = [ %s ];" % ", ".join(owner.getJSConstructor() for owner in review.owners)) document.addInternalScript("var updateCheckInterval = %d;" % user.getPreference(db, "review.updateCheckInterval")); log.html.addResources(document) document.addInternalScript(review.getJS()) target = body.div("main") basic = target.table('paleyellow basic', align='center') basic.col(width='10%') basic.col(width='60%') basic.col(width='30%') h1 = basic.tr().td('h1', colspan=3).h1() h1.text("r/%d: " % review.id) h1.span(id="summary").text("%s" % review.summary, linkify=linkify.Context(db=db, review=review)) h1.a("edit", href="javascript:editSummary();").text("[edit]") def linkToCommit(commit): cursor.execute("SELECT 1 FROM commits JOIN changesets ON (child=commits.id) JOIN reviewchangesets ON (changeset=changesets.id) WHERE sha1=%s AND review=%s", (commit.sha1, review.id)) if cursor.fetchone(): return "%s/%s?review=%d" % (review.repository.name, commit.sha1, review.id) return "%s/%s" % (review.repository.name, commit.sha1) def row(heading, value, help, right=None, linkify=False, cellId=None): main_row = basic.tr('line') main_row.td('heading').text("%s:" % heading) if right is False: colspan = 2 else: colspan = None if callable(value): value(main_row.td('value', id=cellId, colspan=colspan).preformatted()) else: main_row.td('value', id=cellId, colspan=colspan).preformatted().text(value, linkify=linkify, repository=review.repository) if right is False: pass elif callable(right): right(main_row.td('right', valign='bottom')) else: main_row.td('right').text() if help: basic.tr('help').td('help', colspan=3).text(help) def renderBranchName(target): target.code("branch").text(review.branch.name, linkify=linkify.Context()) if repository.name != user.getPreference(db, "defaultRepository"): target.text(" in ") target.code("repository").text("%s:%s" % (configuration.base.HOSTNAME, repository.path)) cursor.execute("""SELECT id, remote, remote_name, disabled, previous FROM trackedbranches WHERE repository=%s AND local_name=%s""", (repository.id, review.branch.name)) row = cursor.fetchone() if row: trackedbranch_id, remote, remote_name, disabled, previous = row target.p("tracking disabled" if disabled else "tracking").text("tracking") target.code("branch").text(remote_name, linkify=linkify.Context(remote=remote)) target.text(" in ") target.code("repository").text(remote, linkify=linkify.Context()) if previous: target.span("lastupdate").script(type="text/javascript").text("document.write('(last fetched: ' + shortDate(new Date(%d)) + ')');" % (calendar.timegm(previous.utctimetuple()) * 1000)) if user in review.owners: buttons = target.div("buttons") if disabled: buttons.button("enabletracking", onclick="enableTracking(%d);" % trackedbranch_id).text("Enable Tracking") else: buttons.button("disabletracking", onclick="triggerUpdate(%d);" % trackedbranch_id).text("Update Now") buttons.button("disabletracking", onclick="disableTracking(%d);" % trackedbranch_id).text("Disable Tracking") def renderReviewers(target): if review.reviewers: for index, reviewer in enumerate(review.reviewers): if index != 0: target.text(", ") span = target.span("user %s" % reviewer.status) span.span("name").text(reviewer.fullname) if reviewer.status == 'absent': span.span("status").text(" (%s)" % reviewer.getAbsence(db)) elif reviewer.status == 'retired': span.span("status").text(" (retired)") else: target.i().text("No reviewers.") cursor.execute("""SELECT reviewfilters.id, reviewfilters.uid, reviewfilters.directory, reviewfilters.file FROM reviewfilters JOIN users ON (reviewfilters.uid=users.id) WHERE reviewfilters.review=%s AND reviewfilters.type='reviewer' AND users.status!='retired'""", (review.id,)) rows = cursor.fetchall() reviewer_filters_hidden = [] if rows: table = target.table("reviewfilters reviewers") row = table.thead().tr("h1") row.th("h1", colspan=4).text("Custom filters:") filter_data = {} reviewfilters = {} for filter_id, user_id, directory_id, file_id in rows: filter_user = dbutils.User.fromId(db, user_id) if file_id: path = dbutils.describe_file(db, file_id) else: path = dbutils.describe_directory(db, directory_id) + "/" reviewfilters.setdefault(filter_user.fullname, []).append(path) filter_data[(filter_user.fullname, path)] = (filter_id, filter_user) count = 0 tbody = table.tbody() for fullname in sorted(reviewfilters.keys()): original_paths = sorted(reviewfilters[fullname]) trimmed_paths = diff.File.eliminateCommonPrefixes(original_paths[:]) first = True for original_path, trimmed_path in zip(original_paths, trimmed_paths): row = tbody.tr("filter") if first: row.td("username", rowspan=len(original_paths)).text(fullname) row.td("reviews", rowspan=len(original_paths)).text("reviews") first = False row.td("path").span().innerHTML(trimmed_path) filter_id, filter_user = filter_data[(fullname, original_path)] href = "javascript:removeReviewFilter(%d, %s, 'reviewer', %s, %s);" % (filter_id, filter_user.getJSConstructor(), htmlutils.jsify(original_path), "true" if filter_user != user else "false") row.td("remove").a(href=href).text("[remove]") count += 1 tfoot = table.tfoot() tfoot.tr().td(colspan=4).text("%d line%s hidden" % (count, "s" if count > 1 else "")) if count > 10: tbody.setAttribute("class", "hidden") reviewer_filters_hidden.append(True) else: tfoot.setAttribute("class", "hidden") reviewer_filters_hidden.append(False) buttons = target.div("buttons") if reviewer_filters_hidden: buttons.button("showfilters", onclick="toggleReviewFilters('reviewers', $(this));").text("%s Custom Filters" % ("Show" if reviewer_filters_hidden[0] else "Hide")) if review.applyfilters and review.repository.parent and not review.applyparentfilters: buttons.button("applyparentfilters", onclick="applyParentFilters();").text("Apply Upstream Filters") buttons.button("addreviewer", onclick="addReviewer();").text("Add Reviewer") buttons.button("manage", onclick="location.href='managereviewers?review=%d';" % review.id).text("Manage Assignments") def renderWatchers(target): if review.watchers: for index, watcher in enumerate(review.watchers): if index != 0: target.text(", ") span = target.span("user %s" % watcher.status) span.span("name").text(watcher.fullname) if watcher.status == 'absent': span.span("status").text(" (%s)" % watcher.getAbsence(db)) elif watcher.status == 'retired': span.span("status").text(" (retired)") else: target.i().text("No watchers.") cursor.execute("""SELECT reviewfilters.id, reviewfilters.uid, reviewfilters.directory, reviewfilters.file FROM reviewfilters JOIN users ON (reviewfilters.uid=users.id) WHERE reviewfilters.review=%s AND reviewfilters.type='watcher' AND users.status!='retired'""", (review.id,)) rows = cursor.fetchall() watcher_filters_hidden = [] if rows: table = target.table("reviewfilters watchers") row = table.thead().tr("h1") row.th("h1", colspan=4).text("Custom filters:") filter_data = {} reviewfilters = {} for filter_id, user_id, directory_id, file_id in rows: filter_user = dbutils.User.fromId(db, user_id) if file_id: path = dbutils.describe_file(db, file_id) else: path = dbutils.describe_directory(db, directory_id) + "/" reviewfilters.setdefault(filter_user.fullname, []).append(path) filter_data[(filter_user.fullname, path)] = (filter_id, filter_user) count = 0 tbody = table.tbody() for fullname in sorted(reviewfilters.keys()): original_paths = sorted(reviewfilters[fullname]) trimmed_paths = diff.File.eliminateCommonPrefixes(original_paths[:]) first = True for original_path, trimmed_path in zip(original_paths, trimmed_paths): row = tbody.tr("filter") if first: row.td("username", rowspan=len(original_paths)).text(fullname) row.td("reviews", rowspan=len(original_paths)).text("watches") first = False row.td("path").span().innerHTML(trimmed_path) filter_id, filter_user = filter_data[(fullname, original_path)] href = "javascript:removeReviewFilter(%d, %s, 'watcher', %s, %s);" % (filter_id, filter_user.getJSConstructor(), htmlutils.jsify(original_path), "true" if filter_user != user else "false") row.td("remove").a(href=href).text("[remove]") count += 1 tfoot = table.tfoot() tfoot.tr().td(colspan=4).text("%d line%s hidden" % (count, "s" if count > 1 else "")) if count > 10: tbody.setAttribute("class", "hidden") watcher_filters_hidden.append(True) else: tfoot.setAttribute("class", "hidden") watcher_filters_hidden.append(False) buttons = target.div("buttons") if watcher_filters_hidden: buttons.button("showfilters", onclick="toggleReviewFilters('watchers', $(this));").text("%s Custom Filters" % ("Show" if watcher_filters_hidden[0] else "Hide")) buttons.button("addwatcher", onclick="addWatcher();").text("Add Watcher") if user not in review.reviewers and user not in review.owners: if user not in review.watchers: buttons.button("watch", onclick="watchReview();").text("Watch Review") elif review.watchers[user] == "manual": buttons.button("watch", onclick="unwatchReview();").text("Stop Watching Review") def renderEditOwners(target): target.button("description", onclick="editOwners();").text("Edit Owners") def renderEditDescription(target): target.button("description", onclick="editDescription();").text("Edit Description") def renderRecipientList(target): cursor.execute("SELECT uid, fullname, include FROM reviewrecipientfilters JOIN users ON (uid=id) WHERE review=%s", (review.id,)) default_include = True included = dict((owner.fullname, owner.id) for owner in review.owners) excluded = {} for user_id, fullname, include in cursor: if user_id == 0: default_include = include elif include: included[fullname] = user_id elif user_id not in review.owners: excluded[fullname] = user_id mode = None users = None buttons = [] opt_in_button = False opt_out_button = False if default_include: if excluded: mode = "Everyone except " users = excluded opt_out_button = user.fullname not in excluded opt_in_button = not opt_out_button else: mode = "Everyone." opt_out_button = True else: if included: mode = "No-one except " users = included opt_in_button = user.fullname not in included opt_out_button = not opt_in_button else: mode = "No-one at all." opt_in_button = True if user in review.owners or user in review.reviewers or user in review.watchers: if opt_in_button: buttons.append(("Include me, please!", "includeRecipient(%d);" % user.id)) if opt_out_button: buttons.append(("Exclude me, please!", "excludeRecipient(%d);" % user.id)) target.span("mode").text(mode) if users: container = target.span("users") first = True for fullname in sorted(users.keys()): if first: first = False else: container.text(", ") container.span("user", critic_user_id=users[fullname]).text(fullname) container.text(".") if buttons: container = target.div("buttons") for label, onclick in buttons: container.button(onclick=onclick).text(label) row("Branch", renderBranchName, "The branch containing the commits to review.", right=False) row("Owner%s" % ("s" if len(review.owners) > 1 else ""), ", ".join(owner.fullname for owner in review.owners), "The users who created and/or owns the review.", right=renderEditOwners) if review.description: row("Description", review.description, "A longer description of the changes to be reviewed.", linkify=linkToCommit, cellId="description", right=renderEditDescription) row("Reviewers", renderReviewers, "Users responsible for reviewing the changes in this review.", right=False) row("Watchers", renderWatchers, "Additional users who receive e-mails about updates to this review.", right=False) row("Recipient List", renderRecipientList, "Users (among the reviewers and watchers) who will receive any e-mails about the review.", right=False) profiler.check("basic") review_state = review.getReviewState(db) profiler.check("review state") progress = target.table('paleyellow progress', align='center') progress_header = progress.tr().td('h1', colspan=3).h1() progress_header.text("Review Progress") progress_header_right = progress_header.span("right") progress_header_right.text("Display log: ") progress_header_right.a(href="showreviewlog?review=%d&granularity=module" % review.id).text("[per module]") progress_header_right.text() progress_header_right.a(href="showreviewlog?review=%d&granularity=file" % review.id).text("[per file]") progress_h1 = progress.tr().td('percent', colspan=3).h1() title_data = { 'id': 'r/%d' % review.id, 'summary': review.summary, 'progress': str(review_state) } if review.state == "closed": progress_h1.img(src=htmlutils.getStaticResourceURI("seal-of-approval-left.png"), style="position: absolute; margin-left: -80px; margin-top: -100px") progress_h1.text("Finished!") elif review.state == "dropped": progress_h1.text("Dropped...") elif review.state == "open" and review_state.accepted: progress_h1.img(src=htmlutils.getStaticResourceURI("seal-of-approval-left.png"), style="position: absolute; margin-left: -80px; margin-top: -100px") progress_h1.text("Accepted!") progress_h1.div().span("remark").text("Hurry up and close it before anyone has a change of heart.") else: progress_h1.text(review_state.getProgress()) if review_state.issues: progress_h1.span("comments").text(" and ") progress_h1.text("%d" % review_state.issues) progress_h1.span("comments").text(" issue%s" % (review_state.issues > 1 and "s" or "")) if review_state.getPercentReviewed() != 100.0: cursor = db.cursor() cursor.execute("""SELECT 1 FROM reviewfiles LEFT OUTER JOIN reviewuserfiles ON (reviewuserfiles.file=reviewfiles.id) WHERE reviewfiles.review=%s AND reviewfiles.state='pending' AND reviewuserfiles.uid IS NULL""", (review.id,)) if cursor.fetchone(): progress.tr().td('stuck', colspan=3).a(href="showreviewlog?review=%d&granularity=file&unassigned=yes" % review.id).text("Not all changes have a reviewer assigned!") cursor.execute("""SELECT uid, MIN(reviewuserfiles.time) FROM reviewfiles JOIN reviewuserfiles ON (reviewuserfiles.file=reviewfiles.id) WHERE reviewfiles.review=%s AND reviewfiles.state='pending' GROUP BY reviewuserfiles.uid""", (review.id,)) def total_seconds(delta): return delta.days * 60 * 60 * 24 + delta.seconds now = datetime.datetime.now() pending_reviewers = [(dbutils.User.fromId(db, user_id), total_seconds(now - timestamp)) for (user_id, timestamp) in cursor.fetchall() if total_seconds(now - timestamp) > 60 * 60 * 8] if pending_reviewers: progress.tr().td('stragglers', colspan=3).text("Needs review from") for reviewer, seconds in pending_reviewers: if reviewer.status == 'retired': continue elif reviewer.status == 'absent': warning = " absent" elif not reviewer.getPreference(db, "email.activated"): warning = " no-email" else: warning = "" if seconds < 60 * 60 * 24: hours = seconds / (60 * 60) duration = " (%d hour%s)" % (hours, "s" if hours > 1 else "") elif seconds < 60 * 60 * 24 * 7: days = seconds / (60 * 60 * 24) duration = " (%d day%s)" % (days, "s" if days > 1 else "") elif seconds < 60 * 60 * 24 * 30: weeks = seconds / (60 * 60 * 24 * 7) duration = " (%d week%s)" % (weeks, "s" if weeks > 1 else "") else: duration = " (wake up!)" progress.tr().td('straggler' + warning, colspan=3).text("%s%s" % (reviewer.fullname, duration)) if user in review.owners: progress.tr().td('pinging', colspan=3).span().text("Send a message to these users by pinging the review.") title_format = user.getPreference(db, 'ui.title.showReview') try: document.setTitle(title_format % title_data) except Exception, exc: document.setTitle(traceback.format_exception_only(type(exc), exc)[0].strip())