コード例 #1
0
def user(username):
    "List all grants for a user, including the grants the user has access to."
    user = anubis.user.get_user(username=username)
    if user is None:
        return utils.error("No such user.", flask.url_for("home"))
    if not anubis.user.allow_view(user):
        return utils.error("You may not view the user's grants.", flask.url_for("home"))
    grants = utils.get_docs_view("grants", "user", user["username"])
    grants.extend(utils.get_docs_view("grants", "access", user["username"]))
    return flask.render_template("grants/user.html", user=user, grants=grants)
コード例 #2
0
ファイル: reviews.py プロジェクト: pekrau/Anubis
def reviewer(username):
    """List all reviews by the given reviewer (user).
    If the user is reviewer in only one call, redirect to that page.
    """
    user = anubis.user.get_user(username=username)
    if user is None:
        return utils.error("No such user.", flask.url_for("home"))
    if not anubis.user.allow_view(user):
        return utils.error(
            "You may not view the user's reviews.",
            flask.url_for("call.display", cid=call["identifier"]),
        )

    reviewer_calls = [
        anubis.call.get_call(r.value) for r in flask.g.db.view(
            "calls", "reviewer", key=user["username"], reduce=False)
    ]
    # Reviews in only one call; redirect to its reviews page for the reviewer.
    if len(reviewer_calls) == 1:
        return flask.redirect(
            flask.url_for(
                "reviews.call_reviewer",
                cid=reviewer_calls[0]["identifier"],
                username=username,
            ))

    reviews = utils.get_docs_view("reviews", "reviewer", user["username"])
    return flask.render_template(
        "reviews/reviewer.html",
        user=user,
        reviewer_calls=reviewer_calls,
        reviews=reviews,
    )
コード例 #3
0
ファイル: reviews.py プロジェクト: pekrau/Anubis
def proposal_xlsx(pid):
    "Produce an XLSX file of all reviewers and reviews for a proposal."
    proposal = anubis.proposal.get_proposal(pid)
    if proposal is None:
        return utils.error("No such proposal.", flask.url_for("home"))
    if not anubis.proposal.allow_view(proposal):
        return utils.error(
            "You may not view the proposal.",
            flask.url_for("call.display", cid=call["identifier"]),
        )
    call = anubis.call.get_call(proposal["call"])
    if not anubis.call.allow_view_reviews(call):
        return utils.error(
            "You may not view the reviews of the call.",
            flask.url_for("call.display", cid=call["identifier"]),
        )

    reviews = utils.get_docs_view("reviews", "proposal",
                                  proposal["identifier"])
    if not (flask.g.am_admin or anubis.call.am_chair(call)):
        reviews = [
            r for r in reviews
            if r["reviewer"] != flask.g.current_user["username"]
            and r.get("finalized")
        ]
    reviews_lookup = {f"{pid} {r['reviewer']}": r for r in reviews}
    content = get_reviews_xlsx(call, [proposal], reviews_lookup)
    response = flask.make_response(content)
    response.headers.set("Content-Type", constants.XLSX_MIMETYPE)
    response.headers.set("Content-Disposition",
                         "attachment",
                         filename=f"{pid}_reviews.xlsx")
    return response
コード例 #4
0
ファイル: reviews.py プロジェクト: pekrau/Anubis
def proposal(pid):
    "List all reviewers and reviews for a proposal."
    proposal = anubis.proposal.get_proposal(pid)
    if proposal is None:
        return utils.error("No such proposal.", flask.url_for("home"))

    call = anubis.call.get_call(proposal["call"])
    if not anubis.call.allow_view_reviews(call):
        return utils.error(
            "You may not view the reviews of the call.",
            flask.url_for("call.display", cid=call["identifier"]),
        )

    reviews = utils.get_docs_view("reviews", "proposal",
                                  proposal["identifier"])
    # For ordinary reviewer, list only finalized reviews.
    if flask.g.am_admin or flask.g.am_staff or anubis.call.am_chair(call):
        only_finalized = False
    else:
        only_finalized = True
        reviews = [r for r in reviews if r.get("finalized")]
    allow_create_review = anubis.review.allow_create(proposal)
    reviews_lookup = {r["reviewer"]: r for r in reviews}
    return flask.render_template(
        "reviews/proposal.html",
        proposal=proposal,
        call=call,
        allow_create_review=allow_create_review,
        reviewers=call["reviewers"],
        reviews_lookup=reviews_lookup,
        only_finalized=only_finalized,
    )
