codeforlife-portal 5.32.2__py2.py3-none-any.whl → 8.9.9__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.
Files changed (393) hide show
  1. cfl_common/common/__init__.py +1 -0
  2. cfl_common/common/app_settings.py +66 -0
  3. cfl_common/common/apps.py +6 -0
  4. cfl_common/common/context_processors.py +9 -0
  5. cfl_common/common/csp_config.py +85 -0
  6. cfl_common/common/helpers/__init__.py +0 -0
  7. cfl_common/common/helpers/data_migration_loader.py +42 -0
  8. cfl_common/common/helpers/emails.py +393 -0
  9. cfl_common/common/helpers/generators.py +52 -0
  10. cfl_common/common/helpers/organisation.py +10 -0
  11. cfl_common/common/mail.py +201 -0
  12. cfl_common/common/migrations/0001_initial.py +240 -0
  13. cfl_common/common/migrations/0002_emailverification.py +55 -0
  14. cfl_common/common/migrations/0003_aimmocharacter.py +31 -0
  15. cfl_common/common/migrations/0004_add_aimmocharacters.py +17 -0
  16. cfl_common/common/migrations/0005_add_worksheets.py +8 -0
  17. cfl_common/common/migrations/0006_update_aimmo_character_image_path.py +17 -0
  18. cfl_common/common/migrations/0007_add_pdf_names_to_first_two_worksheets.py +8 -0
  19. cfl_common/common/migrations/0008_unlock_worksheet_3.py +11 -0
  20. cfl_common/common/migrations/0009_add_blocked_time_to_teacher_and_student.py +24 -0
  21. cfl_common/common/migrations/0010_remove_teacher_title.py +18 -0
  22. cfl_common/common/migrations/0011_student_login_id.py +18 -0
  23. cfl_common/common/migrations/0012_usersession.py +39 -0
  24. cfl_common/common/migrations/0013_class_school.py +42 -0
  25. cfl_common/common/migrations/0014_login_type.py +29 -0
  26. cfl_common/common/migrations/0015_dailyactivity.py +31 -0
  27. cfl_common/common/migrations/0016_joinreleasestudent.py +42 -0
  28. cfl_common/common/migrations/0017_copy_email_to_username.py +18 -0
  29. cfl_common/common/migrations/0018_update_aimmo_character_image_path.py +15 -0
  30. cfl_common/common/migrations/0019_aimmocharacter_alt.py +16 -0
  31. cfl_common/common/migrations/0020_class_is_active_and_null_access_code.py +23 -0
  32. cfl_common/common/migrations/0021_school_is_active.py +28 -0
  33. cfl_common/common/migrations/0022_school_cleanup.py +29 -0
  34. cfl_common/common/migrations/0023_userprofile_aimmo_badges.py +22 -0
  35. cfl_common/common/migrations/0024_teacher_invited_by.py +25 -0
  36. cfl_common/common/migrations/0025_schoolteacherinvitation.py +47 -0
  37. cfl_common/common/migrations/0026_teacher_remove_join_request.py +22 -0
  38. cfl_common/common/migrations/0027_class_created_by.py +25 -0
  39. cfl_common/common/migrations/0028_coding_club_downloads.py +23 -0
  40. cfl_common/common/migrations/0029_dynamicelement.py +22 -0
  41. cfl_common/common/migrations/0030_add_maintenance_banner.py +25 -0
  42. cfl_common/common/migrations/0031_improve_admin_panel.py +56 -0
  43. cfl_common/common/migrations/0032_dailyactivity_level_control_submits.py +18 -0
  44. cfl_common/common/migrations/0033_password_reset_tracking_fields.py +23 -0
  45. cfl_common/common/migrations/0034_dailyactivity_daily_school_student_lockout_reset.py +18 -0
  46. cfl_common/common/migrations/0035_rename_lockout_fields.py +27 -0
  47. cfl_common/common/migrations/0036_rename_awaiting_email_verification_userprofile_is_verified.py +17 -0
  48. cfl_common/common/migrations/0037_migrate_email_verification.py +21 -0
  49. cfl_common/common/migrations/0038_delete_emailverification.py +16 -0
  50. cfl_common/common/migrations/0039_copy_email_to_username.py +18 -0
  51. cfl_common/common/migrations/0040_school_county.py +18 -0
  52. cfl_common/common/migrations/0041_populate_gb_counties.py +27 -0
  53. cfl_common/common/migrations/0042_totalactivity.py +25 -0
  54. cfl_common/common/migrations/0043_add_total_activity.py +30 -0
  55. cfl_common/common/migrations/0044_update_activity_models.py +33 -0
  56. cfl_common/common/migrations/0045_otp.py +23 -0
  57. cfl_common/common/migrations/0046_alter_school_country.py +19 -0
  58. cfl_common/common/migrations/0047_delete_school_postcode.py +16 -0
  59. cfl_common/common/migrations/0048_unique_school_names.py +42 -0
  60. cfl_common/common/migrations/0049_anonymise_orphan_users.py +29 -0
  61. cfl_common/common/migrations/0050_anonymise_orphan_schools.py +30 -0
  62. cfl_common/common/migrations/0051_verify_returning_users.py +26 -0
  63. cfl_common/common/migrations/0052_add_cse_fields.py +68 -0
  64. cfl_common/common/migrations/0053_clean_class_data.py +24 -0
  65. cfl_common/common/migrations/0054_delete_aimmo_models.py +20 -0
  66. cfl_common/common/migrations/0055_alter_schoolteacherinvitation_token.py +18 -0
  67. cfl_common/common/migrations/0056_set_non_school_teachers_as_non_admins.py +25 -0
  68. cfl_common/common/migrations/0057_teacher_teacher__is_admin.py +19 -0
  69. cfl_common/common/migrations/0058_userprofile_google_refresh_token_and_more.py +24 -0
  70. cfl_common/common/migrations/__init__.py +0 -0
  71. cfl_common/common/models.py +557 -0
  72. cfl_common/common/permissions.py +84 -0
  73. cfl_common/common/tests/__init__.py +0 -0
  74. cfl_common/common/tests/test_migration_anonymise_orphan_schools.py +30 -0
  75. cfl_common/common/tests/test_migration_anonymise_orphan_users.py +30 -0
  76. cfl_common/common/tests/test_migration_blocked_time.py +15 -0
  77. cfl_common/common/tests/test_migration_remove_teacher_title.py +13 -0
  78. cfl_common/common/tests/test_migration_unique_school_names.py +33 -0
  79. cfl_common/common/tests/test_migration_verify_returning_users.py +59 -0
  80. cfl_common/common/tests/test_models.py +87 -0
  81. cfl_common/common/tests/utils/__init__.py +0 -0
  82. cfl_common/common/tests/utils/classes.py +38 -0
  83. cfl_common/common/tests/utils/email.py +67 -0
  84. cfl_common/common/tests/utils/organisation.py +41 -0
  85. cfl_common/common/tests/utils/student.py +123 -0
  86. cfl_common/common/tests/utils/teacher.py +73 -0
  87. cfl_common/common/tests/utils/user.py +27 -0
  88. cfl_common/common/utils.py +56 -0
  89. cfl_common/setup.py +61 -0
  90. codeforlife_portal-8.9.9.dist-info/METADATA +226 -0
  91. {codeforlife_portal-5.32.2.dist-info → codeforlife_portal-8.9.9.dist-info}/RECORD +341 -238
  92. {codeforlife_portal-5.32.2.dist-info → codeforlife_portal-8.9.9.dist-info}/WHEEL +1 -1
  93. codeforlife_portal-8.9.9.dist-info/licenses/LICENSE.md +3 -0
  94. {codeforlife_portal-5.32.2.dist-info → codeforlife_portal-8.9.9.dist-info}/top_level.txt +1 -0
  95. deploy/middleware/maintenance.py +25 -0
  96. deploy/middleware/screentime_warning.py +29 -0
  97. deploy/middleware/security.py +5 -6
  98. deploy/middleware/session_timeout.py +31 -0
  99. deploy/middleware/tmp_basic_auth.py +41 -0
  100. example_project/portal_test_settings.py +239 -0
  101. example_project/settings.py +156 -17
  102. example_project/urls.py +5 -6
  103. portal/__init__.py +1 -1
  104. portal/admin.py +142 -29
  105. portal/app_settings.py +10 -7
  106. portal/forms/dotmailer.py +6 -4
  107. portal/forms/invite_teacher.py +19 -10
  108. portal/forms/organisation.py +137 -68
  109. portal/forms/play.py +53 -98
  110. portal/forms/registration.py +70 -164
  111. portal/forms/teach.py +147 -121
  112. portal/handlers.py +1 -2
  113. portal/helpers/decorators.py +30 -10
  114. portal/helpers/password.py +86 -47
  115. portal/helpers/ratelimit.py +32 -15
  116. portal/helpers/regexes.py +5 -0
  117. portal/helpers/request_handlers.py +10 -0
  118. portal/migrations/0044_auto_20150430_0959.py +6 -2
  119. portal/mixins/__init__.py +1 -0
  120. portal/mixins/cron_mixin.py +12 -0
  121. portal/permissions/__init__.py +1 -0
  122. portal/permissions/is_cron_request_from_google.py +14 -0
  123. portal/static/portal/img/10_years_anniversary.png +0 -0
  124. portal/static/portal/img/RR_logo_grass_background.png +0 -0
  125. portal/static/portal/img/coding_club_hero.jpg +0 -0
  126. portal/static/portal/img/coding_club_python_pack.png +0 -0
  127. portal/static/portal/img/facebook.png +0 -0
  128. portal/static/portal/img/gitbook.png +0 -0
  129. portal/static/portal/img/howe_dell_1.png +0 -0
  130. portal/static/portal/img/howe_dell_2.png +0 -0
  131. portal/static/portal/img/howe_dell_3.png +0 -0
  132. portal/static/portal/img/logo_cfl.png +0 -0
  133. portal/static/portal/img/logo_cfl_powered.svg +35 -0
  134. portal/static/portal/img/logo_cfl_reminder_cards.jpg +0 -0
  135. portal/static/portal/img/logo_ocado_group.png +0 -0
  136. portal/static/portal/img/logo_python_den.svg +21 -0
  137. portal/static/portal/img/long_europe_map.png +0 -0
  138. portal/static/portal/img/python_den.png +0 -0
  139. portal/static/portal/img/python_den_banner.svg +26 -0
  140. portal/static/portal/img/rapid_router_landing_hero.png +0 -0
  141. portal/static/portal/img/rr_advanced.png +0 -0
  142. portal/static/portal/img/ten_year_map_pin.svg +1 -0
  143. portal/static/portal/img/thumbnail_educate_rapid_router.png +0 -0
  144. portal/static/portal/img/thumbnail_educate_resources.png +0 -0
  145. portal/static/portal/img/thumbnail_play_rapid_router.png +0 -0
  146. portal/static/portal/img/thumbnail_python_den.png +0 -0
  147. portal/static/portal/img/twitter.png +0 -0
  148. portal/static/portal/js/carouselCards.js +25 -0
  149. portal/static/portal/js/common.js +96 -1
  150. portal/static/portal/js/independentLogin.js +16 -0
  151. portal/static/portal/js/independentRegistration.js +86 -0
  152. portal/static/portal/js/levelControl.js +77 -0
  153. portal/static/portal/js/lib/jquery.min.js +2 -0
  154. portal/static/portal/js/organisation_manage.js +142 -14
  155. portal/static/portal/js/passwordStrength.js +154 -64
  156. portal/static/portal/js/resetPassword.js +23 -0
  157. portal/static/portal/js/riveted.min.js +238 -239
  158. portal/static/portal/js/school.js +13 -0
  159. portal/static/portal/js/studentLogin.js +16 -0
  160. portal/static/portal/js/teacherEditStudent.js +23 -0
  161. portal/static/portal/js/teacherLogin.js +16 -0
  162. portal/static/portal/js/tenYearMap.js +14 -0
  163. portal/static/portal/sass/colorbox.scss +0 -1
  164. portal/static/portal/sass/modules/_colours.scss +1 -0
  165. portal/static/portal/sass/modules/_levels.scss +1 -1
  166. portal/static/portal/sass/modules/_mixins.scss +21 -0
  167. portal/static/portal/sass/partials/_banners.scss +4 -177
  168. portal/static/portal/sass/partials/_buttons.scss +12 -15
  169. portal/static/portal/sass/partials/_carousel.scss +129 -0
  170. portal/static/portal/sass/partials/_footer.scss +21 -22
  171. portal/static/portal/sass/partials/_forms.scss +60 -5
  172. portal/static/portal/sass/partials/_grids.scss +34 -61
  173. portal/static/portal/sass/partials/_header.scss +28 -20
  174. portal/static/portal/sass/partials/_images.scss +292 -39
  175. portal/static/portal/sass/partials/_popup.scss +18 -15
  176. portal/static/portal/sass/partials/_tables.scss +12 -20
  177. portal/static/portal/sass/partials/_text.scss +6 -10
  178. portal/static/portal/sass/styles.scss +0 -1
  179. portal/static/portal/video/code for life .pdf +0 -0
  180. portal/strings/about.py +5 -0
  181. portal/strings/coding_club.py +9 -0
  182. portal/strings/play.py +6 -5
  183. portal/strings/teach.py +1 -1
  184. portal/strings/teacher_resources.py +2 -8
  185. portal/strings/ten_year_map.py +13 -0
  186. portal/templates/403.html +2 -2
  187. portal/templates/404.html +1 -1
  188. portal/templates/500.html +2 -2
  189. portal/templates/{captcha → django_recaptcha}/includes/js_v2_invisible.html +3 -3
  190. portal/templates/{captcha → django_recaptcha}/widget_v2_invisible.html +2 -2
  191. portal/templates/email.html +4 -2
  192. portal/templates/maintenance.html +34 -0
  193. portal/templates/portal/about.html +94 -62
  194. portal/templates/portal/base.html +176 -152
  195. portal/templates/portal/coding_club.html +100 -0
  196. portal/templates/portal/contribute.html +56 -52
  197. portal/templates/portal/email_invitation_sent.html +1 -1
  198. portal/templates/portal/email_style_template.html +374 -0
  199. portal/templates/portal/email_verification_failed.html +1 -1
  200. portal/templates/portal/email_verification_needed.html +9 -9
  201. portal/templates/portal/form_shapes.html +20 -8
  202. portal/templates/portal/getinvolved.html +6 -6
  203. portal/templates/portal/home.html +35 -10
  204. portal/templates/portal/home_learning.html +19 -19
  205. portal/templates/portal/locked_out.html +0 -1
  206. portal/templates/portal/locked_out_school_student.html +16 -0
  207. portal/templates/portal/login/independent_student.html +31 -15
  208. portal/templates/portal/login/student.html +10 -7
  209. portal/templates/portal/login/student_class_code.html +7 -4
  210. portal/templates/portal/login/teacher.html +34 -17
  211. portal/templates/portal/partials/banner.html +18 -4
  212. portal/templates/portal/partials/benefits.html +1 -1
  213. portal/templates/portal/partials/card_list.html +34 -24
  214. portal/templates/portal/partials/character_list.html +5 -5
  215. portal/templates/portal/partials/cookie_list.html +161 -0
  216. portal/templates/portal/partials/delete_popup.html +18 -0
  217. portal/templates/portal/partials/footer.html +57 -26
  218. portal/templates/portal/partials/header.html +118 -117
  219. portal/templates/portal/partials/hero_card.html +4 -3
  220. portal/templates/portal/partials/info_popup.html +3 -3
  221. portal/templates/portal/partials/invite_admin_teacher.html +23 -0
  222. portal/templates/portal/partials/popup.html +7 -2
  223. portal/templates/portal/partials/register_newsletter_tickbox.html +2 -5
  224. portal/templates/portal/partials/screentime_popup.html +14 -0
  225. portal/templates/portal/partials/service_unavailable_popup.html +17 -0
  226. portal/templates/portal/partials/session_popup.html +19 -0
  227. portal/templates/portal/play/student_dashboard.html +42 -29
  228. portal/templates/portal/play/student_edit_account.html +64 -9
  229. portal/templates/portal/play.html +61 -41
  230. portal/templates/portal/privacy_notice.html +697 -0
  231. portal/templates/portal/register.html +122 -92
  232. portal/templates/portal/reset_password.html +20 -40
  233. portal/templates/portal/reset_password_confirm.html +9 -4
  234. portal/templates/portal/reset_password_email_sent.html +15 -13
  235. portal/templates/portal/teach/base_registering.html +1 -1
  236. portal/templates/portal/teach/class.html +4 -6
  237. portal/templates/portal/teach/dashboard.html +212 -117
  238. portal/templates/portal/teach/invited.html +90 -0
  239. portal/templates/portal/teach/onboarding_classes.html +5 -3
  240. portal/templates/portal/teach/onboarding_print.html +1 -1
  241. portal/templates/portal/teach/onboarding_school.html +26 -139
  242. portal/templates/portal/teach/onboarding_students.html +1 -1
  243. portal/templates/portal/teach/teacher_dismiss_students.html +73 -55
  244. portal/templates/portal/teach/teacher_edit_class.html +168 -11
  245. portal/templates/portal/teach/teacher_edit_student.html +12 -5
  246. portal/templates/portal/teach/teacher_move_all_classes.html +25 -38
  247. portal/templates/portal/teach/teacher_move_students_to_class.html +1 -1
  248. portal/templates/portal/teach.html +61 -42
  249. portal/templates/portal/ten_year_map.html +147 -0
  250. portal/templates/portal/terms.html +191 -42
  251. portal/templates/two_factor/core/login.html +71 -59
  252. portal/templates/two_factor/core/setup.html +58 -49
  253. portal/templates/two_factor/profile/disable.html +1 -1
  254. portal/templates/two_factor/profile/profile.html +35 -17
  255. portal/templatetags/app_tags.py +59 -84
  256. portal/templatetags/card_list_tags.py +0 -4
  257. portal/tests/base_test.py +14 -3
  258. portal/tests/conftest.py +0 -15
  259. portal/tests/migrations/test_migration_make_portaladmin_teacher.py +2 -6
  260. portal/tests/migrations/test_migration_preview_users.py +3 -9
  261. portal/tests/migrations/test_migration_remove_guardian.py +1 -3
  262. portal/tests/migrations/test_migration_use_common_models.py +2 -6
  263. portal/tests/migrations/test_migration_verify_portaladmin.py +1 -3
  264. portal/tests/pageObjects/portal/admin/admin_base_page.py +0 -21
  265. portal/tests/pageObjects/portal/base_page.py +16 -26
  266. portal/tests/pageObjects/portal/email_verification_needed_page.py +3 -2
  267. portal/tests/pageObjects/portal/game_page.py +12 -19
  268. portal/tests/pageObjects/portal/home_page.py +13 -15
  269. portal/tests/pageObjects/portal/independent_login_page.py +13 -17
  270. portal/tests/pageObjects/portal/password_reset_form_page.py +20 -4
  271. portal/tests/pageObjects/portal/password_reset_page.py +25 -0
  272. portal/tests/pageObjects/portal/play/account_page.py +18 -27
  273. portal/tests/pageObjects/portal/play/dashboard_page.py +4 -4
  274. portal/tests/pageObjects/portal/play/join_school_or_club_page.py +8 -10
  275. portal/tests/pageObjects/portal/play/play_base_page.py +5 -3
  276. portal/tests/pageObjects/portal/signup_page.py +28 -59
  277. portal/tests/pageObjects/portal/student_login_class_code.py +6 -9
  278. portal/tests/pageObjects/portal/student_login_page.py +6 -8
  279. portal/tests/pageObjects/portal/teach/add_independent_student_to_class_page.py +3 -3
  280. portal/tests/pageObjects/portal/teach/added_independent_student_to_class_page.py +3 -1
  281. portal/tests/pageObjects/portal/teach/class_page.py +36 -13
  282. portal/tests/pageObjects/portal/teach/dashboard_page.py +43 -84
  283. portal/tests/pageObjects/portal/teach/dismiss_students_page.py +7 -5
  284. portal/tests/pageObjects/portal/teach/edit_student_page.py +10 -8
  285. portal/tests/pageObjects/portal/teach/move_class_page.py +5 -10
  286. portal/tests/pageObjects/portal/teach/move_classes_page.py +4 -2
  287. portal/tests/pageObjects/portal/teach/move_students_disambiguate_page.py +4 -2
  288. portal/tests/pageObjects/portal/teach/move_students_page.py +6 -13
  289. portal/tests/pageObjects/portal/teach/onboarding_classes_page.py +5 -3
  290. portal/tests/pageObjects/portal/teach/onboarding_organisation_page.py +11 -49
  291. portal/tests/pageObjects/portal/teach/onboarding_student_list_page.py +7 -12
  292. portal/tests/pageObjects/portal/teach/onboarding_students_page.py +4 -27
  293. portal/tests/pageObjects/portal/teach/teach_base_page.py +6 -4
  294. portal/tests/pageObjects/portal/teacher_login_page.py +10 -16
  295. portal/tests/selenium_test_case.py +3 -43
  296. portal/tests/snapshots/snap_test_partials.py +11 -165
  297. portal/tests/test_2FA.py +15 -33
  298. portal/tests/test_admin.py +15 -97
  299. portal/tests/test_api.py +212 -73
  300. portal/tests/test_captcha_forms.py +2 -2
  301. portal/tests/test_class.py +374 -24
  302. portal/tests/test_emails.py +83 -20
  303. portal/tests/{test_newsletter_footer.py → test_global_forms.py} +5 -5
  304. portal/tests/test_helper_methods.py +30 -0
  305. portal/tests/test_independent_student.py +255 -144
  306. portal/tests/test_invite_teacher.py +318 -10
  307. portal/tests/test_middleware.py +138 -15
  308. portal/tests/test_organisation.py +78 -262
  309. portal/tests/test_partials.py +0 -88
  310. portal/tests/test_ratelimit.py +218 -36
  311. portal/tests/test_school_student.py +35 -40
  312. portal/tests/test_security.py +12 -31
  313. portal/tests/test_teacher.py +425 -325
  314. portal/tests/test_teacher_student.py +103 -91
  315. portal/tests/test_views.py +900 -76
  316. portal/tests/utils/classes.py +2 -2
  317. portal/tests/utils/messages.py +13 -28
  318. portal/urls.py +237 -164
  319. portal/views/admin.py +0 -332
  320. portal/views/api.py +82 -68
  321. portal/views/cron/__init__.py +1 -0
  322. portal/views/cron/user.py +322 -0
  323. portal/views/dotmailer.py +9 -1
  324. portal/views/email.py +33 -77
  325. portal/views/google_analytics.py +28 -0
  326. portal/views/home.py +126 -97
  327. portal/views/legal.py +13 -0
  328. portal/views/login/independent_student.py +5 -5
  329. portal/views/login/student.py +51 -14
  330. portal/views/login/teacher.py +2 -6
  331. portal/views/organisation.py +20 -189
  332. portal/views/registration.py +97 -17
  333. portal/views/student/edit_account_details.py +99 -72
  334. portal/views/student/play.py +81 -62
  335. portal/views/teacher/dashboard.py +421 -149
  336. portal/views/teacher/teach.py +226 -177
  337. portal/views/two_factor/__init__.py +0 -0
  338. portal/views/two_factor/core.py +28 -0
  339. portal/views/two_factor/form.py +7 -0
  340. portal/views/two_factor/profile.py +11 -0
  341. codeforlife_portal-5.32.2.dist-info/LICENSE.md +0 -577
  342. codeforlife_portal-5.32.2.dist-info/METADATA +0 -38
  343. deploy/permissions.py +0 -2
  344. example_project/manage.py +0 -10
  345. portal/autoconfig.py +0 -140
  346. portal/csp_config.py +0 -60
  347. portal/forms/add_game.py +0 -33
  348. portal/helpers/location.py +0 -121
  349. portal/static/portal/img/kurono_hero.jpg +0 -0
  350. portal/static/portal/img/kurono_landing_hero.png +0 -0
  351. portal/static/portal/img/kurono_logo.svg +0 -1
  352. portal/static/portal/img/kurono_logo_grey_background.svg +0 -1
  353. portal/static/portal/img/kurono_logo_mark.svg +0 -1
  354. portal/static/portal/img/kurono_resources_hero.jpg +0 -0
  355. portal/static/portal/img/kurono_story.png +0 -0
  356. portal/static/portal/img/ocado-swirl.svg +0 -22
  357. portal/static/portal/img/thumbnail_educate_kurono.png +0 -0
  358. portal/static/portal/img/thumbnail_educate_resources_and_progress_tracking.png +0 -0
  359. portal/static/portal/img/thumbnail_kurono_resources.png +0 -0
  360. portal/static/portal/img/thumbnail_play_kurono.png +0 -0
  361. portal/static/portal/img/x_close_video.png +0 -0
  362. portal/static/portal/js/aimmoGame.js +0 -106
  363. portal/static/portal/js/deleteWorkspaces.js +0 -14
  364. portal/static/portal/js/fuzzySchoolLookup.js +0 -46
  365. portal/static/portal/js/lib/jquery-3.5.1.min.js +0 -2
  366. portal/static/portal/js/lib/jquery-ui-1.12.1.min.js +0 -13
  367. portal/static/portal/sass/partials/_videos.scss +0 -10
  368. portal/static/portal/video/aimmo_play_now_background_video.mp4 +0 -0
  369. portal/strings/student_aimmo_dashboard.py +0 -6
  370. portal/templates/portal/admin/aggregated_data.html +0 -35
  371. portal/templates/portal/admin/map.html +0 -70
  372. portal/templates/portal/mouseflow.html +0 -9
  373. portal/templates/portal/partials/aimmo_games_table.html +0 -83
  374. portal/templates/portal/partials/register_over_required_age_tickbox.html +0 -9
  375. portal/templates/portal/play/independent_student_dashboard.html +0 -64
  376. portal/templates/portal/play/student_aimmo_dashboard.html +0 -63
  377. portal/templates/portal/privacy_policy.html +0 -483
  378. portal/templates/portal/reset_password_email.html +0 -9
  379. portal/templates/portal/teach/invite.html +0 -25
  380. portal/templates/portal/teach/teacher_aimmo_dashboard.html +0 -95
  381. portal/templates/portal/teach/teacher_resources.html +0 -68
  382. portal/templatetags/character_list_tags.py +0 -16
  383. portal/tests/pageObjects/portal/kurono_teacher_dashboard_page.py +0 -49
  384. portal/tests/pageObjects/portal/student_password_reset_form_page.py +0 -23
  385. portal/tests/pageObjects/portal/teach/onboarding_revoke_request_page.py +0 -20
  386. portal/tests/pageObjects/portal/teacher_password_reset_form_page.py +0 -23
  387. portal/tests/test_aimmo_dashboards.py +0 -172
  388. portal/tests/test_location.py +0 -217
  389. portal/tests/utils/aimmo_games.py +0 -30
  390. portal/views/aimmo/dashboard.py +0 -119
  391. portal/views/privacy_policy.py +0 -9
  392. portal/views/teacher/teacher_resources.py +0 -42
  393. {portal/views/aimmo → cfl_common}/__init__.py +0 -0
