diff --git a/assets/src/bundles/add_forge/moderation-dashboard.js b/assets/src/bundles/add_forge/moderation-dashboard.js
index eaaee4bd..2035ebbe 100644
--- a/assets/src/bundles/add_forge/moderation-dashboard.js
+++ b/assets/src/bundles/add_forge/moderation-dashboard.js
@@ -1,62 +1,72 @@
/**
* Copyright (C) 2022 The Software Heritage developers
* See the AUTHORS file at the top-level directory of this distribution
* License: GNU Affero General Public License version 3, or any later version
* See top-level LICENSE file for more information
*/
import {getHumanReadableDate} from 'utils/functions';
export function onModerationPageLoad() {
populateModerationList();
}
export async function populateModerationList() {
$('#swh-add-forge-now-moderation-list')
.on('error.dt', (e, settings, techNote, message) => {
$('#swh-add-forge-now-moderation-list-error').text(message);
})
.DataTable({
serverSide: true,
processing: true,
searching: true,
info: false,
dom: '<<"d-flex justify-content-between align-items-center"f' +
'<"#list-exclude">l>rt<"bottom"ip>>',
ajax: {
'url': Urls.add_forge_request_list_datatables()
},
columns: [
{
data: 'id',
name: 'id',
render: function(data, type, row, meta) {
const dashboardUrl = Urls.add_forge_now_request_dashboard(data);
return `${data}`;
}
},
{
data: 'submission_date',
name: 'submission_date',
render: getHumanReadableDate
},
{
data: 'forge_type',
name: 'forge_type',
render: $.fn.dataTable.render.text()
},
{
data: 'forge_url',
name: 'forge_url',
render: $.fn.dataTable.render.text()
},
+ {
+ data: 'last_moderator',
+ name: 'last_moderator',
+ render: $.fn.dataTable.render.text()
+ },
+ {
+ data: 'last_modified_date',
+ name: 'last_modified_date',
+ render: getHumanReadableDate
+ },
{
data: 'status',
name: 'status',
render: function(data, type, row, meta) {
return swh.add_forge.formatRequestStatusName(data);
}
}
]
});
}
diff --git a/swh/web/add_forge_now/views.py b/swh/web/add_forge_now/views.py
index 0566d070..d769bcd1 100644
--- a/swh/web/add_forge_now/views.py
+++ b/swh/web/add_forge_now/views.py
@@ -1,127 +1,126 @@
# Copyright (C) 2022 The Software Heritage developers
# See the AUTHORS file at the top-level directory of this distribution
# License: GNU Affero General Public License version 3, or any later version
# See top-level LICENSE file for more information
from typing import Any, Dict, List
from django.conf.urls import url
from django.core.paginator import Paginator
from django.db.models import Q
from django.http.request import HttpRequest
from django.http.response import HttpResponse, JsonResponse
from django.shortcuts import render
from swh.web.add_forge_now.models import Request as AddForgeRequest
from swh.web.api.views.add_forge_now import (
AddForgeNowRequestPublicSerializer,
AddForgeNowRequestSerializer,
)
from swh.web.common.utils import has_add_forge_now_permission
def add_forge_request_list_datatables(request: HttpRequest) -> HttpResponse:
"""Dedicated endpoint used by datatables to display the add-forge
requests in the Web UI.
"""
draw = int(request.GET.get("draw", 0))
add_forge_requests = AddForgeRequest.objects.all()
table_data: Dict[str, Any] = {
"recordsTotal": add_forge_requests.count(),
"draw": draw,
}
search_value = request.GET.get("search[value]")
column_order = request.GET.get("order[0][column]")
field_order = request.GET.get(f"columns[{column_order}][name]", "id")
order_dir = request.GET.get("order[0][dir]", "desc")
if field_order:
if order_dir == "desc":
field_order = "-" + field_order
add_forge_requests = add_forge_requests.order_by(field_order)
per_page = int(request.GET.get("length", 10))
page_num = int(request.GET.get("start", 0)) // per_page + 1
if search_value:
add_forge_requests = add_forge_requests.filter(
Q(forge_type__icontains=search_value)
| Q(forge_url__icontains=search_value)
| Q(status__icontains=search_value)
)
if (
int(request.GET.get("user_requests_only", "0"))
and request.user.is_authenticated
):
add_forge_requests = add_forge_requests.filter(
submitter_name=request.user.username
)
paginator = Paginator(add_forge_requests, per_page)
page = paginator.page(page_num)
if has_add_forge_now_permission(request.user):
requests = AddForgeNowRequestSerializer(page.object_list, many=True).data
else:
requests = AddForgeNowRequestPublicSerializer(page.object_list, many=True).data
- results = [dict(request) for request in requests]
-
+ results = [dict(req) for req in requests]
table_data["recordsFiltered"] = add_forge_requests.count()
table_data["data"] = results
return JsonResponse(table_data)
FORGE_TYPES: List[str] = [
"bitbucket",
"cgit",
"gitlab",
"gitea",
"heptapod",
]
def create_request_create(request):
"""View to create a new 'add_forge_now' request."""
return render(
request,
"add_forge_now/creation_form.html",
{"forge_types": FORGE_TYPES},
)
def create_request_list(request):
"""View to list existing 'add_forge_now' requests."""
return render(
request,
"add_forge_now/list.html",
)
def create_request_help(request):
"""View to explain 'add_forge_now'."""
return render(
request,
"add_forge_now/help.html",
)
urlpatterns = [
url(
r"^add-forge/request/list/datatables/$",
add_forge_request_list_datatables,
name="add-forge-request-list-datatables",
),
url(r"^add-forge/request/create/$", create_request_create, name="forge-add-create"),
url(r"^add-forge/request/list/$", create_request_list, name="forge-add-list"),
url(r"^add-forge/request/help/$", create_request_help, name="forge-add-help"),
]
diff --git a/swh/web/api/views/add_forge_now.py b/swh/web/api/views/add_forge_now.py
index 55917bc6..10a5f8ff 100644
--- a/swh/web/api/views/add_forge_now.py
+++ b/swh/web/api/views/add_forge_now.py
@@ -1,367 +1,396 @@
# Copyright (C) 2022 The Software Heritage developers
# See the AUTHORS file at the top-level directory of this distribution
# License: GNU Affero General Public License version 3, or any later version
# See top-level LICENSE file for more information
import json
from typing import Any, Dict, Union
from django.core.exceptions import ObjectDoesNotExist
from django.core.paginator import Paginator
from django.db import transaction
+from django.db.models.query import QuerySet
from django.forms import CharField, ModelForm
from django.http import HttpResponseBadRequest
from django.http.request import HttpRequest
from django.http.response import HttpResponse, HttpResponseForbidden
from rest_framework import serializers
from rest_framework.request import Request
from rest_framework.response import Response
from swh.web.add_forge_now.models import Request as AddForgeRequest
from swh.web.add_forge_now.models import RequestActorRole as AddForgeNowRequestActorRole
from swh.web.add_forge_now.models import RequestHistory as AddForgeNowRequestHistory
from swh.web.add_forge_now.models import RequestStatus as AddForgeNowRequestStatus
from swh.web.api.apidoc import api_doc, format_docstring
from swh.web.api.apiurls import api_route
from swh.web.auth.utils import ADD_FORGE_MODERATOR_PERMISSION
from swh.web.common.exc import BadInputExc
from swh.web.common.utils import has_add_forge_now_permission, reverse
def _block_while_testing():
"""Replaced by tests to check concurrency behavior"""
pass
class AddForgeNowRequestForm(ModelForm):
forge_contact_comment = CharField(
required=False,
)
class Meta:
model = AddForgeRequest
fields = (
"forge_type",
"forge_url",
"forge_contact_email",
"forge_contact_name",
"forge_contact_comment",
"submitter_forward_username",
)
class AddForgeNowRequestHistoryForm(ModelForm):
new_status = CharField(
max_length=200,
required=False,
)
class Meta:
model = AddForgeNowRequestHistory
fields = ("text", "new_status")
class AddForgeNowRequestSerializer(serializers.ModelSerializer):
+
+ last_moderator = serializers.SerializerMethodField()
+ last_modified_date = serializers.SerializerMethodField()
+ history: Dict[int, QuerySet] = {}
+
class Meta:
model = AddForgeRequest
fields = "__all__"
+ def _gethistory(self, request):
+ if request.id not in self.history:
+ self.history[request.id] = AddForgeNowRequestHistory.objects.filter(
+ request=request
+ ).order_by("id")
+ return self.history[request.id]
+
+ def get_last_moderator(self, request):
+ last_history_with_moderator = (
+ self._gethistory(request).filter(actor_role="MODERATOR").last()
+ )
+ return (
+ last_history_with_moderator.actor if last_history_with_moderator else "None"
+ )
+
+ def get_last_modified_date(self, request):
+ last_history = self._gethistory(request).last()
+ return (
+ last_history.date.isoformat().replace("+00:00", "Z")
+ if last_history
+ else None
+ )
+
class AddForgeNowRequestPublicSerializer(serializers.ModelSerializer):
"""Serializes AddForgeRequest without private fields."""
class Meta:
model = AddForgeRequest
fields = ("id", "forge_url", "forge_type", "status", "submission_date")
class AddForgeNowRequestHistorySerializer(serializers.ModelSerializer):
class Meta:
model = AddForgeNowRequestHistory
exclude = ("request",)
class AddForgeNowRequestHistoryPublicSerializer(serializers.ModelSerializer):
class Meta:
model = AddForgeNowRequestHistory
fields = ("id", "date", "new_status", "actor_role")
@api_route(
r"/add-forge/request/create/",
"api-1-add-forge-request-create",
methods=["POST"],
)
@api_doc("/add-forge/request/create")
@format_docstring()
@transaction.atomic
def api_add_forge_request_create(request: Union[HttpRequest, Request]) -> HttpResponse:
"""
.. http:post:: /api/1/add-forge/request/create/
Create a new request to add a forge to the list of those crawled regularly
by Software Heritage.
.. warning::
That endpoint is not publicly available and requires authentication
in order to be able to request it.
{common_headers}
:[0-9]+)/update/",
"api-1-add-forge-request-update",
methods=["POST"],
)
@api_doc("/add-forge/request/update", tags=["hidden"])
@format_docstring()
@transaction.atomic
def api_add_forge_request_update(
request: Union[HttpRequest, Request], id: int
) -> HttpResponse:
"""
.. http:post:: /api/1/add-forge/request/update/
Update a request to add a forge to the list of those crawled regularly
by Software Heritage.
.. warning::
That endpoint is not publicly available and requires authentication
in order to be able to request it.
{common_headers}
:[0-9]+)/get/",
"api-1-add-forge-request-get",
methods=["GET"],
)
@api_doc("/add-forge/request/get")
@format_docstring()
def api_add_forge_request_get(request: Request, id: int):
"""
.. http:get:: /api/1/add-forge/request/get/
Return all details about an add-forge request.
{common_headers}
:param int id: add-forge request identifier
:statuscode 200: request details successfully returned
:statuscode 400: request identifier does not exist
"""
try:
add_forge_request = AddForgeRequest.objects.get(id=id)
except ObjectDoesNotExist:
raise BadInputExc("Request id does not exist")
request_history = AddForgeNowRequestHistory.objects.filter(
request=add_forge_request
).order_by("id")
if request.user.is_authenticated and request.user.has_perm(
ADD_FORGE_MODERATOR_PERMISSION
):
data = AddForgeNowRequestSerializer(add_forge_request).data
history = AddForgeNowRequestHistorySerializer(request_history, many=True).data
else:
data = AddForgeNowRequestPublicSerializer(add_forge_request).data
history = AddForgeNowRequestHistoryPublicSerializer(
request_history, many=True
).data
return {"request": data, "history": history}
diff --git a/swh/web/templates/add_forge_now/requests-moderation.html b/swh/web/templates/add_forge_now/requests-moderation.html
index d13b60fc..fc9c73d7 100644
--- a/swh/web/templates/add_forge_now/requests-moderation.html
+++ b/swh/web/templates/add_forge_now/requests-moderation.html
@@ -1,45 +1,47 @@
{% extends "../layout.html" %}
{% comment %}
Copyright (C) 2022 The Software Heritage developers
See the AUTHORS file at the top-level directory of this distribution
License: GNU Affero General Public License version 3, or any later version
See top-level LICENSE file for more information
{% endcomment %}
{% load render_bundle from webpack_loader %}
{% load static %}
{% block header %}
{% render_bundle 'add_forge' %}
{% endblock %}
{% block title %}{{ heading }} – Software Heritage archive{% endblock %}
{% block navbar-content %}
Add forge now moderation
{% endblock %}
{% block content %}
ID |
Submission date |
Forge type |
Forge URL |
+ Moderator Name |
+ Last Modified Date |
Status |
{% endblock %}
diff --git a/swh/web/tests/api/views/test_add_forge_now.py b/swh/web/tests/api/views/test_add_forge_now.py
index a6e7453c..58eeb752 100644
--- a/swh/web/tests/api/views/test_add_forge_now.py
+++ b/swh/web/tests/api/views/test_add_forge_now.py
@@ -1,537 +1,548 @@
# Copyright (C) 2022 The Software Heritage developers
# See the AUTHORS file at the top-level directory of this distribution
# License: GNU Affero General Public License version 3, or any later version
# See top-level LICENSE file for more information
import datetime
import threading
import time
from typing import Dict
from urllib.parse import urlencode
import iso8601
import pytest
from swh.web.add_forge_now.models import Request
from swh.web.common.utils import reverse
from swh.web.tests.utils import (
check_api_get_responses,
check_api_post_response,
check_http_post_response,
)
@pytest.mark.django_db
def test_add_forge_request_create_anonymous_user(api_client):
url = reverse("api-1-add-forge-request-create")
check_api_post_response(api_client, url, status_code=403)
@pytest.mark.django_db
def test_add_forge_request_create_empty(api_client, regular_user):
api_client.force_login(regular_user)
url = reverse("api-1-add-forge-request-create")
resp = check_api_post_response(api_client, url, status_code=400)
assert '"forge_type"' in resp.data["reason"]
ADD_FORGE_DATA_FORGE1: Dict = {
"forge_type": "gitlab",
"forge_url": "https://gitlab.example.org",
"forge_contact_email": "admin@gitlab.example.org",
"forge_contact_name": "gitlab.example.org admin",
"forge_contact_comment": "user marked as owner in forge members",
"submitter_forward_username": True,
}
ADD_FORGE_DATA_FORGE2: Dict = {
"forge_type": "gitea",
"forge_url": "https://gitea.example.org",
"forge_contact_email": "admin@gitea.example.org",
"forge_contact_name": "gitea.example.org admin",
"forge_contact_comment": "user marked as owner in forge members",
"submitter_forward_username": True,
}
ADD_FORGE_DATA_FORGE3: Dict = {
"forge_type": "heptapod",
"forge_url": "https://heptapod.host/",
"forge_contact_email": "admin@example.org",
"forge_contact_name": "heptapod admin",
"forge_contact_comment": "", # authorized empty or null comment
"submitter_forward_username": False,
}
ADD_FORGE_DATA_FORGE4: Dict = {
**ADD_FORGE_DATA_FORGE3,
"forge_url": "https://heptapod2.host/",
"submitter_forward_username": "on",
}
ADD_FORGE_DATA_FORGE5: Dict = {
**ADD_FORGE_DATA_FORGE3,
"forge_url": "https://heptapod3.host/",
"submitter_forward_username": "off",
}
@pytest.mark.django_db(transaction=True, reset_sequences=True)
@pytest.mark.parametrize(
"add_forge_data",
[
ADD_FORGE_DATA_FORGE1,
ADD_FORGE_DATA_FORGE2,
ADD_FORGE_DATA_FORGE3,
ADD_FORGE_DATA_FORGE4,
],
)
def test_add_forge_request_create_success_post(
api_client, regular_user, add_forge_data
):
api_client.force_login(regular_user)
url = reverse("api-1-add-forge-request-create")
date_before = datetime.datetime.now(tz=datetime.timezone.utc)
resp = check_api_post_response(
api_client,
url,
data=add_forge_data,
status_code=201,
)
date_after = datetime.datetime.now(tz=datetime.timezone.utc)
consent = add_forge_data["submitter_forward_username"]
# map the expected result with what's expectedly read from the db to ease comparison
expected_consent_bool = consent == "on" if isinstance(consent, str) else consent
assert resp.data == {
**add_forge_data,
"id": resp.data["id"],
"status": "PENDING",
"submission_date": resp.data["submission_date"],
"submitter_name": regular_user.username,
"submitter_email": regular_user.email,
"submitter_forward_username": expected_consent_bool,
+ "last_moderator": resp.data["last_moderator"],
+ "last_modified_date": resp.data["last_modified_date"],
}
assert date_before < iso8601.parse_date(resp.data["submission_date"]) < date_after
request = Request.objects.all().last()
assert request.forge_url == add_forge_data["forge_url"]
assert request.submitter_name == regular_user.username
@pytest.mark.django_db(transaction=True, reset_sequences=True)
def test_add_forge_request_create_success_form_encoded(client, regular_user):
client.force_login(regular_user)
url = reverse("api-1-add-forge-request-create")
date_before = datetime.datetime.now(tz=datetime.timezone.utc)
resp = check_http_post_response(
client,
url,
request_content_type="application/x-www-form-urlencoded",
data=urlencode(ADD_FORGE_DATA_FORGE1),
status_code=201,
)
date_after = datetime.datetime.now(tz=datetime.timezone.utc)
assert resp.data == {
**ADD_FORGE_DATA_FORGE1,
- "id": 1,
+ "id": resp.data["id"],
"status": "PENDING",
"submission_date": resp.data["submission_date"],
"submitter_name": regular_user.username,
"submitter_email": regular_user.email,
+ "last_moderator": resp.data["last_moderator"],
+ "last_modified_date": resp.data["last_modified_date"],
}
assert date_before < iso8601.parse_date(resp.data["submission_date"]) < date_after
request = Request.objects.all()[0]
assert request.forge_url == ADD_FORGE_DATA_FORGE1["forge_url"]
assert request.submitter_name == regular_user.username
@pytest.mark.django_db(transaction=True, reset_sequences=True)
def test_add_forge_request_create_duplicate(api_client, regular_user):
api_client.force_login(regular_user)
url = reverse("api-1-add-forge-request-create")
check_api_post_response(
api_client,
url,
data=ADD_FORGE_DATA_FORGE1,
status_code=201,
)
check_api_post_response(
api_client,
url,
data=ADD_FORGE_DATA_FORGE1,
status_code=409,
)
requests = Request.objects.all()
assert len(requests) == 1
@pytest.mark.django_db(transaction=True, reset_sequences=True)
def test_add_forge_request_update_anonymous_user(api_client):
url = reverse("api-1-add-forge-request-update", url_args={"id": 1})
check_api_post_response(api_client, url, status_code=403)
@pytest.mark.django_db(transaction=True, reset_sequences=True)
def test_add_forge_request_update_regular_user(api_client, regular_user):
api_client.force_login(regular_user)
url = reverse("api-1-add-forge-request-update", url_args={"id": 1})
check_api_post_response(api_client, url, status_code=403)
@pytest.mark.django_db(transaction=True, reset_sequences=True)
def test_add_forge_request_update_non_existent(api_client, add_forge_moderator):
api_client.force_login(add_forge_moderator)
url = reverse("api-1-add-forge-request-update", url_args={"id": 1})
check_api_post_response(api_client, url, status_code=400)
def create_add_forge_request(api_client, regular_user, data=ADD_FORGE_DATA_FORGE1):
api_client.force_login(regular_user)
url = reverse("api-1-add-forge-request-create")
return check_api_post_response(
api_client,
url,
data=data,
status_code=201,
)
@pytest.mark.django_db(transaction=True, reset_sequences=True)
def test_add_forge_request_update_empty(api_client, regular_user, add_forge_moderator):
create_add_forge_request(api_client, regular_user)
api_client.force_login(add_forge_moderator)
url = reverse("api-1-add-forge-request-update", url_args={"id": 1})
check_api_post_response(api_client, url, status_code=400)
@pytest.mark.django_db(transaction=True, reset_sequences=True)
def test_add_forge_request_update_missing_field(
api_client, regular_user, add_forge_moderator
):
create_add_forge_request(api_client, regular_user)
api_client.force_login(add_forge_moderator)
url = reverse("api-1-add-forge-request-update", url_args={"id": 1})
check_api_post_response(api_client, url, data={}, status_code=400)
check_api_post_response(
api_client, url, data={"new_status": "REJECTED"}, status_code=400
)
@pytest.mark.django_db(transaction=True, reset_sequences=True)
def test_add_forge_request_update(api_client, regular_user, add_forge_moderator):
create_add_forge_request(api_client, regular_user)
api_client.force_login(add_forge_moderator)
url = reverse("api-1-add-forge-request-update", url_args={"id": 1})
check_api_post_response(
api_client, url, data={"text": "updating request"}, status_code=200
)
check_api_post_response(
api_client,
url,
data={"new_status": "REJECTED", "text": "request rejected"},
status_code=200,
)
@pytest.mark.django_db(transaction=True, reset_sequences=True)
def test_add_forge_request_update_invalid_new_status(
api_client, regular_user, add_forge_moderator
):
create_add_forge_request(api_client, regular_user)
api_client.force_login(add_forge_moderator)
url = reverse("api-1-add-forge-request-update", url_args={"id": 1})
check_api_post_response(
api_client,
url,
data={"new_status": "ACCEPTED", "text": "request accepted"},
status_code=400,
)
@pytest.mark.django_db(transaction=True, reset_sequences=True)
def test_add_forge_request_update_status_concurrent(
api_client, regular_user, add_forge_moderator, mocker
):
_block_while_testing = mocker.patch(
"swh.web.api.views.add_forge_now._block_while_testing"
)
_block_while_testing.side_effect = lambda: time.sleep(1)
create_add_forge_request(api_client, regular_user)
api_client.force_login(add_forge_moderator)
url = reverse("api-1-add-forge-request-update", url_args={"id": 1})
worker_ended = False
def worker():
nonlocal worker_ended
check_api_post_response(
api_client,
url,
data={"new_status": "WAITING_FOR_FEEDBACK", "text": "waiting for message"},
status_code=200,
)
worker_ended = True
# this thread will first modify the request status to WAITING_FOR_FEEDBACK
thread = threading.Thread(target=worker)
thread.start()
# the other thread (slower) will attempt to modify the request status to REJECTED
# but it will not be allowed as the first faster thread already modified it
# and REJECTED state can not be reached from WAITING_FOR_FEEDBACK one
time.sleep(0.5)
check_api_post_response(
api_client,
url,
data={"new_status": "REJECTED", "text": "request accepted"},
status_code=400,
)
thread.join()
assert worker_ended
@pytest.mark.django_db(transaction=True, reset_sequences=True)
def test_add_forge_request_list_anonymous(api_client, regular_user):
url = reverse("api-1-add-forge-request-list")
resp = check_api_get_responses(api_client, url, status_code=200)
assert resp.data == []
create_add_forge_request(api_client, regular_user)
resp = check_api_get_responses(api_client, url, status_code=200)
add_forge_request = {
"forge_url": ADD_FORGE_DATA_FORGE1["forge_url"],
"forge_type": ADD_FORGE_DATA_FORGE1["forge_type"],
"status": "PENDING",
"submission_date": resp.data[0]["submission_date"],
- "id": 1,
+ "id": resp.data[0]["id"],
}
assert resp.data == [add_forge_request]
create_add_forge_request(api_client, regular_user, data=ADD_FORGE_DATA_FORGE2)
resp = check_api_get_responses(api_client, url, status_code=200)
other_forge_request = {
"forge_url": ADD_FORGE_DATA_FORGE2["forge_url"],
"forge_type": ADD_FORGE_DATA_FORGE2["forge_type"],
"status": "PENDING",
"submission_date": resp.data[0]["submission_date"],
- "id": 2,
+ "id": resp.data[0]["id"],
}
assert resp.data == [other_forge_request, add_forge_request]
@pytest.mark.django_db(transaction=True, reset_sequences=True)
def test_add_forge_request_list_moderator(
api_client, regular_user, add_forge_moderator
):
url = reverse("api-1-add-forge-request-list")
create_add_forge_request(api_client, regular_user)
create_add_forge_request(api_client, regular_user, data=ADD_FORGE_DATA_FORGE2)
api_client.force_login(add_forge_moderator)
resp = check_api_get_responses(api_client, url, status_code=200)
add_forge_request = {
**ADD_FORGE_DATA_FORGE1,
"status": "PENDING",
"submission_date": resp.data[1]["submission_date"],
"submitter_name": regular_user.username,
"submitter_email": regular_user.email,
- "id": 1,
+ "last_moderator": resp.data[1]["last_moderator"],
+ "last_modified_date": resp.data[1]["last_modified_date"],
+ "id": resp.data[1]["id"],
}
other_forge_request = {
**ADD_FORGE_DATA_FORGE2,
"status": "PENDING",
"submission_date": resp.data[0]["submission_date"],
"submitter_name": regular_user.username,
"submitter_email": regular_user.email,
- "id": 2,
+ "last_moderator": resp.data[0]["last_moderator"],
+ "last_modified_date": resp.data[0]["last_modified_date"],
+ "id": resp.data[0]["id"],
}
assert resp.data == [other_forge_request, add_forge_request]
@pytest.mark.django_db(transaction=True, reset_sequences=True)
def test_add_forge_request_list_pagination(
api_client, regular_user, api_request_factory
):
create_add_forge_request(api_client, regular_user)
create_add_forge_request(api_client, regular_user, data=ADD_FORGE_DATA_FORGE2)
url = reverse("api-1-add-forge-request-list", query_params={"per_page": 1})
resp = check_api_get_responses(api_client, url, 200)
assert len(resp.data) == 1
request = api_request_factory.get(url)
next_url = reverse(
"api-1-add-forge-request-list",
query_params={"page": 2, "per_page": 1},
request=request,
)
assert resp["Link"] == f'<{next_url}>; rel="next"'
resp = check_api_get_responses(api_client, next_url, 200)
assert len(resp.data) == 1
prev_url = reverse(
"api-1-add-forge-request-list",
query_params={"page": 1, "per_page": 1},
request=request,
)
assert resp["Link"] == f'<{prev_url}>; rel="previous"'
@pytest.mark.django_db(transaction=True, reset_sequences=True)
def test_add_forge_request_list_submitter_filtering(
api_client, regular_user, regular_user2
):
create_add_forge_request(api_client, regular_user)
create_add_forge_request(api_client, regular_user2, data=ADD_FORGE_DATA_FORGE2)
api_client.force_login(regular_user)
url = reverse(
"api-1-add-forge-request-list", query_params={"user_requests_only": 1}
)
resp = check_api_get_responses(api_client, url, status_code=200)
assert len(resp.data) == 1
@pytest.mark.django_db(transaction=True, reset_sequences=True)
def test_add_forge_request_get(api_client, regular_user, add_forge_moderator):
resp = create_add_forge_request(api_client, regular_user)
submission_date = resp.data["submission_date"]
url = reverse("api-1-add-forge-request-update", url_args={"id": 1})
api_client.force_login(add_forge_moderator)
check_api_post_response(
api_client,
url,
data={"new_status": "WAITING_FOR_FEEDBACK", "text": "waiting for message"},
status_code=200,
)
api_client.logout()
url = reverse("api-1-add-forge-request-get", url_args={"id": 1})
resp = check_api_get_responses(api_client, url, status_code=200)
assert resp.data == {
"request": {
"forge_url": ADD_FORGE_DATA_FORGE1["forge_url"],
"forge_type": ADD_FORGE_DATA_FORGE1["forge_type"],
"id": 1,
"status": "WAITING_FOR_FEEDBACK",
"submission_date": submission_date,
},
"history": [
{
"id": 1,
"actor_role": "SUBMITTER",
"date": resp.data["history"][0]["date"],
"new_status": "PENDING",
},
{
"id": 2,
"actor_role": "MODERATOR",
"date": resp.data["history"][1]["date"],
"new_status": "WAITING_FOR_FEEDBACK",
},
],
}
@pytest.mark.django_db(transaction=True, reset_sequences=True)
def test_add_forge_request_get_moderator(api_client, regular_user, add_forge_moderator):
resp = create_add_forge_request(api_client, regular_user)
submission_date = resp.data["submission_date"]
url = reverse("api-1-add-forge-request-update", url_args={"id": 1})
api_client.force_login(add_forge_moderator)
check_api_post_response(
api_client,
url,
data={"new_status": "WAITING_FOR_FEEDBACK", "text": "waiting for message"},
status_code=200,
)
url = reverse("api-1-add-forge-request-get", url_args={"id": 1})
resp = check_api_get_responses(api_client, url, status_code=200)
+ resp.data["history"] = [dict(history_item) for history_item in resp.data["history"]]
assert resp.data == {
"request": {
**ADD_FORGE_DATA_FORGE1,
"id": 1,
"status": "WAITING_FOR_FEEDBACK",
"submission_date": submission_date,
"submitter_name": regular_user.username,
"submitter_email": regular_user.email,
+ "last_moderator": add_forge_moderator.username,
+ "last_modified_date": resp.data["history"][1]["date"],
},
"history": [
{
"id": 1,
"text": "",
"actor": regular_user.username,
"actor_role": "SUBMITTER",
"date": resp.data["history"][0]["date"],
"new_status": "PENDING",
},
{
"id": 2,
"text": "waiting for message",
"actor": add_forge_moderator.username,
"actor_role": "MODERATOR",
"date": resp.data["history"][1]["date"],
"new_status": "WAITING_FOR_FEEDBACK",
},
],
}
@pytest.mark.django_db(transaction=True, reset_sequences=True)
def test_add_forge_request_get_invalid(api_client):
url = reverse("api-1-add-forge-request-get", url_args={"id": 3})
check_api_get_responses(api_client, url, status_code=400)