コード例 #5
0
ファイル: reviews.py プロジェクト: pekrau/Anubis
def call(cid):
    "List all reviews for a call."
    call = anubis.call.get_call(cid)
    if call is None:
        return utils.error("No such call.", flask.url_for("home"))
    if not anubis.call.allow_view(call):
        return utils.error("You may not view the call.", flask.url_for("home"))
    if not anubis.call.allow_view_reviews(call):
        return utils.error(
            "You may not view the reviews of the call.",
            flask.url_for("call.display", cid=call["identifier"]),
        )

    proposals = anubis.proposals.get_call_proposals(call, submitted=True)
    for proposal in proposals:
        proposal["allow_create_review"] = anubis.review.allow_create(proposal)
    reviews = utils.get_docs_view("reviews", "call", call["identifier"])
    # For ordinary reviewer, list only finalized reviews.
    if flask.g.am_admin or flask.g.am_staff or anubis.call.am_chair(call):
        only_finalized = False
    else:
        only_finalized = True
        reviews = [r for r in reviews if r.get("finalized")]
    reviews_lookup = {f"{r['proposal']} {r['reviewer']}": r for r in reviews}
    return flask.render_template(
        "reviews/call.html",
        call=call,
        proposals=proposals,
        reviews_lookup=reviews_lookup,
        only_finalized=only_finalized,
    )
コード例 #6
0
ファイル: reviews.py プロジェクト: pekrau/Anubis
def call_xlsx(cid):
    "Produce an XLSX file of all reviews for a call."
    call = anubis.call.get_call(cid)
    if call is None:
        return utils.error("No such call.", flask.url_for("home"))
    if not anubis.call.allow_view(call):
        return utils.error("You may not view the call.", flask.url_for("home"))
    if not anubis.call.allow_view_reviews(call):
        return utils.error(
            "You may not view the reviews of the call.",
            flask.url_for("call.display", cid=call["identifier"]),
        )

    proposals = anubis.proposals.get_call_proposals(call, submitted=True)
    reviews = utils.get_docs_view("reviews", "call", call["identifier"])
    # For ordinary reviewer, list only finalized reviews.
    if not (flask.g.am_admin or anubis.call.am_chair(call)):
        reviews = [
            r for r in reviews
            if r["reviewer"] != flask.g.current_user["username"]
            and r.get("finalized")
        ]
    reviews_lookup = {f"{r['proposal']} {r['reviewer']}": r for r in reviews}
    content = get_reviews_xlsx(call, proposals, reviews_lookup)
    response = flask.make_response(content)
    response.headers.set("Content-Type", constants.XLSX_MIMETYPE)
    response.headers.set("Content-Disposition",
                         "attachment",
                         filename=f"{cid}_reviews.xlsx")
    return response
コード例 #7
0
ファイル: reviews.py プロジェクト: pekrau/Anubis
def call_reviewer(cid, username):
    "List all reviews in the call by the reviewer (user)."
    call = anubis.call.get_call(cid)
    if call is None:
        return utils.error("No such call.", flask.url_for("home"))
    user = anubis.user.get_user(username=username)
    if user is None:
        return utils.error("No such user.", flask.url_for("home"))
    if user["username"] not in call["reviewers"]:
        return utils.error("The user is not a reviewer in the call.",
                           flask.url_for("home"))
    if not (user["username"] == flask.g.current_user["username"]
            or anubis.call.allow_view_reviews(call)):
        return utils.error(
            "You may not view the user's reviews.",
            flask.url_for("call.display", cid=call["identifier"]),
        )

    proposals = anubis.proposals.get_call_proposals(call, submitted=True)
    for proposal in proposals:
        proposal["allow_create_review"] = anubis.review.allow_create(proposal)
    reviews = utils.get_docs_view("reviews", "call_reviewer",
                                  [call["identifier"], user["username"]])
    reviews_lookup = {r["proposal"]: r for r in reviews}
    return flask.render_template(
        "reviews/call_reviewer.html",
        call=call,
        proposals=proposals,
        user=user,
        reviews_lookup=reviews_lookup,
    )
