diff --git a/aurweb/routers/accounts.py b/aurweb/routers/accounts.py index 152b0a15..f45b55da 100644 --- a/aurweb/routers/accounts.py +++ b/aurweb/routers/accounts.py @@ -612,8 +612,7 @@ 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) + context = await make_variable_context(request, _("Account") + username) user = db.query(models.User, models.User.Username == username).first() if not user: diff --git a/aurweb/routers/packages.py b/aurweb/routers/packages.py index 27a30205..b0da3bf9 100644 --- a/aurweb/routers/packages.py +++ b/aurweb/routers/packages.py @@ -318,7 +318,8 @@ async def pkgbase_comments_post( @router.get("/pkgbase/{name}/comments/{id}/form") @auth_required(True, login=False) -async def pkgbase_comment_form(request: Request, name: str, id: int): +async def pkgbase_comment_form(request: Request, name: str, id: int, + next: str = Query(default=None)): """ Produce a comment form for comment {id}. """ pkgbase = get_pkg_or_base(name, models.PackageBase) comment = pkgbase.comments.filter(models.PackageComment.ID == id).first() @@ -331,6 +332,11 @@ async def pkgbase_comment_form(request: Request, name: str, id: int): context = await make_single_context(request, pkgbase) context["comment"] = comment + if not next: + next = f"/pkgbase/{name}" + + context["next"] = next + form = render_raw_template( request, "partials/packages/comment_form.html", context) return JSONResponse({"form": form}) @@ -341,7 +347,8 @@ async def pkgbase_comment_form(request: Request, name: str, id: int): async def pkgbase_comment_post( request: Request, name: str, id: int, comment: str = Form(default=str()), - enable_notifications: bool = Form(default=False)): + enable_notifications: bool = Form(default=False), + next: str = Form(default=None)): pkgbase = get_pkg_or_base(name, models.PackageBase) db_comment = get_pkgbase_comment(pkgbase, id) @@ -366,14 +373,18 @@ async def pkgbase_comment_post( PackageBase=pkgbase) update_comment_render(db_comment.ID) + if not next: + next = f"/pkgbase/{pkgbase.Name}" + # Redirect to the pkgbase page anchored to the updated comment. - return RedirectResponse(f"/pkgbase/{pkgbase.Name}#comment-{db_comment.ID}", + return RedirectResponse(f"{next}#comment-{db_comment.ID}", status_code=HTTPStatus.SEE_OTHER) @router.post("/pkgbase/{name}/comments/{id}/delete") @auth_required(True, redirect="/pkgbase/{name}/comments/{id}/delete") -async def pkgbase_comment_delete(request: Request, name: str, id: int): +async def pkgbase_comment_delete(request: Request, name: str, id: int, + next: str = Form(default=None)): pkgbase = get_pkg_or_base(name, models.PackageBase) comment = get_pkgbase_comment(pkgbase, id) @@ -390,13 +401,16 @@ async def pkgbase_comment_delete(request: Request, name: str, id: int): comment.Deleter = request.user comment.DelTS = now - return RedirectResponse(f"/pkgbase/{name}", - status_code=HTTPStatus.SEE_OTHER) + if not next: + next = f"/pkgbase/{name}" + + return RedirectResponse(next, status_code=HTTPStatus.SEE_OTHER) @router.post("/pkgbase/{name}/comments/{id}/undelete") @auth_required(True, redirect="/pkgbase/{name}/comments/{id}/undelete") -async def pkgbase_comment_undelete(request: Request, name: str, id: int): +async def pkgbase_comment_undelete(request: Request, name: str, id: int, + next: str = Form(default=None)): pkgbase = get_pkg_or_base(name, models.PackageBase) comment = get_pkgbase_comment(pkgbase, id) @@ -412,13 +426,16 @@ async def pkgbase_comment_undelete(request: Request, name: str, id: int): comment.Deleter = None comment.DelTS = None - return RedirectResponse(f"/pkgbase/{name}", - status_code=HTTPStatus.SEE_OTHER) + if not next: + next = f"/pkgbase/{name}" + + return RedirectResponse(next, status_code=HTTPStatus.SEE_OTHER) @router.post("/pkgbase/{name}/comments/{id}/pin") @auth_required(True, redirect="/pkgbase/{name}/comments/{id}/pin") -async def pkgbase_comment_pin(request: Request, name: str, id: int): +async def pkgbase_comment_pin(request: Request, name: str, id: int, + next: str = Form(default=None)): pkgbase = get_pkg_or_base(name, models.PackageBase) comment = get_pkgbase_comment(pkgbase, id) @@ -434,13 +451,16 @@ async def pkgbase_comment_pin(request: Request, name: str, id: int): with db.begin(): comment.PinnedTS = now - return RedirectResponse(f"/pkgbase/{name}", - status_code=HTTPStatus.SEE_OTHER) + if not next: + next = f"/pkgbase/{name}" + + return RedirectResponse(next, status_code=HTTPStatus.SEE_OTHER) @router.post("/pkgbase/{name}/comments/{id}/unpin") @auth_required(True, redirect="/pkgbase/{name}/comments/{id}/unpin") -async def pkgbase_comment_unpin(request: Request, name: str, id: int): +async def pkgbase_comment_unpin(request: Request, name: str, id: int, + next: str = Form(default=None)): pkgbase = get_pkg_or_base(name, models.PackageBase) comment = get_pkgbase_comment(pkgbase, id) @@ -455,8 +475,10 @@ async def pkgbase_comment_unpin(request: Request, name: str, id: int): with db.begin(): comment.PinnedTS = 0 - return RedirectResponse(f"/pkgbase/{name}", - status_code=HTTPStatus.SEE_OTHER) + if not next: + next = f"/pkgbase/{name}" + + return RedirectResponse(next, status_code=HTTPStatus.SEE_OTHER) @router.get("/pkgbase/{name}/comaintainers") diff --git a/templates/account/comments.html b/templates/account/comments.html index a1012b1b..4a516dca 100644 --- a/templates/account/comments.html +++ b/templates/account/comments.html @@ -18,35 +18,50 @@ {% for comment in comments %} - {% set commented_at = comment.CommentTS | dt | as_timezone(timezone) %} -

