mirror of
https://gitlab.archlinux.org/archlinux/aurweb.git
synced 2025-02-03 10:43:03 +01:00
feat(fastapi): add /pkgbase/{name}/merge (post)
Changes: - `via` is not required in FastAPI. We deduce the involved requests via their PackageBaseName / MergeBaseName columns and set them to Accepted when merged. - When erroneous input is given, the error is now presented on the merge page instead of sending the user to the pkgbase page. Signed-off-by: Kevin Morris <kevr@0cost.org>
This commit is contained in:
parent
bc9bb045ed
commit
5fb75b9614
3 changed files with 278 additions and 2 deletions
|
@ -13,7 +13,7 @@ from aurweb import db, defaults, l10n, logging, models, util
|
||||||
from aurweb.auth import auth_required
|
from aurweb.auth import auth_required
|
||||||
from aurweb.models.package_request import ACCEPTED_ID, PENDING_ID, REJECTED_ID
|
from aurweb.models.package_request import ACCEPTED_ID, PENDING_ID, REJECTED_ID
|
||||||
from aurweb.models.relation_type import CONFLICTS_ID
|
from aurweb.models.relation_type import CONFLICTS_ID
|
||||||
from aurweb.models.request_type import DELETION_ID
|
from aurweb.models.request_type import DELETION_ID, MERGE, MERGE_ID
|
||||||
from aurweb.packages.search import PackageSearch
|
from aurweb.packages.search import PackageSearch
|
||||||
from aurweb.packages.util import get_pkg_or_base, get_pkgbase_comment, query_notified, query_voted
|
from aurweb.packages.util import get_pkg_or_base, get_pkgbase_comment, query_notified, query_voted
|
||||||
from aurweb.scripts import notify, popupdate
|
from aurweb.scripts import notify, popupdate
|
||||||
|
@ -1053,6 +1053,7 @@ async def pkgbase_delete_post(request: Request, name: str,
|
||||||
return render_template(request, "packages/delete.html", context,
|
return render_template(request, "packages/delete.html", context,
|
||||||
status_code=HTTPStatus.BAD_REQUEST)
|
status_code=HTTPStatus.BAD_REQUEST)
|
||||||
|
|
||||||
|
# Obtain deletion locks and delete the packages.
|
||||||
packages = pkgbase.packages.all()
|
packages = pkgbase.packages.all()
|
||||||
for package in packages:
|
for package in packages:
|
||||||
delete_package(request.user, package)
|
delete_package(request.user, package)
|
||||||
|
@ -1343,3 +1344,137 @@ async def pkgbase_merge_get(request: Request, name: str,
|
||||||
|
|
||||||
return render_template(request, "pkgbase/merge.html", context,
|
return render_template(request, "pkgbase/merge.html", context,
|
||||||
status_code=status_code)
|
status_code=status_code)
|
||||||
|
|
||||||
|
|
||||||
|
def pkgbase_merge_instance(request: Request, pkgbase: models.PackageBase,
|
||||||
|
target: models.PackageBase):
|
||||||
|
pkgbasename = str(pkgbase.Name)
|
||||||
|
|
||||||
|
# Collect requests related to this merge.
|
||||||
|
query = pkgbase.requests.filter(
|
||||||
|
models.PackageRequest.ReqTypeID == MERGE_ID
|
||||||
|
)
|
||||||
|
|
||||||
|
requests = query.filter(
|
||||||
|
models.PackageRequest.MergeBaseName == target.Name).all()
|
||||||
|
reject_requests = query.filter(
|
||||||
|
models.PackageRequest.MergeBaseName != target.Name).all()
|
||||||
|
|
||||||
|
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.")
|
||||||
|
conn = db.ConnectionExecutor(db.get_engine().raw_connection())
|
||||||
|
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(
|
||||||
|
conn, 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:
|
||||||
|
notif.PackageBase = target
|
||||||
|
for vote in pkgbase.package_votes:
|
||||||
|
vote.PackageBase = target
|
||||||
|
|
||||||
|
with db.begin():
|
||||||
|
# Delete pkgbase and its packages now that everything's merged.
|
||||||
|
for pkg in pkgbase.packages:
|
||||||
|
db.session.delete(pkg)
|
||||||
|
db.session.delete(pkgbase)
|
||||||
|
|
||||||
|
# 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(
|
||||||
|
conn, request.user.ID, pkgreq.ID, pkgreq.status_display())
|
||||||
|
notifs.append(notif)
|
||||||
|
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
# Log this out for accountability purposes.
|
||||||
|
logger.info(f"Trusted User '{request.user.Username}' merged "
|
||||||
|
f"'{pkgbasename}' into '{target.Name}'.")
|
||||||
|
|
||||||
|
# Send our notifications array.
|
||||||
|
util.apply_all(notifs, lambda n: n.send())
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/pkgbase/{name}/merge")
|
||||||
|
@auth_required(redirect="/pkgbase/{name}/merge")
|
||||||
|
async def pkgbase_merge_post(request: Request, name: str,
|
||||||
|
into: str = Form(default=str()),
|
||||||
|
confirm: bool = Form(default=False),
|
||||||
|
next: str = Form(default=str())):
|
||||||
|
|
||||||
|
pkgbase = get_pkg_or_base(name, models.PackageBase)
|
||||||
|
context = await make_variable_context(request, "Package Merging")
|
||||||
|
context["pkgbase"] = pkgbase
|
||||||
|
|
||||||
|
# TODO: Lookup errors from credential instead of hardcoding them.
|
||||||
|
if not request.user.has_credential("CRED_PKGBASE_MERGE"):
|
||||||
|
context["errors"] = [
|
||||||
|
"Only Trusted Users and Developers can merge packages."]
|
||||||
|
return render_template(request, "pkgbase/merge.html", context,
|
||||||
|
status_code=HTTPStatus.UNAUTHORIZED)
|
||||||
|
|
||||||
|
if not confirm:
|
||||||
|
context["errors"] = ["The selected packages have not been deleted, "
|
||||||
|
"check the confirmation checkbox."]
|
||||||
|
return render_template(request, "pkgbase/merge.html", context,
|
||||||
|
status_code=HTTPStatus.BAD_REQUEST)
|
||||||
|
|
||||||
|
try:
|
||||||
|
target = get_pkg_or_base(into, models.PackageBase)
|
||||||
|
except HTTPException:
|
||||||
|
context["errors"] = [
|
||||||
|
"Cannot find package to merge votes and comments into."]
|
||||||
|
return render_template(request, "pkgbase/merge.html", context,
|
||||||
|
status_code=HTTPStatus.BAD_REQUEST)
|
||||||
|
|
||||||
|
if pkgbase == target:
|
||||||
|
context["errors"] = ["Cannot merge a package base with itself."]
|
||||||
|
return render_template(request, "pkgbase/merge.html", context,
|
||||||
|
status_code=HTTPStatus.BAD_REQUEST)
|
||||||
|
|
||||||
|
# Merge pkgbase into target.
|
||||||
|
pkgbase_merge_instance(request, pkgbase, target)
|
||||||
|
|
||||||
|
# Run popupdate on the target.
|
||||||
|
conn = db.ConnectionExecutor(db.get_engine().raw_connection())
|
||||||
|
popupdate.run_single(conn, target)
|
||||||
|
|
||||||
|
if not next:
|
||||||
|
next = f"/pkgbase/{target.Name}"
|
||||||
|
|
||||||
|
# Redirect to the newly merged into package.
|
||||||
|
return RedirectResponse(next, status_code=HTTPStatus.SEE_OTHER)
|
||||||
|
|
|
@ -1,6 +1,15 @@
|
||||||
{% extends "partials/layout.html" %}
|
{% extends "partials/layout.html" %}
|
||||||
|
|
||||||
{% block pageContent %}
|
{% block pageContent %}
|
||||||
|
|
||||||
|
{% if errors %}
|
||||||
|
<ul class="errorlist">
|
||||||
|
{% for error in errors %}
|
||||||
|
<li>{{ error | tr }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<div class="box">
|
<div class="box">
|
||||||
<h2>{{ "Merge Package" | tr }}: {{ pkgbase.Name }}</h2>
|
<h2>{{ "Merge Package" | tr }}: {{ pkgbase.Name }}</h2>
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@ from unittest import mock
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from fastapi.testclient import TestClient
|
from fastapi.testclient import TestClient
|
||||||
|
from sqlalchemy import and_
|
||||||
|
|
||||||
from aurweb import asgi, db, defaults
|
from aurweb import asgi, db, defaults
|
||||||
from aurweb.models.account_type import USER_ID, AccountType
|
from aurweb.models.account_type import USER_ID, AccountType
|
||||||
|
@ -24,7 +25,7 @@ from aurweb.models.package_relation import PackageRelation
|
||||||
from aurweb.models.package_request import ACCEPTED_ID, REJECTED_ID, PackageRequest
|
from aurweb.models.package_request import ACCEPTED_ID, REJECTED_ID, PackageRequest
|
||||||
from aurweb.models.package_vote import PackageVote
|
from aurweb.models.package_vote import PackageVote
|
||||||
from aurweb.models.relation_type import PROVIDES_ID, RelationType
|
from aurweb.models.relation_type import PROVIDES_ID, RelationType
|
||||||
from aurweb.models.request_type import DELETION_ID, RequestType
|
from aurweb.models.request_type import DELETION_ID, MERGE_ID, RequestType
|
||||||
from aurweb.models.user import User
|
from aurweb.models.user import User
|
||||||
from aurweb.testing import setup_test_db
|
from aurweb.testing import setup_test_db
|
||||||
from aurweb.testing.html import get_errors, get_successes, parse_root
|
from aurweb.testing.html import get_errors, get_successes, parse_root
|
||||||
|
@ -2391,3 +2392,134 @@ def test_packages_post_delete(caplog: pytest.fixture, client: TestClient,
|
||||||
expected = (f"Privileged user '{tu_user.Username}' deleted the "
|
expected = (f"Privileged user '{tu_user.Username}' deleted the "
|
||||||
f"following packages: {str(packages)}.")
|
f"following packages: {str(packages)}.")
|
||||||
assert expected in caplog.text
|
assert expected in caplog.text
|
||||||
|
|
||||||
|
|
||||||
|
def test_pkgbase_merge_post_unauthorized(client: TestClient, user: User,
|
||||||
|
package: Package):
|
||||||
|
cookies = {"AURSID": user.login(Request(), "testPassword")}
|
||||||
|
endpoint = f"/pkgbase/{package.PackageBase.Name}/merge"
|
||||||
|
with client as request:
|
||||||
|
resp = request.post(endpoint, cookies=cookies)
|
||||||
|
assert resp.status_code == int(HTTPStatus.UNAUTHORIZED)
|
||||||
|
|
||||||
|
|
||||||
|
def test_pkgbase_merge_post_unconfirmed(client: TestClient, tu_user: User,
|
||||||
|
package: Package):
|
||||||
|
cookies = {"AURSID": tu_user.login(Request(), "testPassword")}
|
||||||
|
endpoint = f"/pkgbase/{package.PackageBase.Name}/merge"
|
||||||
|
with client as request:
|
||||||
|
resp = request.post(endpoint, cookies=cookies)
|
||||||
|
assert resp.status_code == int(HTTPStatus.BAD_REQUEST)
|
||||||
|
errors = get_errors(resp.text)
|
||||||
|
expected = ("The selected packages have not been deleted, "
|
||||||
|
"check the confirmation checkbox.")
|
||||||
|
assert errors[0].text.strip() == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_pkgbase_merge_post_invalid_into(client: TestClient, tu_user: User,
|
||||||
|
package: Package):
|
||||||
|
cookies = {"AURSID": tu_user.login(Request(), "testPassword")}
|
||||||
|
endpoint = f"/pkgbase/{package.PackageBase.Name}/merge"
|
||||||
|
with client as request:
|
||||||
|
resp = request.post(endpoint, data={
|
||||||
|
"into": "not_real",
|
||||||
|
"confirm": True
|
||||||
|
}, cookies=cookies)
|
||||||
|
assert resp.status_code == int(HTTPStatus.BAD_REQUEST)
|
||||||
|
errors = get_errors(resp.text)
|
||||||
|
expected = "Cannot find package to merge votes and comments into."
|
||||||
|
assert errors[0].text.strip() == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_pkgbase_merge_post_self_invalid(client: TestClient, tu_user: User,
|
||||||
|
package: Package):
|
||||||
|
cookies = {"AURSID": tu_user.login(Request(), "testPassword")}
|
||||||
|
endpoint = f"/pkgbase/{package.PackageBase.Name}/merge"
|
||||||
|
with client as request:
|
||||||
|
resp = request.post(endpoint, data={
|
||||||
|
"into": package.PackageBase.Name,
|
||||||
|
"confirm": True
|
||||||
|
}, cookies=cookies)
|
||||||
|
assert resp.status_code == int(HTTPStatus.BAD_REQUEST)
|
||||||
|
errors = get_errors(resp.text)
|
||||||
|
expected = "Cannot merge a package base with itself."
|
||||||
|
assert errors[0].text.strip() == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_pkgbase_merge_post(client: TestClient, tu_user: User,
|
||||||
|
packages: List[Package]):
|
||||||
|
package, target = packages[:2]
|
||||||
|
pkgname = package.Name
|
||||||
|
pkgbasename = package.PackageBase.Name
|
||||||
|
|
||||||
|
# Create a merge request destined for another target.
|
||||||
|
# This will allow our test code to exercise closing
|
||||||
|
# such a request after merging the pkgbase in question.
|
||||||
|
with db.begin():
|
||||||
|
pkgreq = db.create(PackageRequest,
|
||||||
|
User=tu_user,
|
||||||
|
ReqTypeID=MERGE_ID,
|
||||||
|
PackageBase=package.PackageBase,
|
||||||
|
PackageBaseName=pkgbasename,
|
||||||
|
MergeBaseName="test",
|
||||||
|
Comments="Test comment.",
|
||||||
|
ClosureComment="Test closure.")
|
||||||
|
|
||||||
|
# Vote for the package.
|
||||||
|
cookies = {"AURSID": tu_user.login(Request(), "testPassword")}
|
||||||
|
endpoint = f"/pkgbase/{package.PackageBase.Name}/vote"
|
||||||
|
with client as request:
|
||||||
|
resp = request.post(endpoint, cookies=cookies)
|
||||||
|
assert resp.status_code == int(HTTPStatus.SEE_OTHER)
|
||||||
|
|
||||||
|
# Enable notifications.
|
||||||
|
endpoint = f"/pkgbase/{package.PackageBase.Name}/notify"
|
||||||
|
with client as request:
|
||||||
|
resp = request.post(endpoint, cookies=cookies)
|
||||||
|
assert resp.status_code == int(HTTPStatus.SEE_OTHER)
|
||||||
|
|
||||||
|
# Comment on the package.
|
||||||
|
endpoint = f"/pkgbase/{package.PackageBase.Name}/comments"
|
||||||
|
with client as request:
|
||||||
|
resp = request.post(endpoint, data={
|
||||||
|
"comment": "Test comment."
|
||||||
|
}, cookies=cookies)
|
||||||
|
assert resp.status_code == int(HTTPStatus.SEE_OTHER)
|
||||||
|
|
||||||
|
# Save these relationships for later comparison.
|
||||||
|
comments = package.PackageBase.comments.all()
|
||||||
|
notifs = package.PackageBase.notifications.all()
|
||||||
|
votes = package.PackageBase.package_votes.all()
|
||||||
|
|
||||||
|
# Merge the package into target.
|
||||||
|
endpoint = f"/pkgbase/{package.PackageBase.Name}/merge"
|
||||||
|
with client as request:
|
||||||
|
resp = request.post(endpoint, data={
|
||||||
|
"into": target.PackageBase.Name,
|
||||||
|
"confirm": True
|
||||||
|
}, cookies=cookies)
|
||||||
|
assert resp.status_code == int(HTTPStatus.SEE_OTHER)
|
||||||
|
loc = resp.headers.get("location")
|
||||||
|
assert loc == f"/pkgbase/{target.PackageBase.Name}"
|
||||||
|
|
||||||
|
# Assert that the original comments, notifs and votes we setup
|
||||||
|
# got migrated to target as intended.
|
||||||
|
assert comments == target.PackageBase.comments.all()
|
||||||
|
assert notifs == target.PackageBase.notifications.all()
|
||||||
|
assert votes == target.PackageBase.package_votes.all()
|
||||||
|
|
||||||
|
# ...and that the package got deleted.
|
||||||
|
package = db.query(Package).filter(Package.Name == pkgname).first()
|
||||||
|
assert package is None
|
||||||
|
|
||||||
|
# Our fake target request should have gotten rejected.
|
||||||
|
assert pkgreq.Status == REJECTED_ID
|
||||||
|
assert pkgreq.Closer is not None
|
||||||
|
|
||||||
|
# A PackageRequest is always created when merging this way.
|
||||||
|
pkgreq = db.query(PackageRequest).filter(
|
||||||
|
and_(PackageRequest.ReqTypeID == MERGE_ID,
|
||||||
|
PackageRequest.PackageBaseName == pkgbasename,
|
||||||
|
PackageRequest.MergeBaseName == target.PackageBase.Name)
|
||||||
|
).first()
|
||||||
|
assert pkgreq is not None
|
||||||
|
|
Loading…
Add table
Reference in a new issue