コード例 #8
0
ファイル: reviews.py プロジェクト: pekrau/Anubis
def call_reviewer_xlsx(cid, username):
    "Produce an XLSX file of all reviews in the call by the reviewer (user)."
    call = anubis.call.get_call(cid)
    if call is None:
        return utils.error("No such call.", flask.url_for("home"))
    user = anubis.user.get_user(username=username)
    if user is None:
        return utils.error("No such user.", flask.url_for("home"))
    if user["username"] not in call["reviewers"]:
        return utils.error("The user is not a reviewer in the call.",
                           flask.url_for("home"))
    if not (user["username"] == flask.g.current_user["username"]
            or anubis.call.allow_view_reviews(call)):
        return utils.error(
            "You may not view the user's reviews.",
            flask.url_for("call.display", cid=call["identifier"]),
        )

    proposals = anubis.proposals.get_call_proposals(call, submitted=True)
    reviews = utils.get_docs_view("reviews", "call_reviewer",
                                  [call["identifier"], user["username"]])
    reviews_lookup = {f"{r['proposal']} {username}": r for r in reviews}
    content = get_reviews_xlsx(call, proposals, reviews_lookup)
    response = flask.make_response(content)
    response.headers.set("Content-Type", constants.XLSX_MIMETYPE)
    response.headers.set("Content-Disposition",
                         "attachment",
                         filename=f"{cid}_{username}_reviews.xlsx")
    return response
コード例 #9
0
def call_zip(cid):
    """Return a zip file containing the XLSX file of all grants for a call
    and all documents in all grant dossiers.
    """
    call = anubis.call.get_call(cid)
    if call is None:
        return utils.error("No such call.", flask.url_for("home"))
    if not anubis.call.allow_view(call):
        return utils.error("You may not view the call.", flask.url_for("home"))
    if not anubis.call.allow_view_grants(call):
        return utils.error(
            "You may not view the grants of the call.",
            flask.url_for("call.display", cid=call["identifier"]),
        )
    # Colon ':' is a problematic character in filenames; replace by dash '_'
    cid = cid.replace(":", "-")
    grants = utils.get_docs_view("grants", "call", call["identifier"])
    output = io.BytesIO()
    with zipfile.ZipFile(output, "w") as outfile:
        outfile.writestr(f"{cid}_grants.xlsx", get_call_grants_xlsx(call, grants))
        for grant in grants:
            for document in anubis.grant.get_grant_documents(grant):
                outfile.writestr(document["filename"], document["content"])
    response = flask.make_response(output.getvalue())
    response.headers.set("Content-Type", constants.ZIP_MIMETYPE)
    response.headers.set(
        "Content-Disposition", "attachment", filename=f"{cid}_grants.zip"
    )
    return response
