From 60bb4b232365d90d2efad0f6e57e28e7db156efd Mon Sep 17 00:00:00 2001 From: Stefan Kairinos Date: Tue, 3 Dec 2024 15:54:23 +0000 Subject: [PATCH] feat: worksheet control (#1751) --- ...ve_episode_indy_worksheet_link_and_more.py | 100 +++++++++++ game/migrations/0111_create_worksheets.py | 149 ++++++++++++++++ .../0112_worksheet_locked_classes.py | 21 +++ game/models.py | 59 +++++-- .../game/python_den_level_selection.html | 163 ++++++++++-------- game/views/level_selection.py | 32 ++-- 6 files changed, 422 insertions(+), 102 deletions(-) create mode 100644 game/migrations/0110_remove_episode_indy_worksheet_link_and_more.py create mode 100644 game/migrations/0111_create_worksheets.py create mode 100644 game/migrations/0112_worksheet_locked_classes.py diff --git a/game/migrations/0110_remove_episode_indy_worksheet_link_and_more.py b/game/migrations/0110_remove_episode_indy_worksheet_link_and_more.py new file mode 100644 index 000000000..4357a4a8d --- /dev/null +++ b/game/migrations/0110_remove_episode_indy_worksheet_link_and_more.py @@ -0,0 +1,100 @@ +# Generated by Django 4.2.16 on 2024-12-02 15:17 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ("game", "0109_create_episodes_23_and_24"), + ] + + operations = [ + migrations.RemoveField( + model_name="episode", + name="indy_worksheet_link", + ), + migrations.RemoveField( + model_name="episode", + name="lesson_plan_link", + ), + migrations.RemoveField( + model_name="episode", + name="slides_link", + ), + migrations.RemoveField( + model_name="episode", + name="student_worksheet_link", + ), + migrations.RemoveField( + model_name="episode", + name="video_link", + ), + migrations.CreateModel( + name="Worksheet", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "lesson_plan_link", + models.CharField( + blank=True, default=None, max_length=500, null=True + ), + ), + ( + "slides_link", + models.CharField( + blank=True, default=None, max_length=500, null=True + ), + ), + ( + "student_worksheet_link", + models.CharField( + blank=True, default=None, max_length=500, null=True + ), + ), + ( + "indy_worksheet_link", + models.CharField( + blank=True, default=None, max_length=500, null=True + ), + ), + ( + "video_link", + models.CharField( + blank=True, default=None, max_length=500, null=True + ), + ), + ( + "before_level", + models.OneToOneField( + blank=True, + default=None, + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="after_worksheet", + to="game.level", + ), + ), + ( + "episode", + models.ForeignKey( + blank=True, + default=None, + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="worksheets", + to="game.episode", + ), + ), + ], + ), + ] diff --git a/game/migrations/0111_create_worksheets.py b/game/migrations/0111_create_worksheets.py new file mode 100644 index 000000000..933d908d2 --- /dev/null +++ b/game/migrations/0111_create_worksheets.py @@ -0,0 +1,149 @@ +from django.apps.registry import Apps +from django.db import migrations + + +def create_worksheets(apps: Apps, *args): + Episode = apps.get_model("game", "Episode") + Worksheet = apps.get_model("game", "Worksheet") + Level = apps.get_model("game", "Level") + + Worksheet.objects.bulk_create( + [ + Worksheet( + episode=Episode.objects.get(pk=16), + before_level=None, + lesson_plan_link="https://code-for-life.gitbook.io/python-lessons-with-raspberry-pi-ide/DGiFT28ihVJK7ghZQHqu/python-1-output-operators-and-data", + slides_link="https://4077022412-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FwAuC4Q5WQz4ea2O2b2JP%2Fuploads%2FEPwYyCp6pw2WqdeV84OY%2FPython%201%20-%20Output%2C%20Operators%20and%20Data.pptx?alt=media&token=771964a8-acab-4503-aa3d-034f24a93d3d", + student_worksheet_link="https://code-for-life.gitbook.io/student-resources/python-den-student-resources/worksheet-1-output-operators-and-data", + indy_worksheet_link="https://code-for-life.gitbook.io/independent-student-resources/python-den-resources-beta/session-1-output-operators-and-data", + video_link="https://youtu.be/ve0RTsLGli0", + ), + Worksheet( + episode=Episode.objects.get(pk=17), + before_level=None, + lesson_plan_link="https://code-for-life.gitbook.io/python-lessons-with-raspberry-pi-ide/DGiFT28ihVJK7ghZQHqu/python-2-variables-input-and-casting", + slides_link="https://4077022412-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FwAuC4Q5WQz4ea2O2b2JP%2Fuploads%2FLFOUoXuOPUmeOdZ09cHW%2FPython%202%20-%20Variables%2C%20Input%20and%20Casting.pptx?alt=media&token=ef6d1581-36bd-4885-9a99-4dac53eee4c3", + student_worksheet_link="https://code-for-life.gitbook.io/student-resources/python-den-student-resources/worksheet-2-variables-input-and-casting", + indy_worksheet_link="https://code-for-life.gitbook.io/independent-student-resources/python-den-resources-beta/session-2-variables-input-and-casting", + video_link="https://www.youtube.com/watch?v=H8askW-zd3I", + ), + Worksheet( + episode=Episode.objects.get(pk=18), + before_level=None, + lesson_plan_link="https://code-for-life.gitbook.io/python-lessons-with-raspberry-pi-ide/DGiFT28ihVJK7ghZQHqu/python-3-selection", + slides_link="https://4077022412-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FwAuC4Q5WQz4ea2O2b2JP%2Fuploads%2F2mfkW4jWmJutkItlEnje%2FPython%203%20-%20Selection.pptx?alt=media&token=f6056de3-ef31-4031-aab8-be95da17d4cd", + student_worksheet_link="https://code-for-life.gitbook.io/student-resources/python-den-student-resources/worksheet-3-selection", + indy_worksheet_link="https://code-for-life.gitbook.io/independent-student-resources/python-den-resources-beta/session-3-selection", + video_link="https://www.youtube.com/watch?v=3XiQ97kP7H8", + ), + Worksheet( + episode=Episode.objects.get(pk=19), + before_level=None, + lesson_plan_link="https://code-for-life.gitbook.io/python-lessons-with-raspberry-pi-ide/DGiFT28ihVJK7ghZQHqu/python-4-complex-selection", + slides_link="https://4077022412-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FwAuC4Q5WQz4ea2O2b2JP%2Fuploads%2Fgx68HomHKoxDxcoC72n6%2FPython%204%20-%20Complex%20Selection.pptx?alt=media&token=7d7c2c77-57a8-42a9-980b-015ff9b5b818", + student_worksheet_link="https://code-for-life.gitbook.io/student-resources/python-den-student-resources/worksheet-4-complex-selection", + indy_worksheet_link="https://code-for-life.gitbook.io/independent-student-resources/python-den-resources-beta/session-4-complex-selection", + video_link="https://youtu.be/QOe5G-ZvWoc", + ), + Worksheet( + episode=Episode.objects.get(pk=12), + before_level=Level.objects.get(pk=156), + lesson_plan_link="https://code-for-life.gitbook.io/python-lessons-with-raspberry-pi-ide/DGiFT28ihVJK7ghZQHqu/python-5-iteration-part-1", + slides_link="https://4077022412-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FwAuC4Q5WQz4ea2O2b2JP%2Fuploads%2FG1pLAqvXg6s6hGtnb5ss%2FPython%205%20-%20Iteration%201.pptx?alt=media&token=d9bf3e3f-bf12-4227-89b7-70515e16dcc9", + student_worksheet_link="https://code-for-life.gitbook.io/student-resources/python-den-student-resources/worksheet-5-iteration-part-1", + indy_worksheet_link="https://code-for-life.gitbook.io/independent-student-resources/python-den-resources-beta/session-5-iteration-part-1", + video_link="https://youtu.be/nJm3cWSkoi0", + ), + Worksheet( + episode=Episode.objects.get(pk=12), + before_level=None, + lesson_plan_link="https://code-for-life.gitbook.io/python-lessons-with-raspberry-pi-ide/DGiFT28ihVJK7ghZQHqu/python-6-iteration-part-2", + slides_link="https://4077022412-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FwAuC4Q5WQz4ea2O2b2JP%2Fuploads%2FcEO90C76g4N17ss2K5PN%2FPython%206%20-%20Iteration%202.pptx?alt=media&token=df237a62-21ad-44a8-a4fe-bc9eed24092b", + student_worksheet_link="https://code-for-life.gitbook.io/student-resources/python-den-student-resources/worksheet-6-iteration-part-2", + indy_worksheet_link="https://code-for-life.gitbook.io/independent-student-resources/python-den-resources-beta/session-6-iteration-part-2", + video_link="https://youtu.be/kf-EavpnBNg", + ), + Worksheet( + episode=Episode.objects.get(pk=13), + before_level=Level.objects.get(pk=157), + lesson_plan_link="https://code-for-life.gitbook.io/python-lessons-with-raspberry-pi-ide/DGiFT28ihVJK7ghZQHqu/python-7-selection-in-a-loop", + slides_link="https://4077022412-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FwAuC4Q5WQz4ea2O2b2JP%2Fuploads%2FPtj4c6IjIafaUPN8k4u1%2FPython%207%20-%20Selection%20in%20a%20loop.pptx?alt=media&token=9f5fce8e-bb72-4e38-8715-89ec7bcc6d6c", + student_worksheet_link="https://code-for-life.gitbook.io/student-resources/python-den-student-resources/worksheet-7-selection-in-a-loop", + indy_worksheet_link="https://code-for-life.gitbook.io/independent-student-resources/python-den-resources-beta/session-7-selection-in-a-loop", + video_link="https://www.youtube.com/watch?v=WV_PLCcohMg", + ), + Worksheet( + episode=Episode.objects.get(pk=14), + before_level=Level.objects.get(pk=169), + lesson_plan_link="https://code-for-life.gitbook.io/python-lessons-with-raspberry-pi-ide/DGiFT28ihVJK7ghZQHqu/python-8-indeterminate-loops", + slides_link="https://4077022412-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FwAuC4Q5WQz4ea2O2b2JP%2Fuploads%2FkxewOibLrvKLpJpuDBRH%2FPython%208%20-%20Indeterminate%20Loops.pptx?alt=media&token=ddf212d5-f0a7-47b2-8a6b-420ca2cccc12", + student_worksheet_link="https://code-for-life.gitbook.io/student-resources/python-den-student-resources/worksheet-8-indeterminate-loops", + indy_worksheet_link="https://code-for-life.gitbook.io/independent-student-resources/python-den-resources-beta/session-8-indeterminate-loops", + video_link="https://www.youtube.com/watch?v=XgMGI8UzMDM", + ), + Worksheet( + episode=Episode.objects.get(pk=20), + before_level=None, + lesson_plan_link="https://code-for-life.gitbook.io/python-lessons-with-raspberry-pi-ide/DGiFT28ihVJK7ghZQHqu/python-9-string-manipulation", + slides_link="https://4077022412-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FwAuC4Q5WQz4ea2O2b2JP%2Fuploads%2FOAj7ngrHzqNAJgcYLz55%2FPython%209%20-%20String%20manipulation.pptx?alt=media&token=a7bdb069-6321-4828-8218-d4eabf4605ad", + student_worksheet_link="https://code-for-life.gitbook.io/student-resources/python-den-student-resources/worksheet-9-string-manipulation", + indy_worksheet_link="https://code-for-life.gitbook.io/independent-student-resources/python-den-resources-beta/session-9-string-manipulation", + video_link="https://www.youtube.com/watch?v=E4AMg57_eoI", + ), + Worksheet( + episode=Episode.objects.get(pk=21), + before_level=None, + lesson_plan_link="https://code-for-life.gitbook.io/python-lessons-with-raspberry-pi-ide/DGiFT28ihVJK7ghZQHqu/python-10-1d-lists", + slides_link="https://4077022412-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FwAuC4Q5WQz4ea2O2b2JP%2Fuploads%2FZdt2shodTgZjNH4ai7ky%2FPython%2010%20-1D%20Lists.pptx?alt=media&token=66bdc866-2144-4203-b55e-93fa04b977a3", + student_worksheet_link="https://code-for-life.gitbook.io/student-resources/python-den-student-resources/worksheet-10-1d-lists", + indy_worksheet_link="https://code-for-life.gitbook.io/independent-student-resources/python-den-resources-beta/session-10-1d-lists", + video_link="https://www.youtube.com/watch?v=Kb_-7IqWV0E", + ), + Worksheet( + episode=Episode.objects.get(pk=15), + before_level=Level.objects.get(pk=184), + lesson_plan_link="https://code-for-life.gitbook.io/python-lessons-with-raspberry-pi-ide/DGiFT28ihVJK7ghZQHqu/python-11-using-for-loops", + slides_link="https://4077022412-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FwAuC4Q5WQz4ea2O2b2JP%2Fuploads%2F8AjR4b5mO4yOzEIVNI4V%2FPython%2011%20-%20For%20loops.pptx?alt=media&token=5eaa671c-a5eb-43a3-9f13-3478349027e5", + student_worksheet_link="https://code-for-life.gitbook.io/student-resources/python-den-student-resources/worksheet-11-for-loops", + indy_worksheet_link="https://code-for-life.gitbook.io/independent-student-resources/python-den-resources-beta/session-11-using-for-loops", + video_link="https://www.youtube.com/watch?v=jNT_gQx9-k8", + ), + Worksheet( + episode=Episode.objects.get(pk=23), + before_level=None, + lesson_plan_link="https://code-for-life.gitbook.io/python-lessons-with-raspberry-pi-ide/EMEzsyyl4uRclyA9LDGN/python-12-2d-lists", + slides_link="https://4077022412-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FwAuC4Q5WQz4ea2O2b2JP%2Fuploads%2F3Q4X7ZDk1TNej5c2e7LW%2FPython%2012%202D%20Lists.pptx?alt=media&token=847d9d69-4610-4c67-a649-0c6e0f88d372", + student_worksheet_link="https://code-for-life.gitbook.io/student-resources/python-den-student-resources/worksheet-12-2d-lists", + indy_worksheet_link="https://code-for-life.gitbook.io/independent-student-resources/python-den-resources-beta/session-12-2d-lists", + video_link="https://www.youtube.com/watch?v=MBU49ivZk6w", + ), + Worksheet( + episode=Episode.objects.get(pk=24), + before_level=None, + lesson_plan_link="https://code-for-life.gitbook.io/python-lessons-with-raspberry-pi-ide/EMEzsyyl4uRclyA9LDGN/python-13-procedures-and-functions", + slides_link="https://4077022412-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FwAuC4Q5WQz4ea2O2b2JP%2Fuploads%2Fmr6FvFzY3LVuaGT1zd6d%2FPython%2013%20Procedures%20and%20Functions.pptx?alt=media&token=c5835e00-8d42-4567-8cf4-868b0f23dc0a", + student_worksheet_link="https://code-for-life.gitbook.io/student-resources/python-den-student-resources/worksheet-13-procedures-and-functions", + indy_worksheet_link="https://code-for-life.gitbook.io/independent-student-resources/python-den-resources-beta/session-13-procedures-and-functions", + video_link="https://www.youtube.com/watch?v=LJMfI7P3Dzk", + ), + ] + ) + + +def delete_worksheets(apps: Apps, *args): + Worksheet = apps.get_model("game", "Worksheet") + Worksheet.objects.all().delete() + + +class Migration(migrations.Migration): + + dependencies = [ + ("game", "0110_remove_episode_indy_worksheet_link_and_more"), + ] + + operations = [ + migrations.RunPython( + code=create_worksheets, + reverse_code=delete_worksheets, + ) + ] diff --git a/game/migrations/0112_worksheet_locked_classes.py b/game/migrations/0112_worksheet_locked_classes.py new file mode 100644 index 000000000..a13d4b9e4 --- /dev/null +++ b/game/migrations/0112_worksheet_locked_classes.py @@ -0,0 +1,21 @@ +# Generated by Django 4.2.16 on 2024-12-03 11:01 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("common", "0055_alter_schoolteacherinvitation_token"), + ("game", "0111_create_worksheets"), + ] + + operations = [ + migrations.AddField( + model_name="worksheet", + name="locked_classes", + field=models.ManyToManyField( + blank=True, related_name="locked_worksheets", to="common.class" + ), + ), + ] diff --git a/game/models.py b/game/models.py index 78efb6e11..9de1012e8 100644 --- a/game/models.py +++ b/game/models.py @@ -1,8 +1,10 @@ +import typing as t from builtins import str from common.models import Class, Student, UserProfile from django.contrib.auth.models import User from django.db import models +from django.db.models.query import QuerySet def theme_choices(): @@ -42,6 +44,8 @@ class Meta: class Episode(models.Model): """Variables prefixed with r_ signify they are parameters for random level generation""" + + worksheets: QuerySet["Worksheet"] name = models.CharField(max_length=200) next_episode = models.ForeignKey( @@ -60,22 +64,6 @@ class Episode(models.Model): r_traffic_lights = models.BooleanField(default=False) r_cows = models.BooleanField(default=False) - lesson_plan_link = models.CharField( - max_length=500, null=True, blank=True, default=None - ) - slides_link = models.CharField( - max_length=500, null=True, blank=True, default=None - ) - student_worksheet_link = models.CharField( - max_length=500, null=True, blank=True, default=None - ) - indy_worksheet_link = models.CharField( - max_length=500, null=True, blank=True, default=None - ) - video_link = models.CharField( - max_length=500, null=True, blank=True, default=None - ) - @property def first_level(self): return self.levels[0] @@ -144,6 +132,8 @@ def sort_levels(levels): class Level(models.Model): + after_worksheet: t.Optional["Worksheet"] + name = models.CharField(max_length=100) episode = models.ForeignKey( Episode, blank=True, null=True, default=None, on_delete=models.PROTECT @@ -332,3 +322,40 @@ class Attempt(models.Model): def elapsed_time(self): return self.finish_time - self.start_time + + +class Worksheet(models.Model): + episode = models.ForeignKey( + Episode, + blank=True, + null=True, + default=None, + on_delete=models.PROTECT, + related_name="worksheets", + ) + before_level = models.OneToOneField( + Level, + blank=True, + null=True, + default=None, + on_delete=models.PROTECT, + related_name="after_worksheet", + ) + lesson_plan_link = models.CharField( + max_length=500, null=True, blank=True, default=None + ) + slides_link = models.CharField( + max_length=500, null=True, blank=True, default=None + ) + student_worksheet_link = models.CharField( + max_length=500, null=True, blank=True, default=None + ) + indy_worksheet_link = models.CharField( + max_length=500, null=True, blank=True, default=None + ) + video_link = models.CharField( + max_length=500, null=True, blank=True, default=None + ) + locked_classes = models.ManyToManyField( + Class, blank=True, related_name="locked_worksheets" + ) diff --git a/game/templates/game/python_den_level_selection.html b/game/templates/game/python_den_level_selection.html index 019da611d..9c6be6710 100644 --- a/game/templates/game/python_den_level_selection.html +++ b/game/templates/game/python_den_level_selection.html @@ -76,26 +76,26 @@

Introduction to Python

{% if user|is_logged_in_as_teacher %}
- + Lesson plan
- + Slides
{% elif user|is_independent_student %}
- + Worksheet
- + Video
{% else %}
- + Worksheet
{% endif %} @@ -146,46 +146,52 @@

Introduction to Python

- {% if user|is_logged_in %} - - {% if user|is_logged_in_as_teacher %} - - - {% elif user|is_independent_student %} - - - {% else %} -
- - {% endif %} - {% else %} -

In order to access the full content for this course, please log in.

- - - {% endif %} {% for level in episode.levels %} + {% for worksheet in episode.worksheets %} + {% if worksheet.before_level == level.id %} + {% if not user|is_logged_in or user.new_student.class_field in worksheet.locked_classes.all %} + {% if not user|is_logged_in %} +

In order to access the full content for this course, please log in.

+ {% endif %} + + + {% else %} + + {% if user|is_logged_in_as_teacher %} + + + {% elif user|is_independent_student %} + + + {% else %} +
+ + {% endif %} + {% endif %} + {% endif %} + {% endfor %} {% if user|is_logged_in_as_student and user.new_student.class_field in level.locked_for_class.all %}

@@ -214,46 +220,51 @@

Introduction to Python

{% endif %} {% endfor %} - {% if episode.id == 12 %} - {% if user|is_logged_in %} - - {% if user|is_logged_in_as_teacher %} -
- - Lesson plan + {% for worksheet in episode.worksheets %} + {% if not worksheet.before_level %} + {% if not user|is_logged_in or user.new_student.class_field in worksheet.locked_classes.all %} + {% if not user|is_logged_in %} +

In order to access the full content for this course, please log in.

+ {% endif %} + - {% elif user|is_independent_student %} - - {% else %} -
-
- - Worksheet + + {% if user|is_logged_in_as_teacher %} + + + {% elif user|is_independent_student %} + + + {% else %} +
+ + {% endif %} {% endif %} - {% else %} - - {% endif %} - {% endif %} + {% endfor %}
diff --git a/game/views/level_selection.py b/game/views/level_selection.py index 21c8cb2ac..9779757bd 100644 --- a/game/views/level_selection.py +++ b/game/views/level_selection.py @@ -2,16 +2,16 @@ from builtins import str +import game.level_management as level_management +import game.messages as messages from django.core.cache import cache from django.db.models import Max from django.shortcuts import render from django.utils.safestring import mark_safe - -import game.level_management as level_management -import game.messages as messages from game import app_settings, random_road from game.cache import cached_episode -from game.models import Attempt, Episode, Level +from game.models import Attempt, Episode, Level, Worksheet + from .level_editor import play_anonymous_level @@ -58,11 +58,6 @@ def fetch_episode_data_from_database(early_access, start, end): "last_level": maxName, "random_levels_enabled": episode.r_random_levels_enabled, "difficulty": episode.difficulty, - "lesson_plan_link": episode.lesson_plan_link, - "slides_link": episode.slides_link, - "student_worksheet_link": episode.student_worksheet_link, - "indy_worksheet_link": episode.indy_worksheet_link, - "video_link": episode.video_link, } episode_data.append(e) @@ -93,6 +88,18 @@ def fetch_episode_data(early_access, start=1, end=12): dict(level, title=get_level_title(level["name"])) for level in episode["levels"] ], + worksheets=[ + { + "id": worksheet.id, + "before_level": worksheet.before_level_id, + "lesson_plan_link": worksheet.lesson_plan_link, + "slides_link": worksheet.slides_link, + "student_worksheet_link": worksheet.student_worksheet_link, + "indy_worksheet_link": worksheet.indy_worksheet_link, + "video_link": worksheet.video_link, + } + for worksheet in Worksheet.objects.filter(episode=episode["id"]).order_by("-before_level") + ], ) for episode in data ] @@ -132,7 +139,7 @@ def get_blockly_episodes(request): def get_python_episodes(request): return fetch_episode_data( - app_settings.EARLY_ACCESS_FUNCTION(request), 16, 22 + app_settings.EARLY_ACCESS_FUNCTION(request), 16, 24 ) @@ -244,6 +251,11 @@ def levels(request, language): id=level["id"] ).locked_for_class + for worksheet in episode["worksheets"]: + worksheet["locked_classes"] = Worksheet.objects.get( + id=worksheet["id"] + ).locked_classes + context["pythonEpisodes"] = python_episodes return context