aurweb/aurweb/routers/accounts.py
Kevin Morris 021a1c8fb6 add /accounts/ (get, post) routes
Slight markup changes, same style overall and same
form parameters as the PHP implementation.

In addition, we've disabled the "left" and "right"
navigation buttons when we're at the border of the
table.

CSS Changes:

- Added similar styling to submit `<buttons>` that submit `<input>` had.
- Added .results tr td[align="{left,right}"] styling to align
  the result table's `More -->` button to the right of the table.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-07-01 11:08:56 -07:00

751 lines
30 KiB
Python

import copy
import logging
import typing
from datetime import datetime
from http import HTTPStatus
from fastapi import APIRouter, Form, HTTPException, Request
from fastapi.responses import HTMLResponse, RedirectResponse
from sqlalchemy import and_, func, or_
import aurweb.config
from aurweb import db, l10n, time, util
from aurweb.auth import account_type_required, auth_required
from aurweb.captcha import get_captcha_answer, get_captcha_salts, get_captcha_token
from aurweb.l10n import get_translator_for_request
from aurweb.models.accepted_term import AcceptedTerm
from aurweb.models.account_type import (DEVELOPER, DEVELOPER_ID, TRUSTED_USER, TRUSTED_USER_AND_DEV, TRUSTED_USER_AND_DEV_ID,
TRUSTED_USER_ID, USER_ID, AccountType)
from aurweb.models.ban import Ban
from aurweb.models.ssh_pub_key import SSHPubKey, get_fingerprint
from aurweb.models.term import Term
from aurweb.models.user import User
from aurweb.scripts.notify import ResetKeyNotification
from aurweb.templates import make_context, make_variable_context, render_template
router = APIRouter()
logger = logging.getLogger(__name__)
@router.get("/passreset", response_class=HTMLResponse)
@auth_required(False)
async def passreset(request: Request):
context = await make_variable_context(request, "Password Reset")
return render_template(request, "passreset.html", context)
@router.post("/passreset", response_class=HTMLResponse)
@auth_required(False)
async def passreset_post(request: Request,
user: str = Form(...),
resetkey: str = Form(default=None),
password: str = Form(default=None),
confirm: str = Form(default=None)):
from aurweb.db import session
context = await make_variable_context(request, "Password Reset")
# The user parameter being required, we can match against
user = db.query(User, or_(User.Username == user,
User.Email == user)).first()
if not user:
context["errors"] = ["Invalid e-mail."]
return render_template(request, "passreset.html", context,
status_code=int(HTTPStatus.NOT_FOUND))
if resetkey:
context["resetkey"] = resetkey
if not user.ResetKey or resetkey != user.ResetKey:
context["errors"] = ["Invalid e-mail."]
return render_template(request, "passreset.html", context,
status_code=int(HTTPStatus.NOT_FOUND))
if not user or not password:
context["errors"] = ["Missing a required field."]
return render_template(request, "passreset.html", context,
status_code=int(HTTPStatus.BAD_REQUEST))
if password != confirm:
# If the provided password does not match the provided confirm.
context["errors"] = ["Password fields do not match."]
return render_template(request, "passreset.html", context,
status_code=int(HTTPStatus.BAD_REQUEST))
if len(password) < User.minimum_passwd_length():
# Translate the error here, which simplifies error output
# in the jinja2 template.
_ = get_translator_for_request(request)
context["errors"] = [_(
"Your password must be at least %s characters.") % (
str(User.minimum_passwd_length()))]
return render_template(request, "passreset.html", context,
status_code=int(HTTPStatus.BAD_REQUEST))
# We got to this point; everything matched up. Update the password
# and remove the ResetKey.
user.ResetKey = str()
user.update_password(password)
if user.session:
session.delete(user.session)
session.commit()
# Render ?step=complete.
return RedirectResponse(url="/passreset?step=complete",
status_code=int(HTTPStatus.SEE_OTHER))
# If we got here, we continue with issuing a resetkey for the user.
resetkey = db.make_random_value(User, User.ResetKey)
user.ResetKey = resetkey
session.commit()
executor = db.ConnectionExecutor(db.get_engine().raw_connection())
ResetKeyNotification(executor, user.ID).send()
# Render ?step=confirm.
return RedirectResponse(url="/passreset?step=confirm",
status_code=int(HTTPStatus.SEE_OTHER))
def process_account_form(request: Request, user: User, args: dict):
""" Process an account form. All fields are optional and only checks
requirements in the case they are present.
```
context = await make_variable_context(request, "Accounts")
ok, errors = process_account_form(request, user, **kwargs)
if not ok:
context["errors"] = errors
return render_template(request, "some_account_template.html", context)
```
:param request: An incoming FastAPI request
:param user: The user model of the account being processed
:param args: A dictionary of arguments generated via request.form()
:return: A (passed processing boolean, list of errors) tuple
"""
# Get a local translator.
_ = get_translator_for_request(request)
host = request.client.host
ban = db.query(Ban, Ban.IPAddress == host).first()
if ban:
return False, [
"Account registration has been disabled for your " +
"IP address, probably due to sustained spam attacks. " +
"Sorry for the inconvenience."
]
if request.user.is_authenticated():
if not request.user.valid_password(args.get("passwd", None)):
return False, ["Invalid password."]
email = args.get("E", None)
username = args.get("U", None)
if not email or not username:
return False, ["Missing a required field."]
username_min_len = aurweb.config.getint("options", "username_min_len")
username_max_len = aurweb.config.getint("options", "username_max_len")
if not util.valid_username(args.get("U")):
return False, [
"The username is invalid.",
[
_("It must be between %s and %s characters long") % (
username_min_len, username_max_len),
"Start and end with a letter or number",
"Can contain only one period, underscore or hyphen.",
]
]
password = args.get("P", None)
if password:
confirmation = args.get("C", None)
if not util.valid_password(password):
return False, [
_("Your password must be at least %s characters.") % (
username_min_len)
]
elif not confirmation:
return False, ["Please confirm your new password."]
elif password != confirmation:
return False, ["Password fields do not match."]
backup_email = args.get("BE", None)
homepage = args.get("HP", None)
pgp_key = args.get("K", None)
ssh_pubkey = args.get("PK", None)
language = args.get("L", None)
timezone = args.get("TZ", None)
def username_exists(username):
return and_(User.ID != user.ID,
func.lower(User.Username) == username.lower())
def email_exists(email):
return and_(User.ID != user.ID,
func.lower(User.Email) == email.lower())
if not util.valid_email(email):
return False, ["The email address is invalid."]
elif backup_email and not util.valid_email(backup_email):
return False, ["The backup email address is invalid."]
elif homepage and not util.valid_homepage(homepage):
return False, [
"The home page is invalid, please specify the full HTTP(s) URL."]
elif pgp_key and not util.valid_pgp_fingerprint(pgp_key):
return False, ["The PGP key fingerprint is invalid."]
elif ssh_pubkey and not util.valid_ssh_pubkey(ssh_pubkey):
return False, ["The SSH public key is invalid."]
elif language and language not in l10n.SUPPORTED_LANGUAGES:
return False, ["Language is not currently supported."]
elif timezone and timezone not in time.SUPPORTED_TIMEZONES:
return False, ["Timezone is not currently supported."]
elif db.query(User, username_exists(username)).first():
# If the username already exists...
return False, [
_("The username, %s%s%s, is already in use.") % (
"<strong>", username, "</strong>")
]
elif db.query(User, email_exists(email)).first():
# If the email already exists...
return False, [
_("The address, %s%s%s, is already in use.") % (
"<strong>", email, "</strong>")
]
def ssh_fingerprint_exists(fingerprint):
return and_(SSHPubKey.UserID != user.ID,
SSHPubKey.Fingerprint == fingerprint)
if ssh_pubkey:
fingerprint = get_fingerprint(ssh_pubkey.strip().rstrip())
if fingerprint is None:
return False, ["The SSH public key is invalid."]
if db.query(SSHPubKey, ssh_fingerprint_exists(fingerprint)).first():
return False, [
_("The SSH public key, %s%s%s, is already in use.") % (
"<strong>", fingerprint, "</strong>")
]
captcha_salt = args.get("captcha_salt", None)
if captcha_salt and captcha_salt not in get_captcha_salts():
return False, ["This CAPTCHA has expired. Please try again."]
captcha = args.get("captcha", None)
if captcha:
answer = get_captcha_answer(get_captcha_token(captcha_salt))
if captcha != answer:
return False, ["The entered CAPTCHA answer is invalid."]
return True, []
def make_account_form_context(context: dict,
request: Request,
user: User,
args: dict):
""" Modify a FastAPI context and add attributes for the account form.
:param context: FastAPI context
:param request: FastAPI request
:param user: Target user
:param args: Persistent arguments: request.form()
:return: FastAPI context adjusted for account form
"""
# Do not modify the original context.
context = copy.copy(context)
context["account_types"] = [
(1, "Normal User"),
(2, "Trusted User")
]
user_account_type_id = context.get("account_types")[0][0]
if request.user.has_credential("CRED_ACCOUNT_EDIT_DEV"):
context["account_types"].append((3, "Developer"))
context["account_types"].append((4, "Trusted User & Developer"))
if request.user.is_authenticated():
context["username"] = args.get("U", user.Username)
context["account_type"] = args.get("T", user.AccountType.ID)
context["suspended"] = args.get("S", user.Suspended)
context["email"] = args.get("E", user.Email)
context["hide_email"] = args.get("H", user.HideEmail)
context["backup_email"] = args.get("BE", user.BackupEmail)
context["realname"] = args.get("R", user.RealName)
context["homepage"] = args.get("HP", user.Homepage or str())
context["ircnick"] = args.get("I", user.IRCNick)
context["pgp"] = args.get("K", user.PGPKey or str())
context["lang"] = args.get("L", user.LangPreference)
context["tz"] = args.get("TZ", user.Timezone)
ssh_pk = user.ssh_pub_key.PubKey if user.ssh_pub_key else str()
context["ssh_pk"] = args.get("PK", ssh_pk)
context["cn"] = args.get("CN", user.CommentNotify)
context["un"] = args.get("UN", user.UpdateNotify)
context["on"] = args.get("ON", user.OwnershipNotify)
context["inactive"] = args.get("J", user.InactivityTS != 0)
else:
context["username"] = args.get("U", str())
context["account_type"] = args.get("T", user_account_type_id)
context["suspended"] = args.get("S", False)
context["email"] = args.get("E", str())
context["hide_email"] = args.get("H", False)
context["backup_email"] = args.get("BE", str())
context["realname"] = args.get("R", str())
context["homepage"] = args.get("HP", str())
context["ircnick"] = args.get("I", str())
context["pgp"] = args.get("K", str())
context["lang"] = args.get("L", context.get("language"))
context["tz"] = args.get("TZ", context.get("timezone"))
context["ssh_pk"] = args.get("PK", str())
context["cn"] = args.get("CN", True)
context["un"] = args.get("UN", False)
context["on"] = args.get("ON", True)
context["inactive"] = args.get("J", False)
context["password"] = args.get("P", str())
context["confirm"] = args.get("C", str())
return context
@router.get("/register", response_class=HTMLResponse)
@auth_required(False)
async def account_register(request: Request,
U: str = Form(default=str()), # Username
E: str = Form(default=str()), # Email
H: str = Form(default=False), # Hide Email
BE: str = Form(default=None), # Backup Email
R: str = Form(default=None), # Real Name
HP: str = Form(default=None), # Homepage
I: str = Form(default=None), # IRC Nick
K: str = Form(default=None), # PGP Key FP
L: str = Form(default=aurweb.config.get(
"options", "default_lang")),
TZ: str = Form(default=aurweb.config.get(
"options", "default_timezone")),
PK: str = Form(default=None),
CN: bool = Form(default=False), # Comment Notify
CU: bool = Form(default=False), # Update Notify
CO: bool = Form(default=False), # Owner Notify
captcha: str = Form(default=str())):
context = await make_variable_context(request, "Register")
context["captcha_salt"] = get_captcha_salts()[0]
context = make_account_form_context(context, request, None, dict())
return render_template(request, "register.html", context)
@router.post("/register", response_class=HTMLResponse)
@auth_required(False)
async def account_register_post(request: Request,
U: str = Form(default=str()), # Username
E: str = Form(default=str()), # Email
H: str = Form(default=False), # Hide Email
BE: str = Form(default=None), # Backup Email
R: str = Form(default=''), # Real Name
HP: str = Form(default=None), # Homepage
I: str = Form(default=None), # IRC Nick
K: str = Form(default=None), # PGP Key
L: str = Form(default=aurweb.config.get(
"options", "default_lang")),
TZ: str = Form(default=aurweb.config.get(
"options", "default_timezone")),
PK: str = Form(default=None), # SSH PubKey
CN: bool = Form(default=False),
UN: bool = Form(default=False),
ON: bool = Form(default=False),
captcha: str = Form(default=None),
captcha_salt: str = Form(...)):
from aurweb.db import session
context = await make_variable_context(request, "Register")
args = dict(await request.form())
context = make_account_form_context(context, request, None, args)
ok, errors = process_account_form(request, request.user, args)
if not ok:
# If the field values given do not meet the requirements,
# return HTTP 400 with an error.
context["errors"] = errors
return render_template(request, "register.html", context,
status_code=int(HTTPStatus.BAD_REQUEST))
if not captcha:
context["errors"] = ["The CAPTCHA is missing."]
return render_template(request, "register.html", context,
status_code=int(HTTPStatus.BAD_REQUEST))
# Create a user with no password with a resetkey, then send
# an email off about it.
resetkey = db.make_random_value(User, User.ResetKey)
# By default, we grab the User account type to associate with.
account_type = db.query(AccountType,
AccountType.AccountType == "User").first()
# Create a user given all parameters available.
user = db.create(User, Username=U, Email=E, HideEmail=H, BackupEmail=BE,
RealName=R, Homepage=HP, IRCNick=I, PGPKey=K,
LangPreference=L, Timezone=TZ, CommentNotify=CN,
UpdateNotify=UN, OwnershipNotify=ON, ResetKey=resetkey,
AccountType=account_type)
# If a PK was given and either one does not exist or the given
# PK mismatches the existing user's SSHPubKey.PubKey.
if PK:
# Get the second element in the PK, which is the actual key.
pubkey = PK.strip().rstrip()
parts = pubkey.split(" ")
if len(parts) == 3:
# Remove the host part.
pubkey = parts[0] + " " + parts[1]
fingerprint = get_fingerprint(pubkey)
user.ssh_pub_key = SSHPubKey(UserID=user.ID,
PubKey=pubkey,
Fingerprint=fingerprint)
session.commit()
# Send a reset key notification to the new user.
executor = db.ConnectionExecutor(db.get_engine().raw_connection())
ResetKeyNotification(executor, user.ID).send()
context["complete"] = True
context["user"] = user
return render_template(request, "register.html", context)
def cannot_edit(request, user):
""" Return a 401 HTMLResponse if the request user doesn't
have authorization, otherwise None. """
has_dev_cred = request.user.has_credential("CRED_ACCOUNT_EDIT_DEV",
approved=[user])
if not has_dev_cred:
return HTMLResponse(status_code=int(HTTPStatus.UNAUTHORIZED))
return None
@router.get("/account/{username}/edit", response_class=HTMLResponse)
@auth_required(True)
async def account_edit(request: Request,
username: str):
user = db.query(User, User.Username == username).first()
response = cannot_edit(request, user)
if response:
return response
context = await make_variable_context(request, "Accounts")
context["user"] = user
context = make_account_form_context(context, request, user, dict())
return render_template(request, "account/edit.html", context)
@router.post("/account/{username}/edit", response_class=HTMLResponse)
@auth_required(True)
async def account_edit_post(request: Request,
username: str,
U: str = Form(default=str()), # Username
J: bool = Form(default=False),
E: str = Form(default=str()), # Email
H: str = Form(default=False), # Hide Email
BE: str = Form(default=None), # Backup Email
R: str = Form(default=None), # Real Name
HP: str = Form(default=None), # Homepage
I: str = Form(default=None), # IRC Nick
K: str = Form(default=None), # PGP Key
L: str = Form(aurweb.config.get(
"options", "default_lang")),
TZ: str = Form(aurweb.config.get(
"options", "default_timezone")),
P: str = Form(default=str()), # New Password
C: str = Form(default=None), # Password Confirm
PK: str = Form(default=None), # PubKey
CN: bool = Form(default=False), # Comment Notify
UN: bool = Form(default=False), # Update Notify
ON: bool = Form(default=False), # Owner Notify
passwd: str = Form(default=str())):
from aurweb.db import session
user = session.query(User).filter(User.Username == username).first()
response = cannot_edit(request, user)
if response:
return response
context = await make_variable_context(request, "Accounts")
context["user"] = user
args = dict(await request.form())
context = make_account_form_context(context, request, user, args)
ok, errors = process_account_form(request, user, args)
if not passwd:
context["errors"] = ["Invalid password."]
return render_template(request, "account/edit.html", context,
status_code=int(HTTPStatus.BAD_REQUEST))
if not ok:
context["errors"] = errors
return render_template(request, "account/edit.html", context,
status_code=int(HTTPStatus.BAD_REQUEST))
# Set all updated fields as needed.
user.Username = U or user.Username
user.Email = E or user.Email
user.HideEmail = bool(H)
user.BackupEmail = BE or user.BackupEmail
user.RealName = R or user.RealName
user.Homepage = HP or user.Homepage
user.IRCNick = I or user.IRCNick
user.PGPKey = K or user.PGPKey
user.InactivityTS = datetime.utcnow().timestamp() if J else 0
# If we update the language, update the cookie as well.
if L and L != user.LangPreference:
request.cookies["AURLANG"] = L
user.LangPreference = L
context["language"] = L
# If we update the timezone, also update the cookie.
if TZ and TZ != user.Timezone:
user.Timezone = TZ
request.cookies["AURTZ"] = TZ
context["timezone"] = TZ
user.CommentNotify = bool(CN)
user.UpdateNotify = bool(UN)
user.OwnershipNotify = bool(ON)
# If a PK is given, compare it against the target user's PK.
if PK:
# Get the second token in the public key, which is the actual key.
pubkey = PK.strip().rstrip()
parts = pubkey.split(" ")
if len(parts) == 3:
# Remove the host part.
pubkey = parts[0] + " " + parts[1]
fingerprint = get_fingerprint(pubkey)
if not user.ssh_pub_key:
# No public key exists, create one.
user.ssh_pub_key = SSHPubKey(UserID=user.ID,
PubKey=pubkey,
Fingerprint=fingerprint)
elif user.ssh_pub_key.PubKey != pubkey:
# A public key already exists, update it.
user.ssh_pub_key.PubKey = pubkey
user.ssh_pub_key.Fingerprint = fingerprint
elif user.ssh_pub_key:
# Else, if the user has a public key already, delete it.
session.delete(user.ssh_pub_key)
# Commit changes, if any.
session.commit()
if P and not user.valid_password(P):
# Remove the fields we consumed for passwords.
context["P"] = context["C"] = str()
# If a password was given and it doesn't match the user's, update it.
user.update_password(P)
if user == request.user:
# If the target user is the request user, login with
# the updated password and update AURSID.
request.cookies["AURSID"] = user.login(request, P)
if not errors:
context["complete"] = True
# Update cookies with requests, in case they were changed.
response = render_template(request, "account/edit.html", context)
return util.migrate_cookies(request, response)
account_template = (
"account/show.html",
["Account", "{}"],
["username"] # Query parameters to replace in the title string.
)
@router.get("/account/{username}")
@auth_required(True, template=account_template,
status_code=HTTPStatus.UNAUTHORIZED)
async def account(request: Request, username: str):
_ = l10n.get_translator_for_request(request)
context = await make_variable_context(request, _("Account") + username)
user = db.query(User, User.Username == username).first()
if not user:
raise HTTPException(status_code=int(HTTPStatus.NOT_FOUND))
context["user"] = user
return render_template(request, "account/show.html", context)
@router.get("/accounts/")
@auth_required(True)
@account_type_required({TRUSTED_USER, DEVELOPER, TRUSTED_USER_AND_DEV})
async def accounts(request: Request):
context = make_context(request, "Accounts")
return render_template(request, "account/search.html", context)
@router.post("/accounts/")
@auth_required(True)
@account_type_required({TRUSTED_USER, DEVELOPER, TRUSTED_USER_AND_DEV})
async def accounts_post(request: Request,
O: int = Form(default=0), # Offset
SB: str = Form(default=str()), # Search By
U: str = Form(default=str()), # Username
T: str = Form(default=str()), # Account Type
S: bool = Form(default=False), # Suspended
E: str = Form(default=str()), # Email
R: str = Form(default=str()), # Real Name
I: str = Form(default=str()), # IRC Nick
K: str = Form(default=str())): # PGP Key
context = await make_variable_context(request, "Accounts")
context["pp"] = pp = 50 # Hits per page.
offset = max(O, 0) # Minimize offset at 0.
context["offset"] = offset # Offset.
context["params"] = dict(await request.form())
if "O" in context["params"]:
context["params"].pop("O")
# Setup order by criteria based on SB.
order_by_columns = {
"t": (AccountType.ID.asc(), User.Username.asc()),
"r": (User.RealName.asc(), AccountType.ID.asc()),
"i": (User.IRCNick.asc(), AccountType.ID.asc()),
}
default_order = (User.Username.asc(), AccountType.ID.asc())
order_by = order_by_columns.get(SB, default_order)
# Convert parameter T to an AccountType ID.
account_types = {
"u": USER_ID,
"t": TRUSTED_USER_ID,
"d": DEVELOPER_ID,
"td": TRUSTED_USER_AND_DEV_ID
}
account_type_id = account_types.get(T, None)
# Get a query handle to users, populate the total user
# count into a jinja2 context variable.
query = db.query(User).join(AccountType)
context["total_users"] = query.count()
# Populate this list with any additional statements to
# be ANDed together.
statements = []
if account_type_id is not None:
statements.append(AccountType.ID == account_type_id)
if U:
statements.append(User.Username.like(f"%{U}%"))
if S:
statements.append(User.Suspended == S)
if E:
statements.append(User.Email.like(f"%{E}%"))
if R:
statements.append(User.RealName.like(f"%{R}%"))
if I:
statements.append(User.IRCNick.like(f"%{I}%"))
if K:
statements.append(User.PGPKey.like(f"%{K}%"))
# Filter the query by combining all statements added above into
# an AND statement, unless there's just one statement, which
# we pass on to filter() as args.
if statements:
query = query.filter(and_(*statements))
# Finally, order and truncate our users for the current page.
users = query.order_by(*order_by).limit(pp).offset(offset)
context["users"] = users
return render_template(request, "account/index.html", context)
def render_terms_of_service(request: Request,
context: dict,
terms: typing.Iterable):
if not terms:
return RedirectResponse("/", status_code=int(HTTPStatus.SEE_OTHER))
context["unaccepted_terms"] = terms
return render_template(request, "tos/index.html", context)
@router.get("/tos")
@auth_required(True, redirect="/")
async def terms_of_service(request: Request):
# Query the database for terms that were previously accepted,
# but now have a bumped Revision that needs to be accepted.
diffs = db.query(Term).join(AcceptedTerm).filter(
AcceptedTerm.Revision < Term.Revision).all()
# Query the database for any terms that have not yet been accepted.
unaccepted = db.query(Term).filter(
~Term.ID.in_(db.query(AcceptedTerm.TermsID))).all()
# Translate the 'Terms of Service' part of our page title.
_ = l10n.get_translator_for_request(request)
title = f"AUR {_('Terms of Service')}"
context = await make_variable_context(request, title)
accept_needed = sorted(unaccepted + diffs)
return render_terms_of_service(request, context, accept_needed)
@router.post("/tos")
@auth_required(True, redirect="/")
async def terms_of_service_post(request: Request,
accept: bool = Form(default=False)):
# Query the database for terms that were previously accepted,
# but now have a bumped Revision that needs to be accepted.
diffs = db.query(Term).join(AcceptedTerm).filter(
AcceptedTerm.Revision < Term.Revision).all()
# Query the database for any terms that have not yet been accepted.
unaccepted = db.query(Term).filter(
~Term.ID.in_(db.query(AcceptedTerm.TermsID))).all()
if not accept:
# Translate the 'Terms of Service' part of our page title.
_ = l10n.get_translator_for_request(request)
title = f"AUR {_('Terms of Service')}"
context = await make_variable_context(request, title)
# We already did the database filters here, so let's just use
# them instead of reiterating the process in terms_of_service.
accept_needed = sorted(unaccepted + diffs)
return render_terms_of_service(request, context, accept_needed)
# For each term we found, query for the matching accepted term
# and update its Revision to the term's current Revision.
for term in diffs:
accepted_term = request.user.accepted_terms.filter(
AcceptedTerm.TermsID == term.ID).first()
accepted_term.Revision = term.Revision
# For each term that was never accepted, accept it!
for term in unaccepted:
db.create(AcceptedTerm, User=request.user,
Term=term, Revision=term.Revision,
autocommit=False)
if diffs or unaccepted:
# If we had any terms to update, commit the changes.
db.commit()
return RedirectResponse("/", status_code=int(HTTPStatus.SEE_OTHER))