コード例 #10
0
ファイル: reviews.py プロジェクト: pekrau/Anubis
def call_reviewer_zip(cid, username):
    """Return a zip file containing the XLSX file of all reviews
    in the call by the reviewer (user), and all documents for the proposals
    to be reviewed.
    """
    call = anubis.call.get_call(cid)
    if call is None:
        return utils.error("No such call.", flask.url_for("home"))
    user = anubis.user.get_user(username=username)
    if user is None:
        return utils.error("No such user.", flask.url_for("home"))
    if user["username"] not in call["reviewers"]:
        return utils.error("The user is not a reviewer in the call.",
                           flask.url_for("home"))
    if not (user["username"] == flask.g.current_user["username"]
            or anubis.call.allow_view_reviews(call)):
        return utils.error(
            "You may not view the user's reviews.",
            flask.url_for("call.display", cid=call["identifier"]),
        )

    proposals = anubis.proposals.get_call_proposals(call, submitted=True)
    reviews = utils.get_docs_view("reviews", "call_reviewer",
                                  [call["identifier"], user["username"]])
    reviews_lookup = {f"{r['proposal']} {username}": r for r in reviews}
    output = io.BytesIO()
    with zipfile.ZipFile(output, "w") as zip:
        zip.writestr(
            f"{cid}_{username}_reviews.xlsx",
            get_reviews_xlsx(call, proposals, reviews_lookup),
        )
        # Filter away proposals not to be reviewed by the user.
        proposals = [
            p for p in proposals
            if f"{p['identifier']} {username}" in reviews_lookup
        ]
        zip.writestr(
            f"{call['identifier']}_selected_proposals.xlsx",
            anubis.proposals.get_call_xlsx(call, proposals=proposals),
        )
        for proposal in proposals:
            for field in call["proposal"]:
                if field["type"] == constants.DOCUMENT:
                    try:
                        doc = anubis.proposal.get_document(
                            proposal, field["identifier"])
                    except KeyError:
                        pass
                    else:
                        zip.writestr(doc["filename"], doc["content"])
    response = flask.make_response(output.getvalue())
    response.headers.set("Content-Type", constants.ZIP_MIMETYPE)
    response.headers.set(
        "Content-Disposition",
        "attachment",
        filename=f"{call['identifier']}_reviewer_{username}.zip",
    )
    return response
コード例 #11
0
def get_review_score_fields(call, proposals):
    """Return a dictionary of the score banner fields in the reviews.
    Compute the score means and stdevs. If there are more than two score
    fields, then also compute the mean of the means and the stdev of the means.
    This is done over all finalized reviews for each proposal.
    Store the values in the proposal document.
    """
    fields = dict(
        [
            (f["identifier"], f)
            for f in call["review"]
            if f.get("banner") and f["type"] == constants.SCORE
        ]
    )
    for proposal in proposals:
        reviews = utils.get_docs_view("reviews", "proposal", proposal["identifier"])
        # Only include finalized reviews in the calculation.
        reviews = [r for r in reviews if r.get("finalized")]
        scores = dict([(id, list()) for id in fields])
        for review in reviews:
            for id in fields:
                value = review["values"].get(id)
                if value is not None:
                    scores[id].append(float(value))
        proposal["scores"] = dict()
        for id in fields:
            proposal["scores"][id] = d = dict()
            d["n"] = len(scores[id])
            try:
                d["mean"] = round(statistics.mean(scores[id]), 1)
            except statistics.StatisticsError:
                d["mean"] = None
            try:
                d["stdev"] = round(statistics.stdev(scores[id]), 1)
            except statistics.StatisticsError:
                d["stdev"] = None
        if len(fields) >= 2:
            mean_scores = [
                d["mean"] for d in proposal["scores"].values() if d["mean"] is not None
            ]
            try:
                mean_means = round(statistics.mean(mean_scores), 1)
            except statistics.StatisticsError:
                mean_means = None
            proposal["scores"]["__mean__"] = mean_means
            try:
                stdev_means = round(statistics.stdev(mean_scores), 1)
            except statistics.StatisticsError:
                stdev_means = None
            proposal["scores"]["__mean__"] = mean_means
            proposal["scores"]["__stdev__"] = stdev_means
    return fields