- {{ - "Commented on package %s%s%s on %s%s%s" | tr - | format( - '', - comment.PackageBase.Name, - "", - '' | format( - username, - comment.ID - ), - commented_at.strftime("%Y-%m-%d %H:%M"), - "" - ) | safe - }} -

-
-
- {% if comment.RenderedComment %} - {{ comment.RenderedComment | safe }} - {% else %} - {{ comment.Comments }} + {% set header_cls = "comment-header" %} + {% if comment.Deleter %} + {% set header_cls = "%s %s" | format(header_cls, "comment-deleted") %} + {% endif %} + + {% if not comment.Deleter or request.user.has_credential("CRED_COMMENT_VIEW_DELETED", approved=[comment.Deleter]) %} + + {% set commented_at = comment.CommentTS | dt | as_timezone(timezone) %} +

+ {{ + "Commented on package %s%s%s on %s%s%s" | tr + | format( + '' | format(comment.PackageBase.Name), + comment.PackageBase.Name, + "", + '' | format( + username, + comment.ID + ), + commented_at.strftime("%Y-%m-%d %H:%M"), + "" + ) | safe + }} + {% if comment.Editor %} + {% set edited_on = comment.EditedTS | dt | as_timezone(timezone) %} + + ({{ "edited on %s by %s" | tr + | format(edited_on.strftime('%Y-%m-%d %H:%M'), + '%s' | format( + comment.Editor.Username, comment.Editor.Username)) + | safe + }}) + {% endif %} -

-
+ + {% include "partials/comment_actions.html" %} + + + {% include "partials/comment_content.html" %} + {% endif %} {% endfor %} + {% endblock %} diff --git a/templates/partials/comment_actions.html b/templates/partials/comment_actions.html new file mode 100644 index 00000000..b21b90bd --- /dev/null +++ b/templates/partials/comment_actions.html @@ -0,0 +1,100 @@ +{% set pkgbasename = comment.PackageBase.Name %} + +{% if not comment.Deleter %} + {% if request.user.has_credential('CRED_COMMENT_DELETE', approved=[comment.User]) %} +
+
+ + +
+
+ {% endif %} + + {% if request.user.has_credential('CRED_COMMENT_EDIT', approved=[comment.User]) %} + + {{ 'Edit comment' | tr }} + + + {# Set the edit event listener for this link. We must do this here + so that we can utilize Jinja2's values. #} + + + {% endif %} + + {% if request.user.has_credential("CRED_COMMENT_PIN", approved=[comment.PackageBase.Maintainer]) %} + {% if comment.PinnedTS %} +
+
+ + +
+
+ {% else %} +
+
+ + +
+
+ {% endif %} + {% endif %} +{% elif request.user.has_credential("CRED_COMMENT_UNDELETE", approved=[comment.User]) %} +
+
+ + +
+
+{% endif %} diff --git a/templates/partials/comment_content.html b/templates/partials/comment_content.html new file mode 100644 index 00000000..f89dc505 --- /dev/null +++ b/templates/partials/comment_content.html @@ -0,0 +1,15 @@ + +{% set article_cls = "article-content" %} +{% if comment.Deleter %} + {% set article_cls = "%s %s" | format(article_cls, "comment-deleted") %} +{% endif %} + +
+
+ {% if comment.RenderedComment %} + {{ comment.RenderedComment | safe }} + {% else %} + {{ comment.Comments }} + {% endif %} +
+
diff --git a/templates/partials/head.html b/templates/partials/head.html index 8bfde020..21c79887 100644 --- a/templates/partials/head.html +++ b/templates/partials/head.html @@ -15,5 +15,8 @@ + + + AUR ({{ language }}) - {{ title | tr }} diff --git a/templates/partials/packages/comment.html b/templates/partials/packages/comment.html index 533ac40d..676a7a73 100644 --- a/templates/partials/packages/comment.html +++ b/templates/partials/packages/comment.html @@ -34,77 +34,10 @@ | safe }}) - {% endif %} - {% if not comment.Deleter %} - {% if request.user.has_credential('CRED_COMMENT_DELETE', approved=[comment.User]) %} -
-
- -
-
- {% endif %} + {% endif %} - {% if request.user.has_credential('CRED_COMMENT_EDIT', approved=[comment.User]) %} - Edit comment - {% endif %} + {% include "partials/comment_actions.html" %} + - {% if request.user.has_credential("CRED_COMMENT_PIN", approved=[pkgbase.Maintainer]) %} - {% if comment.PinnedTS %} -
-
- -
-
- {% else %} -
-
- -
-
- {% endif %} - {% endif %} - {% elif request.user.has_credential("CRED_COMMENT_UNDELETE", approved=[comment.User]) %} -
-
- -
-
- {% endif %} - -
-
- {% if comment.RenderedComment %} - {{ comment.RenderedComment | safe }} - {% else %} - {{ comment.Comments }} - {% endif %} -
-
+ {% include "partials/comment_content.html" %} {% endif %} diff --git a/templates/partials/packages/comment_form.html b/templates/partials/packages/comment_form.html index c1c25f87..5368e784 100644 --- a/templates/partials/packages/comment_form.html +++ b/templates/partials/packages/comment_form.html @@ -13,6 +13,7 @@ Routes:
+

