codeforlife-portal 8.7.0__py2.py3-none-any.whl → 8.7.1__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of codeforlife-portal might be problematic. Click here for more details.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codeforlife-portal
3
- Version: 8.7.0
3
+ Version: 8.7.1
4
4
  Classifier: Programming Language :: Python
5
5
  Classifier: Programming Language :: Python :: 3.12
6
6
  Classifier: Framework :: Django
@@ -8,7 +8,7 @@ Description-Content-Type: text/markdown
8
8
  License-File: LICENSE.md
9
9
  Requires-Dist: asgiref==3.8.1; python_version >= "3.8"
10
10
  Requires-Dist: certifi==2025.4.26; python_version >= "3.6"
11
- Requires-Dist: cfl-common==8.7.0
11
+ Requires-Dist: cfl-common==8.7.1
12
12
  Requires-Dist: chardet==5.2.0; python_version >= "3.7"
13
13
  Requires-Dist: charset-normalizer==3.4.2; python_version >= "3.7"
14
14
  Requires-Dist: diff-match-patch==20241021; python_version >= "3.7"
@@ -60,7 +60,7 @@ Requires-Dist: asttokens==3.0.0; python_version >= "3.8" and extra == "dev"
60
60
  Requires-Dist: attrs==25.3.0; python_version >= "3.8" and extra == "dev"
61
61
  Requires-Dist: black==25.1.0; python_version >= "3.9" and extra == "dev"
62
62
  Requires-Dist: certifi==2025.4.26; python_version >= "3.6" and extra == "dev"
63
- Requires-Dist: cfl-common==8.7.0; extra == "dev"
63
+ Requires-Dist: cfl-common==8.7.1; extra == "dev"
64
64
  Requires-Dist: charset-normalizer==3.4.2; python_version >= "3.7" and extra == "dev"
65
65
  Requires-Dist: click==8.1.8; python_version >= "3.7" and extra == "dev"
66
66
  Requires-Dist: coverage[toml]==7.8.0; python_version >= "3.9" and extra == "dev"
@@ -87,7 +87,7 @@ cfl_common/common/tests/utils/organisation.py,sha256=vNgKFtU3VPcWRnZfh82yCS90PLA
87
87
  cfl_common/common/tests/utils/student.py,sha256=PLd980iSlxmMoB8J3C2pVjNC5xHdVxfAkJXzhv_dRhg,3814
88
88
  cfl_common/common/tests/utils/teacher.py,sha256=KQ_NAl4yQqiX_zwcULQjkovc29JPhnkLR5Nk3Ljzbpg,2661
89
89
  cfl_common/common/tests/utils/user.py,sha256=NvLzZLVP4jy5Hn1iztOYF_BTQ9WsbSmuWMEzGzhAsRU,919
90
- codeforlife_portal-8.7.0.dist-info/licenses/LICENSE.md,sha256=9AbRlCDqD2D1tPibimysFv3zg3AIc49-eyv9aEsyq9w,115
90
+ codeforlife_portal-8.7.1.dist-info/licenses/LICENSE.md,sha256=9AbRlCDqD2D1tPibimysFv3zg3AIc49-eyv9aEsyq9w,115
91
91
  deploy/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
92
92
  deploy/captcha.py,sha256=MbOBuGnbT_SOIltSjP1XMOLrfo1DldCilaVAEim0vM4,23
93
93
  deploy/views.py,sha256=7mY2zNPkaBFrlWGByJHp2zFqHIHSMc0YArjE_sFWMMU,131
@@ -111,7 +111,7 @@ example_project/portal_test_settings.py,sha256=AaXwSSB2A6qu6My_7cBciQa_jONy084uq
111
111
  example_project/settings.py,sha256=RRSHhAgJVDn4uNZG395V6_Td7jwsSaBGNwQBZ_KJm0Y,5973
112
112
  example_project/urls.py,sha256=FUTzHPlUS1O5kqMHjL5V4L552N2ln7uTDXcw9wjKUto,422
113
113
  example_project/wsgi.py,sha256=U1W6WzZxZaIdYZ5tks7w9fqp5WS5qvn2iThsVcskrWw,829
114
- portal/__init__.py,sha256=nW2S9cxog2B_2xeN534jt1o6PE7ow-7Hd0PLILs5K5o,22
114
+ portal/__init__.py,sha256=a3FQRJoDSFET9iDvdpENKACBRF5lwOJHqpR4Y9QjQY4,22
115
115
  portal/admin.py,sha256=RKJizTF6dPJKmGPZw7nZUM0X8jkiTjgyKhLQxtvHJ0I,6148
116
116
  portal/app_settings.py,sha256=DhWLQOwM0zVOXE3O5TNKbMM9K6agfLuCsHOdr1J7xEI,651
117
117
  portal/backends.py,sha256=2Dss6_WoQwPuDzJUF1yEaTQTNG4eUrD12ujJQ5cp5Tc,812
@@ -541,7 +541,7 @@ portal/tests/test_emails.py,sha256=pLr06j3uMBxP1raoZQWzUTBVFvsEDFtUh85J8OnqCwE,9
541
541
  portal/tests/test_global_forms.py,sha256=GIm_oSN4VsfaO--E2SMRu8CwVraan0UBj-_LE_tu8w0,833
