Skip to content

Commit f2c0cde

Browse files
authored
Merge pull request #475 from Itqan-community/feat/recitation-folder-visibility-and-default
Feat/recitation folder visibility and default
2 parents a6f29d6 + acd6c9c commit f2c0cde

18 files changed

Lines changed: 405 additions & 33 deletions

apps/content/api/portal/recitation_folders.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ class FolderOut(Schema):
2929
name_en: str | None = None
3030
slug: str
3131
is_default: bool
32+
is_visible: bool
3233
tracks_count: int = 0
3334
created_at: AwareDatetime
3435
updated_at: AwareDatetime
@@ -46,6 +47,8 @@ class FolderCreateIn(Schema):
4647
class FolderPatchIn(Schema):
4748
name_ar: str | None = Field(default=None, max_length=255)
4849
name_en: str | None = Field(default=None, max_length=255)
50+
is_visible: bool | None = None
51+
is_default: bool | None = None
4952

5053

5154
# --- Helpers ---
@@ -107,7 +110,14 @@ def create_folder(request: Request, recitation_slug: str, data: FolderCreateIn):
107110
"recitations/{recitation_slug}/folders/{folder_slug}/",
108111
response={
109112
200: FolderOut,
110-
400: NinjaErrorResponse[Literal["folder_name_required"]],
113+
400: NinjaErrorResponse[
114+
Literal[
115+
"folder_name_required",
116+
"cannot_hide_default_folder",
117+
"cannot_unset_default_folder",
118+
"cannot_set_hidden_folder_as_default",
119+
]
120+
],
111121
401: NinjaErrorResponse[Literal["authentication_error"]],
112122
403: NinjaErrorResponse[Literal["permission_denied"]],
113123
404: NinjaErrorResponse[Literal["recitation_not_found"]] | NinjaErrorResponse[Literal["folder_not_found"]],

apps/content/api/portal/recitations.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ class FolderOut(Schema):
6565
name: str
6666
slug: str
6767
is_default: bool
68+
is_visible: bool
6869

6970

7071
class RecitationListOut(Schema):
@@ -136,7 +137,7 @@ def resolve_ayah_timings_url(obj: Asset) -> str | None:
136137
versions = list(obj.versions.filter(state=VersionStateChoice.PUBLISHED))
137138

138139
version = None
139-
if default_folder:
140+
if default_folder and default_folder.is_visible:
140141
version = next((v for v in versions if v.name == default_folder.slug), None)
141142
if version is None:
142143
# Recitations created before folders may still have a legacy version row.

apps/content/api/public/recitation_list.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
from apps.content.repositories.recitation import RecitationRepository
88
from apps.content.services.recitation import RecitationService
9-
from apps.content.services.recitation_folder_resolution import sorted_asset_folders
9+
from apps.content.services.recitation_folder_resolution import visible_asset_folders
1010
from apps.core.ninja_utils.ordering_base import ordering
1111
from apps.core.ninja_utils.router import ItqanRouter
1212
from apps.core.ninja_utils.searching_base import searching
@@ -77,7 +77,7 @@ def resolve_surahs_count(obj):
7777
@staticmethod
7878
def resolve_folders(obj):
7979
# Lets a consumer discover which ?folder= values this recitation accepts.
80-
return sorted_asset_folders(obj)
80+
return visible_asset_folders(obj)
8181

8282

8383
class RecitationFilter(FilterSchema):

apps/content/api/public/recitation_track_list.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ def list_recitation_tracks(
133133
Q(asset__restricted_for_tenant=False),
134134
prefetch_timings=True,
135135
folder=folder,
136+
require_visible_folder=True,
136137
)
137138

138139
all_results = []

apps/content/api/tenant/recitation_list.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from apps.content.models import Asset
77
from apps.content.repositories.recitation import RecitationRepository
88
from apps.content.services.recitation import RecitationService
9-
from apps.content.services.recitation_folder_resolution import sorted_asset_folders
9+
from apps.content.services.recitation_folder_resolution import visible_asset_folders
1010
from apps.core.ninja_utils.ordering_base import ordering
1111
from apps.core.ninja_utils.request import Request
1212
from apps.core.ninja_utils.router import ItqanRouter
@@ -54,7 +54,7 @@ class RecitationListOut(Schema):
5454
@staticmethod
5555
def resolve_folders(obj):
5656
# Lets a consumer discover which ?folder= values this recitation accepts.
57-
return sorted_asset_folders(obj)
57+
return visible_asset_folders(obj)
5858

5959

6060
class RecitationFilter(FilterSchema):

apps/content/api/tenant/recitation_track_list.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ def list_recitation_tracks(request: Request, asset_id: int, folder: str | None =
6262
if not asset:
6363
raise Http404(str(_("No asset matches the given query.")))
6464

65-
tracks = service.get_asset_tracks(asset_id, request.publisher_q("asset__publisher"), folder=folder)
65+
tracks = service.get_asset_tracks(
66+
asset_id, request.publisher_q("asset__publisher"), folder=folder, require_visible_folder=True
67+
)
6668

6769
return tracks

apps/content/cache.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33

44
from django.core.cache import cache
55

6+
from apps.core.ninja_utils.paginations import DEFAULT_PAGE_SIZE, PUBLIC_RECITATION_MAX_PAGE_SIZE
7+
68
# Stands in for "caller did not name a folder" in cache keys, so the default-folder
79
# response gets its own entry without a DB lookup to resolve the real slug.
810
DEFAULT_FOLDER_CACHE_TOKEN = "__default__"
@@ -62,3 +64,24 @@ def invalidate_recitation_tracks_cache(asset_id: int) -> None:
6264
recitation_asset_meta_cache_key(asset_id),
6365
]
6466
)
67+
68+
69+
def invalidate_recitation_folder_cache(asset_id: int, folder) -> None:
70+
"""
71+
Bust public recitation caches after a folder's visibility or default status changes.
72+
73+
Per-folder response keys for every page size cannot be enumerated cheaply, so the
74+
asset meta key is cleared to force rebuilds; known folder tokens are deleted when
75+
present so the common first-page case is immediate.
76+
"""
77+
invalidate_recitation_tracks_cache(asset_id)
78+
79+
tokens = {folder.slug, folder_cache_token(folder.slug)}
80+
for name in (folder.name, folder.name_ar, folder.name_en):
81+
if name:
82+
tokens.add(folder_cache_token(name))
83+
84+
for token in tokens:
85+
for page in (1,):
86+
for page_size in (DEFAULT_PAGE_SIZE, PUBLIC_RECITATION_MAX_PAGE_SIZE):
87+
cache.delete(recitation_response_cache_key(asset_id, page, page_size, token))
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
from django.db import migrations, models
2+
3+
4+
class Migration(migrations.Migration):
5+
dependencies = [
6+
("content", "0052_merge_20260826_1701"),
7+
]
8+
9+
operations = [
10+
migrations.AddField(
11+
model_name="recitationfolder",
12+
name="is_visible",
13+
field=models.BooleanField(
14+
db_index=True,
15+
default=True,
16+
help_text="When false, the folder is omitted from public/tenant APIs and its timings export is withdrawn.",
17+
),
18+
),
19+
]

apps/content/models.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -712,6 +712,11 @@ class RecitationFolder(BaseModel):
712712
default=False,
713713
help_text="The folder served when the API caller does not specify one. Exactly one per asset.",
714714
)
715+
is_visible = models.BooleanField(
716+
default=True,
717+
db_index=True,
718+
help_text="When false, the folder is omitted from public/tenant APIs and its timings export is withdrawn.",
719+
)
715720

716721
class Meta:
717722
constraints = [

apps/content/repositories/recitation_folder.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ def create_folder(
5656
is_default=is_default,
5757
)
5858

59-
def update_folder(self, folder: RecitationFolder, fields: dict[str, str | None]) -> RecitationFolder:
59+
def update_folder(self, folder: RecitationFolder, fields: dict[str, str | bool | None]) -> RecitationFolder:
6060
"""Apply field updates to a folder and save it."""
6161
for field, value in fields.items():
6262
setattr(folder, field, value)

0 commit comments

Comments
 (0)