{{ "Git commit identifiers referencing commits in the AUR package " "repository and URLs are converted to links automatically." | tr }} diff --git a/templates/partials/packages/comments.html b/templates/partials/packages/comments.html index 56b5ab03..6e6b9a47 100644 --- a/templates/partials/packages/comments.html +++ b/templates/partials/packages/comments.html @@ -39,72 +39,3 @@ {% endfor %} {% endif %} - - diff --git a/test/test_accounts_routes.py b/test/test_accounts_routes.py index 6c79cd33..188f7048 100644 --- a/test/test_accounts_routes.py +++ b/test/test_accounts_routes.py @@ -1625,3 +1625,10 @@ def test_post_terms_of_service(): response = request.get("/tos", cookies=cookies, allow_redirects=False) assert response.status_code == int(HTTPStatus.SEE_OTHER) assert response.headers.get("location") == "/" + + +def test_account_comments_not_found(): + cookies = {"AURSID": user.login(Request(), "testPassword")} + with client as request: + resp = request.get("/account/non-existent/comments", cookies=cookies) + assert resp.status_code == int(HTTPStatus.NOT_FOUND) diff --git a/test/test_packages_routes.py b/test/test_packages_routes.py index 063c8f11..b4a582e3 100644 --- a/test/test_packages_routes.py +++ b/test/test_packages_routes.py @@ -1055,6 +1055,13 @@ def test_pkgbase_comments_missing_comment(client: TestClient, maintainer: User, def test_pkgbase_comments(client: TestClient, maintainer: User, user: User, package: Package): + """ This test includes tests against the following routes: + - POST /pkgbase/{name}/comments + - GET /pkgbase/{name} (to check comments) + - Tested against a comment created with the POST route + - GET /pkgbase/{name}/comments/{id}/form + - Tested against a comment created with the POST route + """ cookies = {"AURSID": maintainer.login(Request(), "testPassword")} pkgbasename = package.PackageBase.Name endpoint = f"/pkgbase/{pkgbasename}/comments" diff --git a/web/html/js/comment-edit.js b/web/html/js/comment-edit.js new file mode 100644 index 00000000..4898c8d4 --- /dev/null +++ b/web/html/js/comment-edit.js @@ -0,0 +1,61 @@ +function add_busy_indicator(sibling) { + const img = document.createElement('img'); + img.src = "/images/ajax-loader.gif"; + img.classList.add('ajax-loader'); + img.style.height = 11; + img.style.width = 16; + img.alt = "Busy…"; + + sibling.insertAdjacentElement('afterend', img); +} + +function remove_busy_indicator(sibling) { + const elem = sibling.nextElementSibling; + elem.parentNode.removeChild(elem); +} + +function getParentsUntil(elem, className) { + // Limit to 10 depth + for ( ; elem && elem !== document; elem = elem.parentNode) { + if (elem.matches(className)) { + break; + } + } + + return elem; +} + +function handleEditCommentClick(event, pkgbasename) { + event.preventDefault(); + const parent_element = getParentsUntil(event.target, '.comment-header'); + const parent_id = parent_element.id; + const comment_id = parent_id.substr(parent_id.indexOf('-') + 1); + // The div class="article-content" which contains the comment + const edit_form = parent_element.nextElementSibling; + + const url = "/pkgbase/" + pkgbasename + "/comments/" + comment_id + "/form?"; + + add_busy_indicator(event.target); + + fetch(url + new URLSearchParams({ next: window.location.pathname }), { + method: 'GET', + credentials: 'same-origin' + }) + .then(function(response) { + if (!response.ok) { + throw Error(response.statusText); + } + return response.json(); + }) + .then(function(data) { + remove_busy_indicator(event.target); + edit_form.innerHTML = data.form; + edit_form.querySelector('textarea').focus(); + }) + .catch(function(error) { + remove_busy_indicator(event.target); + console.error(error); + }); + + return false; +}