From cea5748ee9974f9d21c7a01426c4a985fd8a5010 Mon Sep 17 00:00:00 2001 From: faucomte97 Date: Wed, 4 Sep 2024 17:11:22 +0100 Subject: [PATCH] feat: Python Den level control --- Pipfile.lock | 128 +++--- portal/forms/teach.py | 262 +++++++++--- .../portal/teach/teacher_edit_class.html | 58 ++- portal/views/teacher/teach.py | 377 ++++++++++++++---- 4 files changed, 622 insertions(+), 203 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index 5ab437a7f..5f33f6ccb 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -188,13 +188,6 @@ "markers": "python_version >= '3.8'", "version": "==4.1.1" }, - "django-js-reverse": { - "hashes": [ - "sha256:2a392d169f44e30b883c30dfcfd917a14167ce8fe196c99d2385b31c90d77aa0", - "sha256:8134c2ab6307c945edfa90671ca65e85d6c1754d48566bdd6464be259cc80c30" - ], - "version": "==0.9.1" - }, "django-otp": { "hashes": [ "sha256:8ba5ab9bd2738c7321376c349d7cce49cf4404e79f6804e0a3cc462a91728e18", @@ -237,6 +230,14 @@ ], "version": "==2.0.6" }, + "django-reverse-js": { + "hashes": [ + "sha256:7d626f4d660604e4c2623a494a08ddb70587375110cf4e7bb6f56eeb4471630f", + "sha256:89c15e3f1bd656a6f7c7f3641144c0a79d42ec692182c8edd6a91061674f6b62" + ], + "markers": "python_version >= '3.10'", + "version": "==0.1.7" + }, "django-sekizai": { "hashes": [ "sha256:5c5e16845d37ce822fc655ce79ec02715191b3d03330b550997bcb842cf24fdf", @@ -303,61 +304,62 @@ }, "numpy": { "hashes": [ - "sha256:08801848a40aea24ce16c2ecde3b756f9ad756586fb2d13210939eb69b023f5b", - "sha256:0937e54c09f7a9a68da6889362ddd2ff584c02d015ec92672c099b61555f8911", - "sha256:0ab32eb9170bf8ffcbb14f11613f4a0b108d3ffee0832457c5d4808233ba8977", - "sha256:0abb3916a35d9090088a748636b2c06dc9a6542f99cd476979fb156a18192b84", - "sha256:0af3a5987f59d9c529c022c8c2a64805b339b7ef506509fba7d0556649b9714b", - "sha256:10e2350aea18d04832319aac0f887d5fcec1b36abd485d14f173e3e900b83e33", - "sha256:15ef8b2177eeb7e37dd5ef4016f30b7659c57c2c0b57a779f1d537ff33a72c7b", - "sha256:1f817c71683fd1bb5cff1529a1d085a57f02ccd2ebc5cd2c566f9a01118e3b7d", - "sha256:24003ba8ff22ea29a8c306e61d316ac74111cebf942afbf692df65509a05f111", - "sha256:30014b234f07b5fec20f4146f69e13cfb1e33ee9a18a1879a0142fbb00d47673", - "sha256:343e3e152bf5a087511cd325e3b7ecfd5b92d369e80e74c12cd87826e263ec06", - "sha256:378cb4f24c7d93066ee4103204f73ed046eb88f9ad5bb2275bb9fa0f6a02bd36", - "sha256:398049e237d1aae53d82a416dade04defed1a47f87d18d5bd615b6e7d7e41d1f", - "sha256:3a3336fbfa0d38d3deacd3fe7f3d07e13597f29c13abf4d15c3b6dc2291cbbdd", - "sha256:442596f01913656d579309edcd179a2a2f9977d9a14ff41d042475280fc7f34e", - "sha256:44e44973262dc3ae79e9063a1284a73e09d01b894b534a769732ccd46c28cc62", - "sha256:54139e0eb219f52f60656d163cbe67c31ede51d13236c950145473504fa208cb", - "sha256:5474dad8c86ee9ba9bb776f4b99ef2d41b3b8f4e0d199d4f7304728ed34d0300", - "sha256:54c6a63e9d81efe64bfb7bcb0ec64332a87d0b87575f6009c8ba67ea6374770b", - "sha256:624884b572dff8ca8f60fab591413f077471de64e376b17d291b19f56504b2bb", - "sha256:6326ab99b52fafdcdeccf602d6286191a79fe2fda0ae90573c5814cd2b0bc1b8", - "sha256:652e92fc409e278abdd61e9505649e3938f6d04ce7ef1953f2ec598a50e7c195", - "sha256:6c1de77ded79fef664d5098a66810d4d27ca0224e9051906e634b3f7ead134c2", - "sha256:76368c788ccb4f4782cf9c842b316140142b4cbf22ff8db82724e82fe1205dce", - "sha256:7a894c51fd8c4e834f00ac742abad73fc485df1062f1b875661a3c1e1fb1c2f6", - "sha256:7dc90da0081f7e1da49ec4e398ede6a8e9cc4f5ebe5f9e06b443ed889ee9aaa2", - "sha256:848c6b5cad9898e4b9ef251b6f934fa34630371f2e916261070a4eb9092ffd33", - "sha256:899da829b362ade41e1e7eccad2cf274035e1cb36ba73034946fccd4afd8606b", - "sha256:8ab81ccd753859ab89e67199b9da62c543850f819993761c1e94a75a814ed667", - "sha256:8fb49a0ba4d8f41198ae2d52118b050fd34dace4b8f3fb0ee34e23eb4ae775b1", - "sha256:9156ca1f79fc4acc226696e95bfcc2b486f165a6a59ebe22b2c1f82ab190384a", - "sha256:9523f8b46485db6939bd069b28b642fec86c30909cea90ef550373787f79530e", - "sha256:a0756a179afa766ad7cb6f036de622e8a8f16ffdd55aa31f296c870b5679d745", - "sha256:a0cdef204199278f5c461a0bed6ed2e052998276e6d8ab2963d5b5c39a0500bc", - "sha256:ab83adc099ec62e044b1fbb3a05499fa1e99f6d53a1dde102b2d85eff66ed324", - "sha256:b34fa5e3b5d6dc7e0a4243fa0f81367027cb6f4a7215a17852979634b5544ee0", - "sha256:b47c551c6724960479cefd7353656498b86e7232429e3a41ab83be4da1b109e8", - "sha256:c4cd94dfefbefec3f8b544f61286584292d740e6e9d4677769bc76b8f41deb02", - "sha256:c4f982715e65036c34897eb598d64aef15150c447be2cfc6643ec7a11af06574", - "sha256:d8f699a709120b220dfe173f79c73cb2a2cab2c0b88dd59d7b49407d032b8ebd", - "sha256:dd94ce596bda40a9618324547cfaaf6650b1a24f5390350142499aa4e34e53d1", - "sha256:de844aaa4815b78f6023832590d77da0e3b6805c644c33ce94a1e449f16d6ab5", - "sha256:e5f0642cdf4636198a4990de7a71b693d824c56a757862230454629cf62e323d", - "sha256:f07fa2f15dabe91259828ce7d71b5ca9e2eb7c8c26baa822c825ce43552f4883", - "sha256:f15976718c004466406342789f31b6673776360f3b1e3c575f25302d7e789575", - "sha256:f358ea9e47eb3c2d6eba121ab512dfff38a88db719c38d1e67349af210bc7529", - "sha256:f505264735ee074250a9c78247ee8618292091d9d1fcc023290e9ac67e8f1afa", - "sha256:f5ebbf9fbdabed208d4ecd2e1dfd2c0741af2f876e7ae522c2537d404ca895c3", - "sha256:f6b26e6c3b98adb648243670fddc8cab6ae17473f9dc58c51574af3e64d61211", - "sha256:f8e93a01a35be08d31ae33021e5268f157a2d60ebd643cfc15de6ab8e4722eb1", - "sha256:fe76d75b345dc045acdbc006adcb197cc680754afd6c259de60d358d60c93736", - "sha256:ffbd6faeb190aaf2b5e9024bac9622d2ee549b7ec89ef3a9373fa35313d44e0e" + "sha256:046356b19d7ad1890c751b99acad5e82dc4a02232013bd9a9a712fddf8eb60f5", + "sha256:0b8cc2715a84b7c3b161f9ebbd942740aaed913584cae9cdc7f8ad5ad41943d0", + "sha256:0d07841fd284718feffe7dd17a63a2e6c78679b2d386d3e82f44f0108c905550", + "sha256:13cc11c00000848702322af4de0147ced365c81d66053a67c2e962a485b3717c", + "sha256:13ce49a34c44b6de5241f0b38b07e44c1b2dcacd9e36c30f9c2fcb1bb5135db7", + "sha256:24c2ad697bd8593887b019817ddd9974a7f429c14a5469d7fad413f28340a6d2", + "sha256:251105b7c42abe40e3a689881e1793370cc9724ad50d64b30b358bbb3a97553b", + "sha256:2ca4b53e1e0b279142113b8c5eb7d7a877e967c306edc34f3b58e9be12fda8df", + "sha256:3269c9eb8745e8d975980b3a7411a98976824e1fdef11f0aacf76147f662b15f", + "sha256:397bc5ce62d3fb73f304bec332171535c187e0643e176a6e9421a6e3eacef06d", + "sha256:3fc5eabfc720db95d68e6646e88f8b399bfedd235994016351b1d9e062c4b270", + "sha256:50a95ca3560a6058d6ea91d4629a83a897ee27c00630aed9d933dff191f170cd", + "sha256:52ac2e48f5ad847cd43c4755520a2317f3380213493b9d8a4c5e37f3b87df504", + "sha256:53e27293b3a2b661c03f79aa51c3987492bd4641ef933e366e0f9f6c9bf257ec", + "sha256:57eb525e7c2a8fdee02d731f647146ff54ea8c973364f3b850069ffb42799647", + "sha256:5889dd24f03ca5a5b1e8a90a33b5a0846d8977565e4ae003a63d22ecddf6782f", + "sha256:59ca673ad11d4b84ceb385290ed0ebe60266e356641428c845b39cd9df6713ab", + "sha256:6435c48250c12f001920f0751fe50c0348f5f240852cfddc5e2f97e007544cbe", + "sha256:6e5a9cb2be39350ae6c8f79410744e80154df658d5bea06e06e0ac5bb75480d5", + "sha256:7be6a07520b88214ea85d8ac8b7d6d8a1839b0b5cb87412ac9f49fa934eb15d5", + "sha256:7c803b7934a7f59563db459292e6aa078bb38b7ab1446ca38dd138646a38203e", + "sha256:7dd86dfaf7c900c0bbdcb8b16e2f6ddf1eb1fe39c6c8cca6e94844ed3152a8fd", + "sha256:8661c94e3aad18e1ea17a11f60f843a4933ccaf1a25a7c6a9182af70610b2313", + "sha256:8ae0fd135e0b157365ac7cc31fff27f07a5572bdfc38f9c2d43b2aff416cc8b0", + "sha256:910b47a6d0635ec1bd53b88f86120a52bf56dcc27b51f18c7b4a2e2224c29f0f", + "sha256:913cc1d311060b1d409e609947fa1b9753701dac96e6581b58afc36b7ee35af6", + "sha256:920b0911bb2e4414c50e55bd658baeb78281a47feeb064ab40c2b66ecba85553", + "sha256:950802d17a33c07cba7fd7c3dcfa7d64705509206be1606f196d179e539111ed", + "sha256:981707f6b31b59c0c24bcda52e5605f9701cb46da4b86c2e8023656ad3e833cb", + "sha256:98ce7fb5b8063cfdd86596b9c762bf2b5e35a2cdd7e967494ab78a1fa7f8b86e", + "sha256:99f4a9ee60eed1385a86e82288971a51e71df052ed0b2900ed30bc840c0f2e39", + "sha256:9a8e06c7a980869ea67bbf551283bbed2856915f0a792dc32dd0f9dd2fb56728", + "sha256:ae8ce252404cdd4de56dcfce8b11eac3c594a9c16c231d081fb705cf23bd4d9e", + "sha256:afd9c680df4de71cd58582b51e88a61feed4abcc7530bcd3d48483f20fc76f2a", + "sha256:b49742cdb85f1f81e4dc1b39dcf328244f4d8d1ded95dea725b316bd2cf18c95", + "sha256:b5613cfeb1adfe791e8e681128f5f49f22f3fcaa942255a6124d58ca59d9528f", + "sha256:bab7c09454460a487e631ffc0c42057e3d8f2a9ddccd1e60c7bb8ed774992480", + "sha256:c8a0e34993b510fc19b9a2ce7f31cb8e94ecf6e924a40c0c9dd4f62d0aac47d9", + "sha256:caf5d284ddea7462c32b8d4a6b8af030b6c9fd5332afb70e7414d7fdded4bfd0", + "sha256:cea427d1350f3fd0d2818ce7350095c1a2ee33e30961d2f0fef48576ddbbe90f", + "sha256:d0cf7d55b1051387807405b3898efafa862997b4cba8aa5dbe657be794afeafd", + "sha256:d10c39947a2d351d6d466b4ae83dad4c37cd6c3cdd6d5d0fa797da56f710a6ae", + "sha256:d2b9cd92c8f8e7b313b80e93cedc12c0112088541dcedd9197b5dee3738c1201", + "sha256:d4c57b68c8ef5e1ebf47238e99bf27657511ec3f071c465f6b1bccbef12d4136", + "sha256:d51fc141ddbe3f919e91a096ec739f49d686df8af254b2053ba21a910ae518bf", + "sha256:e097507396c0be4e547ff15b13dc3866f45f3680f789c1a1301b07dadd3fbc78", + "sha256:e30356d530528a42eeba51420ae8bf6c6c09559051887196599d96ee5f536468", + "sha256:e8d5f8a8e3bc87334f025194c6193e408903d21ebaeb10952264943a985066ca", + "sha256:e8dfa9e94fc127c40979c3eacbae1e61fda4fe71d84869cc129e2721973231ef", + "sha256:f212d4f46b67ff604d11fff7cc62d36b3e8714edf68e44e9760e19be38c03eb0", + "sha256:f7506387e191fe8cdb267f912469a3cccc538ab108471291636a96a54e599556", + "sha256:fac6e277a41163d27dfab5f4ec1f7a83fac94e170665a4a50191b545721c6521", + "sha256:fcd8f556cdc8cfe35e70efb92463082b7f43dd7e547eb071ffc36abc0ca4699b" ], "markers": "python_version >= '3.10'", - "version": "==2.1.0" + "version": "==2.1.1" }, "pandas": { "hashes": [ @@ -602,10 +604,10 @@ }, "rapid-router": { "hashes": [ - "sha256:a559d6989752eea20635024575eac34dd6eb3870e4045b6688b3f783fb54df91", - "sha256:acd9e9a67d18fd54af9cb6251e31335b75993c0315345804e5f18c47fbf51eac" + "sha256:99564a45129e7698d8540fe928aed0e66c43474514a86c28d343ea23ea51c39e", + "sha256:b3017c86049e6120e1d1aa716eae9528d6faa7a0b5321b0ddfcb208c996fa263" ], - "version": "==6.3.6" + "version": "==6.4.3" }, "reportlab": { "hashes": [ diff --git a/portal/forms/teach.py b/portal/forms/teach.py index 622a6e6ca..15f49e063 100644 --- a/portal/forms/teach.py +++ b/portal/forms/teach.py @@ -20,25 +20,37 @@ class InvitedTeacherForm(forms.Form): 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) @@ -49,16 +61,22 @@ 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) @@ -67,31 +85,42 @@ 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( - widget=forms.PasswordInput(attrs={"placeholder": "Current password"}), help_text="Enter your current password" + widget=forms.PasswordInput(attrs={"placeholder": "Current password"}), + help_text="Enter your current password", ) def __init__(self, user, *args, **kwargs): @@ -113,7 +142,9 @@ 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): @@ -122,11 +153,15 @@ def check_password_errors(self, password, confirm_password, current_password): 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", ) @@ -160,7 +195,9 @@ 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 @@ -186,7 +223,8 @@ def show_invalid_login_message(self): class ClassCreationForm(forms.Form): class_name = forms.CharField( - widget=forms.TextInput(attrs={"placeholder": "Class name"}), help_text="Enter a class name" + widget=forms.TextInput(attrs={"placeholder": "Class name"}), + help_text="Enter a class name", ) teacher = forms.ChoiceField(help_text="Select teacher", required=False) classmate_progress = forms.BooleanField( @@ -201,7 +239,12 @@ def __init__(self, *args, teacher=None, **kwargs): if teacher is not None: # Place current teacher at the top - teacher_choices = [(teacher.id, f"{teacher.new_user.first_name} {teacher.new_user.last_name}")] + teacher_choices = [ + ( + teacher.id, + f"{teacher.new_user.first_name} {teacher.new_user.last_name}", + ) + ] # Get coworkers and add them to the choices if the teacher is an admin if teacher.is_admin: @@ -212,7 +255,10 @@ def __init__(self, *args, teacher=None, **kwargs): ) for coworker in coworkers: teacher_choices.append( - (coworker.id, f"{coworker.new_user.first_name} {coworker.new_user.last_name}") + ( + coworker.id, + f"{coworker.new_user.first_name} {coworker.new_user.last_name}", + ) ) self.fields["teacher"].choices = teacher_choices @@ -231,19 +277,35 @@ class ClassEditForm(forms.Form): ] join_choices.extend( [ - (str(hours), "Allow external requests to this class for the next " + str(hours) + " hours") + ( + str(hours), + "Allow external requests to this class for the next " + + str(hours) + + " hours", + ) for hours in range(4, 28, 4) ] ) join_choices.extend( [ - (str(days * 24), "Allow external requests to this class for the next " + str(days) + " days") + ( + str(days * 24), + "Allow external requests to this class for the next " + + str(days) + + " days", + ) for days in range(2, 5) ] ) - join_choices.append(("1000", "Always allow external requests to this class (not recommended)")) + join_choices.append( + ( + "1000", + "Always allow external requests to this class (not recommended)", + ) + ) name = forms.CharField( - widget=forms.TextInput(attrs={"placeholder": "Enter class name"}), help_text="Enter class name" + widget=forms.TextInput(attrs={"placeholder": "Enter class name"}), + help_text="Enter class name", ) classmate_progress = forms.BooleanField( label="Allow students to see their classmates' progress?", @@ -264,7 +326,7 @@ class ClassLevelControlForm(forms.Form): def __init__(self, *args, **kwargs): super(ClassLevelControlForm, self).__init__(*args, **kwargs) - episodes = Episode.objects.filter(pk__in=range(1, 10)) + episodes = Episode.objects.filter(pk__in=range(1, 22)) for episode in episodes: levels = [] @@ -275,27 +337,40 @@ def __init__(self, *args, **kwargs): levels_choices = [(level.id, level.name) for level in levels] self.fields[episode.name] = forms.MultipleChoiceField( - choices=itertools.chain(levels_choices), widget=forms.CheckboxSelectMultiple(), required=False + choices=itertools.chain(levels_choices), + widget=forms.CheckboxSelectMultiple(), + required=False, ) class ClassMoveForm(forms.Form): new_teacher = forms.ChoiceField( - label="New teacher to take over class", help_text="Select teacher", widget=forms.Select() + label="New teacher to take over class", + help_text="Select teacher", + widget=forms.Select(), ) def __init__(self, teachers, *args, **kwargs): self.teachers = teachers teacher_choices = [] for teacher in teachers: - teacher_choices.append((teacher.id, teacher.new_user.first_name + " " + teacher.new_user.last_name)) + teacher_choices.append( + ( + teacher.id, + teacher.new_user.first_name + + " " + + teacher.new_user.last_name, + ) + ) super(ClassMoveForm, self).__init__(*args, **kwargs) self.fields["new_teacher"].choices = teacher_choices class TeacherEditStudentForm(forms.Form): name = forms.CharField( - label="Name", widget=forms.TextInput(attrs={"placeholder": "Name"}), help_text="Choose a name" + label="Name", + widget=forms.TextInput(attrs={"placeholder": "Name"}), + help_text="Choose a name", ) def __init__(self, student, *args, **kwargs): @@ -307,14 +382,24 @@ 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 @@ -323,12 +408,16 @@ 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): @@ -372,19 +461,32 @@ 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) @@ -403,7 +505,9 @@ 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): @@ -428,28 +532,47 @@ 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 class TeacherMoveStudentDisambiguationForm(forms.Form): orig_name = forms.CharField( label="Original Name", - widget=forms.TextInput(attrs={"readonly": "readonly", "placeholder": "Original Name", "type": "hidden"}), + widget=forms.TextInput( + attrs={ + "readonly": "readonly", + "placeholder": "Original Name", + "type": "hidden", + } + ), + ) + name = forms.CharField( + label="Name", + widget=forms.TextInput( + attrs={"placeholder": "Name", "style": "margin : 0px"} + ), ) - name = forms.CharField(label="Name", 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): @@ -468,28 +591,44 @@ def clean(self): class TeacherDismissStudentsForm(forms.Form): orig_name = forms.CharField( help_text="Original student name", - widget=forms.TextInput(attrs={"readonly": "readonly", "placeholder": "Original Name", "class": "m-0"}), + widget=forms.TextInput( + attrs={ + "readonly": "readonly", + "placeholder": "Original Name", + "class": "m-0", + } + ), ) 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 @@ -547,7 +686,10 @@ def clean(self): class TeacherAddExternalStudentForm(forms.Form): - name = forms.CharField(label="Student name", widget=forms.TextInput(attrs={"placeholder": "Name"})) + name = forms.CharField( + label="Student name", + widget=forms.TextInput(attrs={"placeholder": "Name"}), + ) def __init__(self, klass, *args, **kwargs): self.klass = klass @@ -557,9 +699,17 @@ 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 f9a669b38..912706e04 100644 --- a/portal/templates/portal/teach/teacher_edit_class.html +++ b/portal/templates/portal/teach/teacher_edit_class.html @@ -89,7 +89,7 @@
External requests setting
-
Rapid Router access settings
+
Levels access settings

You may control access to levels here by selecting what you wish to display to the students.

@@ -140,6 +140,62 @@
Rapid Router levels
{% endfor %} +
+
Python Den levels
+ +
+ {% for episode in python_episodes %} + {% if episode.levels %} +
+
+ +
+ +
+
+
+ {% for level in episode.levels %} + {% if level.name < 1010 %} +

{{level.name|stringformat:"i"|slice:"3:4"}}: {{level.title.strip | safe}}

+ {% else %} +

{{level.name|stringformat:"i"|slice:"2:4"}}: {{level.title.strip | safe}}

+ {% endif %} + {% endfor %} +
+
+ {% for input in level_control_form|get_dict_item:episode.name %} +
+ {{ input }} +
+ {% endfor %} +
+
+
+
+ {% endif %} + {% endfor %}
diff --git a/portal/views/teacher/teach.py b/portal/views/teacher/teach.py index 437b6ae9f..fa5116d90 100644 --- a/portal/views/teacher/teach.py +++ b/portal/views/teacher/teach.py @@ -7,8 +7,20 @@ from uuid import uuid4 from common.helpers.emails import send_verification_email -from common.helpers.generators import generate_access_code, generate_login_id, generate_password, get_hashed_login_id -from common.models import Class, DailyActivity, JoinReleaseStudent, Student, Teacher, TotalActivity +from common.helpers.generators import ( + generate_access_code, + generate_login_id, + generate_password, + get_hashed_login_id, +) +from common.models import ( + Class, + DailyActivity, + JoinReleaseStudent, + Student, + Teacher, + TotalActivity, +) from common.permissions import check_teacher_authorised, logged_in_as_teacher from django.contrib import messages as messages from django.contrib.auth.decorators import login_required, user_passes_test @@ -21,7 +33,7 @@ from django.urls import reverse, reverse_lazy from django.utils import timezone from django.views.decorators.http import require_POST -from game.views.level_selection import get_blockly_episodes +from game.views.level_selection import get_blockly_episodes, get_python_episodes from portal.views.registration import handle_reset_password_tracking from reportlab.lib.colors import black, red from reportlab.lib.pagesizes import A4 @@ -47,7 +59,9 @@ STUDENT_PASSWORD_LENGTH = 6 REMINDER_CARDS_PDF_ROWS = 8 REMINDER_CARDS_PDF_COLUMNS = 1 -REMINDER_CARDS_PDF_WARNING_TEXT = "Please ensure students keep login details in a secure place" +REMINDER_CARDS_PDF_WARNING_TEXT = ( + "Please ensure students keep login details in a secure place" +) @login_required(login_url=reverse_lazy("teacher_login")) @@ -57,7 +71,9 @@ def teacher_onboarding_create_class(request): Onboarding view for creating a class (and organisation if there isn't one, yet) """ teacher = request.user.new_teacher - requests = Student.objects.filter(pending_class_request__teacher=teacher, new_user__is_active=True) + requests = Student.objects.filter( + pending_class_request__teacher=teacher, new_user__is_active=True + ) if not teacher.school: return HttpResponseRedirect(reverse_lazy("onboarding-organisation")) @@ -67,10 +83,16 @@ def teacher_onboarding_create_class(request): if form.is_valid(): created_class = create_class(form, teacher) messages.success( - request, "The class '{className}' has been created successfully.".format(className=created_class.name) + request, + "The class '{className}' has been created successfully.".format( + className=created_class.name + ), ) return HttpResponseRedirect( - reverse_lazy("onboarding-class", kwargs={"access_code": created_class.access_code}) + reverse_lazy( + "onboarding-class", + kwargs={"access_code": created_class.access_code}, + ) ) else: form = ClassCreationForm(teacher=teacher) @@ -78,7 +100,9 @@ def teacher_onboarding_create_class(request): classes = Class.objects.filter(teacher=teacher) return render( - request, "portal/teach/onboarding_classes.html", {"form": form, "classes": classes, "requests": requests} + request, + "portal/teach/onboarding_classes.html", + {"form": form, "classes": classes, "requests": requests}, ) @@ -96,7 +120,10 @@ def create_class(form, class_teacher, class_creator=None): def generate_student_url(request, student, login_id): return request.build_absolute_uri( - reverse("student_direct_login", kwargs={"user_id": student.new_user.id, "login_id": login_id}) + reverse( + "student_direct_login", + kwargs={"user_id": student.new_user.id, "login_id": login_id}, + ) ) @@ -106,7 +133,9 @@ def process_edit_class(request, access_code, onboarding_done, next_url): """ klass = get_object_or_404(Class, access_code=access_code) teacher = request.user.new_teacher - students = Student.objects.filter(class_field=klass, new_user__is_active=True).order_by("new_user__first_name") + students = Student.objects.filter( + class_field=klass, new_user__is_active=True + ).order_by("new_user__first_name") check_teacher_authorised(request, klass.teacher) @@ -121,14 +150,24 @@ def process_edit_class(request, access_code, onboarding_done, next_url): login_id, hashed_login_id = generate_login_id() new_student = Student.objects.schoolFactory( - klass=klass, name=name, password=password, login_id=hashed_login_id + klass=klass, + name=name, + password=password, + login_id=hashed_login_id, ) - TotalActivity.objects.update(student_registrations=F("student_registrations") + 1) + TotalActivity.objects.update( + student_registrations=F("student_registrations") + 1 + ) login_url = generate_student_url(request, new_student, login_id) students_info.append( - {"id": new_student.new_user.id, "name": name, "password": password, "login_url": login_url} + { + "id": new_student.new_user.id, + "name": name, + "password": password, + "login_url": login_url, + } ) return render( @@ -140,7 +179,10 @@ def process_edit_class(request, access_code, onboarding_done, next_url): "onboarding_done": onboarding_done, "query_data": json.dumps(students_info), "class_url": request.build_absolute_uri( - reverse("student_login", kwargs={"access_code": klass.access_code}) + reverse( + "student_login", + kwargs={"access_code": klass.access_code}, + ) ), }, ) @@ -169,7 +211,10 @@ def teacher_onboarding_edit_class(request, access_code): Adding students to a class during the onboarding process """ return process_edit_class( - request, access_code, onboarding_done=False, next_url="portal/teach/onboarding_students.html" + request, + access_code, + onboarding_done=False, + next_url="portal/teach/onboarding_students.html", ) @@ -179,7 +224,12 @@ def teacher_view_class(request, access_code): """ Adding students to a class after the onboarding process has been completed """ - return process_edit_class(request, access_code, onboarding_done=True, next_url="portal/teach/class.html") + return process_edit_class( + request, + access_code, + onboarding_done=True, + next_url="portal/teach/class.html", + ) @require_POST @@ -191,11 +241,16 @@ def teacher_delete_class(request, access_code): # check user authorised to see class check_teacher_authorised(request, klass.teacher) - if Student.objects.filter(class_field=klass, new_user__is_active=True).exists(): + if Student.objects.filter( + class_field=klass, new_user__is_active=True + ).exists(): messages.info( - request, "This class still has students, please remove or delete them all before deleting the class." + request, + "This class still has students, please remove or delete them all before deleting the class.", + ) + return HttpResponseRedirect( + reverse_lazy("view_class", kwargs={"access_code": access_code}) ) - return HttpResponseRedirect(reverse_lazy("view_class", kwargs={"access_code": access_code})) klass.anonymise() @@ -212,7 +267,9 @@ def teacher_delete_students(request, access_code): # get student objects for students to be deleted, confirming they are in the class student_ids = json.loads(request.POST.get("transfer_students", "[]")) - students = [get_object_or_404(Student, id=i, class_field=klass) for i in student_ids] + students = [ + get_object_or_404(Student, id=i, class_field=klass) for i in student_ids + ] def __anonymise(user): # Delete all personal data from inactive user and mark as inactive. @@ -234,7 +291,9 @@ def __anonymise(user): else: # otherwise, just delete student.new_user.delete() - return HttpResponseRedirect(reverse_lazy("view_class", kwargs={"access_code": access_code})) + return HttpResponseRedirect( + reverse_lazy("view_class", kwargs={"access_code": access_code}) + ) @login_required(login_url=reverse_lazy("teacher_login")) @@ -248,7 +307,9 @@ def teacher_edit_class(request, access_code): """ klass = get_object_or_404(Class, access_code=access_code) old_teacher = klass.teacher - other_teachers = Teacher.objects.filter(school=old_teacher.school).exclude(user=old_teacher.user) + other_teachers = Teacher.objects.filter(school=old_teacher.school).exclude( + user=old_teacher.user + ) # check user authorised to see class check_teacher_authorised(request, klass.teacher) @@ -256,11 +317,17 @@ def teacher_edit_class(request, access_code): external_requests_message = klass.get_requests_message() blockly_episodes = get_blockly_episodes(request) + python_episodes = get_python_episodes(request) locked_levels = klass.locked_levels.all() locked_levels_ids = [locked_level.id for locked_level in locked_levels] - form = ClassEditForm(initial={"name": klass.name, "classmate_progress": klass.classmates_data_viewable}) + form = ClassEditForm( + initial={ + "name": klass.name, + "classmate_progress": klass.classmates_data_viewable, + } + ) level_control_form = ClassLevelControlForm() class_move_form = ClassMoveForm(other_teachers) @@ -272,7 +339,9 @@ def teacher_edit_class(request, access_code): elif "level_control_submit" in request.POST: level_control_form = ClassLevelControlForm(request.POST) if level_control_form.is_valid(): - return process_level_control_form(request, klass, blockly_episodes) + return process_level_control_form( + request, klass, blockly_episodes, python_episodes + ) elif "class_move_submit" in request.POST: class_move_form = ClassMoveForm(other_teachers, request.POST) if class_move_form.is_valid(): @@ -286,6 +355,7 @@ def teacher_edit_class(request, access_code): "class_move_form": class_move_form, "level_control_form": level_control_form, "blockly_episodes": blockly_episodes, + "python_episodes": python_episodes, "locked_levels": locked_levels_ids, "class": klass, "external_requests_message": external_requests_message, @@ -305,12 +375,17 @@ def process_edit_class_form(request, klass, form): # Setting to off klass.always_accept_requests = False klass.accept_requests_until = None - messages.info(request, "Class set successfully to never receive requests from external students.") + messages.info( + request, + "Class set successfully to never receive requests from external students.", + ) elif hours < 1000: # Setting to number of hours klass.always_accept_requests = False - klass.accept_requests_until = timezone.now() + timedelta(hours=hours) + klass.accept_requests_until = timezone.now() + timedelta( + hours=hours + ) messages.info( request, "Class set successfully to receive requests from external students until " @@ -324,35 +399,53 @@ def process_edit_class_form(request, klass, form): klass.always_accept_requests = True klass.accept_requests_until = None messages.info( - request, "Class set successfully to always receive requests from external students (not recommended)" + request, + "Class set successfully to always receive requests from external students (not recommended)", ) klass.name = name klass.classmates_data_viewable = classmate_progress klass.save() - messages.success(request, "The class's settings have been changed successfully.") + messages.success( + request, "The class's settings have been changed successfully." + ) - return HttpResponseRedirect(reverse_lazy("view_class", kwargs={"access_code": klass.access_code})) + return HttpResponseRedirect( + reverse_lazy("view_class", kwargs={"access_code": klass.access_code}) + ) -def process_level_control_form(request, klass, blockly_episodes): +def process_level_control_form( + request, klass, 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. :param klass: The class for which the levels are being locked / unlocked. - :param blockly_episodes: The set of Blockly Episodes in the game. + :param blockly_episodes: The set of Blockly Episodes (Rapid Router). + :param blockly_episodes: The set of Python Episodes (Python Den). :return: A redirect to the teacher dashboard with a success message. """ levels_to_lock_ids = [] - mark_levels_to_lock_in_episodes(request, blockly_episodes, levels_to_lock_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 + ) klass.locked_levels.clear() - [klass.locked_levels.add(levels_to_lock_id) for levels_to_lock_id in levels_to_lock_ids] + [ + klass.locked_levels.add(levels_to_lock_id) + for levels_to_lock_id in levels_to_lock_ids + ] messages.success(request, "Your level preferences have been saved.") - activity_today = DailyActivity.objects.get_or_create(date=datetime.now().date())[0] + activity_today = DailyActivity.objects.get_or_create( + date=datetime.now().date() + )[0] activity_today.level_control_submits += 1 activity_today.save() @@ -375,10 +468,14 @@ def mark_levels_to_lock_in_episodes(request, episodes, levels_to_lock_ids): [ 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 str(episode_level["id"]) + not in request.POST.getlist(episode_name) ] else: - [levels_to_lock_ids.append(episode_level["id"]) for episode_level in episode_levels] + [ + levels_to_lock_ids.append(episode_level["id"]) + for episode_level in episode_levels + ] def process_move_class_form(request, klass, form): @@ -388,7 +485,10 @@ def process_move_class_form(request, klass, form): klass.teacher = new_teacher klass.save() - messages.success(request, "The class has been successfully assigned to a different teacher.") + messages.success( + request, + "The class has been successfully assigned to a different teacher.", + ) return HttpResponseRedirect(reverse_lazy("dashboard")) @@ -401,7 +501,9 @@ def teacher_edit_student(request, pk): student = get_object_or_404(Student, id=pk) check_teacher_authorised(request, student.class_field.teacher) - name_form = TeacherEditStudentForm(student, initial={"name": student.new_user.first_name}) + name_form = TeacherEditStudentForm( + student, initial={"name": student.new_user.first_name} + ) password_form = TeacherSetStudentPass() set_password_mode = False @@ -415,16 +517,24 @@ def teacher_edit_student(request, pk): student.new_user.save() student.save() - messages.success(request, "The student's details have been changed successfully.") + messages.success( + request, + "The student's details have been changed successfully.", + ) return HttpResponseRedirect( - reverse_lazy("view_class", kwargs={"access_code": student.class_field.access_code}) + reverse_lazy( + "view_class", + kwargs={"access_code": student.class_field.access_code}, + ) ) else: password_form = TeacherSetStudentPass(request.POST) if password_form.is_valid(): - return process_reset_password_form(request, student, password_form) + return process_reset_password_form( + request, student, password_form + ) set_password_mode = True return render( @@ -448,7 +558,10 @@ def process_reset_password_form(request, student, password_form): uuidstr = uuid4().hex login_id = get_hashed_login_id(uuidstr) login_url = request.build_absolute_uri( - reverse("student_direct_login", kwargs={"user_id": student.new_user.id, "login_id": uuidstr}) + reverse( + "student_direct_login", + kwargs={"user_id": student.new_user.id, "login_id": uuidstr}, + ) ) students_info = [ @@ -464,7 +577,9 @@ def process_reset_password_form(request, student, password_form): student.new_user.set_password(new_password) student.new_user.save() student.login_id = login_id - clear_ratelimit_cache_for_user(f"{student.new_user.first_name},{student.class_field.access_code}") + clear_ratelimit_cache_for_user( + f"{student.new_user.first_name},{student.class_field.access_code}" + ) student.blocked_time = datetime.now(tz=pytz.utc) - timedelta(days=1) student.save() @@ -477,7 +592,10 @@ def process_reset_password_form(request, student, password_form): "onboarding_done": True, "query_data": json.dumps(students_info), "class_url": request.build_absolute_uri( - reverse("student_login", kwargs={"access_code": student.class_field.access_code}) + reverse( + "student_login", + kwargs={"access_code": student.class_field.access_code}, + ) ), }, ) @@ -495,7 +613,9 @@ def teacher_dismiss_students(request, access_code): # get student objects for students to be dismissed, confirming they are in the class student_ids = json.loads(request.POST.get("transfer_students", "[]")) - students = [get_object_or_404(Student, id=i, class_field=klass) for i in student_ids] + students = [ + get_object_or_404(Student, id=i, class_field=klass) for i in student_ids + ] TeacherDismissStudentsFormSet = formset_factory( wraps(TeacherDismissStudentsForm)(partial(TeacherDismissStudentsForm)), @@ -506,11 +626,17 @@ def teacher_dismiss_students(request, access_code): if is_right_dismiss_form(request): formset = TeacherDismissStudentsFormSet(request.POST) if formset.is_valid(): - return process_dismiss_student_form(request, formset, klass, access_code) + return process_dismiss_student_form( + request, formset, klass, access_code + ) else: initial_data = [ - {"orig_name": student.new_user.first_name, "name": student.new_user.first_name, "email": ""} + { + "orig_name": student.new_user.first_name, + "name": student.new_user.first_name, + "email": "", + } for student in students ] @@ -537,7 +663,11 @@ def process_dismiss_student_form(request, formset, klass, access_code): failed_users.append(data["orig_name"]) continue - student = get_object_or_404(Student, class_field=klass, new_user__first_name__iexact=data["orig_name"]) + student = get_object_or_404( + Student, + class_field=klass, + new_user__first_name__iexact=data["orig_name"], + ) student.class_field = None student.new_user.first_name = data["name"] @@ -549,13 +679,20 @@ def process_dismiss_student_form(request, formset, klass, access_code): student.user.save() # log the data - joinrelease = JoinReleaseStudent.objects.create(student=student, action_type=JoinReleaseStudent.RELEASE) + joinrelease = JoinReleaseStudent.objects.create( + student=student, action_type=JoinReleaseStudent.RELEASE + ) joinrelease.save() - send_verification_email(request, student.new_user, data, school=klass.teacher.school) + send_verification_email( + request, student.new_user, data, school=klass.teacher.school + ) if not failed_users: - messages.success(request, "The students have been released successfully from the class.") + messages.success( + request, + "The students have been released successfully from the class.", + ) else: messages.warning( request, @@ -563,7 +700,9 @@ def process_dismiss_student_form(request, formset, klass, access_code): "Please make sure the email has not been registered to another account.", ) - return HttpResponseRedirect(reverse_lazy("view_class", kwargs={"access_code": access_code})) + return HttpResponseRedirect( + reverse_lazy("view_class", kwargs={"access_code": access_code}) + ) @login_required(login_url=reverse_lazy("teacher_login")) @@ -578,7 +717,9 @@ def teacher_class_password_reset(request, access_code): check_teacher_authorised(request, klass.teacher) student_ids = json.loads(request.POST.get("transfer_students", "[]")) - students = [get_object_or_404(Student, id=i, class_field=klass) for i in student_ids] + students = [ + get_object_or_404(Student, id=i, class_field=klass) for i in student_ids + ] students_info = [] handle_reset_password_tracking(request, "SCHOOL_STUDENT", access_code) @@ -600,7 +741,9 @@ def teacher_class_password_reset(request, access_code): student.new_user.set_password(password) student.new_user.save() student.login_id = hashed_login_id - clear_ratelimit_cache_for_user(f"{student.new_user.first_name},{access_code}") + clear_ratelimit_cache_for_user( + f"{student.new_user.first_name},{access_code}" + ) student.blocked_time = datetime.now(tz=pytz.utc) - timedelta(days=1) student.save() @@ -614,7 +757,9 @@ def teacher_class_password_reset(request, access_code): "students_info": students_info, "query_data": json.dumps(students_info), "class_url": request.build_absolute_uri( - reverse("student_login", kwargs={"access_code": klass.access_code}) + reverse( + "student_login", kwargs={"access_code": klass.access_code} + ) ), }, ) @@ -644,7 +789,11 @@ def teacher_move_students(request, access_code): return render( request, "portal/teach/teacher_move_students.html", - {"transfer_students": transfer_students, "old_class": klass, "form": form}, + { + "transfer_students": transfer_students, + "old_class": klass, + "form": form, + }, ) @@ -660,34 +809,50 @@ def teacher_move_students_to_class(request, access_code): check_if_move_authorised(request, old_class, new_class) - transfer_students_ids = json.loads(request.POST.get("transfer_students", "[]")) + transfer_students_ids = json.loads( + request.POST.get("transfer_students", "[]") + ) # get student objects for students to be transferred, confirming they are in the old class still - transfer_students = [get_object_or_404(Student, id=i, class_field=old_class) for i in transfer_students_ids] + transfer_students = [ + get_object_or_404(Student, id=i, class_field=old_class) + for i in transfer_students_ids + ] # get new class' students - new_class_students = Student.objects.filter(class_field=new_class, new_user__is_active=True).order_by( - "new_user__first_name" - ) + new_class_students = Student.objects.filter( + class_field=new_class, new_user__is_active=True + ).order_by("new_user__first_name") TeacherMoveStudentDisambiguationFormSet = formset_factory( - wraps(TeacherMoveStudentDisambiguationForm)(partial(TeacherMoveStudentDisambiguationForm)), + wraps(TeacherMoveStudentDisambiguationForm)( + partial(TeacherMoveStudentDisambiguationForm) + ), extra=0, formset=BaseTeacherMoveStudentsDisambiguationFormSet, ) if is_right_move_form(request): - formset = TeacherMoveStudentDisambiguationFormSet(new_class, request.POST) + formset = TeacherMoveStudentDisambiguationFormSet( + new_class, request.POST + ) if formset.is_valid(): - return process_move_students_form(request, formset, old_class, new_class) + return process_move_students_form( + request, formset, old_class, new_class + ) else: # format the students for the form initial_data = [ - {"orig_name": student.new_user.first_name, "name": student.new_user.first_name} + { + "orig_name": student.new_user.first_name, + "name": student.new_user.first_name, + } for student in transfer_students ] - formset = TeacherMoveStudentDisambiguationFormSet(new_class, initial=initial_data) + formset = TeacherMoveStudentDisambiguationFormSet( + new_class, initial=initial_data + ) return render( request, @@ -722,7 +887,9 @@ def process_move_students_form(request, formset, old_class, new_class): for name_update in formset.cleaned_data: student = get_object_or_404( - Student, class_field=old_class, new_user__first_name__iexact=name_update["orig_name"] + Student, + class_field=old_class, + new_user__first_name__iexact=name_update["orig_name"], ) student.class_field = new_class student.new_user.first_name = name_update["name"] @@ -730,8 +897,14 @@ def process_move_students_form(request, formset, old_class, new_class): student.save() student.new_user.save() - messages.success(request, "The students have been transferred successfully.") - return HttpResponseRedirect(reverse_lazy("view_class", kwargs={"access_code": old_class.access_code})) + messages.success( + request, "The students have been transferred successfully." + ) + return HttpResponseRedirect( + reverse_lazy( + "view_class", kwargs={"access_code": old_class.access_code} + ) + ) class DownloadType(Enum): @@ -764,7 +937,9 @@ def teacher_print_reminder_cards(request, access_code): CARD_INNER_HEIGHT = CARD_HEIGHT - CARD_PADDING * 2 - logo_image = ImageReader(staticfiles_storage.path("portal/img/logo_cfl_reminder_cards.jpg")) + logo_image = ImageReader( + staticfiles_storage.path("portal/img/logo_cfl_reminder_cards.jpg") + ) klass = get_object_or_404(Class, access_code=access_code) # Check auth @@ -772,8 +947,12 @@ def teacher_print_reminder_cards(request, access_code): # Use data from the query string if given student_data = get_student_data(request) - student_login_link = request.build_absolute_uri(reverse("student_login_access_code")) - class_login_link = request.build_absolute_uri(reverse("student_login", kwargs={"access_code": access_code})) + student_login_link = request.build_absolute_uri( + reverse("student_login_access_code") + ) + class_login_link = request.build_absolute_uri( + reverse("student_login", kwargs={"access_code": access_code}) + ) # Now draw everything x = 0 @@ -785,10 +964,17 @@ def teacher_print_reminder_cards(request, access_code): if current_student_count % (NUM_X * NUM_Y) == 0: p.setFillColor(red) p.setFont("Helvetica-Bold", 10) - p.drawString(PAGE_MARGIN, PAGE_MARGIN / 2, REMINDER_CARDS_PDF_WARNING_TEXT) + p.drawString( + PAGE_MARGIN, PAGE_MARGIN / 2, REMINDER_CARDS_PDF_WARNING_TEXT + ) left = PAGE_MARGIN + x * CARD_WIDTH + x * INTER_CARD_MARGIN * 2 - bottom = PAGE_HEIGHT - PAGE_MARGIN - (y + 1) * CARD_HEIGHT - y * INTER_CARD_MARGIN + bottom = ( + PAGE_HEIGHT + - PAGE_MARGIN + - (y + 1) * CARD_HEIGHT + - y * INTER_CARD_MARGIN + ) inner_bottom = bottom + CARD_PADDING @@ -808,7 +994,12 @@ def teacher_print_reminder_cards(request, access_code): anchor="w", ) - text_left = left + INTER_CARD_MARGIN + (logo_image.getSize()[0] / logo_image.getSize()[1]) * card_logo_height + text_left = ( + left + + INTER_CARD_MARGIN + + (logo_image.getSize()[0] / logo_image.getSize()[1]) + * card_logo_height + ) # student details p.setFillColor(black) @@ -821,9 +1012,19 @@ def teacher_print_reminder_cards(request, access_code): p.setFont("Helvetica-BoldOblique", 12) p.drawString(text_left, inner_bottom + CARD_INNER_HEIGHT * 0.6, "OR") p.setFont("Helvetica", 12) - p.drawString(text_left + 22, inner_bottom + CARD_INNER_HEIGHT * 0.6, f"class link: {class_login_link}") - p.drawString(text_left, inner_bottom + CARD_INNER_HEIGHT * 0.3, f"Name: {student['name']}") - p.drawString(text_left, inner_bottom, f"Password: {student['password']}") + p.drawString( + text_left + 22, + inner_bottom + CARD_INNER_HEIGHT * 0.6, + f"class link: {class_login_link}", + ) + p.drawString( + text_left, + inner_bottom + CARD_INNER_HEIGHT * 0.3, + f"Name: {student['name']}", + ) + p.drawString( + text_left, inner_bottom, f"Password: {student['password']}" + ) x = (x + 1) % NUM_X y = compute_show_page_character(p, x, y, NUM_Y) @@ -842,13 +1043,17 @@ def teacher_print_reminder_cards(request, access_code): @user_passes_test(logged_in_as_teacher, login_url=reverse_lazy("teacher_login")) def teacher_download_csv(request, access_code): response = HttpResponse(content_type="text/csv") - response["Content-Disposition"] = 'attachment; filename="student_login_urls.csv"' + response[ + "Content-Disposition" + ] = 'attachment; filename="student_login_urls.csv"' klass = get_object_or_404(Class, access_code=access_code) # Check auth check_teacher_authorised(request, klass.teacher) - class_url = request.build_absolute_uri(reverse("student_login", kwargs={"access_code": access_code})) + class_url = request.build_absolute_uri( + reverse("student_login", kwargs={"access_code": access_code}) + ) # Use data from the query string if given student_data = get_student_data(request) @@ -856,7 +1061,9 @@ def teacher_download_csv(request, access_code): writer = csv.writer(response) writer.writerow([access_code, class_url]) for student in student_data: - writer.writerow([student["name"], student["password"], student["login_url"]]) + writer.writerow( + [student["name"], student["password"], student["login_url"]] + ) count_student_details_click(DownloadType.CSV) @@ -884,7 +1091,9 @@ def compute_show_page_end(p, x, y): def count_student_pack_downloads_click(student_pack_type): - activity_today = DailyActivity.objects.get_or_create(date=datetime.now().date())[0] + activity_today = DailyActivity.objects.get_or_create( + date=datetime.now().date() + )[0] if DownloadType(student_pack_type) == DownloadType.PRIMARY_PACK: activity_today.primary_coding_club_downloads += 1 elif DownloadType(student_pack_type) == DownloadType.PYTHON_PACK: @@ -895,7 +1104,9 @@ def count_student_pack_downloads_click(student_pack_type): def count_student_details_click(download_type): - activity_today = DailyActivity.objects.get_or_create(date=datetime.now().date())[0] + activity_today = DailyActivity.objects.get_or_create( + date=datetime.now().date() + )[0] if download_type == DownloadType.CSV: activity_today.csv_click_count += 1