Skip to content

Commit

Permalink
feat(source/chant detail): add JSONResponseMixin
Browse files Browse the repository at this point in the history
  • Loading branch information
dchiller committed Dec 17, 2024
1 parent 012d63a commit d011a81
Show file tree
Hide file tree
Showing 5 changed files with 92 additions and 41 deletions.
8 changes: 4 additions & 4 deletions django/cantusdb_project/main_app/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ def user_can_proofread_chant(user: User, chant: Chant) -> bool:
return user_can_proofread_source(user, source)


def user_can_proofread_source(user: User, source: Source) -> bool:
def user_can_proofread_source(user: Union[User, AnonymousUser], source: Source) -> bool:
"""
Checks if the user can access the proofreading page of a given Source.
Used in SourceBrowseChantsView.
Expand All @@ -81,7 +81,7 @@ def user_can_proofread_source(user: User, source: Source) -> bool:
return user_is_pm or (user_is_editor and user_is_assigned_to_source)


def user_can_view_source(user: User, source: Source) -> bool:
def user_can_view_source(user: Union[User, AnonymousUser], source: Source) -> bool:
"""
Checks if the user can view an unpublished Source on the site.
Used in ChantDetail, SequenceDetail, and SourceDetail views.
Expand Down Expand Up @@ -149,7 +149,7 @@ def user_can_create_sources(user: User) -> bool:
).exists()


def user_can_edit_source(user: User, source: Source) -> bool:
def user_can_edit_source(user: Union[User, AnonymousUser], source: Source) -> bool:
"""
Checks if the user has permission to edit a Source object.
Used in SourceDetail, SourceEdit, and SourceDelete views.
Expand Down Expand Up @@ -182,7 +182,7 @@ def user_can_view_user_detail(viewing_user: User, user: User) -> bool:
return viewing_user.is_authenticated or user.is_indexer


def user_can_manage_source_editors(user: User) -> bool:
def user_can_manage_source_editors(user: Union[User, AnonymousUser]) -> bool:
"""
Checks if the user has permission to change the editors assigned to a Source.
Used in SourceDetailView.
Expand Down
61 changes: 35 additions & 26 deletions django/cantusdb_project/main_app/tests/test_views/test_chant.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,25 +29,31 @@
from main_app.tests.test_functions import mock_requests_get
from main_app.models import Chant, Source, Feast, Service
from main_app.views.chant import get_feast_selector_options
from users.models import User as UserAnnotation


# Create a Faker instance with locale set to Latin
faker = Faker("la")


class ChantDetailViewTest(TestCase):
pm_group: ClassVar[Group]
pm_user: ClassVar[UserAnnotation]

@classmethod
def setUpTestData(cls):
Group.objects.create(name="project manager")
def setUpTestData(cls) -> None:
cls.pm_group = Group.objects.create(name="project manager")
cls.pm_user = get_user_model().objects.create(email="[email protected]")
cls.pm_user.groups.add(cls.pm_group)

def test_url_and_templates(self):
def test_url_and_templates(self) -> None:
chant = make_fake_chant()
response = self.client.get(reverse("chant-detail", args=[chant.id]))
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, "base.html")
self.assertTemplateUsed(response, "chant_detail.html")

def test_context_folios(self):
def test_context_folios(self) -> None:
# create a source and several chants in it
source = make_fake_source()
chant = make_fake_chant(source=source, folio="001r")
Expand All @@ -62,7 +68,7 @@ def test_context_folios(self):
folios = response.context["folios"]
self.assertEqual(list(folios), ["001r", "001v", "002r", "002v"])

def test_context_previous_and_next_folio(self):
def test_context_previous_and_next_folio(self) -> None:
# create a source and several chants in it
source = make_fake_source()
# three folios: 001r, 001v, 002r
Expand Down Expand Up @@ -93,7 +99,7 @@ def test_context_previous_and_next_folio(self):
self.assertEqual(response.context["previous_folio"], "001v")
self.assertIsNone(response.context["next_folio"])

def test_published_vs_unpublished(self):
def test_published_vs_unpublished(self) -> None:
source = make_fake_source()
chant = make_fake_chant(source=source)

Expand All @@ -107,21 +113,11 @@ def test_published_vs_unpublished(self):
response = self.client.get(reverse("chant-detail", args=[chant.id]))
self.assertEqual(response.status_code, 403)

