Skip to content

Commit

Permalink
Migrate to class based views
Browse files Browse the repository at this point in the history
  • Loading branch information
bartTC committed Apr 22, 2024
1 parent f5c4c8f commit 103fecb
Show file tree
Hide file tree
Showing 5 changed files with 163 additions and 105 deletions.
3 changes: 2 additions & 1 deletion dynamic_raw_id/static/dynamic_raw_id/js/dynamic_raw_id.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ if (!windowname_to_id) {
function dismissRelatedLookupPopup(win, chosenId) {
const name = windowname_to_id(win.name);
const elem = document.getElementById(name);
if (elem.className.indexOf('vManyToManyRawIdAdminField') !== -1 && elem.value) {

if (elem.className.includes('vManyToManyRawIdAdminField') && elem.value) {
elem.value += `,${chosenId}`;
} else {
elem.value = chosenId;
Expand Down
75 changes: 52 additions & 23 deletions dynamic_raw_id/tests/test_integrity.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,13 +61,25 @@ def sample_obj(db: Any) -> dict[str, Any]:
return obj


def get_labelview_url(multi: bool = False) -> str:
@pytest.fixture()
def labelview_url() -> str:
"""
Create a URL to the JSON view that creates the dynamic labels.
""" ""
return reverse(
"dynamic_raw_id:dynamic_raw_id_label",
kwargs={"app_name": "testapp", "model_name": "modeltotest"},
)


@pytest.fixture()
def labelview_url_multi() -> str:
"""
Create a URL to the JSON view that creates the dynamic labels.
For multiple responses.
""" ""
name = multi and "dynamic_raw_id_multi_label" or "dynamic_raw_id_label"
return reverse(
f"dynamic_raw_id:{name}",
"dynamic_raw_id:dynamic_raw_id_multi_label",
kwargs={"app_name": "testapp", "model_name": "modeltotest"},
)

Expand All @@ -94,75 +106,92 @@ def test_change_integrity(admin_client: Client, sample_obj: Any) -> None:
assertContains(response, "dynamic_raw_id-related-lookup")


def test_labelview_unauthed(client: Client, sample_obj: Any) -> None:
def test_labelview_unauthed(
client: Client, sample_obj: Any, labelview_url: str
) -> None:
"""
Label view required authentication and a staff account
"""
response = client.get(get_labelview_url(), follow=True)

response = client.get(labelview_url, follow=True)
assert response.status_code == 404


def test_labelview_no_permission(
client: Client, staff_user: User, sample_obj: Any
client: Client, staff_user: User, sample_obj: Any, labelview_url: str
) -> None:
"""
Valid Label view request is denied if user has no change permisson for the app.
"""
client.login(username="staff", password="staff") # noqa: S106 Hardcoded password
response = client.get(
get_labelview_url(multi=True), {"id": sample_obj.pk}, follow=True
)
response = client.get(labelview_url, {"id": sample_obj.pk}, follow=True)
assert response.status_code == 403


def test_labelview(admin_client: Client, sample_obj: Any) -> None:
def test_labelview(admin_client: Client, sample_obj: Any, labelview_url: str) -> None:
"""
Call the labelview directly (what usually an Ajax JS call would do)
and check for proper response.
"""
response = admin_client.get(get_labelview_url(), {"id": sample_obj.pk}, follow=True)
response = admin_client.get(labelview_url, {"id": sample_obj.pk}, follow=True)
assert response.status_code == 200


def test_multi_labelview(admin_client: Client, sample_obj: Any) -> None:
def test_multi_labelview(
admin_client: Client, sample_obj: Any, labelview_url_multi: str
) -> None:
"""
Call the labelview directly (what usually an Ajax JS call would do)
and check for proper response. Exect a multi response.
"""
response = admin_client.get(
get_labelview_url(multi=True), {"id": sample_obj.pk}, follow=True
labelview_url_multi, {"id": f"{sample_obj.pk},{sample_obj.pk}"}, follow=True
)
assert response.status_code == 200


def test_invalid_id(admin_client: Client) -> None:
def test_invalid_id(admin_client: Client, labelview_url: str) -> None:
"""
Test label view with where model primary key is invalid.
It will just render an empty label, as if no ID was given.
"""
response = admin_client.get(get_labelview_url(), {"id": "wrong"}, follow=True)
assert response.status_code == 400
response = admin_client.get(labelview_url, {"id": "wrong"}, follow=True)
assert response.status_code == 200


def test_id_does_not_exist(admin_client: Client) -> None:
def test_invalid_multi_id(admin_client: Client, labelview_url_multi: str) -> None:
"""
Test label view with where model primary key is invalid.
It will just render an empty label, as if no ID was given.
"""
response = admin_client.get(
labelview_url_multi, {"id": ["very,wrong"]}, follow=True
)
assert response.status_code == 200


def test_id_does_not_exist(admin_client: Client, labelview_url: str) -> None:
"""
Test label view with where a model primary key does not exist.
It will just render an empty label, as if no ID was given.
"""
response = admin_client.get(get_labelview_url(), {"id": "123456"}, follow=True)
assert response.status_code == 400
response = admin_client.get(labelview_url, {"id": "123456"}, follow=True)
assert response.status_code == 200


def test_no_id(admin_client: Client) -> None:
def test_no_id(admin_client: Client, labelview_url: str) -> None:
"""
Test label view with no ID given.
"""
response = admin_client.get(get_labelview_url(), follow=True)
response = admin_client.get(labelview_url, follow=True)
assert response.status_code == 400


def test_invalid_appname(admin_client: Client) -> None:
def test_invalid_appname(admin_client: Client, labelview_url: str) -> None:
"""
Test label view with invalid app/model name.
"""
url = get_labelview_url().replace("testapp", "foobar")

url = labelview_url.replace("testapp", "foobar")
response = admin_client.get(url, {"id": "123456"}, follow=True)
assert response.status_code == 400
12 changes: 3 additions & 9 deletions dynamic_raw_id/urls.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,18 @@
from django.urls import path

from dynamic_raw_id.views import label_view
from .views import LabelView, MultiLabelView

app_name = "dynamic_raw_id"

urlpatterns = [
path(
"<slug:app_name>/<slug:model_name>/multiple/",
label_view,
{
"multi": True,
"template_object_name": "objects",
"template_name": "dynamic_raw_id/multi_label.html",
},
MultiLabelView.as_view(),
name="dynamic_raw_id_multi_label",
),
path(
"<slug:app_name>/<slug:model_name>/",
label_view,
{"template_name": "dynamic_raw_id/label.html"},
LabelView.as_view(),
name="dynamic_raw_id_label",
),
]
161 changes: 101 additions & 60 deletions dynamic_raw_id/views.py
Original file line number Diff line number Diff line change
@@ -1,70 +1,111 @@
from __future__ import annotations

from typing import TYPE_CHECKING, Any

from django.apps import apps
from django.conf import settings
from django.contrib.auth.decorators import user_passes_test
from django.db.models.base import ModelBase
from django.http import (
HttpRequest,
HttpResponse,
HttpResponseBadRequest,
HttpResponseForbidden,
HttpResponseNotFound,
)
from django.shortcuts import render
from django.urls import reverse
from django.views.generic import TemplateView

if TYPE_CHECKING:
from django.template import Context


class LabelView(TemplateView):
app_name: str
model_name: str
model: ModelBase
template_name = "dynamic_raw_id/label.html"
template_object_name: str = "object"

def dispatch(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
self.app_name = kwargs["app_name"]
self.model_name = kwargs["model_name"]

# User must be authorized and at least staff level.
# Intentionally raising a NotFound here to not indicate this is an API response.
if not request.user.is_authenticated or not request.user.is_staff:
return HttpResponseNotFound()

# User must have 'view' permission of the given app_name/model_name.
if not request.user.has_perm(f"{self.app_name}.view_{self.model_name}"):
return HttpResponseForbidden()

# The list of to obtained objects is in GET.id.
# No need to resume if we didn't get it.
if "id" not in request.GET:
msg = "No list of objects given"
return HttpResponseBadRequest(msg)

# Make sure, the given app_name/model_name exists.
try:
self.model = apps.get_model(self.app_name, self.model_name)
except LookupError:
return HttpResponseBadRequest()

return super().dispatch(request, *args, **kwargs)

def get_template_names(self) -> list[str]:
return [
f"dynamic_raw_id/{self.app_name}/{self.model_name}.html",
self.template_name,
]

def get_obj_context(self) -> tuple[str, Any] | None:
try:
obj = self.model.objects.get(pk=self.request.GET["id"])
except (self.model.DoesNotExist, ValueError):
return None

return (
str(obj),
reverse(f"admin:{self.app_name}_{self.model_name}_change", args=[obj.pk]),
)

def get_context_data(self, **kwargs: Any) -> Context:
context = super().get_context_data(**kwargs)
context.update(**{self.template_object_name: self.get_obj_context()})
return context


class MultiLabelView(LabelView):
"""
Same as LabelView, but accepts multiple GET.id values,
given as a comma separated list.
"""

multi: bool = False
template_name = "dynamic_raw_id/multi_label.html"
template_object_name: str = "objects"

def get_obj_context(self) -> list[tuple[str, Any]] | None:
try:
object_id_list = self.model.objects.filter(
pk__in=self.request.GET["id"].split(",")
)
except ValueError:
return None

objects = self.model.objects.filter(pk__in=object_id_list)
return [
(
str(obj),
reverse(
f"admin:{self.app_name}_{self.model_name}_change", args=[obj.pk]
),
)
for obj in objects
]

@user_passes_test(lambda u: u.is_staff)
def label_view( # noqa: PLR0913 Too Many arguments
request: HttpRequest,
app_name: str,
model_name: str,
template_name: str,
multi: bool = False,
template_object_name: str = "object",
) -> HttpResponse:
# The list of to obtained objects is in GET.id. No need to resume if we
# didn't get it.
if not request.GET.get("id"):
msg = "No list of objects given"
return HttpResponseBadRequest(settings.DEBUG and msg or "")

# Given objects are either an integer or a comma-separated list of
# integers. Validate them and ignore invalid values. Also strip them
# in case the user entered values by hand, such as '1, 2,3'.
object_list = [pk.strip() for pk in request.GET["id"].split(",")]

# Make sure this model exists and the user has 'change' permission for it.
# If he doesn't have this permission, Django would not display the
# change_list in the popup, and the user was never able to select objects.
try:
model = apps.get_model(app_name, model_name)
except LookupError:
msg = f"Model {app_name}.{model_name} does not exist."
return HttpResponseBadRequest(msg)

# Check 'view' permission
if not request.user.has_perm(f"{app_name}.view_{model_name}"):
return HttpResponseForbidden()

try:
if multi:
model_template = f"dynamic_raw_id/{app_name}/multi_{model_name}.html"
objs = model.objects.filter(pk__in=object_list)
objects = [
(obj, reverse(f"admin:{app_name}_{model_name}_change", args=[obj.pk]))
for obj in objs
]
extra_context = {template_object_name: objects}
else:
model_template = f"dynamic_raw_id/{app_name}/{model_name}.html"
obj = model.objects.get(pk=object_list[0])
change_url = reverse(f"admin:{app_name}_{model_name}_change", args=[obj.pk])
extra_context = {template_object_name: (obj, change_url)}

# most likely, the pk wasn't convertable
except ValueError:
msg = "ValueError during lookup"
return HttpResponseBadRequest(msg)
except model.DoesNotExist:
msg = "Model instance does not exist"
return HttpResponseBadRequest(msg)

return render(request, (model_template, template_name), extra_context)
def get_template_names(self) -> list[str]:
return [
f"dynamic_raw_id/{self.app_name}/multi_{self.model_name}.html",
self.template_name,
]
17 changes: 5 additions & 12 deletions dynamic_raw_id/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,19 @@
from django.utils.encoding import force_str

if TYPE_CHECKING:
from django.forms.renderers import BaseRenderer
from django.template import Context


class DynamicRawIDWidget(widgets.ForeignKeyRawIdWidget):
template_name: str = "dynamic_raw_id/admin/widgets/dynamic_raw_id_field.html"

def get_context(self, name: str, value: Any, attrs: dict[str, Any]) -> Context:
attrs.setdefault("class", "vForeignKeyRawIdAdminField")

context = super().get_context(name, value, attrs)
app_name = self.rel.model._meta.app_label # noqa: SLF001 Private member accessed
model_name = self.rel.model._meta.object_name.lower() # noqa: SLF001 Private member accessed

attrs.setdefault("class", "vForeignKeyRawIdAdminField")

context.update(
name=name,
app_name=app_name,
Expand All @@ -48,13 +47,7 @@ def media(self) -> forms.Media:


class DynamicRawIDMultiIdWidget(DynamicRawIDWidget):
def render(
self,
name: str,
value: Any,
attrs: dict[str, Any] | None = None,
renderer: BaseRenderer | None = None,
) -> str:
attrs["class"] = "vManyToManyRawIdAdminField"
def get_context(self, name: str, value: Any, attrs: dict[str, Any]) -> Context:
attrs.setdefault("class", "vManyToManyRawIdAdminField")
value = ",".join([force_str(v) for v in value]) if value else ""
return super().render(name, value, attrs, renderer=renderer)
return super().get_context(name, value, attrs)

0 comments on commit 103fecb

Please sign in to comment.