@@ -1,68 +0,0 @@
1
- {% extends 'portal/base.html' %}
2
- {% load staticfiles %}
3
- {% load app_tags %}
4
- {% load banner_tags %}
5
-
6
- {% block subNav %}
7
- {% banner banner_name="BANNER" %}
8
- <div class="sticky-subnav sub-nav sub-nav--teacher">
9
- <div class="container">
10
- <div class="row">
11
- <div class="col-sm-12 sub-nav">
12
- <span class="iconify sub-nav--icon" data-icon="mdi:information-outline"></span>
13
- <p class="flex-grow-1">
14
- <strong>We have moved our resources to Gitbook.</strong>
15
- </p>
16
- </div>
17
- </div>
18
- </div>
19
-
20
- </div>
21
- {% endblock subNav %}
22
-
23
- {% block content %}
24
- <div id="resources_page"></div>
25
-
26
- <div class="background container">
27
- <div class="row">
28
- <div class="col-sm-12 text-center">
29
- <h4>You can find our resources on our new Gitbook space</h4>
30
- </div>
31
- </div>
32
-
33
- <div class="row d-flex">
34
- <div class="col-sm-6 d-flex flex-column">
35
- <div class="flex-grow-1">
36
- <p>Our resources have been moved to Gitbook to give you better experience. Please visit our dedicated
37
- {% if rapid_router_resources %}
38
- <a href="https://code-for-life.gitbook.io/teaching-resources/" target="_blank">Code for Life Space</a>
39
- {% else %}
40
- <a href="https://code-for-life.gitbook.io/teaching-resources/v/kurono-teaching-resources/"
41
- target="_blank">Code for Life Space</a>
42
- {% endif %}
43
- to find everything you need from lesson plans to solutions.
44
- </p>
45
- <p>This space is only available to teachers.</p>
46
- </div>
47
- <div>
48
- {% if rapid_router_resources %}
49
- <a href="https://code-for-life.gitbook.io/teaching-resources/"
50
- class="button button--primary button--icon" target="_blank">
51
- Open Rapid Router resources<span class="iconify" data-icon="mdi:open-in-new"></span>
52
- </a>
53
- {% else %}
54
- <a href="https://code-for-life.gitbook.io/teaching-resources/v/kurono-teaching-resources/"
55
- class="button button--primary button--icon" target="_blank">
56
- Open Kurono resources<span class="iconify" data-icon="mdi:open-in-new"></span>
57
- </a>
58
- {% endif %}
59
- </div>
60
- </div>
61
-
62
- <div class="col-sm-6">
63
- <img src="{% static 'portal/img/gitbook_space.png' %}">
64
- </div>
65
- </div>
66
- </div>
67
-
68
- {% endblock content %}
@@ -1,16 +0,0 @@
1
- from django import template
2
- from common.models import AimmoCharacter
3
-
4
- register = template.Library()
5
-
6
-
7
- @register.inclusion_tag("portal/partials/character_list.html")
8
- def character_list():
9
- """
10
- Registers the inclusion tag for the character card list partial.
11
- The template currently expects a list of elements which each contain the following:
12
- - name: the heading of the card
13
- - image_path: the path to the card's image
14
- - description: the text paragraph of the card
15
- """
16
- return {"characters": AimmoCharacter.objects.sorted()}
@@ -1,49 +0,0 @@
1
- from __future__ import absolute_import
2
-
3
- from .base_page import BasePage
4
-
5
-
6
- class KuronoTeacherDashboardPage(BasePage):
7
- def __init__(self, browser):
8
- super(KuronoTeacherDashboardPage, self).__init__(browser)
9
-
10
- assert self.on_correct_page("kurono_teacher_dashboard_page")
11
-
12
- def create_game(self, class_id):
13
- self._click_add_game_dropdown()
14
-
15
- self.browser.find_element_by_id(f"class_{class_id}").click()
16
-
17
- return self
18
-
19
- def change_game_worksheet(self, worksheet_id):
20
- self._click_change_worksheet_dropdown()
21
-
22
- self.browser.find_element_by_id(f"worksheet_{worksheet_id}").click()
23
-
24
- self.confirm_dialog()
25
-
26
- return self
27
-
28
- def delete_games(self, game_ids):
29
- # Tick checkboxes
30
- for game_id in game_ids:
31
- self.browser.find_element_by_xpath(
32
- f"//input[@name='game_ids' and @value='{game_id}']"
33
- ).click()
34
-
35
- # Click delete
36
- self.browser.find_element_by_id("deleteGamesButton").click()
37
-
38
- self.confirm_dialog()
39
-
40
- return self
41
-
42
- def _click_change_worksheet_confirm_button(self):
43
- self.browser.find_element_by_id("confirm_button").click()
44
-
45
- def _click_add_game_dropdown(self):
46
- self.browser.find_element_by_id("add_class_dropdown").click()
47
-
48
- def _click_change_worksheet_dropdown(self):
49
- self.browser.find_element_by_id("worksheets_dropdown").click()
@@ -1,23 +0,0 @@
1
- from __future__ import absolute_import
2
-
3
- from .base_page import BasePage
4
- from .home_page import HomePage
5
-
6
-
7
- class StudentPasswordResetFormPage(BasePage):
8
- def __init__(self, browser):
9
- super(StudentPasswordResetFormPage, self).__init__(browser)
10
-
11
- assert self.on_correct_page("reset_password_student_page")
12
-
13
- def cancel(self):
14
- self.browser.find_element_by_id("cancel_button").click()
15
-
16
- return HomePage(self.browser)
17
-
18
- def reset_username_submit(self, username):
19
- self.browser.find_element_by_id("id_username").send_keys(username)
20
-
21
- self.browser.find_element_by_id("reset_button").click()
22
-
23
- return self
@@ -1,20 +0,0 @@
1
- from __future__ import absolute_import
2
-
3
- from .teach_base_page import TeachBasePage
4
-
5
-
6
- class OnboardingRevokeRequestPage(TeachBasePage):
7
- def __init__(self, browser):
8
- super(OnboardingRevokeRequestPage, self).__init__(browser)
9
-
10
- assert self.on_correct_page("onboarding_revoke_request_page")
11
-
12
- def check_organisation_name(self, name, postcode):
13
- text = "You have a pending request to join %s, %s" % (name, postcode)
14
- return text in self.browser.find_element_by_tag_name("body").text
15
-
16
- def revoke_join(self):
17
- self.browser.find_element_by_name("revoke_join_request").click()
18
- import portal.tests.pageObjects.portal.teach.onboarding_organisation_page as onboarding_organisation_page
19
-
20
- return onboarding_organisation_page.OnboardingOrganisationPage(self.browser)
@@ -1,23 +0,0 @@
1
- from __future__ import absolute_import
2
-
3
- from .base_page import BasePage
4
- from .home_page import HomePage
5
-
6
-
7
- class TeacherPasswordResetFormPage(BasePage):
8
- def __init__(self, browser):
9
- super(TeacherPasswordResetFormPage, self).__init__(browser)
10
-
11
- assert self.on_correct_page("reset_password_teacher_page")
12
-
13
- def cancel(self):
14
- self.browser.find_element_by_id("cancel_button").click()
15
- return HomePage(self.browser)
16
-
17
- def reset_email_submit(self, email):
18
- self.browser.find_element_by_id("id_email").send_keys(email)
19
-
20
- self.wait_for_element_by_id("reset_button")
21
-
22
- self.browser.find_element_by_id("reset_button").click()
23
- return self
@@ -1,172 +0,0 @@
1
- import pytest
2
- from aimmo.models import Game
3
- from aimmo.worksheets import WORKSHEETS
4
- from common.models import Class
5
- from common.tests.utils.classes import create_class_directly
6
- from common.tests.utils.organisation import (
7
- create_organisation_directly,
8
- )
9
- from common.tests.utils.student import create_school_student_directly
10
- from common.tests.utils.teacher import signup_teacher_directly
11
- from django.test.client import Client
12
- from django.urls.base import reverse
13
-
14
- from .base_test import BaseTest
15
- from .conftest import IndependentStudent, SchoolStudent
16
-
17
-
18
- @pytest.mark.django_db
19
- def test_student_cannot_access_teacher_dashboard(
20
- student1: SchoolStudent, class1: Class
21
- ):
22
- """
23
- Given you are logged in as a student,
24
- When you try to access the teacher dashboard,
25
- Then you cannot access it and are instead redirected.
26
- """
27
- c = Client()
28
- url = reverse("student_login", kwargs={"access_code": class1.access_code})
29
- data = {
30
- "username": student1.username,
31
- "password": student1.password,
32
- }
33
-
34
- c.post(url, data)
35
-
36
- student_dashboard_url = reverse("student_aimmo_dashboard")
37
-
38
- response_s = c.get(student_dashboard_url)
39
-
40
- assert response_s.status_code == 200
41
-
42
- teacher_dashboard_url = reverse("teacher_aimmo_dashboard")
43
-
44
- response = c.get(teacher_dashboard_url)
45
-
46
- assert response.status_code == 302
47
-
48
-
49
- @pytest.mark.django_db
50
- def test_indep_student_cannot_access_dashboard(
51
- independent_student1: IndependentStudent,
52
- ):
53
- """
54
- Given you are logged in as an independent student,
55
- When you try to access the student dashboard,
56
- Then you can access it but the context only has the banner.
57
- """
58
- c = Client()
59
- url = reverse("independent_student_login")
60
- data = {
61
- "username": independent_student1.username,
62
- "password": independent_student1.password,
63
- }
64
-
65
- c.post(url, data)
66
-
67
- student_dashboard_url = reverse("student_aimmo_dashboard")
68
-
69
- response = c.get(student_dashboard_url)
70
-
71
- assert response.status_code == 200
72
- assert "BANNER" in response.context
73
- assert "HERO_CARD" not in response.context
74
- assert "CARD_LIST" not in response.context
75
-
76
-
77
- @pytest.mark.django_db
78
- def test_student_aimmo_dashboard_loads(
79
- student1: SchoolStudent, class1: Class, aimmo_game1: Game
80
- ):
81
- """
82
- Given an aimmo game is linked to a class,
83
- When a student of that class goes on the Student Kurono Dashboard page,
84
- Then the page loads and the context contains the hero card and card list
85
- associated to the aimmo game.
86
-
87
- Then, given that the class no longer has a game linked to it,
88
- When the student goes on the same page,
89
- Then the page still loads but the context no longer contains the hero card
90
- or the card list elements.
91
- """
92
- c = Client()
93
- student_login_url = reverse(
94
- "student_login", kwargs={"access_code": class1.access_code}
95
- )
96
- data = {
97
- "username": student1.username,
98
- "password": student1.password,
99
- }
100
-
101
- c.post(student_login_url, data)
102
-
103
- student_dashboard_url = reverse("student_aimmo_dashboard")
104
- response = c.get(student_dashboard_url)
105
-
106
- assert response.status_code == 200
107
- assert "HERO_CARD" in response.context
108
- assert "CARD_LIST" in response.context
109
-
110
- aimmo_game1.delete()
111
-
112
- url = reverse("student_aimmo_dashboard")
113
- response = c.get(url)
114
-
115
- assert response.status_code == 200
116
- assert "HERO_CARD" not in response.context
117
- assert "CARD_LIST" not in response.context
118
-
119
-
120
- # Selenium tests
121
- class TestAimmoDashboardFrontend(BaseTest):
122
- def test_worksheet_dropdown_changes_worksheet(self):
123
- teacher_email, teacher_password = signup_teacher_directly()
124
- create_organisation_directly(teacher_email)
125
- klass, class_name, access_code = create_class_directly(teacher_email)
126
- student_name, student_password, _ = create_school_student_directly(access_code)
127
-
128
- worksheet1 = WORKSHEETS.get(1)
129
- worksheet2 = WORKSHEETS.get(2)
130
-
131
- self.selenium.get(self.live_server_url)
132
- page = (
133
- self.go_to_homepage()
134
- .go_to_teacher_login_page()
135
- .login(teacher_email, teacher_password)
136
- )
137
- page = page.go_to_kurono_teacher_dashboard_page().create_game(klass.id)
138
-
139
- game = Game.objects.get(game_class=klass)
140
-
141
- assert game.worksheet == worksheet1
142
-
143
- page.change_game_worksheet(worksheet2.id)
144
-
145
- game = Game.objects.get(game_class=klass)
146
-
147
- assert game.worksheet == worksheet2
148
-
149
- def test_delete_games(self):
150
- teacher_email, teacher_password = signup_teacher_directly()
151
- create_organisation_directly(teacher_email)
152
-
153
- klass1, _, _ = create_class_directly(teacher_email)
154
- game1 = Game(game_class=klass1)
155
- game1.save()
156
-
157
- klass2, _, _ = create_class_directly(teacher_email)
158
- game2 = Game(game_class=klass2)
159
- game2.save()
160
-
161
- assert Game.objects.count() == 2
162
-
163
- self.selenium.get(self.live_server_url)
164
- page = (
165
- self.go_to_homepage()
166
- .go_to_teacher_login_page()
167
- .login(teacher_email, teacher_password)
168
- )
169
- page.go_to_kurono_teacher_dashboard_page().delete_games([game1.id, game2.id])
170
-
171
- assert Game.objects.filter(is_archived=False).count() == 0
172
- assert Game.objects.filter(is_archived=True).count() == 2
@@ -1,217 +0,0 @@
1
- import json
2
- import os
3
- import unittest
4
-
5
- import requests
6
- import responses
7
-
8
- from portal.helpers.location import lookup_coord, lookup_country
9
-
10
- DATA_DIR = os.path.join(os.path.dirname(__file__), "data")
11
-
12
- # Returns a string read from the given file that can be deserialised into json objects
13
- def read_json_from_file(filename):
14
- data = ""
15
-
16
- if filename:
17
- f = open(filename, "r")
18
- data = f.read()
19
- f.close()
20
-
21
- return data
22
-
23
-
24
- # Returns path to the file that contains the json response
25
- def datafile(filename):
26
- return os.path.join(DATA_DIR, filename)
27
-
28
-
29
- MAPS_API_GEOCODE_JSON = "https://maps.googleapis.com/maps/api/geocode/json?"
30
-
31
-
32
- class TestLocation(unittest.TestCase):
33
- def assert_default_coord(self, town, lat, lng):
34
- # default values returned when error occurs in lookup_coord()
35
- self.assertEqual((town, lat, lng), (0, 0, 0))
36
-
37
- def assert_default_country_and_coord(self, country, town, lat, lng):
38
- # default values returned when error occurs in lookup_country()
39
- self.assertEqual((country, town, lat, lng), ("GB", 0, 55.378051, -3.435973))
40
-
41
- @responses.activate
42
- def test_lookup_coord_call_api_once(self):
43
- responses.add(
44
- responses.GET,
45
- MAPS_API_GEOCODE_JSON + "address=SW72AZ&components=country:GB",
46
- body=read_json_from_file(datafile("sw72az_gb.json")),
47
- match_querystring=True,
48
- content_type="application/json",
49
- )
50
- result = lookup_coord("SW72AZ", "GB")
51
- self.assertEqual(len(responses.calls), 1, "API was called more than once")
52
-
53
- @responses.activate
54
- def test_lookup_coord_json_unchanged(self):
55
- responses.add(
56
- responses.GET,
57
- MAPS_API_GEOCODE_JSON + "address=SW72AZ&components=country:GB",
58
- body=read_json_from_file(datafile("sw72az_gb.json")),
59
- match_querystring=True,
60
- content_type="application/json",
61
- )
62
- result = lookup_coord("SW72AZ", "GB")
63
- self.assertEqual(
64
- responses.calls[0].response.json(),
65
- json.loads(read_json_from_file(datafile("sw72az_gb.json"))),
66
- )
67
-
68
- @responses.activate
69
- def test_lookup_coord_valid_postcode_country_gb1(self):
70
- responses.add(
71
- responses.GET,
72
- MAPS_API_GEOCODE_JSON + "address=SW72AZ&components=country:GB",
73
- body=read_json_from_file(datafile("sw72az_gb.json")),
74
- match_querystring=True,
75
- content_type="application/json",
76
- )
77
- result = lookup_coord("SW72AZ", "GB")
78
- self.assertEqual(result, (None, "GB", "London", 51.5005046, -0.1782187))
79
-
80
- @responses.activate
81
- def test_lookup_coord_valid_postcode_country_gb2(self):
82
- responses.add(
83
- responses.GET,
84
- MAPS_API_GEOCODE_JSON + "address=AL10 9NE&components=country:GB",
85
- body=read_json_from_file(datafile("al109ne_gb.json")),
86
- match_querystring=True,
87
- content_type="application/json",
88
- )
89
- result = lookup_coord("AL10 9NE", "GB")
90
- self.assertEqual(result, (None, "GB", "Hatfield", 51.7623259, -0.2438929))
91
-
92
- # Default to coordinates of country
93
- @responses.activate
94
- def test_lookup_coord_invalid_postcode_country_gb(self):
95
- responses.add(
96
- responses.GET,
97
- MAPS_API_GEOCODE_JSON + "address=10000&components=country:GB",
98
- body=read_json_from_file(datafile("10000_gb.json")),
99
- match_querystring=True,
100
- content_type="application/json",
101
- )
102
- result = lookup_coord("10000", "GB")
103
- self.assertEqual(result, (None, "GB", 0, 55.378051, -3.435973))
104
-
105
- @responses.activate
106
- def test_lookup_coord_invalid_postcode_country_kr(self):
107
- responses.add(
108
- responses.GET,
109
- MAPS_API_GEOCODE_JSON + "address=AL109NE&components=country:KR",
110
- body=read_json_from_file(datafile("al109ne_kr.json")),
111
- match_querystring=True,
112
- content_type="application/json",
113
- )
114
- result = lookup_coord("AL109NE", "KR")
115
- self.assertEqual(result, (None, "KR", 0, 35.907757, 127.766922))
116
-
117
- @responses.activate
118
- def test_lookup_coord_connection_error(self):
119
- error, country, town, lat, lng = lookup_coord("AL109NE", "GB")
120
- assert "Connection error" in error
121
- self.assert_default_coord(town, lat, lng)
122
-
123
- @responses.activate
124
- def test_lookup_coord_cannot_decode_json(self):
125
- responses.add(
126
- responses.GET,
127
- MAPS_API_GEOCODE_JSON + "address=AL109NE&components=country:GB",
128
- body="",
129
- match_querystring=True,
130
- content_type="application/json",
131
- )
132
- error, country, town, lat, lng = lookup_coord("AL109NE", "GB")
133
- assert "Value error" in error
134
- self.assert_default_coord(town, lat, lng)
135
-
136
- @responses.activate
137
- def test_lookup_coord_request_error_404(self):
138
- responses.add(
139
- responses.GET,
140
- MAPS_API_GEOCODE_JSON + "address=AL109NE&components=country:GB",
141
- body=read_json_from_file(datafile("al109ne_gb.json")),
142
- status=requests.codes.not_found,
143
- match_querystring=True,
144
- content_type="application/json",
145
- )
146
- error, country, town, lat, lng = lookup_coord("AL109NE", "GB")
147
- assert "Request error" in error
148
- self.assert_default_coord(town, lat, lng)
149
-
150
- @responses.activate
151
- def test_lookup_coord_zero_results(self):
152
- responses.add(
153
- responses.GET,
154
- MAPS_API_GEOCODE_JSON + "address=AL109NE&components=country:AB",
155
- body=read_json_from_file(datafile("al109ne_ab.json")),
156
- match_querystring=True,
157
- content_type="application/json",
158
- )
159
- error, country, town, lat, lng = lookup_coord("AL109NE", "AB")
160
- assert "API error" in error
161
- self.assert_default_coord(town, lat, lng)
162
-
163
- @responses.activate
164
- def test_lookup_country_valid_postcode1(self):
165
- responses.add(
166
- responses.GET,
167
- MAPS_API_GEOCODE_JSON + "components=postal_code:SW72AZ",
168
- body=read_json_from_file(datafile("sw72az.json")),
169
- match_querystring=True,
170
- content_type="application/json",
171
- )
172
- result = lookup_country("SW72AZ")
173
- self.assertEqual(result, (None, "GB", "London", 51.5005046, -0.1782187))
174
-
175
- @responses.activate
176
- def test_lookup_country_valid_postcode1(self):
177
- responses.add(
178
- responses.GET,
179
- MAPS_API_GEOCODE_JSON + "components=postal_code:AL10 9NE",
180
- body=read_json_from_file(datafile("al109ne.json")),
181
- match_querystring=True,
182
- content_type="application/json",
183
- )
184
- result = lookup_country("AL10 9NE")
185
- self.assertEqual(result, (None, "GB", "Hatfield", 51.7623259, -0.2438929))
186
-
187
- @responses.activate
188
- def test_lookup_country_invalid_postcode_zero_results(self):
189
- responses.add(
190
- responses.GET,
191
- MAPS_API_GEOCODE_JSON + "components=postal_code:xxxxx",
192
- body=read_json_from_file(datafile("xxxxx.json")),
193
- match_querystring=True,
194
- content_type="application/json",
195
- )
196
- error, country, town, lat, lng = lookup_country("xxxxx")
197
- assert "API error" in error
198
- self.assert_default_country_and_coord(country, town, lat, lng)
199
-
200
- @responses.activate
201
- def test_lookup_country_connection_error(self):
202
- error, country, town, lat, lng = lookup_country("xxxxx")
203
- assert "Connection error" in error
204
-
205
- @responses.activate
206
- def test_lookup_country_request_error_404(self):
207
- responses.add(
208
- responses.GET,
209
- MAPS_API_GEOCODE_JSON + "components=postal_code:AL109NE",
210
- body=read_json_from_file(datafile("al109ne.json")),
211
- status=requests.codes.not_found,
212
- match_querystring=True,
213
- content_type="application/json",
214
- )
215
- error, country, town, lat, lng = lookup_country("AL109NE")
216
- assert "Request error" in error
217
- self.assert_default_country_and_coord(country, town, lat, lng)
@@ -1,30 +0,0 @@
1
- from aimmo.models import Game
2
- from common.models import Class
3
-
4
-
5
- def generate_name():
6
- name = "Game %d" % generate_name.next_id
7
-
8
- generate_name.next_id += 1
9
-
10
- return name
11
-
12
-
13
- generate_name.next_id = 1
14
-
15
-
16
- def create_aimmo_game_directly(klass: Class, worksheet_id: int) -> Game:
17
- """Generate an aimmo game with the details given.
18
-
19
- Args:
20
- klass (Class): The instance of the class.
21
- worksheet_id (int): The id of the worksheet.
22
-
23
- Returns:
24
- game: Game: The game model instance.
25
- """
26
- name = generate_name()
27
-
28
- game = Game.objects.create(name=name, game_class=klass, worksheet_id=worksheet_id)
29
-
30
- return game