def test_chant_edit_link(self):
def test_chant_edit_link(self) -> None:
source = make_fake_source()
chant = make_fake_chant(
source=source,
folio="001r",
manuscript_full_text_std_spelling="manuscript_full_text_std_spelling",
)
chant = make_fake_chant(source=source, folio="001r")

# have to create project manager user - "View | Edit" toggle only visible for those with edit access for a chant's source
pm_user = get_user_model().objects.create(email="[email protected]")
pm_user.set_password("pass")
pm_user.save()
project_manager = Group.objects.get(name="project manager")
project_manager.user_set.add(pm_user)
self.client.login(email="[email protected]", password="pass")
self.client.force_login(self.pm_user)

response = self.client.get(reverse("chant-detail", args=[chant.id]))
expected_url_fragment = (
Expand All @@ -130,21 +126,23 @@ def test_chant_edit_link(self):

self.assertIn(expected_url_fragment, str(response.content))

def test_chant_with_volpiano_with_no_fulltext(self):
# in the past, a Chant Detail page will error rather than loading properly when the chant has volpiano but no fulltext
def test_chant_with_volpiano_with_no_fulltext(self) -> None:
# in the past, a Chant Detail page will error rather than loading
# properly when the chant has volpiano but no fulltext
source = make_fake_source()
chant = make_fake_chant(
source=source,
volpiano="1---c--g--e---e---d---c---c---f---e---e--d---d---c",
manuscript_full_text=None,
manuscript_full_text_std_spelling=None,
)
chant.manuscript_full_text = None
chant.manuscript_full_text_std_spelling = None
chant.save()

response = self.client.get(reverse("chant-detail", args=[chant.id]))
self.assertEqual(response.status_code, 200)

def test_chant_with_volpiano_with_no_incipit(self):
# in the past, a Chant Detail page will error rather than loading properly when the chant has volpiano but no fulltext/incipit
def test_chant_with_volpiano_with_no_incipit(self) -> None:
# in the past, a Chant Detail page will error rather than loading properly
# when the chant has volpiano but no fulltext/incipit
source = make_fake_source()
chant = make_fake_chant(
source=source,
Expand All @@ -157,6 +155,17 @@ def test_chant_with_volpiano_with_no_incipit(self):
response = self.client.get(reverse("chant-detail", args=[chant.id]))
self.assertEqual(response.status_code, 200)

def test_json_response(self) -> None:
chant = make_fake_chant()
response = self.client.get(
reverse("chant-detail", args=[chant.id]), HTTP_ACCEPT="application/json"
)
self.assertEqual(response.status_code, 200)
self.assertEqual(response["Content-Type"], "application/json")
resp_chant = response.json()["chant"]
self.assertEqual(resp_chant["id"], chant.id)
self.assertEqual(resp_chant["manuscript_full_text"], chant.manuscript_full_text)


class SourceEditChantsViewTest(TestCase):
@classmethod
Expand Down
27 changes: 20 additions & 7 deletions django/cantusdb_project/main_app/tests/test_views/test_source.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Group

from main_app.models import Source, Sequence, Chant, Differentia
from main_app.models import Source, Chant, Differentia
from main_app.tests.make_fakes import (
make_fake_source,
make_fake_segment,
Expand Down Expand Up @@ -122,14 +122,14 @@ def test_edit_source(self):


class SourceDetailViewTest(TestCase):
def test_url_and_templates(self):
def test_url_and_templates(self) -> None:
source = make_fake_source()
response = self.client.get(reverse("source-detail", args=[source.id]))
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, "base.html")
self.assertTemplateUsed(response, "source_detail.html")

def test_context_chant_folios(self):
def test_context_chant_folios(self) -> None:
# create a source and several chants in it
source = make_fake_source()
make_fake_chant(source=source, folio="001r")
Expand All @@ -144,7 +144,7 @@ def test_context_chant_folios(self):
folios = response.context["folios"]
self.assertEqual(list(folios), ["001r", "001v", "002r", "002v"])

def test_context_sequence_folios(self):
def test_context_sequence_folios(self) -> None:
# create a sequence source and several sequences in it
bower_segment = make_fake_segment(id=4064, name="Bower Sequence Database")
source = make_fake_source(
Expand All @@ -164,7 +164,7 @@ def test_context_sequence_folios(self):
# the folios should be ordered by the "folio" field
self.assertEqual(folios.query.order_by, ("folio",))

def test_context_sequences(self):
def test_context_sequences(self) -> None:
# create a sequence source and several sequences in it
source = make_fake_source(
segment=make_fake_segment(id=4064, name="Bower Sequence Database"),
Expand All @@ -179,7 +179,7 @@ def test_context_sequences(self):
# the list of sequences should be ordered by the "sequence" field
self.assertEqual(response.context["sequences"].query.order_by, ("s_sequence",))

def test_published_vs_unpublished(self):
def test_published_vs_unpublished(self) -> None:
source = make_fake_source(published=False)
response_1 = self.client.get(reverse("source-detail", args=[source.id]))
self.assertEqual(response_1.status_code, 403)
Expand All @@ -189,7 +189,7 @@ def test_published_vs_unpublished(self):
response_2 = self.client.get(reverse("source-detail", args=[source.id]))
self.assertEqual(response_2.status_code, 200)

def test_chant_list_link(self):
def test_chant_list_link(self) -> None:
cantus_segment = make_fake_segment(id=4063)
cantus_source = make_fake_source(segment=cantus_segment)
chant_list_link = reverse("browse-chants", args=[cantus_source.id])
Expand All @@ -209,6 +209,19 @@ def test_chant_list_link(self):
bower_source_html = str(bower_source_response.content)
self.assertNotIn(bower_chant_list_link, bower_source_html)

def test_json_response(self) -> None:
source = make_fake_source()
response = self.client.get(
reverse(
"source-detail",
args=[source.id],
),
headers={"Accept": "application/json"},
)
self.assertEqual(response.status_code, 200)
self.assertEqual(response["Content-Type"], "application/json")
self.assertEqual(response.json()["source"]["id"], source.id)


class SourceInventoryViewTest(TestCase):
def test_url_and_templates(self):
Expand Down
22 changes: 21 additions & 1 deletion django/cantusdb_project/main_app/views/chant.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
user_can_proofread_chant,
user_can_view_chant,
)
from main_app.mixins import JSONResponseMixin
from users.models import User

CHANT_SEARCH_TEMPLATE_VALUES: tuple[str, ...] = (
Expand Down Expand Up @@ -278,14 +279,33 @@ def get_chants_with_folios(chants_in_feast: QuerySet) -> list:
return list(folios_chants.items())


class ChantDetailView(DetailView): # type: ignore[type-arg]
class ChantDetailView(JSONResponseMixin, DetailView): # type: ignore[type-arg]
"""
Displays a single Chant object. Accessed with ``chants/<int:pk>``
"""

model = Chant
context_object_name = "chant"
template_name = "chant_detail.html"
json_fields = [
"id",
"folio",
"c_sequence",
"cantus_id",
"feast_id",
"service_id",
"genre_id",
"position",
"mode",
"differentia",
"differentiae_database",
"marginalia",
"finalis",
"manuscript_full_text",
"manuscript_full_text_std_spelling",
"volpiano",
"source_id",
]

def get_queryset(self) -> QuerySet[Chant]:
qs = super().get_queryset()
Expand Down
15 changes: 12 additions & 3 deletions django/cantusdb_project/main_app/views/source.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
get_feast_selector_options,
user_can_edit_chants_in_source,
)
from main_app.mixins import JSONResponseMixin

CANTUS_SEGMENT_ID = 4063
BOWER_SEGMENT_ID = 4064
Expand Down Expand Up @@ -205,17 +206,25 @@ def get_context_data(self, **kwargs):
return context


class SourceDetailView(DetailView):
class SourceDetailView(JSONResponseMixin, DetailView): # type: ignore[type-arg]
model = Source
context_object_name = "source"
template_name = "source_detail.html"
json_fields = [
"id",
"description",
"provenance__name",
"date",
"heading",
"short_heading",
]

def get_queryset(self):
def get_queryset(self) -> QuerySet[Source]:
return self.model.objects.select_related(
"holding_institution", "segment", "provenance", "created_by"
).all()

def get_context_data(self, **kwargs):
def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
source = self.object
user = self.request.user

Expand Down

0 comments on commit d011a81

Please sign in to comment.