From 26b1674c9eaa26954df376ba3007af56235a062b Mon Sep 17 00:00:00 2001
From: Kevin Morris
Date: Wed, 8 Dec 2021 17:34:44 -0800
Subject: [PATCH] fix(requests): rework handling of requests
This commit changes several things about how we were handling
package requests.
Modifications (requests):
-------------
- `/requests/{id}/close` no longer provides an Accepted selection.
All manual request closures will cause a rejection.
- Relevent `pkgbase` actions now trigger request closures:
`/pkgbase/{name}/delete` (deletion), `/pkgbase/{name}/merge` (merge)
and `/pkgbase/{name}/disown` (orphan).
- Comment fields have been added to
`/pkgbase/{name}/{delete,merge,disown}`, which is used to set the
`PackageRequest.ClosureComment` on pending requests. If the comment
field is left blank, a closure comment is autogenerated.
- Autogenerated request notifications are only sent out once
as a closure notification.
- Some markup has been fixed.
Modifications (disown/orphan):
-----------------------------
- Orphan requests are now handled through the same path as
deletion/merge.
- We now check for due date when disowning as non-maintainer;
previously, this was only done for display and not functionally.
This check applies to Trusted Users' disowning of a package.
This style of notification flow does reduce our visibility, but
accounting can still be done via the close request; it includes
the action, pkgbase name and the user who accepted it.
Closes #204
Signed-off-by: Kevin Morris
---
aurweb/packages/requests.py | 240 ++++++++++++++
aurweb/routers/packages.py | 260 +++++++--------
po/aurweb.pot | 4 +
templates/packages/delete.html | 6 +
templates/packages/disown.html | 6 +
templates/pkgbase/merge.html | 6 +
templates/requests.html | 1 -
templates/requests/close.html | 18 --
test/test_packages_routes.py | 299 +++++++-----------
test/test_requests.py | 558 +++++++++++++++++++++++++++++++++
10 files changed, 1044 insertions(+), 354 deletions(-)
create mode 100644 aurweb/packages/requests.py
create mode 100644 test/test_requests.py
diff --git a/aurweb/packages/requests.py b/aurweb/packages/requests.py
new file mode 100644
index 00000000..d68f9688
--- /dev/null
+++ b/aurweb/packages/requests.py
@@ -0,0 +1,240 @@
+from datetime import datetime
+from typing import List, Optional, Set
+
+from fastapi import Request
+from sqlalchemy import and_, orm
+
+from aurweb import config, db, l10n, util
+from aurweb.exceptions import InvariantError
+from aurweb.models import PackageBase, PackageRequest, User
+from aurweb.models.package_request import ACCEPTED_ID, PENDING_ID, REJECTED_ID
+from aurweb.models.request_type import DELETION, DELETION_ID, MERGE, MERGE_ID, ORPHAN, ORPHAN_ID
+from aurweb.scripts import notify
+
+
+class ClosureFactory:
+ """ A factory class used to autogenerate closure comments. """
+
+ REQTYPE_NAMES = {
+ DELETION_ID: DELETION,
+ MERGE_ID: MERGE,
+ ORPHAN_ID: ORPHAN
+ }
+
+ def _deletion_closure(self, requester: User,
+ pkgbase: PackageBase,
+ target: PackageBase = None):
+ return (f"[Autogenerated] Accepted deletion for {pkgbase.Name}.")
+
+ def _merge_closure(self, requester: User,
+ pkgbase: PackageBase,
+ target: PackageBase = None):
+ return (f"[Autogenerated] Accepted merge for {pkgbase.Name} "
+ "into {target.Name}.")
+
+ def _orphan_closure(self, requester: User,
+ pkgbase: PackageBase,
+ target: PackageBase = None):
+ return (f"[Autogenerated] Accepted orphan for {pkgbase.Name}.")
+
+ def _rejected_merge_closure(self, requester: User,
+ pkgbase: PackageBase,
+ target: PackageBase = None):
+ return (f"[Autogenerated] Another request to merge {pkgbase.Name} "
+ f"into {target.Name} has rendered this request invalid.")
+
+ def get_closure(self, reqtype_id: int,
+ requester: User,
+ pkgbase: PackageBase,
+ target: PackageBase = None,
+ status: int = ACCEPTED_ID) -> str:
+ """
+ Return a closure comment handled by this class.
+
+ :param reqtype_id: RequestType.ID
+ :param requester: User who is closing a request
+ :param pkgbase: PackageBase instance related to the request
+ :param target: Merge request target PackageBase instance
+ :param status: PackageRequest.Status
+ """
+ reqtype = ClosureFactory.REQTYPE_NAMES.get(reqtype_id)
+
+ partial = str()
+ if status == REJECTED_ID:
+ partial = "_rejected"
+
+ try:
+ handler = getattr(self, f"{partial}_{reqtype}_closure")
+ except AttributeError:
+ raise NotImplementedError("Unsupported 'reqtype_id' value.")
+ return handler(requester, pkgbase, target)
+
+
+def update_closure_comment(pkgbase: PackageBase, reqtype_id: int,
+ comments: str, target: PackageBase = None) -> None:
+ """
+ Update all pending requests related to `pkgbase` with a closure comment.
+
+ In order to persist closure comments through `handle_request`'s
+ algorithm, we must set `PackageRequest.ClosureComment` before calling
+ it. This function can be used to update the closure comment of all
+ package requests related to `pkgbase` and `reqtype_id`.
+
+ If an empty `comments` string is provided, we no-op out of this.
+
+ :param pkgbase: PackageBase instance
+ :param reqtype_id: RequestType.ID
+ :param comments: PackageRequest.ClosureComment to update to
+ :param target: Merge request target PackageBase instance
+ """
+ if not comments:
+ return
+
+ query = pkgbase.requests.filter(
+ and_(PackageRequest.ReqTypeID == reqtype_id,
+ PackageRequest.Status == PENDING_ID))
+ if reqtype_id == MERGE_ID:
+ query = query.filter(PackageRequest.MergeBaseName == target.Name)
+
+ for pkgreq in query:
+ pkgreq.ClosureComment = comments
+
+
+def verify_orphan_request(user: User, pkgbase: PackageBase):
+ """ Verify that an undue orphan request exists in `requests`. """
+ is_maint = user == pkgbase.Maintainer
+ if is_maint:
+ return True
+
+ requests = pkgbase.requests.filter(
+ PackageRequest.ReqTypeID == ORPHAN_ID)
+ for pkgreq in requests:
+ idle_time = config.getint("options", "request_idle_time")
+ time_delta = int(datetime.utcnow().timestamp()) - pkgreq.RequestTS
+ is_due = pkgreq.Status == PENDING_ID and time_delta > idle_time
+ if is_due:
+ # If the requester is the pkgbase maintainer or the
+ # request is already due, we're good to go: return True.
+ return True
+
+ return False
+
+
+def close_pkgreq(pkgreq: PackageRequest, closer: User,
+ pkgbase: PackageBase, target: Optional[PackageBase],
+ status: int) -> None:
+ """
+ Close a package request with `pkgreq`.Status == `status`.
+
+ :param pkgreq: PackageRequest instance
+ :param closer: `pkgreq`.Closer User instance to update to
+ :param pkgbase: PackageBase instance which `pkgreq` is about
+ :param target: Optional PackageBase instance to merge into
+ :param status: `pkgreq`.Status value to update to
+ """
+ now = int(datetime.utcnow().timestamp())
+ pkgreq.Status = status
+ pkgreq.Closer = closer
+ pkgreq.ClosureComment = (
+ pkgreq.ClosureComment or ClosureFactory().get_closure(
+ pkgreq.ReqTypeID, closer, pkgbase, target, status)
+ )
+ pkgreq.ClosedTS = now
+
+
+def handle_request(request: Request, reqtype_id: int,
+ pkgbase: PackageBase,
+ target: PackageBase = None) -> List[notify.Notification]:
+ """
+ Handle package requests before performing an action.
+
+ The actions we're interested in are disown (orphan), delete and
+ merge. There is now an automated request generation and closure
+ notification when a privileged user performs one of these actions
+ without a pre-existing request. They all commit changes to the
+ database, and thus before calling, state should be verified to
+ avoid leaked database records regarding these requests.
+
+ Otherwise, we accept and reject requests based on their state
+ and send out the relevent notifications.
+
+ :param requester: User who needs this a `pkgbase` request handled
+ :param reqtype_id: RequestType.ID
+ :param pkgbase: PackageBase which the request is about
+ :param target: Optional target to merge into
+ """
+ notifs: List[notify.Notification] = []
+
+ # If it's an orphan request, perform further verification
+ # regarding existing requests.
+ if reqtype_id == ORPHAN_ID:
+ if not verify_orphan_request(request.user, pkgbase):
+ _ = l10n.get_translator_for_request(request)
+ raise InvariantError(_(
+ "No due existing orphan requests to accept for %s."
+ ) % pkgbase.Name)
+
+ # Produce a base query for requests related to `pkgbase`, based
+ # on ReqTypeID matching `reqtype_id`, pending status and a correct
+ # PackagBaseName column.
+ query: orm.Query = pkgbase.requests.filter(
+ and_(PackageRequest.ReqTypeID == reqtype_id,
+ PackageRequest.Status == PENDING_ID,
+ PackageRequest.PackageBaseName == pkgbase.Name))
+
+ # Build a query for records we should accept. For merge requests,
+ # this is specific to a matching MergeBaseName. For others, this
+ # just ends up becoming `query`.
+ accept_query: orm.Query = query
+ if target:
+ # If a `target` was supplied, filter by MergeBaseName
+ accept_query = query.filter(
+ PackageRequest.MergeBaseName == target.Name)
+
+ # Build an accept list out of `accept_query`.
+ to_accept: List[PackageRequest] = accept_query.all()
+ accepted_ids: Set[int] = set(p.ID for p in to_accept)
+
+ # Build a reject list out of `query` filtered by IDs not found
+ # in `to_accept`. That is, unmatched records of the same base
+ # query properties.
+ to_reject: List[PackageRequest] = query.filter(
+ ~PackageRequest.ID.in_(accepted_ids)
+ ).all()
+
+ # If we have no requests to accept, create a new one.
+ # This is done to increase tracking of actions occurring
+ # through the website.
+ if not to_accept:
+ with db.begin():
+ pkgreq = db.create(PackageRequest,
+ ReqTypeID=reqtype_id,
+ User=request.user,
+ PackageBase=pkgbase,
+ PackageBaseName=pkgbase.Name,
+ Comments="Autogenerated by aurweb.",
+ ClosureComment=str())
+
+ # If it's a merge request, set MergeBaseName to `target`.Name.
+ if pkgreq.ReqTypeID == MERGE_ID:
+ pkgreq.MergeBaseName = target.Name
+
+ # Add the new request to `to_accept` and allow standard
+ # flow to continue afterward.
+ to_accept.append(pkgreq)
+
+ # Update requests with their new status and closures.
+ with db.begin():
+ util.apply_all(to_accept, lambda p: close_pkgreq(
+ p, request.user, pkgbase, target, ACCEPTED_ID))
+ util.apply_all(to_reject, lambda p: close_pkgreq(
+ p, request.user, pkgbase, target, REJECTED_ID))
+
+ # Create RequestCloseNotifications for all requests involved.
+ for pkgreq in (to_accept + to_reject):
+ notif = notify.RequestCloseNotification(
+ request.user.ID, pkgreq.ID, pkgreq.status_display())
+ notifs.append(notif)
+
+ # Return notifications to the caller for sending.
+ return notifs
diff --git a/aurweb/routers/packages.py b/aurweb/routers/packages.py
index fb9498d4..eb4cab74 100644
--- a/aurweb/routers/packages.py
+++ b/aurweb/routers/packages.py
@@ -4,19 +4,20 @@ from typing import Any, Dict, List
from fastapi import APIRouter, Form, HTTPException, Query, Request, Response
from fastapi.responses import JSONResponse, RedirectResponse
-from sqlalchemy import case
+from sqlalchemy import and_, case
import aurweb.filters
import aurweb.packages.util
from aurweb import db, defaults, l10n, logging, models, util
from aurweb.auth import auth_required, creds
-from aurweb.exceptions import ValidationError
+from aurweb.exceptions import InvariantError, ValidationError
from aurweb.models.package_request import ACCEPTED_ID, PENDING_ID, REJECTED_ID
from aurweb.models.relation_type import CONFLICTS_ID, PROVIDES_ID, REPLACES_ID
-from aurweb.models.request_type import DELETION_ID, MERGE, MERGE_ID
+from aurweb.models.request_type import DELETION_ID, MERGE_ID, ORPHAN_ID
from aurweb.packages import util as pkgutil
from aurweb.packages import validate
+from aurweb.packages.requests import handle_request, update_closure_comment
from aurweb.packages.search import PackageSearch
from aurweb.packages.util import get_pkg_or_base, get_pkgbase_comment, get_pkgreq_by_id, query_notified, query_voted
from aurweb.scripts import notify, popupdate
@@ -115,66 +116,30 @@ async def packages(request: Request) -> Response:
return await packages_get(request, context)
-def create_request_if_missing(requests: List[models.PackageRequest],
- reqtype: models.RequestType,
- user: models.User,
- package: models.Package):
- now = int(datetime.utcnow().timestamp())
- pkgreq = db.query(models.PackageRequest).filter(
- models.PackageRequest.PackageBaseName == package.PackageBase.Name
- ).first()
- if not pkgreq:
- # No PackageRequest existed. Create one.
- comments = "Automatically generated by aurweb."
- closure_comment = "Deleted by aurweb."
- pkgreq = db.create(models.PackageRequest,
- RequestType=reqtype,
- PackageBase=package.PackageBase,
- PackageBaseName=package.PackageBase.Name,
- User=user,
- Status=ACCEPTED_ID,
- Comments=comments,
- ClosureComment=closure_comment,
- ClosedTS=now,
- Closer=user)
- requests.append(pkgreq)
- return pkgreq
-
-
-def delete_package(deleter: models.User, package: models.Package):
- notifications = []
- requests = []
+def delete_package(request: Request, package: models.Package,
+ merge_into: models.PackageBase = None,
+ comments: str = str()):
bases_to_delete = []
+ target = db.query(models.PackageBase).filter(
+ models.PackageBase.Name == merge_into
+ ).first()
+
+ notifs = []
# In all cases, though, just delete the Package in question.
if package.PackageBase.packages.count() == 1:
- reqtype = db.query(models.RequestType).filter(
- models.RequestType.ID == DELETION_ID
- ).first()
-
- with db.begin():
- pkgreq = create_request_if_missing(
- requests, reqtype, deleter, package)
- pkgreq.Status = ACCEPTED_ID
+ notifs = handle_request(request, DELETION_ID, package.PackageBase,
+ target=target)
bases_to_delete.append(package.PackageBase)
- # Prepare DeleteNotification.
- notifications.append(
- notify.DeleteNotification(deleter.ID, package.PackageBase.ID)
- )
+ with db.begin():
+ update_closure_comment(package.PackageBase, DELETION_ID, comments,
+ target=target)
- # For each PackageRequest created, mock up an open and close notification.
- basename = package.PackageBase.Name
- for pkgreq in requests:
- notifications.append(
- notify.RequestOpenNotification(
- deleter.ID, pkgreq.ID, reqtype.Name,
- pkgreq.PackageBase.ID, merge_into=basename or None)
- )
- notifications.append(
- notify.RequestCloseNotification(
- deleter.ID, pkgreq.ID, pkgreq.status_display())
+ # Prepare DeleteNotification.
+ notifs.append(
+ notify.DeleteNotification(request.user.ID, package.PackageBase.ID)
)
# Perform all the deletions.
@@ -183,7 +148,7 @@ def delete_package(deleter: models.User, package: models.Package):
db.delete_all(bases_to_delete)
# Send out all the notifications.
- util.apply_all(notifications, lambda n: n.send())
+ util.apply_all(notifs, lambda n: n.send())
async def make_single_context(request: Request,
@@ -676,22 +641,26 @@ async def pkgbase_request_post(request: Request, name: str,
auto_orphan_age = aurweb.config.getint("options", "auto_orphan_age")
auto_delete_age = aurweb.config.getint("options", "auto_delete_age")
- flagged = pkgbase.OutOfDateTS and pkgbase.OutOfDateTS >= auto_orphan_age
+ ood_ts = pkgbase.OutOfDateTS or 0
+ flagged = ood_ts and (now - ood_ts) >= auto_orphan_age
is_maintainer = pkgbase.Maintainer == request.user
- outdated = now - pkgbase.SubmittedTS <= auto_delete_age
+ outdated = (now - pkgbase.SubmittedTS) <= auto_delete_age
if type == "orphan" and flagged:
+ # This request should be auto-accepted.
with db.begin():
pkgbase.Maintainer = None
pkgreq.Status = ACCEPTED_ID
- db.refresh(pkgreq)
notif = notify.RequestCloseNotification(
request.user.ID, pkgreq.ID, pkgreq.status_display())
notif.send()
+ logger.debug(f"New request #{pkgreq.ID} is marked for auto-orphan.")
elif type == "deletion" and is_maintainer and outdated:
+ # This request should be auto-accepted.
packages = pkgbase.packages.all()
for package in packages:
- delete_package(request.user, package)
+ delete_package(request, package)
+ logger.debug(f"New request #{pkgreq.ID} is marked for auto-deletion.")
# Redirect the submitting user to /packages.
return RedirectResponse("/packages", status_code=HTTPStatus.SEE_OTHER)
@@ -713,31 +682,24 @@ async def requests_close(request: Request, id: int):
@router.post("/requests/{id}/close")
@auth_required()
async def requests_close_post(request: Request, id: int,
- reason: int = Form(default=0),
comments: str = Form(default=str())):
pkgreq = get_pkgreq_by_id(id)
- if not request.user.is_elevated() and request.user != pkgreq.User:
+
+ # `pkgreq`.User can close their own request.
+ approved = [pkgreq.User]
+ if not request.user.has_credential(creds.PKGREQ_CLOSE, approved=approved):
# Request user doesn't have permission here: redirect to '/'.
return RedirectResponse("/", status_code=HTTPStatus.SEE_OTHER)
context = make_context(request, "Close Request")
context["pkgreq"] = pkgreq
- if reason not in {ACCEPTED_ID, REJECTED_ID}:
- # If the provided reason is not valid, send the user back to
- # the closure form with a BAD_REQUEST status.
- return render_template(request, "requests/close.html", context,
- status_code=HTTPStatus.BAD_REQUEST)
-
- if not request.user.is_elevated():
- # If we're closing the request as the user who created it,
- # the reason should just be a REJECTION.
- reason = REJECTED_ID
-
+ now = int(datetime.utcnow().timestamp())
with db.begin():
pkgreq.Closer = request.user
- pkgreq.Status = reason
pkgreq.ClosureComment = comments
+ pkgreq.ClosedTS = now
+ pkgreq.Status = REJECTED_ID
notify_ = notify.RequestCloseNotification(
request.user.ID, pkgreq.ID, pkgreq.status_display())
@@ -932,7 +894,8 @@ async def pkgbase_unvote(request: Request, name: str):
def pkgbase_disown_instance(request: Request, pkgbase: models.PackageBase):
disowner = request.user
- notif = notify.DisownNotification(disowner.ID, pkgbase.ID)
+ notifs = [notify.DisownNotification(disowner.ID, pkgbase.ID)]
+ notifs += handle_request(request, ORPHAN_ID, pkgbase)
if disowner != pkgbase.Maintainer:
with db.begin():
@@ -949,7 +912,7 @@ def pkgbase_disown_instance(request: Request, pkgbase: models.PackageBase):
else:
pkgbase.Maintainer = None
- notif.send()
+ util.apply_all(notifs, lambda n: n.send())
@router.get("/pkgbase/{name}/disown")
@@ -971,6 +934,7 @@ async def pkgbase_disown_get(request: Request, name: str):
@router.post("/pkgbase/{name}/disown")
@auth_required()
async def pkgbase_disown_post(request: Request, name: str,
+ comments: str = Form(default=str()),
confirm: bool = Form(default=False)):
pkgbase = get_pkg_or_base(name, models.PackageBase)
@@ -980,15 +944,24 @@ async def pkgbase_disown_post(request: Request, name: str,
return RedirectResponse(f"/pkgbase/{name}",
HTTPStatus.SEE_OTHER)
+ context = make_context(request, "Disown Package")
+ context["pkgbase"] = pkgbase
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=HTTPStatus.BAD_REQUEST)
- pkgbase_disown_instance(request, pkgbase)
+ with db.begin():
+ update_closure_comment(pkgbase, ORPHAN_ID, comments)
+
+ try:
+ pkgbase_disown_instance(request, pkgbase)
+ except InvariantError as exc:
+ context["errors"] = [str(exc)]
+ return render_template(request, "packages/disown.html", context,
+ status_code=HTTPStatus.BAD_REQUEST)
+
return RedirectResponse(f"/pkgbase/{name}",
status_code=HTTPStatus.SEE_OTHER)
@@ -1032,7 +1005,8 @@ async def pkgbase_delete_get(request: Request, name: str):
@router.post("/pkgbase/{name}/delete")
@auth_required()
async def pkgbase_delete_post(request: Request, name: str,
- confirm: bool = Form(default=False)):
+ confirm: bool = Form(default=False),
+ comments: str = Form(default=str())):
pkgbase = get_pkg_or_base(name, models.PackageBase)
if not request.user.has_credential(creds.PKGBASE_DELETE):
@@ -1047,10 +1021,20 @@ async def pkgbase_delete_post(request: Request, name: str,
return render_template(request, "packages/delete.html", context,
status_code=HTTPStatus.BAD_REQUEST)
+ if comments:
+ # Update any existing deletion requests' ClosureComment.
+ with db.begin():
+ requests = pkgbase.requests.filter(
+ and_(models.PackageRequest.Status == PENDING_ID,
+ models.PackageRequest.ReqTypeID == DELETION_ID)
+ )
+ for pkgreq in requests:
+ pkgreq.ClosureComment = comments
+
# Obtain deletion locks and delete the packages.
packages = pkgbase.packages.all()
for package in packages:
- delete_package(request.user, package)
+ delete_package(request, package, comments=comments)
return RedirectResponse("/packages", status_code=HTTPStatus.SEE_OTHER)
@@ -1190,6 +1174,17 @@ async def packages_adopt(request: Request, package_ids: List[int] = [],
return (True, ["The selected packages have been adopted."])
+def disown_all(request: Request, pkgbases: List[models.PackageBase]) \
+ -> List[str]:
+ errors = []
+ for pkgbase in pkgbases:
+ try:
+ pkgbase_disown_instance(request, pkgbase)
+ except InvariantError as exc:
+ errors.append(str(exc))
+ return errors
+
+
async def packages_disown(request: Request, package_ids: List[int] = [],
confirm: bool = False, **kwargs):
if not package_ids:
@@ -1217,9 +1212,9 @@ async def packages_disown(request: Request, package_ids: List[int] = [],
return (False, ["You are not allowed to disown one "
"of the packages you selected."])
- # Now, really disown the bases.
- for pkgbase in bases:
- pkgbase_disown_instance(request, pkgbase)
+ # Now, disown all the bases if we can.
+ if errors := disown_all(request, bases):
+ return (False, errors)
return (True, ["The selected packages have been disowned."])
@@ -1256,7 +1251,7 @@ async def packages_delete(request: Request, package_ids: List[int] = [],
# using the same method we use in our /pkgbase/{name}/delete route.
for pkg in packages:
deleted_pkgs.append(pkg.Name)
- delete_package(request.user, pkg)
+ delete_package(request, pkg)
# Log out the fact that this happened for accountability.
logger.info(f"Privileged user '{request.user.Username}' deleted the "
@@ -1341,83 +1336,46 @@ async def pkgbase_merge_get(request: Request, name: str,
def pkgbase_merge_instance(request: Request, pkgbase: models.PackageBase,
- target: models.PackageBase):
+ target: models.PackageBase, comments: str = str()):
pkgbasename = str(pkgbase.Name)
- # Collect requests related to this merge.
- query = pkgbase.requests.filter(
- models.PackageRequest.ReqTypeID == MERGE_ID
- )
+ # Create notifications.
+ notifs = handle_request(request, MERGE_ID, pkgbase, target)
- requests = query.filter(
- models.PackageRequest.MergeBaseName == target.Name).all()
- reject_requests = query.filter(
- models.PackageRequest.MergeBaseName != target.Name).all()
+ # Target votes and notifications sets of user IDs that are
+ # looking to be migrated.
+ target_votes = set(v.UsersID for v in target.package_votes)
+ target_notifs = set(n.UserID for n in target.notifications)
- notifs = [] # Used to keep track of notifications over the function.
- closure_comment = (f"Merged into package base {target.Name} by "
- f"{request.user.Username}.")
- rejected_closure_comment = ("Rejected because another merge request "
- "for the same package base was accepted.")
+ with db.begin():
+ # Merge pkgbase's comments.
+ for comment in pkgbase.comments:
+ comment.PackageBase = target
- if not requests:
- # If there are no requests, create one owned by request.user.
- with db.begin():
- pkgreq = db.create(models.PackageRequest,
- ReqTypeID=MERGE_ID,
- User=request.user,
- PackageBase=pkgbase,
- PackageBaseName=pkgbasename,
- MergeBaseName=target.Name,
- Comments="Generated by aurweb.",
- Status=ACCEPTED_ID,
- ClosureComment=closure_comment)
- requests.append(pkgreq)
-
- # Add a notification about the opening to our notifs array.
- notif = notify.RequestOpenNotification(
- request.user.ID, pkgreq.ID, MERGE,
- pkgbase.ID, merge_into=target.Name)
- notifs.append(notif)
-
- with db.begin():
- # Merge pkgbase's comments, notifications and votes into target.
- for comment in pkgbase.comments:
- comment.PackageBase = target
- for notif in pkgbase.notifications:
+ # Merge notifications that don't yet exist in the target.
+ for notif in pkgbase.notifications:
+ if notif.UserID not in target_notifs:
notif.PackageBase = target
- for vote in pkgbase.package_votes:
+
+ # Merge votes that don't yet exist in the target.
+ for vote in pkgbase.package_votes:
+ if vote.UsersID not in target_votes:
vote.PackageBase = target
- with db.begin():
- # Delete pkgbase and its packages now that everything's merged.
- for pkg in pkgbase.packages:
- db.delete(pkg)
- db.delete(pkgbase)
+ # Run popupdate.
+ popupdate.run_single(target)
- # Accept merge requests related to this pkgbase and target.
- for pkgreq in requests:
- pkgreq.Status = ACCEPTED_ID
- pkgreq.ClosureComment = closure_comment
- pkgreq.Closer = request.user
-
- for pkgreq in reject_requests:
- pkgreq.Status = REJECTED_ID
- pkgreq.ClosureComment = rejected_closure_comment
- pkgreq.Closer = request.user
-
- all_requests = requests + reject_requests
- for pkgreq in all_requests:
- # Create notifications for request closure.
- notif = notify.RequestCloseNotification(
- request.user.ID, pkgreq.ID, pkgreq.status_display())
- notifs.append(notif)
+ with db.begin():
+ # Delete pkgbase and its packages now that everything's merged.
+ for pkg in pkgbase.packages:
+ db.delete(pkg)
+ db.delete(pkgbase)
# Log this out for accountability purposes.
logger.info(f"Trusted User '{request.user.Username}' merged "
f"'{pkgbasename}' into '{target.Name}'.")
- # Send our notifications array.
+ # Send notifications.
util.apply_all(notifs, lambda n: n.send())
@@ -1425,6 +1383,7 @@ def pkgbase_merge_instance(request: Request, pkgbase: models.PackageBase,
@auth_required()
async def pkgbase_merge_post(request: Request, name: str,
into: str = Form(default=str()),
+ comments: str = Form(default=str()),
confirm: bool = Form(default=False),
next: str = Form(default=str())):
@@ -1458,8 +1417,11 @@ async def pkgbase_merge_post(request: Request, name: str,
return render_template(request, "pkgbase/merge.html", context,
status_code=HTTPStatus.BAD_REQUEST)
+ with db.begin():
+ update_closure_comment(pkgbase, MERGE_ID, comments, target=target)
+
# Merge pkgbase into target.
- pkgbase_merge_instance(request, pkgbase, target)
+ pkgbase_merge_instance(request, pkgbase, target, comments=comments)
# Run popupdate on the target.
popupdate.run_single(target)
diff --git a/po/aurweb.pot b/po/aurweb.pot
index 0b3996a8..f6d1377b 100644
--- a/po/aurweb.pot
+++ b/po/aurweb.pot
@@ -2300,3 +2300,7 @@ msgstr ""
#: aurweb/routers/accounts.py
msgid "You do not have permission to change this user's account type to %s."
msgstr ""
+
+#: aurweb/packages/requests.py
+msgid "No due existing orphan requests to accept for %s."
+msgstr ""
diff --git a/templates/packages/delete.html b/templates/packages/delete.html
index 6e882d05..8910cce9 100644
--- a/templates/packages/delete.html
+++ b/templates/packages/delete.html
@@ -43,6 +43,12 @@
+
+
+
+
+