コード例 #12
0
def call(cid):
    "List all grants for a call."
    call = anubis.call.get_call(cid)
    if call is None:
        return utils.error("No such call.", flask.url_for("home"))
    if not anubis.call.allow_view(call):
        return utils.error("You may not view the call.", flask.url_for("home"))
    if not anubis.call.allow_view_grants(call):
        return utils.error(
            "You may not view the grants of the call.",
            flask.url_for("call.display", cid=call["identifier"]),
        )

    grants = utils.get_docs_view("grants", "call", call["identifier"])
    # Convert username for grant to full user dict.
    for grant in grants:
        grant["user"] = anubis.user.get_user(grant["user"])
    # There may be accounts that have no emails.
    receiver_emails = [g["user"]["email"] for g in grants]
    receiver_emails = [e for e in receiver_emails if e]
    access_emails = []
    field_emails = []
    for grant in grants:
        access_emails.extend(
            [anubis.user.get_user(a)["email"] for a in grant.get("access_view", [])]
        )
        for field in call["grant"]:
            if field["type"] == constants.EMAIL:
                if field.get("repeat"):
                    n_repeat = grant["values"].get(field["repeat"]) or 0
                    for n in range(1, n_repeat + 1):
                        key = f"{field['identifier']}-{n}"
                        field_emails.append(grant["values"].get(key))
                else:
                    field_emails.append(grant["values"].get(field["identifier"]))
    field_emails = sorted(set([e for e in field_emails if e]))
    access_emails = sorted(set([e for e in access_emails if e]))
    all_emails = sorted(set(receiver_emails).union(access_emails).union(field_emails))
    email_lists = {
        "Grant receivers (= proposal submitters)": ", ".join(receiver_emails),
        "Persons with access to a grant": ", ".join(access_emails),
        "Emails provided in grant fields": ", ".join(field_emails),
        "All emails": ", ".join(all_emails),
    }
    return flask.render_template(
        "grants/call.html", call=call, grants=grants, email_lists=email_lists
    )
コード例 #13
0
def user(username):
    "List all proposals for a user."
    user = anubis.user.get_user(username=username)
    if user is None:
        return utils.error("No such user.", flask.url_for("home"))
    if not anubis.user.allow_view(user):
        return utils.error(
            "You may not view the user's proposals.", flask.url_for("home")
        )
    proposals = get_user_proposals(user["username"])
    proposals.extend(utils.get_docs_view("proposals", "access", user["username"]))
    return flask.render_template(
        "proposals/user.html",
        user=user,
        proposals=proposals,
        allow_view_decision=anubis.decision.allow_view,
    )
コード例 #14
0
ファイル: reviews.py プロジェクト: pekrau/Anubis
def proposal_archived(pid):
    "List all archived reviews for a proposal."
    proposal = anubis.proposal.get_proposal(pid)
    if proposal is None:
        return utils.error("No such proposal.", flask.url_for("home"))

    call = anubis.call.get_call(proposal["call"])
    if not (flask.g.am_admin or anubis.call.am_chair(call)):
        return utils.error(
            "You may not view the archived reviews of the call.",
            flask.url_for("call.display", cid=call["identifier"]),
        )

    reviews = utils.get_docs_view("reviews", "proposal_archived",
                                  proposal["identifier"])
    return flask.render_template("reviews/proposal_archived.html",
                                 reviews=reviews,
                                 proposal=proposal,
                                 call=call)
コード例 #15
0
ファイル: review.py プロジェクト: pekrau/Anubis
 def finish(self):
     "Check rank fields for conflicts with other reviews in same call."
     call = anubis.call.get_call(self["call"])
     proposal = anubis.proposal.get_proposal(self["proposal"])
     reviewer = anubis.user.get_user(self["reviewer"])
     reviews = utils.get_docs_view(
         "reviews", "call_reviewer",
         [call["identifier"], reviewer["username"]])
     rank_fields = [
         f for f in call["review"] if f["type"] == constants.RANK
     ]
     for field in rank_fields:
         value = self["values"].get(field["identifier"])
         if value is None:
             continue
         for review in reviews:
             if self.doc["_id"] == review["_id"]:
                 continue
             if review["values"].get(field["identifier"]) == value:
                 self["errors"][field[
                     "identifier"]] = "Invalid: same value as in another review."
                 break
