give topic cards a colour before anyone picks one
رنگ و آیکونِ مبحث تازه اضافه شده بودند و هنوز هیچکس پرشان نکرده، یعنی کارتها تا وقتی کسی از پنلِ ادمین وقت بگذارد بیرنگ میماندند. حالا وقتی فیلد خالی است، مقدارِ پیشفرض از پالت و آیکونهای خودِ اپ میآید (همان رنگهای طرح و همان آیکونهایی که از lucide وارد میشوند)، تا بومی به نظر برسد نه وصله. چرخشی بر اساسِ جایگاهِ مبحث در پایه است، نه تصادفی: رنگی که بینِ دو بارگذاری عوض شود بدتر از بیرنگ بودن است، و دو مبحثِ کنارِ هم نباید یکرنگ دربیایند. مقدارِ صریحِ ادمین همیشه برنده است. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -150,3 +150,53 @@ class HomeGradeParamTests(TestCase):
|
|||||||
|
|
||||||
def test_an_unknown_grade_is_a_clean_404_not_a_silent_fallback(self):
|
def test_an_unknown_grade_is_a_clean_404_not_a_silent_fallback(self):
|
||||||
self.assertEqual(self.client.get('/api/home?gradeId=999999', **self.auth).status_code, 404)
|
self.assertEqual(self.client.get('/api/home?gradeId=999999', **self.auth).status_code, 404)
|
||||||
|
|
||||||
|
|
||||||
|
class NodeVisualDefaultsTests(TestCase):
|
||||||
|
"""
|
||||||
|
تا وقتی کسی از پنلِ ادمین رنگ نگذاشته، کارتِ مبحث نباید بیرنگ بماند.
|
||||||
|
|
||||||
|
پیشفرض باید *پایدار* باشد نه تصادفی: رنگی که بینِ دو بارگذاری عوض شود،
|
||||||
|
بدتر از بیرنگ بودن است.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
cache.clear()
|
||||||
|
self.grade = Grade.objects.create(title='پایه نهم', icon='', color='')
|
||||||
|
unit = Unit.objects.create(grade=self.grade, title='فصل ۱', order=1)
|
||||||
|
self.nodes = [
|
||||||
|
Node.objects.create(unit=unit, title=f'مبحث {i}', order=i)
|
||||||
|
for i in range(1, 4)
|
||||||
|
]
|
||||||
|
for node in self.nodes:
|
||||||
|
Lesson.objects.create(node=node, title='نوع', intro_text='x')
|
||||||
|
|
||||||
|
user = User.objects.create(phone='09120000003', grade_id=self.grade.id)
|
||||||
|
from accounts.authentication import issue_access_token
|
||||||
|
self.auth = {'HTTP_AUTHORIZATION': f'Bearer {issue_access_token(user.id)}'}
|
||||||
|
|
||||||
|
def _nodes(self):
|
||||||
|
return self.client.get('/api/home', **self.auth).json()['units'][0]['nodes']
|
||||||
|
|
||||||
|
def test_a_node_without_a_colour_still_gets_one(self):
|
||||||
|
for node in self._nodes():
|
||||||
|
self.assertTrue(node['color'].startswith('#'), node)
|
||||||
|
self.assertTrue(node['icon'])
|
||||||
|
|
||||||
|
def test_neighbouring_nodes_do_not_share_a_colour(self):
|
||||||
|
colors = [n['color'] for n in self._nodes()]
|
||||||
|
self.assertEqual(len(set(colors)), len(colors))
|
||||||
|
|
||||||
|
def test_the_same_node_keeps_the_same_colour_between_requests(self):
|
||||||
|
first = [n['color'] for n in self._nodes()]
|
||||||
|
cache.clear()
|
||||||
|
self.assertEqual(first, [n['color'] for n in self._nodes()])
|
||||||
|
|
||||||
|
def test_an_explicit_colour_from_the_admin_wins(self):
|
||||||
|
self.nodes[0].color = '#123456'
|
||||||
|
self.nodes[0].icon = 'Trophy'
|
||||||
|
self.nodes[0].save()
|
||||||
|
cache.clear()
|
||||||
|
first = self._nodes()[0]
|
||||||
|
self.assertEqual(first['color'], '#123456')
|
||||||
|
self.assertEqual(first['icon'], 'Trophy')
|
||||||
|
|||||||
@@ -94,6 +94,16 @@ def _grades_payload() -> dict:
|
|||||||
return cache.get_or_set(key, build, _CACHE_TTL)
|
return cache.get_or_set(key, build, _CACHE_TTL)
|
||||||
|
|
||||||
|
|
||||||
|
# پالت و آیکونِ پیشفرضِ کارتِ مبحث — هر دو از خودِ اپ گرفته شدهاند
|
||||||
|
# (رنگهای موجود در طرح، و آیکونهایی که از lucide وارد میشوند)، تا تا وقتی
|
||||||
|
# کسی از پنلِ ادمین مقدارِ دلخواه نگذاشته، کارتها بیرنگ نمانند.
|
||||||
|
#
|
||||||
|
# چرخشی و بر اساسِ جایگاهِ مبحث است، نه تصادفی: رنگ نباید بینِ دو درخواست
|
||||||
|
# عوض شود، و دو مبحثِ کنارِ هم نباید یکرنگ دربیایند.
|
||||||
|
_NODE_COLORS = ['#ED6956', '#91ADC6', '#97C0C0', '#9B7BFF', '#FF7A3D']
|
||||||
|
_NODE_ICONS = ['Grid3x3', 'TrendingUp', 'LineChart', 'Sigma', 'Compass', 'Repeat']
|
||||||
|
|
||||||
|
|
||||||
def _grade_tree(grade_id: int):
|
def _grade_tree(grade_id: int):
|
||||||
"""اسکلتِ گریدِ [{id,title,nodes:[{id,title,lesson_ids}]}] — بدونِ status/دیتای کاربر.
|
"""اسکلتِ گریدِ [{id,title,nodes:[{id,title,lesson_ids}]}] — بدونِ status/دیتای کاربر.
|
||||||
None یعنی گریدِ ناموجود؛ [] یعنی گریدِ موجودِ بدونِ واحد."""
|
None یعنی گریدِ ناموجود؛ [] یعنی گریدِ موجودِ بدونِ واحد."""
|
||||||
@@ -110,24 +120,25 @@ def _grade_tree(grade_id: int):
|
|||||||
Grade.objects.get(pk=grade_id)
|
Grade.objects.get(pk=grade_id)
|
||||||
.units.prefetch_related('nodes__lessons').all()
|
.units.prefetch_related('nodes__lessons').all()
|
||||||
)
|
)
|
||||||
tree = [
|
tree = []
|
||||||
{
|
position = 0 # شمارندهی سراسریِ مبحث در این پایه، برای چرخشِ رنگ
|
||||||
|
for unit in units:
|
||||||
|
nodes = []
|
||||||
|
for node in unit.nodes.all():
|
||||||
|
nodes.append({
|
||||||
|
'id': str(node.id),
|
||||||
|
'title': node.title,
|
||||||
|
'icon': node.icon or _NODE_ICONS[position % len(_NODE_ICONS)],
|
||||||
|
'color': node.color or _NODE_COLORS[position % len(_NODE_COLORS)],
|
||||||
|
'lesson_ids': [lsn.id for lsn in node.lessons.all()],
|
||||||
|
})
|
||||||
|
position += 1
|
||||||
|
tree.append({
|
||||||
'id': str(unit.id),
|
'id': str(unit.id),
|
||||||
'title': unit.title,
|
'title': unit.title,
|
||||||
'blurb': unit.blurb,
|
'blurb': unit.blurb,
|
||||||
'nodes': [
|
'nodes': nodes,
|
||||||
{
|
})
|
||||||
'id': str(node.id),
|
|
||||||
'title': node.title,
|
|
||||||
'icon': node.icon,
|
|
||||||
'color': node.color,
|
|
||||||
'lesson_ids': [lsn.id for lsn in node.lessons.all()],
|
|
||||||
}
|
|
||||||
for node in unit.nodes.all()
|
|
||||||
],
|
|
||||||
}
|
|
||||||
for unit in units
|
|
||||||
]
|
|
||||||
cache.set(key, tree, _CACHE_TTL)
|
cache.set(key, tree, _CACHE_TTL)
|
||||||
return tree
|
return tree
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user