542
542
  portal/tests/test_helper_methods.py,sha256=-SQCDZm2XUtyXGEp0CHIb_SSC9CPD-XOSnpnY8QclHk,890
543
543
  portal/tests/test_independent_student.py,sha256=NrRjTEr6V4WXpCE74N8LYNVocvLSvddkjuo3dYpfAZc,27245
544
- portal/tests/test_invite_teacher.py,sha256=QKHWVzyd5OLpTOQ0j8p9ZF6DSCWPHcWHZwCpNTFG_6o,12320
544
+ portal/tests/test_invite_teacher.py,sha256=hynk2p63URQocCS8N6-IQnQWkdzggQ-1jGDuWcRSY_Y,14902
545
545
  portal/tests/test_middleware.py,sha256=HV7AdgWyvgrtPyDMw3np5YDbUstFpKif7Qqw-e8gs5s,8258
546
546
  portal/tests/test_organisation.py,sha256=kCMUNzLN6EzaMUBcFkqXwnqLGgOuQxQWIHHt63nhqBs,7574
547
547
  portal/tests/test_partials.py,sha256=cSLNLjdsriROjPxkZlM3HKD3CSKDuKgpaDIdL3fPyBs,1794
@@ -628,13 +628,13 @@ portal/views/student/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hS
628
628
  portal/views/student/edit_account_details.py,sha256=Ba-3D_zzKbX5N01NG5qqBS0ud10B8D5SN8hOpLiGa9U,8468
629
629
  portal/views/student/play.py,sha256=GMxk65bxWOe1Ds2kb6rvuOd1GoAtt5v_9AihLNXoUL0,8768
630
630
  portal/views/teacher/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
631
- portal/views/teacher/dashboard.py,sha256=Y6OhT7h4hgNZAEDZDJP86F7SjrL8W12t0rpHipSYgSQ,27977
631
+ portal/views/teacher/dashboard.py,sha256=Z5PGmyEmNSltCPMzADq2tUN66LHPVrdrtgdMgvPlrkk,28119
632
632
  portal/views/teacher/teach.py,sha256=IXlvsJ_1pX7oSr2SMWdWWC7aCFDA38IgxAoHzy_HHp4,36850
633
633
  portal/views/two_factor/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
634
634
  portal/views/two_factor/core.py,sha256=Lk32z2SN2Pg0rRkK-N-LXMvXC1kKKsH3l692kiSDQ4E,964
635
635
  portal/views/two_factor/form.py,sha256=lnHNKI-BMlpncTuW3zUzjPaJJNuEra2I_nOam0eOKFY,257
636
636
  portal/views/two_factor/profile.py,sha256=SHSg_xHccE5PtD-OfuOkYhREYz_er4bj5ro1RjJ88Yw,393
637
- codeforlife_portal-8.7.0.dist-info/METADATA,sha256=7qpquiwQ0MoxSQLDR-I_zYAtbxZ_WQJARb6jUe3mDeE,12318
638
- codeforlife_portal-8.7.0.dist-info/WHEEL,sha256=JNWh1Fm1UdwIQV075glCn4MVuCRs0sotJIq-J6rbxCU,109
639
- codeforlife_portal-8.7.0.dist-info/top_level.txt,sha256=8e5pdsuIoTqEAMqpelHBjGjLbffcBtgOoggmd2q7nMw,41
640
- codeforlife_portal-8.7.0.dist-info/RECORD,,
637
+ codeforlife_portal-8.7.1.dist-info/METADATA,sha256=obJATb8BqSwTeVWDexVXJ5bM8Rid-sCKkyCIjbRAwC0,12318
638
+ codeforlife_portal-8.7.1.dist-info/WHEEL,sha256=JNWh1Fm1UdwIQV075glCn4MVuCRs0sotJIq-J6rbxCU,109
639
+ codeforlife_portal-8.7.1.dist-info/top_level.txt,sha256=8e5pdsuIoTqEAMqpelHBjGjLbffcBtgOoggmd2q7nMw,41
640
+ codeforlife_portal-8.7.1.dist-info/RECORD,,
portal/__init__.py CHANGED
@@ -1 +1 @@
1
- __version__ = "8.7.0"
1
+ __version__ = "8.7.1"
@@ -5,7 +5,7 @@ from uuid import uuid4
5
5
  import pytest
6
6
  from common.models import SchoolTeacherInvitation, Teacher
7
7
  from common.tests.utils.classes import create_class_directly
8
- from common.tests.utils.organisation import create_organisation_directly
8
+ from common.tests.utils.organisation import create_organisation_directly, join_teacher_to_organisation
9
9
  from common.tests.utils.teacher import signup_teacher_directly
10
10
  from django.contrib.messages import get_messages
11
11
  from django.core import mail
@@ -149,8 +149,66 @@ class TestInviteTeacher(TestCase):
149
149
  "access this page."
150
150
  )
151
151
 
