diff --git a/Pipfile.lock b/Pipfile.lock index 523b77670..f5c4e20aa 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -542,7 +542,7 @@ "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", "version": "==2.9.0.post0" }, "pytz": { @@ -691,7 +691,7 @@ "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", "version": "==1.16.0" }, "sqlparse": { @@ -921,71 +921,71 @@ "toml" ], "hashes": [ - "sha256:0266b62cbea568bd5e93a4da364d05de422110cbed5056d69339bd5af5685433", - "sha256:0573f5cbf39114270842d01872952d301027d2d6e2d84013f30966313cadb529", - "sha256:0ddcb70b3a3a57581b450571b31cb774f23eb9519c2aaa6176d3a84c9fc57671", - "sha256:108bb458827765d538abcbf8288599fee07d2743357bdd9b9dad456c287e121e", - "sha256:14045b8bfd5909196a90da145a37f9d335a5d988a83db34e80f41e965fb7cb42", - "sha256:1a5407a75ca4abc20d6252efeb238377a71ce7bda849c26c7a9bece8680a5d99", - "sha256:2bc3e45c16564cc72de09e37413262b9f99167803e5e48c6156bccdfb22c8327", - "sha256:2d608a7808793e3615e54e9267519351c3ae204a6d85764d8337bd95993581a8", - "sha256:34d23e28ccb26236718a3a78ba72744212aa383141961dd6825f6595005c8b06", - "sha256:37a15573f988b67f7348916077c6d8ad43adb75e478d0910957394df397d2874", - "sha256:3c0317288f032221d35fa4cbc35d9f4923ff0dfd176c79c9b356e8ef8ef2dff4", - "sha256:3c42ec2c522e3ddd683dec5cdce8e62817afb648caedad9da725001fa530d354", - "sha256:3c6b24007c4bcd0b19fac25763a7cac5035c735ae017e9a349b927cfc88f31c1", - "sha256:40cca284c7c310d622a1677f105e8507441d1bb7c226f41978ba7c86979609ab", - "sha256:46f21663e358beae6b368429ffadf14ed0a329996248a847a4322fb2e35d64d3", - "sha256:49ed5ee4109258973630c1f9d099c7e72c5c36605029f3a91fe9982c6076c82b", - "sha256:5c95e0fa3d1547cb6f021ab72f5c23402da2358beec0a8e6d19a368bd7b0fb37", - "sha256:5dd4e4a49d9c72a38d18d641135d2fb0bdf7b726ca60a103836b3d00a1182acd", - "sha256:5e444b8e88339a2a67ce07d41faabb1d60d1004820cee5a2c2b54e2d8e429a0f", - "sha256:60dcf7605c50ea72a14490d0756daffef77a5be15ed1b9fea468b1c7bda1bc3b", - "sha256:623e6965dcf4e28a3debaa6fcf4b99ee06d27218f46d43befe4db1c70841551c", - "sha256:673184b3156cba06154825f25af33baa2671ddae6343f23175764e65a8c4c30b", - "sha256:6cf96ceaa275f071f1bea3067f8fd43bec184a25a962c754024c973af871e1b7", - "sha256:70a56a2ec1869e6e9fa69ef6b76b1a8a7ef709972b9cc473f9ce9d26b5997ce3", - "sha256:77256ad2345c29fe59ae861aa11cfc74579c88d4e8dbf121cbe46b8e32aec808", - "sha256:796c9b107d11d2d69e1849b2dfe41730134b526a49d3acb98ca02f4985eeff7a", - "sha256:7c07de0d2a110f02af30883cd7dddbe704887617d5c27cf373362667445a4c76", - "sha256:7e61b0e77ff4dddebb35a0e8bb5a68bf0f8b872407d8d9f0c726b65dfabe2469", - "sha256:82c809a62e953867cf57e0548c2b8464207f5f3a6ff0e1e961683e79b89f2c55", - "sha256:850cfd2d6fc26f8346f422920ac204e1d28814e32e3a58c19c91980fa74d8289", - "sha256:87ea64b9fa52bf395272e54020537990a28078478167ade6c61da7ac04dc14bc", - "sha256:90746521206c88bdb305a4bf3342b1b7316ab80f804d40c536fc7d329301ee13", - "sha256:951aade8297358f3618a6e0660dc74f6b52233c42089d28525749fc8267dccd2", - "sha256:963e4a08cbb0af6623e61492c0ec4c0ec5c5cf74db5f6564f98248d27ee57d30", - "sha256:987a8e3da7da4eed10a20491cf790589a8e5e07656b6dc22d3814c4d88faf163", - "sha256:9c2eb378bebb2c8f65befcb5147877fc1c9fbc640fc0aad3add759b5df79d55d", - "sha256:a1ab9763d291a17b527ac6fd11d1a9a9c358280adb320e9c2672a97af346ac2c", - "sha256:a3b925300484a3294d1c70f6b2b810d6526f2929de954e5b6be2bf8caa1f12c1", - "sha256:acbb8af78f8f91b3b51f58f288c0994ba63c646bc1a8a22ad072e4e7e0a49f1c", - "sha256:ad32a981bcdedb8d2ace03b05e4fd8dace8901eec64a532b00b15217d3677dd2", - "sha256:aee9cf6b0134d6f932d219ce253ef0e624f4fa588ee64830fcba193269e4daa3", - "sha256:af05bbba896c4472a29408455fe31b3797b4d8648ed0a2ccac03e074a77e2314", - "sha256:b6cce5c76985f81da3769c52203ee94722cd5d5889731cd70d31fee939b74bf0", - "sha256:bb684694e99d0b791a43e9fc0fa58efc15ec357ac48d25b619f207c41f2fd384", - "sha256:c132b5a22821f9b143f87446805e13580b67c670a548b96da945a8f6b4f2efbb", - "sha256:c296263093f099da4f51b3dff1eff5d4959b527d4f2f419e16508c5da9e15e8c", - "sha256:c973b2fe4dc445cb865ab369df7521df9c27bf40715c837a113edaa2aa9faf45", - "sha256:cdd94501d65adc5c24f8a1a0eda110452ba62b3f4aeaba01e021c1ed9cb8f34a", - "sha256:d79d4826e41441c9a118ff045e4bccb9fdbdcb1d02413e7ea6eb5c87b5439d24", - "sha256:dbba8210f5067398b2c4d96b4e64d8fb943644d5eb70be0d989067c8ca40c0f8", - "sha256:df002e59f2d29e889c37abd0b9ee0d0e6e38c24f5f55d71ff0e09e3412a340ec", - "sha256:dfd14bcae0c94004baba5184d1c935ae0d1231b8409eb6c103a5fd75e8ecdc56", - "sha256:e25bacb53a8c7325e34d45dddd2f2fbae0dbc230d0e2642e264a64e17322a777", - "sha256:e2c8e3384c12dfa19fa9a52f23eb091a8fad93b5b81a41b14c17c78e23dd1d8b", - "sha256:e5f2a0f161d126ccc7038f1f3029184dbdf8f018230af17ef6fd6a707a5b881f", - "sha256:e69ad502f1a2243f739f5bd60565d14a278be58be4c137d90799f2c263e7049a", - "sha256:ead9b9605c54d15be228687552916c89c9683c215370c4a44f1f217d2adcc34d", - "sha256:f07ff574986bc3edb80e2c36391678a271d555f91fd1d332a1e0f4b5ea4b6ea9", - "sha256:f2c7a045eef561e9544359a0bf5784b44e55cefc7261a20e730baa9220c83413", - "sha256:f3e8796434a8106b3ac025fd15417315d7a58ee3e600ad4dbcfddc3f4b14342c", - "sha256:f63e21ed474edd23f7501f89b53280014436e383a14b9bd77a648366c81dce7b", - "sha256:fd49c01e5057a451c30c9b892948976f5d38f2cbd04dc556a82743ba8e27ed8c" + "sha256:093896e530c38c8e9c996901858ac63f3d4171268db2c9c8b373a228f459bbc5", + "sha256:09b9f848b28081e7b975a3626e9081574a7b9196cde26604540582da60235fdf", + "sha256:0b0c69f4f724c64dfbfe79f5dfb503b42fe6127b8d479b2677f2b227478db2eb", + "sha256:13618bed0c38acc418896005732e565b317aa9e98d855a0e9f211a7ffc2d6638", + "sha256:13690e923a3932e4fad4c0ebfb9cb5988e03d9dcb4c5150b5fcbf58fd8bddfc4", + "sha256:177f01eeaa3aee4a5ffb0d1439c5952b53d5010f86e9d2667963e632e30082cc", + "sha256:193e3bffca48ad74b8c764fb4492dd875038a2f9925530cb094db92bb5e47bed", + "sha256:1defe91d41ce1bd44b40fabf071e6a01a5aa14de4a31b986aa9dfd1b3e3e414a", + "sha256:1f188a2402f8359cf0c4b1fe89eea40dc13b52e7b4fd4812450da9fcd210181d", + "sha256:202a2d645c5a46b84992f55b0a3affe4f0ba6b4c611abec32ee88358db4bb649", + "sha256:24eda3a24a38157eee639ca9afe45eefa8d2420d49468819ac5f88b10de84f4c", + "sha256:2e4e0f60cb4bd7396108823548e82fdab72d4d8a65e58e2c19bbbc2f1e2bfa4b", + "sha256:379c111d3558272a2cae3d8e57e6b6e6f4fe652905692d54bad5ea0ca37c5ad4", + "sha256:37cda8712145917105e07aab96388ae76e787270ec04bcb9d5cc786d7cbb8443", + "sha256:38c51297b35b3ed91670e1e4efb702b790002e3245a28c76e627478aa3c10d83", + "sha256:3985b9be361d8fb6b2d1adc9924d01dec575a1d7453a14cccd73225cb79243ee", + "sha256:3988665ee376abce49613701336544041f2117de7b7fbfe91b93d8ff8b151c8e", + "sha256:3ac47fa29d8d41059ea3df65bd3ade92f97ee4910ed638e87075b8e8ce69599e", + "sha256:3b4b4299dd0d2c67caaaf286d58aef5e75b125b95615dda4542561a5a566a1e3", + "sha256:3ea8bb1ab9558374c0ab591783808511d135a833c3ca64a18ec927f20c4030f0", + "sha256:3fe47da3e4fda5f1abb5709c156eca207eacf8007304ce3019eb001e7a7204cb", + "sha256:428ac484592f780e8cd7b6b14eb568f7c85460c92e2a37cb0c0e5186e1a0d076", + "sha256:44e6c85bbdc809383b509d732b06419fb4544dca29ebe18480379633623baafb", + "sha256:4674f0daa1823c295845b6a740d98a840d7a1c11df00d1fd62614545c1583787", + "sha256:4be32da0c3827ac9132bb488d331cb32e8d9638dd41a0557c5569d57cf22c9c1", + "sha256:4db3ed6a907b555e57cc2e6f14dc3a4c2458cdad8919e40b5357ab9b6db6c43e", + "sha256:5c52a036535d12590c32c49209e79cabaad9f9ad8aa4cbd875b68c4d67a9cbce", + "sha256:629a1ba2115dce8bf75a5cce9f2486ae483cb89c0145795603d6554bdc83e801", + "sha256:62a66ff235e4c2e37ed3b6104d8b478d767ff73838d1222132a7a026aa548764", + "sha256:63068a11171e4276f6ece913bde059e77c713b48c3a848814a6537f35afb8365", + "sha256:63c19702db10ad79151a059d2d6336fe0c470f2e18d0d4d1a57f7f9713875dcf", + "sha256:644ec81edec0f4ad17d51c838a7d01e42811054543b76d4ba2c5d6af741ce2a6", + "sha256:6535d996f6537ecb298b4e287a855f37deaf64ff007162ec0afb9ab8ba3b8b71", + "sha256:6f4548c5ead23ad13fb7a2c8ea541357474ec13c2b736feb02e19a3085fac002", + "sha256:716a78a342679cd1177bc8c2fe957e0ab91405bd43a17094324845200b2fddf4", + "sha256:74610105ebd6f33d7c10f8907afed696e79c59e3043c5f20eaa3a46fddf33b4c", + "sha256:768939f7c4353c0fac2f7c37897e10b1414b571fd85dd9fc49e6a87e37a2e0d8", + "sha256:86cffe9c6dfcfe22e28027069725c7f57f4b868a3f86e81d1c62462764dc46d4", + "sha256:8aae5aea53cbfe024919715eca696b1a3201886ce83790537d1c3668459c7146", + "sha256:8b2b8503edb06822c86d82fa64a4a5cb0760bb8f31f26e138ec743f422f37cfc", + "sha256:912e95017ff51dc3d7b6e2be158dedc889d9a5cc3382445589ce554f1a34c0ea", + "sha256:9a7b8ac36fd688c8361cbc7bf1cb5866977ece6e0b17c34aa0df58bda4fa18a4", + "sha256:9e89d5c8509fbd6c03d0dd1972925b22f50db0792ce06324ba069f10787429ad", + "sha256:ae270e79f7e169ccfe23284ff5ea2d52a6f401dc01b337efb54b3783e2ce3f28", + "sha256:b07c25d52b1c16ce5de088046cd2432b30f9ad5e224ff17c8f496d9cb7d1d451", + "sha256:b39e6011cd06822eb964d038d5dff5da5d98652b81f5ecd439277b32361a3a50", + "sha256:bd55f8fc8fa494958772a2a7302b0354ab16e0b9272b3c3d83cdb5bec5bd1779", + "sha256:c15b32a7aca8038ed7644f854bf17b663bc38e1671b5d6f43f9a2b2bd0c46f63", + "sha256:c1b4474beee02ede1eef86c25ad4600a424fe36cff01a6103cb4533c6bf0169e", + "sha256:c79c0685f142ca53256722a384540832420dff4ab15fec1863d7e5bc8691bdcc", + "sha256:c9ebfb2507751f7196995142f057d1324afdab56db1d9743aab7f50289abd022", + "sha256:d7ad66e8e50225ebf4236368cc43c37f59d5e6728f15f6e258c8639fa0dd8e6d", + "sha256:d82ab6816c3277dc962cfcdc85b1efa0e5f50fb2c449432deaf2398a2928ab94", + "sha256:d9fd2547e6decdbf985d579cf3fc78e4c1d662b9b0ff7cc7862baaab71c9cc5b", + "sha256:de38add67a0af869b0d79c525d3e4588ac1ffa92f39116dbe0ed9753f26eba7d", + "sha256:e19122296822deafce89a0c5e8685704c067ae65d45e79718c92df7b3ec3d331", + "sha256:e44961e36cb13c495806d4cac67640ac2866cb99044e210895b506c26ee63d3a", + "sha256:e4c81ed2820b9023a9a90717020315e63b17b18c274a332e3b6437d7ff70abe0", + "sha256:e683e6ecc587643f8cde8f5da6768e9d165cd31edf39ee90ed7034f9ca0eefee", + "sha256:f39e2f3530ed1626c66e7493be7a8423b023ca852aacdc91fb30162c350d2a92", + "sha256:f56f49b2553d7dd85fd86e029515a221e5c1f8cb3d9c38b470bc38bde7b8445a", + "sha256:fb9fc32399dca861584d96eccd6c980b69bbcd7c228d06fb74fe53e007aa8ef9" ], "markers": "python_version >= '3.9'", - "version": "==7.6.7" + "version": "==7.6.8" }, "django": { "hashes": [ @@ -1143,12 +1143,12 @@ }, "pytest": { "hashes": [ - "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181", - "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2" + "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6", + "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==8.3.3" + "version": "==8.3.4" }, "pytest-cov": { "hashes": [ @@ -1205,11 +1205,11 @@ }, "rapid-router": { "hashes": [ - "sha256:5019151d61c4873379564b59b0ede6891d9a780fc020c05dcda84681cea99595", - "sha256:d236dd3038d2545ba634625e8019399cdd5f8dcb565d69d0a8eefa8167fcf9bb" + "sha256:817c9b296af5956eb16289751706ffe6b0282be8b8f13fecd892b2cf6112467a", + "sha256:8841f2b8c9d9be81a42c64a4a2cd3a210ee59b83c041ff5feeabcb2ed4ccb9fb" ], "index": "pypi", - "version": "==7.0.1" + "version": "==7.2.0" }, "requests": { "hashes": [ diff --git a/cfl_common/common/models.py b/cfl_common/common/models.py index b589dcdf1..f0a028b86 100644 --- a/cfl_common/common/models.py +++ b/cfl_common/common/models.py @@ -1,4 +1,5 @@ import re +import typing as t from datetime import timedelta from uuid import uuid4 @@ -7,6 +8,10 @@ from django.utils import timezone from django_countries.fields import CountryField +if t.TYPE_CHECKING: + from django.db.models import ManyToManyField + from game.models import Worksheet + class UserProfile(models.Model): user = models.OneToOneField(User, on_delete=models.CASCADE) @@ -43,9 +48,7 @@ def get_queryset(self): class School(models.Model): name = models.CharField(max_length=200, unique=True) - country = CountryField( - blank_label="(select country)", null=True, blank=True - ) + country = CountryField(blank_label="(select country)", null=True, blank=True) # TODO: Create an Address model to house address details county = models.CharField(max_length=50, blank=True, null=True) creation_time = models.DateTimeField(default=timezone.now, null=True) @@ -68,11 +71,7 @@ def classes(self): def admins(self): teachers = self.teacher_school.all() - return ( - [teacher for teacher in teachers if teacher.is_admin] - if teachers - else None - ) + return [teacher for teacher in teachers if teacher.is_admin] if teachers else None def anonymise(self): self.name = uuid4().hex @@ -130,10 +129,7 @@ class Teacher(models.Model): def teaches(self, userprofile): if hasattr(userprofile, "student"): student = userprofile.student - return ( - not student.is_independent() - and student.class_field.teacher == self - ) + return not student.is_independent() and student.class_field.teacher == self def has_school(self): return self.school is not (None or "") @@ -165,14 +161,10 @@ class SchoolTeacherInvitation(models.Model): null=True, on_delete=models.SET_NULL, ) - invited_teacher_first_name = models.CharField( - max_length=150 - ) # Same as User model + invited_teacher_first_name = models.CharField(max_length=150) # Same as User model # TODO: Make not nullable once data has been transferred _invited_teacher_first_name = models.BinaryField(null=True, blank=True) - invited_teacher_last_name = models.CharField( - max_length=150 - ) # Same as User model + invited_teacher_last_name = models.CharField(max_length=150) # Same as User model # TODO: Make not nullable once data has been transferred _invited_teacher_last_name = models.BinaryField(null=True, blank=True) # TODO: Switch to a CharField to be able to hold hashed value @@ -222,10 +214,10 @@ def get_queryset(self): class Class(models.Model): + locked_worksheets: "ManyToManyField[Worksheet]" + name = models.CharField(max_length=200) - teacher = models.ForeignKey( - Teacher, related_name="class_teacher", on_delete=models.CASCADE - ) + teacher = models.ForeignKey(Teacher, related_name="class_teacher", on_delete=models.CASCADE) access_code = models.CharField(max_length=5, null=True) classmates_data_viewable = models.BooleanField(default=False) always_accept_requests = models.BooleanField(default=False) @@ -249,9 +241,7 @@ def __str__(self): def active_game(self): games = self.game_set.filter(game_class=self, is_archived=False) if len(games) >= 1: - assert ( - len(games) == 1 - ) # there should NOT be more than one active game + assert len(games) == 1 # there should NOT be more than one active game return games[0] return None @@ -261,13 +251,8 @@ def has_students(self): def get_requests_message(self): if self.always_accept_requests: - external_requests_message = ( - "This class is currently set to always accept requests." - ) - elif ( - self.accept_requests_until is not None - and (self.accept_requests_until - timezone.now()) >= timedelta() - ): + external_requests_message = "This class is currently set to always accept requests." + elif self.accept_requests_until is not None and (self.accept_requests_until - timezone.now()) >= timedelta(): external_requests_message = ( "This class is accepting external requests until " + self.accept_requests_until.strftime("%d-%m-%Y %H:%M") @@ -275,9 +260,7 @@ def get_requests_message(self): + timezone.get_current_timezone_name() ) else: - external_requests_message = ( - "This class is not currently accepting external requests." - ) + external_requests_message = "This class is not currently accepting external requests." return external_requests_message @@ -299,9 +282,7 @@ class UserSession(models.Model): login_time = models.DateTimeField(default=timezone.now) school = models.ForeignKey(School, null=True, on_delete=models.SET_NULL) class_field = models.ForeignKey(Class, null=True, on_delete=models.SET_NULL) - login_type = models.CharField( - max_length=100, null=True - ) # for student login + login_type = models.CharField(max_length=100, null=True) # for student login def __str__(self): return f"{self.user} login: {self.login_time} type: {self.login_type}" @@ -330,9 +311,7 @@ def schoolFactory(self, klass, name, password, login_id=None): ) def independentStudentFactory(self, name, email, password): - user = User.objects.create_user( - username=email, email=email, password=password, first_name=name - ) + user = User.objects.create_user(username=email, email=email, password=password, first_name=name) user_profile = UserProfile.objects.create(user=user) @@ -391,9 +370,7 @@ class JoinReleaseStudent(models.Model): JOIN = "join" RELEASE = "release" - student = models.ForeignKey( - Student, related_name="student", on_delete=models.CASCADE - ) + student = models.ForeignKey(Student, related_name="student", on_delete=models.CASCADE) # either "release" or "join" action_type = models.CharField(max_length=64) action_time = models.DateTimeField(default=timezone.now) diff --git a/portal/forms/teach.py b/portal/forms/teach.py index 9b30310d9..ffd5123fb 100644 --- a/portal/forms/teach.py +++ b/portal/forms/teach.py @@ -3,14 +3,14 @@ from builtins import map, range, str from common.helpers.emails import send_verification_email -from common.models import Student, stripStudentName, UserSession, Teacher +from common.models import Student, Teacher, UserSession, stripStudentName from django import forms from django.contrib.auth import authenticate from django.contrib.auth.forms import AuthenticationForm from django.contrib.auth.models import User from django_recaptcha.fields import ReCaptchaField from django_recaptcha.widgets import ReCaptchaV2Invisible -from game.models import Episode +from game.models import Episode, Worksheet from portal.forms.error_messages import INVALID_LOGIN_MESSAGE from portal.helpers.password import PasswordStrength, form_clean_password @@ -18,46 +18,34 @@ class InvitedTeacherForm(forms.Form): - prefix = 'teacher_signup' + prefix = "teacher_signup" def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) for field_name, field in self.fields.items(): - field.widget.attrs['id'] = f'id_teacher_signup-{field_name}' + field.widget.attrs["id"] = f"id_teacher_signup-{field_name}" teacher_password = forms.CharField( help_text="Enter a password", - widget=forms.PasswordInput( - attrs={"autocomplete": "off", "placeholder": "Password"} - ), + widget=forms.PasswordInput(attrs={"autocomplete": "off", "placeholder": "Password"}), ) teacher_confirm_password = forms.CharField( help_text="Repeat password", - widget=forms.PasswordInput( - attrs={"autocomplete": "off", "placeholder": "Repeat password"} - ), + widget=forms.PasswordInput(attrs={"autocomplete": "off", "placeholder": "Repeat password"}), ) - consent_ticked = forms.BooleanField( - widget=forms.CheckboxInput(), initial=False, required=True - ) - newsletter_ticked = forms.BooleanField( - widget=forms.CheckboxInput(), initial=False, required=False - ) + consent_ticked = forms.BooleanField(widget=forms.CheckboxInput(), initial=False, required=True) + newsletter_ticked = forms.BooleanField(widget=forms.CheckboxInput(), initial=False, required=False) def clean_teacher_password(self): - return form_clean_password( - self, "teacher_password", PasswordStrength.TEACHER - ) + return form_clean_password(self, "teacher_password", PasswordStrength.TEACHER) def clean(self): if any(self.errors): return password = self.cleaned_data.get("teacher_password", None) - confirm_password = self.cleaned_data.get( - "teacher_confirm_password", None - ) + confirm_password = self.cleaned_data.get("teacher_confirm_password", None) check_passwords(password, confirm_password) @@ -68,22 +56,16 @@ class TeacherSignupForm(InvitedTeacherForm): teacher_first_name = forms.CharField( help_text="Enter your first name", max_length=100, - widget=forms.TextInput( - attrs={"autocomplete": "off", "placeholder": "First name"} - ), + widget=forms.TextInput(attrs={"autocomplete": "off", "placeholder": "First name"}), ) teacher_last_name = forms.CharField( help_text="Enter your last name", max_length=100, - widget=forms.TextInput( - attrs={"autocomplete": "off", "placeholder": "Last name"} - ), + widget=forms.TextInput(attrs={"autocomplete": "off", "placeholder": "Last name"}), ) teacher_email = forms.EmailField( help_text="Enter your email address", - widget=forms.EmailInput( - attrs={"autocomplete": "off", "placeholder": "Email address"} - ), + widget=forms.EmailInput(attrs={"autocomplete": "off", "placeholder": "Email address"}), ) captcha = ReCaptchaField(widget=ReCaptchaV2Invisible) @@ -92,37 +74,27 @@ class TeacherSignupForm(InvitedTeacherForm): class TeacherEditAccountForm(forms.Form): first_name = forms.CharField( max_length=100, - widget=forms.TextInput( - attrs={"placeholder": "First name", "class": "fName"} - ), + widget=forms.TextInput(attrs={"placeholder": "First name", "class": "fName"}), help_text="First name", ) last_name = forms.CharField( max_length=100, - widget=forms.TextInput( - attrs={"placeholder": "Last name", "class": "lName"} - ), + widget=forms.TextInput(attrs={"placeholder": "Last name", "class": "lName"}), help_text="Last name", ) email = forms.EmailField( required=False, - widget=forms.EmailInput( - attrs={"placeholder": "New email address (optional)"} - ), + widget=forms.EmailInput(attrs={"placeholder": "New email address (optional)"}), help_text="New email address (optional)", ) password = forms.CharField( required=False, - widget=forms.PasswordInput( - attrs={"placeholder": "New password (optional)"} - ), + widget=forms.PasswordInput(attrs={"placeholder": "New password (optional)"}), help_text="New password (optional)", ) confirm_password = forms.CharField( required=False, - widget=forms.PasswordInput( - attrs={"placeholder": "Confirm new password (optional)"} - ), + widget=forms.PasswordInput(attrs={"placeholder": "Confirm new password (optional)"}), help_text="Confirm new password (optional)", ) current_password = forms.CharField( @@ -149,9 +121,7 @@ def clean(self): return self.cleaned_data - def check_password_errors( - self, password, confirm_password, current_password - ): + def check_password_errors(self, password, confirm_password, current_password): check_passwords(password, confirm_password) if not self.user.check_password(current_password): @@ -160,15 +130,11 @@ def check_password_errors( class TeacherLoginForm(AuthenticationForm): username = forms.EmailField( - widget=forms.EmailInput( - attrs={"autocomplete": "off", "placeholder": "Email address"} - ), + widget=forms.EmailInput(attrs={"autocomplete": "off", "placeholder": "Email address"}), help_text="Enter your email address", ) password = forms.CharField( - widget=forms.PasswordInput( - attrs={"autocomplete": "off", "placeholder": "Password"} - ), + widget=forms.PasswordInput(attrs={"autocomplete": "off", "placeholder": "Password"}), help_text="Enter your password", ) @@ -202,9 +168,7 @@ def find_user(self, email, user): users = User.objects.filter(email=email) for result in users: - if hasattr(result, "userprofile") and hasattr( - result.userprofile, "teacher" - ): + if hasattr(result, "userprofile") and hasattr(result.userprofile, "teacher"): user = result break @@ -286,9 +250,7 @@ class ClassEditForm(forms.Form): [ ( str(hours), - "Allow external requests to this class for the next " - + str(hours) - + " hours", + "Allow external requests to this class for the next " + str(hours) + " hours", ) for hours in range(4, 28, 4) ] @@ -297,9 +259,7 @@ class ClassEditForm(forms.Form): [ ( str(days * 24), - "Allow external requests to this class for the next " - + str(days) - + " days", + "Allow external requests to this class for the next " + str(days) + " days", ) for days in range(2, 5) ] @@ -333,18 +293,22 @@ class ClassLevelControlForm(forms.Form): def __init__(self, *args, **kwargs): super(ClassLevelControlForm, self).__init__(*args, **kwargs) - episodes = Episode.objects.filter(pk__in=range(1, 22)) + episodes = Episode.objects.filter(pk__in=range(1, 25)) for episode in episodes: - levels = [] - + choices = [] for level in episode.levels: - levels.append(level) + try: + choices.append((f"worksheet:{level.after_worksheet.id}", episode.name)) + except Worksheet.DoesNotExist: + pass + choices.append((f"level:{level.id}", level.name)) - levels_choices = [(level.id, level.name) for level in levels] + for worksheet in episode.worksheets.filter(before_level__isnull=True): + choices.append((f"worksheet:{worksheet.id}", episode.name)) self.fields[episode.name] = forms.MultipleChoiceField( - choices=itertools.chain(levels_choices), + choices=itertools.chain(choices), widget=forms.CheckboxSelectMultiple(), required=False, ) @@ -364,9 +328,7 @@ def __init__(self, teachers, *args, **kwargs): teacher_choices.append( ( teacher.id, - teacher.new_user.first_name - + " " - + teacher.new_user.last_name, + teacher.new_user.first_name + " " + teacher.new_user.last_name, ) ) super(ClassMoveForm, self).__init__(*args, **kwargs) @@ -389,24 +351,14 @@ def clean_name(self): name = stripStudentName(self.cleaned_data.get("name", "")) if name == "": - raise forms.ValidationError( - "'" - + self.cleaned_data.get("name", "") - + "' is not a valid name" - ) + raise forms.ValidationError("'" + self.cleaned_data.get("name", "") + "' is not a valid name") if re.match(re.compile("^[\w -]+$"), name) is None: - raise forms.ValidationError( - "Names may only contain letters, numbers, dashes, underscores, and spaces." - ) + raise forms.ValidationError("Names may only contain letters, numbers, dashes, underscores, and spaces.") - students = Student.objects.filter( - class_field=self.klass, new_user__first_name__iexact=name - ) + students = Student.objects.filter(class_field=self.klass, new_user__first_name__iexact=name) if students.exists() and students[0] != self.student: - raise forms.ValidationError( - "There is already a student called '" + name + "' in this class" - ) + raise forms.ValidationError("There is already a student called '" + name + "' in this class") return name @@ -415,16 +367,12 @@ class TeacherSetStudentPass(forms.Form): password = forms.CharField( label="New password", help_text="Enter new password", - widget=forms.PasswordInput( - attrs={"autocomplete": "off", "placeholder": "Enter new password"} - ), + widget=forms.PasswordInput(attrs={"autocomplete": "off", "placeholder": "Enter new password"}), ) confirm_password = forms.CharField( label="Confirm new password", help_text="Confirm new password", - widget=forms.PasswordInput( - attrs={"autocomplete": "off", "placeholder": "Confirm new password"} - ), + widget=forms.PasswordInput(attrs={"autocomplete": "off", "placeholder": "Confirm new password"}), ) def clean_password(self): @@ -468,32 +416,19 @@ def validateStudentNames(klass, names): def find_clashes(names, students, clashes_found, validationErrors): for name in names: - if ( - students.filter(new_user__first_name__iexact=name).exists() - and name not in clashes_found - ): + if students.filter(new_user__first_name__iexact=name).exists() and name not in clashes_found: validationErrors.append( - forms.ValidationError( - "There is already a student called '" - + name - + "' in this class" - ) + forms.ValidationError("There is already a student called '" + name + "' in this class") ) clashes_found.append(name) def find_duplicates(names, lower_names, validationErrors): duplicates_found = [] - for duplicate in [ - name for name in names if lower_names.count(name.lower()) > 1 - ]: + for duplicate in [name for name in names if lower_names.count(name.lower()) > 1]: if duplicate not in duplicates_found: validationErrors.append( - forms.ValidationError( - "You cannot add more than one student called '" - + duplicate - + "'" - ) + forms.ValidationError("You cannot add more than one student called '" + duplicate + "'") ) duplicates_found.append(duplicate) @@ -512,9 +447,7 @@ def find_illegal_characters(names, validationErrors): def check_passwords(password, confirm_password): if password is not None and password != confirm_password: - raise forms.ValidationError( - "The password and the confirmation password do not match" - ) + raise forms.ValidationError("The password and the confirmation password do not match") class TeacherMoveStudentsDestinationForm(forms.Form): @@ -539,9 +472,7 @@ def __init__(self, classes, *args, **kwargs): + klass.teacher.new_user.last_name, ) ) - super(TeacherMoveStudentsDestinationForm, self).__init__( - *args, **kwargs - ) + super(TeacherMoveStudentsDestinationForm, self).__init__(*args, **kwargs) self.fields["new_class"].choices = class_choices @@ -558,28 +489,20 @@ class TeacherMoveStudentDisambiguationForm(forms.Form): ) name = forms.CharField( label="Name", - widget=forms.TextInput( - attrs={"placeholder": "Name", "style": "margin : 0px"} - ), + widget=forms.TextInput(attrs={"placeholder": "Name", "style": "margin : 0px"}), ) def clean_name(self): name = stripStudentName(self.cleaned_data.get("name", "")) if name == "": - raise forms.ValidationError( - "'" - + self.cleaned_data.get("name", "") - + "' is not a valid name" - ) + raise forms.ValidationError("'" + self.cleaned_data.get("name", "") + "' is not a valid name") return name class BaseTeacherMoveStudentsDisambiguationFormSet(forms.BaseFormSet): def __init__(self, destination, *args, **kwargs): self.destination = destination - super(BaseTeacherMoveStudentsDisambiguationFormSet, self).__init__( - *args, **kwargs - ) + super(BaseTeacherMoveStudentsDisambiguationFormSet, self).__init__(*args, **kwargs) def clean(self): if any(self.errors): @@ -608,34 +531,24 @@ class TeacherDismissStudentsForm(forms.Form): ) name = forms.CharField( help_text="New student name", - widget=forms.TextInput( - attrs={"placeholder": "Enter new student name", "class": "m-0"} - ), + widget=forms.TextInput(attrs={"placeholder": "Enter new student name", "class": "m-0"}), ) email = forms.EmailField( label="Email", help_text="New email address", - widget=forms.EmailInput( - attrs={"placeholder": "Enter email address", "class": "m-0"} - ), + widget=forms.EmailInput(attrs={"placeholder": "Enter email address", "class": "m-0"}), ) confirm_email = forms.EmailField( label="Confirm Email", help_text="Confirm email address", - widget=forms.EmailInput( - attrs={"placeholder": "Confirm email address", "class": "m-0"} - ), + widget=forms.EmailInput(attrs={"placeholder": "Confirm email address", "class": "m-0"}), ) def clean_name(self): name = stripStudentName(self.cleaned_data.get("name", "")) if name == "": - raise forms.ValidationError( - "'" - + self.cleaned_data.get("name", "") - + "' is not a valid name" - ) + raise forms.ValidationError("'" + self.cleaned_data.get("name", "") + "' is not a valid name") return name @@ -706,17 +619,9 @@ def clean_name(self): name = stripStudentName(self.cleaned_data.get("name", "")) if name == "": - raise forms.ValidationError( - "'" - + self.cleaned_data.get("name", "") - + "' is not a valid name" - ) + raise forms.ValidationError("'" + self.cleaned_data.get("name", "") + "' is not a valid name") - if Student.objects.filter( - class_field=self.klass, new_user__first_name__iexact=name - ).exists(): - raise forms.ValidationError( - "There is already a student called '" + name + "' in this class" - ) + if Student.objects.filter(class_field=self.klass, new_user__first_name__iexact=name).exists(): + raise forms.ValidationError("There is already a student called '" + name + "' in this class") return name diff --git a/portal/templates/portal/teach/teacher_edit_class.html b/portal/templates/portal/teach/teacher_edit_class.html index 912706e04..c09afe009 100644 --- a/portal/templates/portal/teach/teacher_edit_class.html +++ b/portal/templates/portal/teach/teacher_edit_class.html @@ -147,20 +147,21 @@
Python Den levels
{% for episode in python_episodes %} - {% if episode.levels %}
diff --git a/portal/tests/test_class.py b/portal/tests/test_class.py index 7e6ae240f..e8c8a41f2 100644 --- a/portal/tests/test_class.py +++ b/portal/tests/test_class.py @@ -30,9 +30,7 @@ def test_delete_class(self): c = Client() - url = reverse( - "teacher_delete_class", kwargs={"access_code": access_code} - ) + url = reverse("teacher_delete_class", kwargs={"access_code": access_code}) # Login as another teacher, try to delete the class and check for 404 c.login(username=email2, password=password2) @@ -136,112 +134,138 @@ def test_level_control(self): old_daily_activity = DailyActivity(date=old_date) old_daily_activity.save() - url = reverse( - "teacher_edit_class", kwargs={"access_code": access_code1} - ) + url = reverse("teacher_edit_class", kwargs={"access_code": access_code1}) # POST request data for locking only the first level data = { "Getting Started": [ - "2", - "3", - "4", - "5", - "6", - "7", - "8", - "9", - "10", - "11", - "12", + "level:2", + "level:3", + "level:4", + "level:5", + "level:6", + "level:7", + "level:8", + "level:9", + "level:10", + "level:11", + "level:12", + ], + "Shortest Route": [ + "level:13", + "level:14", + "level:15", + "level:16", + "level:17", + "level:18", ], - "Shortest Route": ["13", "14", "15", "16", "17", "18"], "Loops and Repetitions": [ - "19", - "20", - "21", - "22", - "23", - "24", - "25", - "26", - "27", - "28", + "level:19", + "level:20", + "level:21", + "level:22", + "level:23", + "level:24", + "level:25", + "level:26", + "level:27", + "level:28", + ], + "Loops with Conditions": [ + "level:29", + "level:30", + "level:31", + "level:32", ], - "Loops with Conditions": ["29", "30", "31", "32"], "If... Only": [ - "33", - "34", - "35", - "36", - "37", - "38", - "39", - "40", - "41", - "42", - "43", + "level:33", + "level:34", + "level:35", + "level:36", + "level:37", + "level:38", + "level:39", + "level:40", + "level:41", + "level:42", + "level:43", + ], + "Traffic Lights": [ + "level:44", + "level:45", + "level:46", + "level:47", + "level:48", + "level:49", + "level:50", ], - "Traffic Lights": ["44", "45", "46", "47", "48", "49", "50"], "Limited Blocks": [ - "53", - "78", - "79", - "80", - "81", - "82", - "83", - "84", - "54", - "55", + "level:53", + "level:78", + "level:79", + "level:80", + "level:81", + "level:82", + "level:83", + "level:84", + "level:54", + "level:55", + ], + "Procedures": [ + # "level:85", + # "level:52", + # "level:60", + # "level:86", + # "level:62", + # "level:87", + # "level:61", ], - "Procedures": ["85", "52", "60", "86", "62", "87", "61"], "Blockly Brain Teasers": [ - "56", - "57", - "58", - "59", - "88", - "91", - "90", - "89", - "110", - "111", - "112", - "92", + "level:56", + "level:57", + "level:58", + "level:59", + "level:88", + "level:91", + "level:90", + "level:89", + "level:110", + "level:111", + "level:112", + "level:92", ], "Introduction to Python": [ - "93", - "63", - "64", - "65", - "94", - "66", - "67", - "68", - "95", - "69", - "96", - "97", + "level:93", + "level:63", + "level:64", + "level:65", + "level:94", + "level:66", + "level:67", + "level:68", + "level:95", + "level:69", + "level:96", + "level:97", ], "Python": [ - "98", - "70", - "71", - "73", - "72", - "99", - "74", - "75", - "100", - "101", - "102", - "103", - "104", - "105", - "106", - "107", - "108", - "109", + "level:98", + "level:70", + "level:71", + "level:73", + "level:72", + "level:99", + "level:74", + "level:75", + "level:100", + "level:101", + "level:102", + "level:103", + "level:104", + "level:105", + "level:106", + "level:107", + "level:108", + "level:109", ], "level_control_submit": "", } @@ -261,118 +285,141 @@ def test_level_control(self): assert str(messages[0]) == "Your level preferences have been saved." # test the old analytic stays the same and the new one is incremented - assert ( - DailyActivity.objects.get(date=old_date).level_control_submits == 0 - ) - assert ( - DailyActivity.objects.get(date=datetime.now()).level_control_submits - == 1 - ) + assert DailyActivity.objects.get(date=old_date).level_control_submits == 0 + assert DailyActivity.objects.get(date=datetime.now()).level_control_submits == 1 # Resubmitting to unlock level 1 data = { "Getting Started": [ - "1", - "2", - "3", - "4", - "5", - "6", - "7", - "8", - "9", - "10", - "11", - "12", + "level:1", + "level:2", + "level:3", + "level:4", + "level:5", + "level:6", + "level:7", + "level:8", + "level:9", + "level:10", + "level:11", + "level:12", + ], + "Shortest Route": [ + "level:13", + "level:14", + "level:15", + "level:16", + "level:17", + "level:18", ], - "Shortest Route": ["13", "14", "15", "16", "17", "18"], "Loops and Repetitions": [ - "19", - "20", - "21", - "22", - "23", - "24", - "25", - "26", - "27", - "28", + "level:19", + "level:20", + "level:21", + "level:22", + "level:23", + "level:24", + "level:25", + "level:26", + "level:27", + "level:28", + ], + "Loops with Conditions": [ + "level:29", + "level:30", + "level:31", + "level:32", ], - "Loops with Conditions": ["29", "30", "31", "32"], "If... Only": [ - "33", - "34", - "35", - "36", - "37", - "38", - "39", - "40", - "41", - "42", - "43", + "level:33", + "level:34", + "level:35", + "level:36", + "level:37", + "level:38", + "level:39", + "level:40", + "level:41", + "level:42", + "level:43", + ], + "Traffic Lights": [ + "level:44", + "level:45", + "level:46", + "level:47", + "level:48", + "level:49", + "level:50", ], - "Traffic Lights": ["44", "45", "46", "47", "48", "49", "50"], "Limited Blocks": [ - "53", - "78", - "79", - "80", - "81", - "82", - "83", - "84", - "54", - "55", + "level:53", + "level:78", + "level:79", + "level:80", + "level:81", + "level:82", + "level:83", + "level:84", + "level:54", + "level:55", + ], + "Procedures": [ + # "level:85", + # "level:52", + # "level:60", + # "level:86", + # "level:62", + # "level:87", + # "level:61", ], - "Procedures": ["85", "52", "60", "86", "62", "87", "61"], "Blockly Brain Teasers": [ - "56", - "57", - "58", - "59", - "88", - "91", - "90", - "89", - "110", - "111", - "112", - "92", + "level:56", + "level:57", + "level:58", + "level:59", + "level:88", + "level:91", + "level:90", + "level:89", + "level:110", + "level:111", + "level:112", + "level:92", ], "Introduction to Python": [ - "93", - "63", - "64", - "65", - "94", - "66", - "67", - "68", - "95", - "69", - "96", - "97", + "level:93", + "level:63", + "level:64", + "level:65", + "level:94", + "level:66", + "level:67", + "level:68", + "level:95", + "level:69", + "level:96", + "level:97", ], "Python": [ - "98", - "70", - "71", - "73", - "72", - "99", - "74", - "75", - "100", - "101", - "102", - "103", - "104", - "105", - "106", - "107", - "108", - "109", + "level:98", + "level:70", + "level:71", + "level:73", + "level:72", + "level:99", + "level:74", + "level:75", + "level:100", + "level:101", + "level:102", + "level:103", + "level:104", + "level:105", + "level:106", + "level:107", + "level:108", + "level:109", ], "level_control_submit": "", } @@ -406,9 +453,7 @@ def test_transfer_class(self): c = Client() - url = reverse( - "teacher_edit_class", kwargs={"access_code": access_code1} - ) + url = reverse("teacher_edit_class", kwargs={"access_code": access_code1}) data = {"new_teacher": teacher2.id, "class_move_submit": ""} # Login as first teacher and transfer class to the second teacher @@ -435,12 +480,7 @@ class TestClassFrontend(BaseTest): def test_create(self): email, password = signup_teacher_directly() create_organisation_directly(email) - page = ( - self.go_to_homepage() - .go_to_teacher_login_page() - .login_no_class(email, password) - .open_classes_tab() - ) + page = self.go_to_homepage().go_to_teacher_login_page().login_no_class(email, password).open_classes_tab() assert page.does_not_have_classes() @@ -455,34 +495,19 @@ def test_create_class_as_admin_for_another_teacher(self): join_teacher_to_organisation(email2, school.name) # Check teacher 2 doesn't have any classes - page = ( - self.go_to_homepage() - .go_to_teacher_login_page() - .login(email2, password2) - .open_classes_tab() - ) + page = self.go_to_homepage().go_to_teacher_login_page().login(email2, password2).open_classes_tab() assert page.does_not_have_classes() page.logout() # Log in as the first teacher and create a class for the second one - page = ( - self.go_to_homepage() - .go_to_teacher_login_page() - .login(email1, password1) - .open_classes_tab() - ) + page = self.go_to_homepage().go_to_teacher_login_page().login(email1, password1).open_classes_tab() page, class_name = create_class(page, teacher_id=teacher2.id) page = TeachClassPage(page.browser) assert is_class_created_message_showing(self.selenium, class_name) page.logout() # Check teacher 2 now has the class - page = ( - self.go_to_homepage() - .go_to_teacher_login_page() - .login(email2, password2) - .open_classes_tab() - ) + page = self.go_to_homepage().go_to_teacher_login_page().login(email2, password2).open_classes_tab() assert page.has_classes() def test_create_dashboard(self): @@ -491,12 +516,7 @@ def test_create_dashboard(self): klass, name, access_code = create_class_directly(email) create_school_student_directly(access_code) - page = ( - self.go_to_homepage() - .go_to_teacher_login_page() - .login(email, password) - .open_classes_tab() - ) + page = self.go_to_homepage().go_to_teacher_login_page().login(email, password).open_classes_tab() page, class_name = create_class(page) @@ -512,12 +532,7 @@ def test_create_dashboard_non_admin(self): klass_2, class_name_2, access_code_2 = create_class_directly(email_2) create_school_student_directly(access_code_2) - page = ( - self.go_to_homepage() - .go_to_teacher_login_page() - .login(email_2, password_2) - .open_classes_tab() - ) + page = self.go_to_homepage().go_to_teacher_login_page().login(email_2, password_2).open_classes_tab() page, class_name_3 = create_class(page) diff --git a/portal/views/teacher/teach.py b/portal/views/teacher/teach.py index a62835ecc..2adf572b3 100644 --- a/portal/views/teacher/teach.py +++ b/portal/views/teacher/teach.py @@ -33,14 +33,13 @@ from django.urls import reverse, reverse_lazy from django.utils import timezone from django.views.decorators.http import require_POST +from game.models import Level from game.views.level_selection import get_blockly_episodes, get_python_episodes from reportlab.lib.colors import black, red from reportlab.lib.pagesizes import A4 from reportlab.lib.utils import ImageReader from reportlab.pdfgen import canvas -from game.models import Level - from portal.forms.teach import ( BaseTeacherDismissStudentsFormSet, BaseTeacherMoveStudentsDisambiguationFormSet, @@ -58,7 +57,6 @@ from portal.helpers.ratelimit import clear_ratelimit_cache_for_user from portal.views.registration import handle_reset_password_tracking - STUDENT_PASSWORD_LENGTH = 6 REMINDER_CARDS_PDF_ROWS = 8 REMINDER_CARDS_PDF_COLUMNS = 1 @@ -391,7 +389,7 @@ def process_edit_class_form(request, klass, form): return HttpResponseRedirect(reverse_lazy("view_class", kwargs={"access_code": klass.access_code})) -def process_level_control_form(request, klass, blockly_episodes, python_episodes): +def process_level_control_form(request, klass: Class, blockly_episodes, python_episodes): """ Find the levels that the user wants to lock and lock them for the specific class. :param request: The request sent by the user submitting the form. @@ -401,12 +399,16 @@ def process_level_control_form(request, klass, blockly_episodes, python_episodes :return: A redirect to the teacher dashboard with a success message. """ levels_to_lock_ids = [] + locked_worksheet_ids = [] - mark_levels_to_lock_in_episodes(request, blockly_episodes, levels_to_lock_ids) - mark_levels_to_lock_in_episodes(request, python_episodes, levels_to_lock_ids) + mark_levels_to_lock_in_episodes(request, blockly_episodes, levels_to_lock_ids, locked_worksheet_ids) + mark_levels_to_lock_in_episodes(request, python_episodes, levels_to_lock_ids, locked_worksheet_ids) klass.locked_levels.clear() [klass.locked_levels.add(levels_to_lock_id) for levels_to_lock_id in levels_to_lock_ids] + klass.locked_worksheets.clear() + for locked_worksheet_id in locked_worksheet_ids: + klass.locked_worksheets.add(locked_worksheet_id) messages.success(request, "Your level preferences have been saved.") activity_today = DailyActivity.objects.get_or_create(date=datetime.now().date())[0] @@ -416,7 +418,7 @@ def process_level_control_form(request, klass, blockly_episodes, python_episodes return HttpResponseRedirect(reverse_lazy("dashboard")) -def mark_levels_to_lock_in_episodes(request, episodes, levels_to_lock_ids): +def mark_levels_to_lock_in_episodes(request, episodes, levels_to_lock_ids, locked_worksheet_ids: list): """ For a given set of Episodes, find which Levels are to be locked. This is done by checking the POST request data. If a Level ID is missing from the request.POST, it means it needs to be locked, and if the entire Episode is missing @@ -428,14 +430,21 @@ def mark_levels_to_lock_in_episodes(request, episodes, levels_to_lock_ids): for episode in episodes: episode_name = episode["name"] episode_levels = episode["levels"] + episode_worksheets = episode["worksheets"] if episode_name in request.POST: [ levels_to_lock_ids.append(episode_level["id"]) for episode_level in episode_levels - if str(episode_level["id"]) not in request.POST.getlist(episode_name) + if f'level:{episode_level["id"]}' not in request.POST.getlist(episode_name) ] + for episode_worksheet in episode_worksheets: + worksheet_id = episode_worksheet["id"] + if f"worksheet:{worksheet_id}" not in request.POST.getlist(episode_name): + locked_worksheet_ids.append(worksheet_id) else: [levels_to_lock_ids.append(episode_level["id"]) for episode_level in episode_levels] + for episode_worksheet in episode_worksheets: + locked_worksheet_ids.append(episode_worksheet["id"]) def process_move_class_form(request, klass, form):