コード例 #16
0
def call_xlsx(cid):
    "Produce an XLSX file of all grants for a call."
    call = anubis.call.get_call(cid)
    if call is None:
        return utils.error("No such call.", flask.url_for("home"))
    if not anubis.call.allow_view(call):
        return utils.error("You may not view the call.", flask.url_for("home"))
    if not anubis.call.allow_view_grants(call):
        return utils.error(
            "You may not view the grants of the call.",
            flask.url_for("call.display", cid=call["identifier"]),
        )

    grants = utils.get_docs_view("grants", "call", call["identifier"])
    grants.sort(key=lambda g: g["identifier"])
    content = get_call_grants_xlsx(call, grants)
    response = flask.make_response(content)
    response.headers.set("Content-Type", constants.XLSX_MIMETYPE)
    response.headers.set(
        "Content-Disposition", "attachment", filename=f"{cid}_grants.xlsx"
    )
    return response
コード例 #17
0
def edit(pid):
    "Edit the proposal."
    proposal = get_proposal(pid)
    if proposal is None:
        return utils.error("No such proposal.", flask.url_for("home"))
    call = anubis.call.get_call(proposal["call"])

    if utils.http_GET():
        if not allow_edit(proposal):
            return utils.error("You are not allowed to edit this proposal.")
        return flask.render_template("proposal/edit.html",
                                     proposal=proposal,
                                     call=call)

    elif utils.http_POST():
        if not allow_edit(proposal):
            return utils.error("You are not allowed to edit this proposal.")
        try:
            with ProposalSaver(proposal) as saver:
                saver["title"] = flask.request.form.get("_title") or None
                saver.set_fields_values(call["proposal"],
                                        form=flask.request.form)
        except ValueError as error:
            return utils.error(error)

        # If a repeat field was changed, then redisplay edit page.
        if saver.repeat_changed:
            return flask.redirect(
                flask.url_for(".edit", pid=proposal["identifier"]))

        if flask.request.form.get("_save") == "submit":
            proposal = get_proposal(pid, refresh=True)  # Get up-to-date info.
            try:
                with ProposalSaver(proposal) as saver:
                    saver.set_submitted()  # Tests whether allowed or not.
            except ValueError as error:
                utils.flash_error(error)
            else:
                utils.flash_message("Proposal saved and submitted.")
                send_submission_email(proposal)

        elif allow_submit(proposal) and not proposal.get("submitted"):
            utils.flash_warning("Proposal was saved but not submitted."
                                " You must explicitly submit it!")
        return flask.redirect(
            flask.url_for(".display", pid=proposal["identifier"]))

    elif utils.http_DELETE():
        if not allow_delete(proposal):
            return utils.error("You are not allowed to delete this proposal.")
        decision = anubis.decision.get_decision(proposal.get("decision"))
        if decision:
            utils.delete(decision)
        reviews = utils.get_docs_view("reviews", "proposal",
                                      proposal["identifier"])
        for review in reviews:
            utils.delete(review)
        utils.delete(proposal)
        utils.flash_message(f"Deleted proposal {pid}.")
        if flask.g.am_admin or flask.g.am_staff:
            url = flask.url_for("proposals.call", cid=call["identifier"])
        else:
            url = flask.url_for("proposals.user", username=proposal["user"])
        return flask.redirect(url)
