diff --git a/aurweb/routers/packages.py b/aurweb/routers/packages.py
index 8bfb680e..af1ebe46 100644
--- a/aurweb/routers/packages.py
+++ b/aurweb/routers/packages.py
@@ -807,3 +807,66 @@ async def pkgbase_unvote(request: Request, name: str):
return RedirectResponse(f"/pkgbase/{name}",
status_code=int(HTTPStatus.SEE_OTHER))
+
+
+def disown_pkgbase(pkgbase: PackageBase, disowner: User):
+ conn = db.ConnectionExecutor(db.get_engine().raw_connection())
+ notif = notify.DisownNotification(conn, disowner.ID, pkgbase.ID)
+
+ if disowner != pkgbase.Maintainer:
+ with db.begin():
+ pkgbase.Maintainer = None
+ else:
+ co = pkgbase.comaintainers.order_by(
+ PackageComaintainer.Priority.asc()
+ ).limit(1).first()
+
+ if co:
+ with db.begin():
+ pkgbase.Maintainer = co.User
+ db.session.delete(co)
+ else:
+ pkgbase.Maintainer = None
+
+ notif.send()
+
+
+@router.get("/pkgbase/{name}/disown")
+@auth_required(True, redirect="/pkgbase/{name}")
+async def pkgbase_disown_get(request: Request, name: str):
+ pkgbase = get_pkg_or_base(name, PackageBase)
+
+ has_cred = request.user.has_credential("CRED_PKGBASE_DISOWN",
+ approved=[pkgbase.Maintainer])
+ if not has_cred:
+ return RedirectResponse(f"/pkgbase/{name}",
+ int(HTTPStatus.SEE_OTHER))
+
+ context = make_context(request, "Disown Package")
+ context["pkgbase"] = pkgbase
+ return render_template(request, "packages/disown.html", context)
+
+
+@router.post("/pkgbase/{name}/disown")
+@auth_required(True, redirect="/pkgbase/{name}")
+async def pkgbase_disown_post(request: Request, name: str,
+ confirm: bool = Form(default=False)):
+ pkgbase = get_pkg_or_base(name, PackageBase)
+
+ has_cred = request.user.has_credential("CRED_PKGBASE_DISOWN",
+ approved=[pkgbase.Maintainer])
+ if not has_cred:
+ return RedirectResponse(f"/pkgbase/{name}",
+ int(HTTPStatus.SEE_OTHER))
+
+ if not confirm:
+ context = make_context(request, "Disown Package")
+ context["pkgbase"] = pkgbase
+ context["errors"] = [("The selected packages have not been disowned, "
+ "check the confirmation checkbox.")]
+ return render_template(request, "packages/disown.html", context,
+ status_code=int(HTTPStatus.EXPECTATION_FAILED))
+
+ disown_pkgbase(pkgbase, request.user)
+ return RedirectResponse(f"/pkgbase/{name}",
+ status_code=int(HTTPStatus.SEE_OTHER))
diff --git a/templates/packages/disown.html b/templates/packages/disown.html
new file mode 100644
index 00000000..8d5a8574
--- /dev/null
+++ b/templates/packages/disown.html
@@ -0,0 +1,55 @@
+{% extends "partials/layout.html" %}
+
+{% block pageContent %}
+
+ {% if errors %}
+
+ {% for error in errors %}
+ - {{ error | tr }}
+ {% endfor %}
+
+ {% endif %}
+
+
+
{{ "Disown Package" | tr }}: {{ pkgbase.Name }}
+
+
+ {{
+ "Use this form to disown the package base %s%s%s which "
+ "includes the following packages: "
+ | tr | format("", pkgbase.Name, "") | safe
+ }}
+
+
+
+ {% for package in pkgbase.packages.all() %}
+ - {{ package.Name }}
+ {% endfor %}
+
+
+
+ {{
+ "By selecting the checkbox, you confirm that you want to "
+ "disown the package." | tr
+ }}
+
+
+
+
+
+{% endblock %}
diff --git a/templates/partials/packages/actions.html b/templates/partials/packages/actions.html
index f1863663..2b26144e 100644
--- a/templates/partials/packages/actions.html
+++ b/templates/partials/packages/actions.html
@@ -121,13 +121,9 @@
{% endif %}
{% if request.user.has_credential("CRED_PKGBASE_DISOWN", approved=[pkgbase.Maintainer]) %}
-
+
+ {{ "Disown Package" | tr }}
+
{% endif %}
diff --git a/test/test_packages_routes.py b/test/test_packages_routes.py
index a03c5920..c9622431 100644
--- a/test/test_packages_routes.py
+++ b/test/test_packages_routes.py
@@ -1780,3 +1780,68 @@ def test_pkgbase_vote(client: TestClient, user: User, package: Package):
vote = pkgbase.package_votes.filter(PackageVote.UsersID == user.ID).first()
assert vote is None
+
+
+def test_pkgbase_disown_as_tu(client: TestClient, tu_user: User,
+ package: Package):
+ cookies = {"AURSID": tu_user.login(Request(), "testPassword")}
+ pkgbase = package.PackageBase
+ endpoint = f"/pkgbase/{pkgbase.Name}/disown"
+
+ # But we do here.
+ with client as request:
+ resp = request.post(endpoint, data={"confirm": True}, cookies=cookies)
+ assert resp.status_code == int(HTTPStatus.SEE_OTHER)
+
+
+def test_pkgbase_disown_as_sole_maintainer(client: TestClient,
+ maintainer: User,
+ package: Package):
+ cookies = {"AURSID": maintainer.login(Request(), "testPassword")}
+ pkgbase = package.PackageBase
+ endpoint = f"/pkgbase/{pkgbase.Name}/disown"
+
+ # But we do here.
+ with client as request:
+ resp = request.post(endpoint, data={"confirm": True}, cookies=cookies)
+ assert resp.status_code == int(HTTPStatus.SEE_OTHER)
+
+
+def test_pkgbase_disown(client: TestClient, user: User, maintainer: User,
+ package: Package):
+ cookies = {"AURSID": maintainer.login(Request(), "testPassword")}
+ user_cookies = {"AURSID": user.login(Request(), "testPassword")}
+ pkgbase = package.PackageBase
+ endpoint = f"/pkgbase/{pkgbase.Name}/disown"
+
+ with db.begin():
+ db.create(PackageComaintainer,
+ User=user,
+ PackageBase=pkgbase,
+ Priority=1)
+
+ # GET as a normal user, which is rejected for lack of credentials.
+ with client as request:
+ resp = request.get(endpoint, cookies=user_cookies,
+ allow_redirects=False)
+ assert resp.status_code == int(HTTPStatus.SEE_OTHER)
+
+ # GET as the maintainer.
+ with client as request:
+ resp = request.get(endpoint, cookies=cookies)
+ assert resp.status_code == int(HTTPStatus.OK)
+
+ # POST as a normal user, which is rejected for lack of credentials.
+ with client as request:
+ resp = request.post(endpoint, cookies=user_cookies)
+ assert resp.status_code == int(HTTPStatus.SEE_OTHER)
+
+ # POST as the maintainer without "confirm".
+ with client as request:
+ resp = request.post(endpoint, cookies=cookies)
+ assert resp.status_code == int(HTTPStatus.EXPECTATION_FAILED)
+
+ # POST as the maintainer with "confirm".
+ with client as request:
+ resp = request.post(endpoint, data={"confirm": True}, cookies=cookies)
+ assert resp.status_code == int(HTTPStatus.SEE_OTHER)