152
+ def test_invite_permissions(self):
153
+ # Create an admin and a standard teacher in the same school
154
+ admin_email, admin_password = signup_teacher_directly()
155
+ school = create_organisation_directly(admin_email)
156
+ create_class_directly(admin_email)
157
+
158
+ standard_email, standard_password = signup_teacher_directly()
159
+ join_teacher_to_organisation(standard_email, school.name)
160
+ create_class_directly(standard_email)
161
+
162
+ # Log in as standard teacher, try inviting a teacher, no invitation should be created
163
+ client = Client()
164
+ client.login(username=standard_email, password=standard_password)
165
+
166
+ dashboard_url = reverse("dashboard")
167
+ data = {
168
+ "teacher_first_name": "Valid",
169
+ "teacher_last_name": "Name",
170
+ "teacher_email": "new@teacher.com",
171
+ "invite_teacher": "",
172
+ }
173
+
174
+ response = client.post(dashboard_url, data)
175
+
176
+ assert response.status_code == 200
177
+ messages = list(response.context["messages"])
178
+ assert len(messages) == 0
179
+ assert len(mail.outbox) == 0
180
+ client.logout()
181
+
182
+ assert not SchoolTeacherInvitation.objects.filter(invited_teacher_email="new@teacher.com").exists()
183
+
184
+ # Log in as admin teacher to invite a teacher
185
+ client.login(username=admin_email, password=admin_password)
186
+
187
+ dashboard_url = reverse("dashboard")
188
+ data = {
189
+ "teacher_first_name": "Valid",
190
+ "teacher_last_name": "Name",
191
+ "teacher_email": "new@teacher.com",
192
+ "invite_teacher": "",
193
+ }
194
+
195
+ client.post(dashboard_url, data)
196
+ client.logout()
197
+
198
+ assert SchoolTeacherInvitation.objects.filter(invited_teacher_email="new@teacher.com").exists()
199
+ invite = SchoolTeacherInvitation.objects.get(invited_teacher_email="new@teacher.com")
200
+
201
+ # Log in as standard teacher, try resending and deleting the invitation, both should fail
202
+ client.login(username=standard_email, password=standard_password)
203
+
204
+ response = client.post(reverse("resend_invite_teacher", kwargs={"token": invite.token}))
205
+ message = list(response.wsgi_request._messages)[0].message
206
+ assert message == "You do not have permission to perform this action or the invite does not exist"
207
+
208
+ response = client.post(reverse("delete_teacher_invite", kwargs={"token": invite.token}))
209
+ message = list(response.wsgi_request._messages)[0].message
210
+ assert message == "You do not have permission to perform this action or the invite does not exist"
152
211
 
153
- class TestTeacherInviteAPI(TestCase):
154
212
  def test_delete_exception(self):
155
213
  email, password = signup_teacher_directly()
156
214
  create_organisation_directly(email)
@@ -29,6 +29,7 @@ from django.utils import timezone
29
29
  from django.views.decorators.http import require_POST
30
30
  from game.level_management import levels_shared_with, unshare_level
31
31
  from game.models import Level
32
+ from game.views.level_selection import is_admin_teacher
32
33
  from two_factor.utils import devices_for_user
33
34
 
34
35
  from portal.forms.invite_teacher import InviteTeacherForm
@@ -160,7 +161,7 @@ def dashboard_teacher_view(request, is_admin):
160
161
  elif request.POST.get("show_onboarding_complete") == "1":
161
162
  show_onboarding_complete = True
162
163
 
163
- elif "invite_teacher" in request.POST:
164
+ elif "invite_teacher" in request.POST and is_admin:
164
165
  invite_teacher_form = InviteTeacherForm(request.POST)
165
166
  if invite_teacher_form.is_valid():
166
167
  data = invite_teacher_form.cleaned_data
@@ -674,7 +675,7 @@ def delete_teacher_invite(request, token):
674
675
  teacher = request.user.new_teacher
675
676
 
676
677
  # auth the user before deletion
677
- if invite is None or teacher.school != invite.school:
678
+ if invite is None or teacher.school != invite.school or not is_admin_teacher(request.user):
678
679
  messages.error(
679
680
  request,
680
681
  "You do not have permission to perform this action or the invite does not exist",
@@ -698,7 +699,7 @@ def resend_invite_teacher(request, token):
698
699
  teacher = request.user.new_teacher
699
700
 
700
701
  # auth the user before deletion
701
- if invite is None or teacher.school != invite.school:
702
+ if invite is None or teacher.school != invite.school or not is_admin_teacher(request.user):
702
703
  messages.error(
703
704
  request,
704
705
  "You do not have permission to perform this action or the invite does not exist",
@@ -706,7 +707,7 @@ def resend_invite_teacher(request, token):
706
707
  else:
707
708
  invite.expiry = timezone.now() + timedelta(days=30)
708
709
  invite.save()
709
- teacher = Teacher.objects.filter(id=invite.from_teacher.id)[0]
710
+ teacher = Teacher.objects.filter(id=invite.from_teacher.id)
710
711
 
711
712
  messages.success(request, "Teacher re-invited!")
712
713