mirror of
https://gitlab.archlinux.org/archlinux/aurweb.git
synced 2025-02-03 10:43:03 +01:00
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 <kevr@0cost.org>
This commit is contained in:
parent
bad57ba502
commit
26b1674c9e
10 changed files with 1044 additions and 354 deletions
240
aurweb/packages/requests.py
Normal file
240
aurweb/packages/requests.py
Normal file
|
@ -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
|
|
@ -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)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue