Skip to content

Commit 16109dd

Browse files
committed
Add support for channel version tokens on v1 public channel endpoint
1 parent bbeeea9 commit 16109dd

3 files changed

Lines changed: 175 additions & 14 deletions

File tree

contentcuration/contentcuration/serializers.py

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,22 @@ def no_field_eval_repr(self):
2222
serializers.ModelSerializer.__repr__ = no_field_eval_repr
2323

2424

25+
def get_thumbnail_encoding(channel):
26+
"""
27+
Historically, we did not set channel.icon_encoding in the Studio database. We
28+
only set it in the exported Kolibri sqlite db. So when Kolibri asks for the channel
29+
information, fall back to the channel thumbnail data if icon_encoding is not set.
30+
"""
31+
if channel.icon_encoding:
32+
return channel.icon_encoding
33+
if channel.thumbnail_encoding:
34+
base64 = channel.thumbnail_encoding.get("base64")
35+
if base64:
36+
return base64
37+
38+
return None
39+
40+
2541
class PublicChannelSerializer(serializers.ModelSerializer):
2642
"""
2743
Called by the public API, primarily used by Kolibri. Contains information more specific to Kolibri's needs.
@@ -41,19 +57,7 @@ def match_tokens(self, channel):
4157
)
4258

4359
def get_thumbnail_encoding(self, channel):
44-
"""
45-
Historically, we did not set channel.icon_encoding in the Studio database. We
46-
only set it in the exported Kolibri sqlite db. So when Kolibri asks for the channel
47-
information, fall back to the channel thumbnail data if icon_encoding is not set.
48-
"""
49-
if channel.icon_encoding:
50-
return channel.icon_encoding
51-
if channel.thumbnail_encoding:
52-
base64 = channel.thumbnail_encoding.get("base64")
53-
if base64:
54-
return base64
55-
56-
return None
60+
return get_thumbnail_encoding(channel)
5761

5862
def generate_kind_count(self, channel):
5963
return channel.published_kind_count and json.loads(channel.published_kind_count)

contentcuration/kolibri_public/tests/test_public_v1_api.py

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from django.core.cache import cache
33
from django.urls import reverse
44

5+
from contentcuration.models import ChannelVersion
56
from contentcuration.tests.base import BaseAPITestCase
67
from contentcuration.tests.testdata import generated_base64encoding
78

@@ -74,3 +75,100 @@ def test_public_channels_endpoint(self):
7475
self.assertEqual(first_channel["name"], self.channel.name)
7576
self.assertEqual(first_channel["id"], self.channel.id)
7677
self.assertEqual(first_channel["icon_encoding"], generated_base64encoding())
78+
79+
def test_public_channel_lookup_with_channel_version_token_uses_channel_version(
80+
self,
81+
):
82+
"""
83+
A channel version token should resolve to the matched ChannelVersion,
84+
not the channel's current published version.
85+
"""
86+
self.channel.main_tree.published = True
87+
self.channel.main_tree.save()
88+
89+
self.channel.version = 7
90+
self.channel.published_data = {
91+
"2": {"version_notes": "v2 notes"},
92+
"4": {"version_notes": "v4 notes"},
93+
"7": {"version_notes": "v7 notes"},
94+
}
95+
self.channel.save()
96+
97+
channel_version, _created = ChannelVersion.objects.get_or_create(
98+
channel=self.channel,
99+
version=4,
100+
defaults={
101+
"kind_count": [],
102+
"included_languages": [],
103+
"resource_count": 0,
104+
"size": 0,
105+
},
106+
)
107+
version_token = channel_version.new_token().token
108+
109+
lookup_url = reverse(
110+
"get_public_channel_lookup",
111+
kwargs={"version": "v1", "identifier": version_token},
112+
)
113+
response = self.client.get(lookup_url)
114+
115+
self.assertEqual(response.status_code, 200)
116+
self.assertEqual(len(response.data), 1)
117+
self.assertEqual(response.data[0]["version"], 4)
118+
self.assertNotEqual(response.data[0]["version"], self.channel.version)
119+
self.assertEqual(
120+
response.data[0]["version_notes"], {2: "v2 notes", 4: "v4 notes"}
121+
)
122+
123+
def test_public_channel_lookup_channel_version_and_channel_tokens_have_same_keys(
124+
self,
125+
):
126+
"""
127+
Lookup responses from channel-version-token and channel-token endpoints
128+
should expose the same top-level keys, even if values differ.
129+
"""
130+
self.channel.main_tree.published = True
131+
self.channel.main_tree.save()
132+
133+
self.channel.version = 9
134+
self.channel.published_data = {
135+
"3": {"version_notes": "v3 notes"},
136+
"9": {"version_notes": "v9 notes"},
137+
}
138+
self.channel.save()
139+
140+
latest_channel_version, _created = ChannelVersion.objects.get_or_create(
141+
channel=self.channel,
142+
version=9,
143+
defaults={
144+
"kind_count": [],
145+
"included_languages": [],
146+
"resource_count": 0,
147+
"size": 0,
148+
},
149+
)
150+
latest_version_token = latest_channel_version.new_token().token
151+
channel_token = self.channel.make_token().token
152+
153+
channel_version_response = self.client.get(
154+
reverse(
155+
"get_public_channel_lookup",
156+
kwargs={"version": "v1", "identifier": latest_version_token},
157+
)
158+
)
159+
channel_response = self.client.get(
160+
reverse(
161+
"get_public_channel_lookup",
162+
kwargs={"version": "v1", "identifier": channel_token},
163+
)
164+
)
165+
166+
self.assertEqual(channel_version_response.status_code, 200)
167+
self.assertEqual(channel_response.status_code, 200)
168+
self.assertEqual(len(channel_version_response.data), 1)
169+
self.assertEqual(len(channel_response.data), 1)
170+
171+
self.assertSetEqual(
172+
set(channel_version_response.data[0].keys()),
173+
set(channel_response.data[0].keys()),
174+
)

contentcuration/kolibri_public/views_v1.py

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import json
2+
from collections import OrderedDict
23

34
from django.conf import settings
45
from django.contrib.sites.models import Site
@@ -19,6 +20,8 @@
1920

2021
from contentcuration.decorators import cache_no_user_data
2122
from contentcuration.models import Channel
23+
from contentcuration.models import ChannelVersion
24+
from contentcuration.serializers import get_thumbnail_encoding
2225
from contentcuration.serializers import PublicChannelSerializer
2326

2427

@@ -28,6 +31,43 @@ def _get_channel_list(version, params, identifier=None):
2831
raise LookupError()
2932

3033

34+
def _get_version_notes(channel, channel_version):
35+
data = {
36+
int(k): v["version_notes"]
37+
for k, v in channel.published_data.items()
38+
if int(k) <= channel_version.version
39+
}
40+
return OrderedDict(sorted(data.items()))
41+
42+
43+
def _serialize_channel_version(channel_version_qs):
44+
channel_version = channel_version_qs.first()
45+
if not channel_version or not channel_version.channel:
46+
return []
47+
48+
channel = channel_version.channel
49+
return [
50+
{
51+
"id": channel.id,
52+
"name": channel.name,
53+
"language": channel.language_id,
54+
"public": channel.public,
55+
"description": channel.description,
56+
"icon_encoding": get_thumbnail_encoding(channel),
57+
"version_notes": _get_version_notes(channel, channel_version),
58+
"version": channel_version.version,
59+
"kind_count": channel_version.kind_count,
60+
"included_languages": channel_version.included_languages,
61+
"total_resource_count": channel_version.resource_count,
62+
"published_size": channel_version.size,
63+
"last_published": channel_version.date_published,
64+
"matching_tokens": [channel_version.secret_token.token]
65+
if channel_version.secret_token
66+
else [],
67+
}
68+
]
69+
70+
3171
def _get_channel_list_v1(params, identifier=None):
3272
keyword = params.get("keyword", "").strip()
3373
language_id = params.get("language", "").strip()
@@ -40,6 +80,20 @@ def _get_channel_list_v1(params, identifier=None):
4080
)
4181
if not channels.exists():
4282
channels = Channel.objects.filter(pk=identifier)
83+
84+
if not channels.exists():
85+
# If channels doesnt exist with the given token, check if this is a token of
86+
# a channel version.
87+
channel_version = ChannelVersion.objects.select_related(
88+
"secret_token", "channel"
89+
).filter(
90+
secret_token__token=identifier,
91+
channel__deleted=False,
92+
channel__main_tree__published=True,
93+
)
94+
if channel_version.exists():
95+
# return early as we won't need to apply the other filters for channel version tokens
96+
return channel_version
4397
else:
4498
channels = Channel.objects.prefetch_related("secret_tokens").filter(
4599
Q(public=True) | Q(secret_tokens__token__in=token_list)
@@ -96,7 +150,12 @@ def get_public_channel_lookup(request, version, identifier):
96150
return HttpResponseNotFound(
97151
_("No channel matching {} found").format(escape(identifier))
98152
)
99-
return Response(PublicChannelSerializer(channel_list, many=True).data)
153+
154+
if channel_list.model == ChannelVersion:
155+
channel_list = _serialize_channel_version(channel_list)
156+
return Response(channel_list)
157+
else:
158+
return Response(PublicChannelSerializer(channel_list, many=True).data)
100159

101160

102161
@api_view(["GET"])

0 commit comments

Comments
 (0)