コード例 #18
0
def get_review_rank_fields_errors(call, proposals):
    """Return a tuple containing a dictionary of the rank banner fields
    in the reviews and a list of errors.
    Compute the ranking factors of each proposal from all finalized reviews.
    Check that the ranks are consecutive for all reviewers.
    """
    fields = dict(
        [
            (f["identifier"], f)
            for f in call["review"]
            if f.get("banner") and f["type"] == constants.RANK
        ]
    )
    errors = []
    for id in fields.keys():
        ranks = dict()  # key: reviewer, value: dict(proposal: rank)
        for proposal in proposals:
            reviews = utils.get_docs_view("reviews", "proposal", proposal["identifier"])
            # Only include finalized reviews in the calculation.
            reviews = [r for r in reviews if r.get("finalized")]
            for review in reviews:
                try:
                    value = review["values"][id]
                    if value is None:
                        raise KeyError
                except KeyError:
                    pass
                else:
                    d = ranks.setdefault(review["reviewer"], dict())
                    d[proposal["identifier"]] = value
        # Check that ranking values start with 1 and are consecutiive.
        for reviewer, values in ranks.items():
            series = list(values.values())
            if series:
                user = anubis.user.get_user(reviewer)
                name = utils.get_fullname(user)
                if min(series) != 1:
                    errors.append(f"{name} reviews '{id}' do not start with 1.")
                elif set(series) != set(range(1, max(series) + 1)):
                    errors.append(f"{name} reviews '{id}' are not consecutive.")
        # For each proposal, compute ranking factor.
        for proposal in proposals:
            factors = []
            for reviewer, values in ranks.items():
                try:
                    value = values[proposal["identifier"]]
                except KeyError:
                    pass
                else:
                    factors.append(float(len(values) - value + 1) / len(values))
            rf = proposal.setdefault("ranking", dict())
            rf[id] = dict()
            try:
                rf[id]["factor"] = round(10.0 * statistics.mean(factors), 1)
            except statistics.StatisticsError:
                rf[id]["factor"] = None
            try:
                rf[id]["stdev"] = round(10.0 * statistics.stdev(factors), 1)
            except statistics.StatisticsError:
                rf[id]["stdev"] = None
    return fields, errors
コード例 #19
0
ファイル: call.py プロジェクト: pekrau/Anubis
def reviewers(cid):
    "Edit the list of reviewers."
    call = get_call(cid)
    if not call:
        return utils.error("No such call.", flask.url_for("home"))

    if utils.http_GET():
        if not allow_view_details(call):
            return utils.error("You are not allowed to edit the reviewers of the call.")
        reviewers = [anubis.user.get_user(r) for r in call["reviewers"]]
        reviewer_emails = [r["email"] for r in reviewers if r["email"]]
        email_lists = {"Emails for reviewers": ", ".join(reviewer_emails)}
        return flask.render_template(
            "call/reviewers.html",
            call=call,
            reviewers=reviewers,
            email_lists=email_lists,
            allow_edit=allow_edit(call),
            allow_view_reviews=allow_view_reviews(call),
        )

    elif utils.http_POST():
        if not allow_edit(call):
            return utils.error("You are not allowed to edit the call.")
        reviewer = flask.request.form.get("reviewer")
        if not reviewer:
            return flask.redirect(flask.url_for(".display", cid=cid))
        user = anubis.user.get_user(username=reviewer)
        if user is None:
            user = anubis.user.get_user(email=reviewer)
        if user is None:
            return utils.error("No such user.")
        if anubis.proposal.get_call_user_proposal(cid, user["username"]):
            utils.flash_warning(
                "User has a proposal in the call. Allowing"
                " her to be a reviewer is questionable."
            )
        with CallSaver(call) as saver:
            try:
                saver["reviewers"].remove(user["username"])
            except ValueError:
                pass
            try:
                saver["chairs"].remove(user["username"])
            except ValueError:
                pass
            saver["reviewers"].append(user["username"])
            if utils.to_bool(flask.request.form.get("chair")):
                saver["chairs"].append(user["username"])
        return flask.redirect(flask.url_for(".reviewers", cid=call["identifier"]))

    elif utils.http_DELETE():
        if not allow_edit(call):
            return utils.error("You are not allowed to edit the call.")
        reviewer = flask.request.form.get("reviewer")
        if utils.get_docs_view(
            "reviews", "call_reviewer", [call["identifier"], reviewer]
        ):
            return utils.error(
                "Cannot remove reviewer which has reviews" " in the call."
            )
        if reviewer:
            with CallSaver(call) as saver:
                try:
                    saver["reviewers"].remove(reviewer)
                except ValueError:
                    pass
                try:
                    saver["chairs"].remove(reviewer)
                except ValueError:
                    pass
        return flask.redirect(flask.url_for(".reviewers", cid=call["identifier"]))