From 43daeaa73f1262f77c9b3b100644fb671e66c6a8 Mon Sep 17 00:00:00 2001
From: Ashley Zhang <69987606+ashleyzhang01@users.noreply.github.com>
Date: Wed, 13 Nov 2024 16:16:47 -0800
Subject: [PATCH] Add mypy (#322)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* π· add mypy
* π fix mypy errors in portal
* π₯user and sublet typing
* π½οΈ fix dining typing
* ποΈ fix subletting tests typing
* π penndata typing errors
* π gsr booking type fixes
* π§ββοΈOMFG NO WAY NO MORE TYPE ERRORS
* π§Ή clean up types files
* π― fix tests
* π extend pre-commit rules
---
.pre-commit-config.yaml | 27 +-
backend/Pipfile | 2 +
backend/Pipfile.lock | 397 ++++++++++--------
backend/dining/api_wrapper.py | 2 +
backend/dining/migrations/0001_initial.py | 4 +-
...002_diningitem_diningstation_diningmenu.py | 4 +-
backend/dining/migrations/0003_venue_name.py | 10 +-
...move_diningtransaction_profile_and_more.py | 25 +-
...gens_diningitem_nutrition_info_and_more.py | 4 +-
...006_remove_diningmenu_stations_and_more.py | 9 +-
backend/dining/models.py | 44 +-
backend/dining/views.py | 14 +-
backend/gsr_booking/admin.py | 16 +-
backend/gsr_booking/api_wrapper.py | 174 ++++----
.../management/commands/change_group.py | 3 +-
.../management/commands/get_reservations.py | 3 +-
.../management/commands/individual_usage.py | 3 +-
.../gsr_booking/migrations/0001_initial.py | 18 +-
.../0001_squashed_0011_merge_20200418_2009.py | 44 +-
.../migrations/0002_auto_20210129_1527.py | 10 +-
.../migrations/0004_alter_gsr_lid.py | 10 +-
.../migrations/0005_reservation.py | 2 +-
.../migrations/0005_usersearchindex.py | 8 +-
.../migrations/0006_auto_20200207_2126.py | 4 +-
.../migrations/0006_auto_20211024_1231.py | 14 +-
.../migrations/0006_gsrbookingcredentials.py | 15 +-
.../0007_delete_gsrbookingcredentials.py | 10 +-
.../migrations/0008_auto_20200202_1218.py | 2 +-
.../migrations/0008_auto_20211112_1657.py | 17 +-
.../migrations/0009_auto_20200202_1232.py | 6 +-
...emove_groupmembership_username_and_more.py | 9 +-
...emove_gsrbooking_reminder_sent_and_more.py | 13 +-
.../0011_alter_reservation_group.py | 6 +-
.../gsr_booking/migrations/0012_gsr_in_use.py | 10 +-
backend/gsr_booking/models.py | 118 +++---
backend/gsr_booking/serializers.py | 59 +--
backend/gsr_booking/urls.py | 4 +-
backend/gsr_booking/views.py | 96 ++---
backend/laundry/api_wrapper.py | 2 +-
.../migrations/0002_auto_20210321_1105.py | 6 +-
backend/laundry/models.py | 26 +-
backend/laundry/views.py | 36 +-
backend/manage.py | 2 +-
backend/penndata/admin.py | 6 +-
.../commands/get_college_house_events.py | 2 +-
.../commands/get_fitness_snapshot.py | 16 +-
.../commands/get_penn_today_events.py | 109 ++---
.../management/commands/get_venture_events.py | 2 +-
.../management/commands/get_wharton_events.py | 10 +-
backend/penndata/migrations/0001_initial.py | 2 +-
.../penndata/migrations/0002_homepageorder.py | 6 +-
.../migrations/0003_analyticsevent.py | 2 +-
.../migrations/0004_analyticsevent_data.py | 6 +-
.../0005_fitnessroom_fitnesssnapshot.py | 4 +-
.../0006_fitnesssnapshot_capacity.py | 10 +-
.../migrations/0007_fitnessroom_image_url.py | 6 +-
.../penndata/migrations/0008_calendarevent.py | 6 +-
.../migrations/0009_auto_20240223_1820.py | 41 +-
.../migrations/0010_auto_20240228_0150.py | 21 +-
...lter_event_event_type_alter_event_start.py | 8 +-
.../migrations/0012_alter_event_event_type.py | 6 +-
backend/penndata/models.py | 69 +--
backend/penndata/serializers.py | 7 +-
backend/penndata/views.py | 73 ++--
backend/pennmobile/admin.py | 25 +-
backend/pennmobile/celery.py | 3 +-
backend/pennmobile/settings/base.py | 4 +-
backend/pennmobile/settings/production.py | 2 +-
backend/pennmobile/test_runner.py | 11 +-
backend/pennmobile/urls.py | 14 +-
backend/portal/admin.py | 32 +-
backend/portal/logic.py | 57 ++-
.../commands/load_target_populations.py | 11 +-
.../management/commands/polls_populate.py | 75 +---
backend/portal/migrations/0001_initial.py | 4 +-
.../migrations/0002_auto_20211003_2225.py | 13 +-
.../0003_alter_targetpopulation_kind.py | 6 +-
backend/portal/migrations/0004_post.py | 2 +-
.../migrations/0005_auto_20211231_1558.py | 34 +-
.../migrations/0006_auto_20220112_1529.py | 35 +-
backend/portal/migrations/0007_post_status.py | 6 +-
.../migrations/0008_alter_post_image_url.py | 6 +-
.../0009_rename_image_url_post_image.py | 12 +-
.../migrations/0010_remove_post_image.py | 11 +-
backend/portal/migrations/0011_post_image.py | 6 +-
.../migrations/0012_remove_post_image.py | 11 +-
backend/portal/migrations/0013_post_image.py | 6 +-
.../migrations/0014_alter_post_post_url.py | 6 +-
.../migrations/0015_auto_20240226_2236.py | 18 +-
backend/portal/models.py | 96 +++--
backend/portal/permissions.py | 65 +--
backend/portal/serializers.py | 87 ++--
backend/portal/views.py | 51 +--
backend/setup.cfg | 3 +-
backend/sublet/admin.py | 6 +-
backend/sublet/migrations/0001_initial.py | 8 +-
.../migrations/0002_auto_20240209_1649.py | 19 +-
.../migrations/0003_alter_sublet_baths.py | 6 +-
.../0004_alter_sublet_external_link.py | 6 +-
backend/sublet/models.py | 76 ++--
backend/sublet/permissions.py | 41 +-
backend/sublet/serializers.py | 29 +-
backend/sublet/views.py | 69 ++-
backend/tests/dining/test_load_venues.py | 4 +-
backend/tests/dining/test_views.py | 52 ++-
backend/tests/gsr_booking/test_gsr_booking.py | 114 ++---
backend/tests/gsr_booking/test_gsr_views.py | 58 +--
backend/tests/gsr_booking/test_gsr_wrapper.py | 66 +--
backend/tests/laundry/test_api_wrapper.py | 12 +-
backend/tests/laundry/test_commands.py | 10 +-
backend/tests/laundry/test_models.py | 10 +-
backend/tests/laundry/test_views.py | 43 +-
backend/tests/penndata/test_views.py | 101 +++--
backend/tests/portal/test_permissions.py | 55 +--
backend/tests/portal/test_polls.py | 133 +++---
backend/tests/portal/test_posts.py | 65 +--
backend/tests/sublet/test_permissions.py | 71 ++--
backend/tests/sublet/test_sublets.py | 122 +++---
backend/tests/user/test_notifs.py | 107 ++---
backend/tests/user/test_user.py | 29 +-
backend/tests/utils/test_email.py | 29 +-
backend/tests/utils/test_r_request.py | 27 +-
.../user/management/commands/clear_cache.py | 3 +-
.../user/management/commands/profile_info.py | 3 +-
.../0002_profile_laundry_preferences.py | 7 +-
.../0003_profile_dining_preferences.py | 7 +-
.../migrations/0004_auto_20210324_1851.py | 7 +-
.../migrations/0005_auto_20211003_2240.py | 18 +-
.../0007_alter_notificationsetting_service.py | 4 +-
.../0008_remove_notificationtoken_dev.py | 11 +-
.../0009_profile_fitness_preferences.py | 2 +-
backend/user/models.py | 33 +-
backend/user/notifications.py | 45 +-
backend/user/serializers.py | 9 +-
backend/user/views.py | 38 +-
backend/utils/email.py | 37 +-
backend/utils/r_request.py | 4 +-
backend/utils/types.py | 75 ++++
138 files changed, 2006 insertions(+), 2077 deletions(-)
create mode 100644 backend/utils/types.py
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index aec50834..c6022dc8 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -3,9 +3,34 @@ repos:
rev: v1.1-mobile-backend
hooks:
- id: black
- args: [-l100]
+ args: [-l100, --config, backend/pyproject.toml]
- id: isort
args: []
- id: flake8
args: [--config, backend/setup.cfg]
- id: detect-private-key
+
+ - repo: https://github.com/pre-commit/mirrors-mypy
+ rev: v1.8.0
+ hooks:
+ - id: mypy
+ additional_dependencies:
+ - types-requests
+ - types-python-dateutil
+ - django-stubs
+ - djangorestframework-stubs
+ - types-PyYAML
+ - types-redis
+ - types-pytz
+ files: ^backend/
+ exclude: ^backend/.*/migrations/.*$
+ args: [
+ --ignore-missing-imports,
+ --disallow-untyped-defs,
+ --check-untyped-defs,
+ --warn-redundant-casts,
+ --no-implicit-optional,
+ --strict-optional,
+ --warn-unused-ignores,
+ --disallow-incomplete-defs,
+ ]
diff --git a/backend/Pipfile b/backend/Pipfile
index d2c29862..43a7aff5 100644
--- a/backend/Pipfile
+++ b/backend/Pipfile
@@ -48,6 +48,8 @@ webdriver-manager = "*"
pre-commit = "*"
alt-profanity-check = "*"
inflection = "*"
+types-redis = "*"
+types-pytz = "*"
[requires]
python_version = "3.11"
diff --git a/backend/Pipfile.lock b/backend/Pipfile.lock
index e1ee3f2c..e1b32e1e 100644
--- a/backend/Pipfile.lock
+++ b/backend/Pipfile.lock
@@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
- "sha256": "f89acb57e8e31f7e049ea1c699698737a30b50d260d20fea7f07d19bcb56c00d"
+ "sha256": "e8c3871357a67fa0783821e10e9633bd4abefb07550453f4c4fb6918ebfb8dfb"
},
"pipfile-spec": 6,
"requires": {
@@ -109,76 +109,76 @@
},
"cffi": {
"hashes": [
- "sha256:011aff3524d578a9412c8b3cfaa50f2c0bd78e03eb7af7aa5e0df59b158efb2f",
- "sha256:0a048d4f6630113e54bb4b77e315e1ba32a5a31512c31a273807d0027a7e69ab",
- "sha256:0bb15e7acf8ab35ca8b24b90af52c8b391690ef5c4aec3d31f38f0d37d2cc499",
- "sha256:0d46ee4764b88b91f16661a8befc6bfb24806d885e27436fdc292ed7e6f6d058",
- "sha256:0e60821d312f99d3e1569202518dddf10ae547e799d75aef3bca3a2d9e8ee693",
- "sha256:0fdacad9e0d9fc23e519efd5ea24a70348305e8d7d85ecbb1a5fa66dc834e7fb",
- "sha256:14b9cbc8f7ac98a739558eb86fabc283d4d564dafed50216e7f7ee62d0d25377",
- "sha256:17c6d6d3260c7f2d94f657e6872591fe8733872a86ed1345bda872cfc8c74885",
- "sha256:1a2ddbac59dc3716bc79f27906c010406155031a1c801410f1bafff17ea304d2",
- "sha256:2404f3de742f47cb62d023f0ba7c5a916c9c653d5b368cc966382ae4e57da401",
- "sha256:24658baf6224d8f280e827f0a50c46ad819ec8ba380a42448e24459daf809cf4",
- "sha256:24aa705a5f5bd3a8bcfa4d123f03413de5d86e497435693b638cbffb7d5d8a1b",
- "sha256:2770bb0d5e3cc0e31e7318db06efcbcdb7b31bcb1a70086d3177692a02256f59",
- "sha256:331ad15c39c9fe9186ceaf87203a9ecf5ae0ba2538c9e898e3a6967e8ad3db6f",
- "sha256:3aa9d43b02a0c681f0bfbc12d476d47b2b2b6a3f9287f11ee42989a268a1833c",
- "sha256:41f4915e09218744d8bae14759f983e466ab69b178de38066f7579892ff2a555",
- "sha256:4304d4416ff032ed50ad6bb87416d802e67139e31c0bde4628f36a47a3164bfa",
- "sha256:435a22d00ec7d7ea533db494da8581b05977f9c37338c80bc86314bec2619424",
- "sha256:45f7cd36186db767d803b1473b3c659d57a23b5fa491ad83c6d40f2af58e4dbb",
- "sha256:48b389b1fd5144603d61d752afd7167dfd205973a43151ae5045b35793232aa2",
- "sha256:4e67d26532bfd8b7f7c05d5a766d6f437b362c1bf203a3a5ce3593a645e870b8",
- "sha256:516a405f174fd3b88829eabfe4bb296ac602d6a0f68e0d64d5ac9456194a5b7e",
- "sha256:5ba5c243f4004c750836f81606a9fcb7841f8874ad8f3bf204ff5e56332b72b9",
- "sha256:5bdc0f1f610d067c70aa3737ed06e2726fd9d6f7bfee4a351f4c40b6831f4e82",
- "sha256:6107e445faf057c118d5050560695e46d272e5301feffda3c41849641222a828",
- "sha256:6327b572f5770293fc062a7ec04160e89741e8552bf1c358d1a23eba68166759",
- "sha256:669b29a9eca6146465cc574659058ed949748f0809a2582d1f1a324eb91054dc",
- "sha256:6ce01337d23884b21c03869d2f68c5523d43174d4fc405490eb0091057943118",
- "sha256:6d872186c1617d143969defeadac5a904e6e374183e07977eedef9c07c8953bf",
- "sha256:6f76a90c345796c01d85e6332e81cab6d70de83b829cf1d9762d0a3da59c7932",
- "sha256:70d2aa9fb00cf52034feac4b913181a6e10356019b18ef89bc7c12a283bf5f5a",
- "sha256:7cbc78dc018596315d4e7841c8c3a7ae31cc4d638c9b627f87d52e8abaaf2d29",
- "sha256:856bf0924d24e7f93b8aee12a3a1095c34085600aa805693fb7f5d1962393206",
- "sha256:8a98748ed1a1df4ee1d6f927e151ed6c1a09d5ec21684de879c7ea6aa96f58f2",
- "sha256:93a7350f6706b31f457c1457d3a3259ff9071a66f312ae64dc024f049055f72c",
- "sha256:964823b2fc77b55355999ade496c54dde161c621cb1f6eac61dc30ed1b63cd4c",
- "sha256:a003ac9edc22d99ae1286b0875c460351f4e101f8c9d9d2576e78d7e048f64e0",
- "sha256:a0ce71725cacc9ebf839630772b07eeec220cbb5f03be1399e0457a1464f8e1a",
- "sha256:a47eef975d2b8b721775a0fa286f50eab535b9d56c70a6e62842134cf7841195",
- "sha256:a8b5b9712783415695663bd463990e2f00c6750562e6ad1d28e072a611c5f2a6",
- "sha256:a9015f5b8af1bb6837a3fcb0cdf3b874fe3385ff6274e8b7925d81ccaec3c5c9",
- "sha256:aec510255ce690d240f7cb23d7114f6b351c733a74c279a84def763660a2c3bc",
- "sha256:b00e7bcd71caa0282cbe3c90966f738e2db91e64092a877c3ff7f19a1628fdcb",
- "sha256:b50aaac7d05c2c26dfd50c3321199f019ba76bb650e346a6ef3616306eed67b0",
- "sha256:b7b6ea9e36d32582cda3465f54c4b454f62f23cb083ebc7a94e2ca6ef011c3a7",
- "sha256:bb9333f58fc3a2296fb1d54576138d4cf5d496a2cc118422bd77835e6ae0b9cb",
- "sha256:c1c13185b90bbd3f8b5963cd8ce7ad4ff441924c31e23c975cb150e27c2bf67a",
- "sha256:c3b8bd3133cd50f6b637bb4322822c94c5ce4bf0d724ed5ae70afce62187c492",
- "sha256:c5d97162c196ce54af6700949ddf9409e9833ef1003b4741c2b39ef46f1d9720",
- "sha256:c815270206f983309915a6844fe994b2fa47e5d05c4c4cef267c3b30e34dbe42",
- "sha256:cab2eba3830bf4f6d91e2d6718e0e1c14a2f5ad1af68a89d24ace0c6b17cced7",
- "sha256:d1df34588123fcc88c872f5acb6f74ae59e9d182a2707097f9e28275ec26a12d",
- "sha256:d6bdcd415ba87846fd317bee0774e412e8792832e7805938987e4ede1d13046d",
- "sha256:db9a30ec064129d605d0f1aedc93e00894b9334ec74ba9c6bdd08147434b33eb",
- "sha256:dbc183e7bef690c9abe5ea67b7b60fdbca81aa8da43468287dae7b5c046107d4",
- "sha256:dca802c8db0720ce1c49cce1149ff7b06e91ba15fa84b1d59144fef1a1bc7ac2",
- "sha256:dec6b307ce928e8e112a6bb9921a1cb00a0e14979bf28b98e084a4b8a742bd9b",
- "sha256:df8bb0010fdd0a743b7542589223a2816bdde4d94bb5ad67884348fa2c1c67e8",
- "sha256:e4094c7b464cf0a858e75cd14b03509e84789abf7b79f8537e6a72152109c76e",
- "sha256:e4760a68cab57bfaa628938e9c2971137e05ce48e762a9cb53b76c9b569f1204",
- "sha256:eb09b82377233b902d4c3fbeeb7ad731cdab579c6c6fda1f763cd779139e47c3",
- "sha256:eb862356ee9391dc5a0b3cbc00f416b48c1b9a52d252d898e5b7696a5f9fe150",
- "sha256:ef9528915df81b8f4c7612b19b8628214c65c9b7f74db2e34a646a0a2a0da2d4",
- "sha256:f3157624b7558b914cb039fd1af735e5e8049a87c817cc215109ad1c8779df76",
- "sha256:f3e0992f23bbb0be00a921eae5363329253c3b86287db27092461c887b791e5e",
- "sha256:f9338cc05451f1942d0d8203ec2c346c830f8e86469903d5126c1f0a13a2bcbb",
- "sha256:ffef8fd58a36fb5f1196919638f73dd3ae0db1a878982b27a9a5a176ede4ba91"
- ],
- "markers": "platform_python_implementation != 'PyPy'",
- "version": "==1.17.0"
+ "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8",
+ "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2",
+ "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1",
+ "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15",
+ "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36",
+ "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824",
+ "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8",
+ "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36",
+ "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17",
+ "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf",
+ "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc",
+ "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3",
+ "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed",
+ "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702",
+ "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1",
+ "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8",
+ "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903",
+ "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6",
+ "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d",
+ "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b",
+ "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e",
+ "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be",
+ "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c",
+ "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683",
+ "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9",
+ "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c",
+ "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8",
+ "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1",
+ "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4",
+ "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655",
+ "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67",
+ "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595",
+ "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0",
+ "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65",
+ "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41",
+ "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6",
+ "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401",
+ "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6",
+ "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3",
+ "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16",
+ "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93",
+ "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e",
+ "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4",
+ "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964",
+ "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c",
+ "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576",
+ "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0",
+ "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3",
+ "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662",
+ "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3",
+ "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff",
+ "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5",
+ "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd",
+ "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f",
+ "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5",
+ "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14",
+ "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d",
+ "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9",
+ "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7",
+ "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382",
+ "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a",
+ "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e",
+ "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a",
+ "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4",
+ "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99",
+ "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87",
+ "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b"
+ ],
+ "markers": "python_version >= '3.8'",
+ "version": "==1.17.1"
},
"cfgv": {
"hashes": [
@@ -317,36 +317,36 @@
},
"cryptography": {
"hashes": [
- "sha256:0663585d02f76929792470451a5ba64424acc3cd5227b03921dab0e2f27b1709",
- "sha256:08a24a7070b2b6804c1940ff0f910ff728932a9d0e80e7814234269f9d46d069",
- "sha256:232ce02943a579095a339ac4b390fbbe97f5b5d5d107f8a08260ea2768be8cc2",
- "sha256:2905ccf93a8a2a416f3ec01b1a7911c3fe4073ef35640e7ee5296754e30b762b",
- "sha256:299d3da8e00b7e2b54bb02ef58d73cd5f55fb31f33ebbf33bd00d9aa6807df7e",
- "sha256:2c6d112bf61c5ef44042c253e4859b3cbbb50df2f78fa8fae6747a7814484a70",
- "sha256:31e44a986ceccec3d0498e16f3d27b2ee5fdf69ce2ab89b52eaad1d2f33d8778",
- "sha256:3d9a1eca329405219b605fac09ecfc09ac09e595d6def650a437523fcd08dd22",
- "sha256:3dcdedae5c7710b9f97ac6bba7e1052b95c7083c9d0e9df96e02a1932e777895",
- "sha256:47ca71115e545954e6c1d207dd13461ab81f4eccfcb1345eac874828b5e3eaaf",
- "sha256:4a997df8c1c2aae1e1e5ac49c2e4f610ad037fc5a3aadc7b64e39dea42249431",
- "sha256:51956cf8730665e2bdf8ddb8da0056f699c1a5715648c1b0144670c1ba00b48f",
- "sha256:5bcb8a5620008a8034d39bce21dc3e23735dfdb6a33a06974739bfa04f853947",
- "sha256:64c3f16e2a4fc51c0d06af28441881f98c5d91009b8caaff40cf3548089e9c74",
- "sha256:6e2b11c55d260d03a8cf29ac9b5e0608d35f08077d8c087be96287f43af3ccdc",
- "sha256:7b3f5fe74a5ca32d4d0f302ffe6680fcc5c28f8ef0dc0ae8f40c0f3a1b4fca66",
- "sha256:844b6d608374e7d08f4f6e6f9f7b951f9256db41421917dfb2d003dde4cd6b66",
- "sha256:9a8d6802e0825767476f62aafed40532bd435e8a5f7d23bd8b4f5fd04cc80ecf",
- "sha256:aae4d918f6b180a8ab8bf6511a419473d107df4dbb4225c7b48c5c9602c38c7f",
- "sha256:ac1955ce000cb29ab40def14fd1bbfa7af2017cca696ee696925615cafd0dce5",
- "sha256:b88075ada2d51aa9f18283532c9f60e72170041bba88d7f37e49cbb10275299e",
- "sha256:cb013933d4c127349b3948aa8aaf2f12c0353ad0eccd715ca789c8a0f671646f",
- "sha256:cc70b4b581f28d0a254d006f26949245e3657d40d8857066c2ae22a61222ef55",
- "sha256:e9c5266c432a1e23738d178e51c2c7a5e2ddf790f248be939448c0ba2021f9d1",
- "sha256:ea9e57f8ea880eeea38ab5abf9fbe39f923544d7884228ec67d666abd60f5a47",
- "sha256:ee0c405832ade84d4de74b9029bedb7b31200600fa524d218fc29bfa371e97f5",
- "sha256:fdcb265de28585de5b859ae13e3846a8e805268a823a12a4da2597f1f5afc9f0"
+ "sha256:0c580952eef9bf68c4747774cde7ec1d85a6e61de97281f2dba83c7d2c806362",
+ "sha256:0f996e7268af62598f2fc1204afa98a3b5712313a55c4c9d434aef49cadc91d4",
+ "sha256:1ec0bcf7e17c0c5669d881b1cd38c4972fade441b27bda1051665faaa89bdcaa",
+ "sha256:281c945d0e28c92ca5e5930664c1cefd85efe80e5c0d2bc58dd63383fda29f83",
+ "sha256:2ce6fae5bdad59577b44e4dfed356944fbf1d925269114c28be377692643b4ff",
+ "sha256:315b9001266a492a6ff443b61238f956b214dbec9910a081ba5b6646a055a805",
+ "sha256:443c4a81bb10daed9a8f334365fe52542771f25aedaf889fd323a853ce7377d6",
+ "sha256:4a02ded6cd4f0a5562a8887df8b3bd14e822a90f97ac5e544c162899bc467664",
+ "sha256:53a583b6637ab4c4e3591a15bc9db855b8d9dee9a669b550f311480acab6eb08",
+ "sha256:63efa177ff54aec6e1c0aefaa1a241232dcd37413835a9b674b6e3f0ae2bfd3e",
+ "sha256:74f57f24754fe349223792466a709f8e0c093205ff0dca557af51072ff47ab18",
+ "sha256:7e1ce50266f4f70bf41a2c6dc4358afadae90e2a1e5342d3c08883df1675374f",
+ "sha256:81ef806b1fef6b06dcebad789f988d3b37ccaee225695cf3e07648eee0fc6b73",
+ "sha256:846da004a5804145a5f441b8530b4bf35afbf7da70f82409f151695b127213d5",
+ "sha256:8ac43ae87929a5982f5948ceda07001ee5e83227fd69cf55b109144938d96984",
+ "sha256:9762ea51a8fc2a88b70cf2995e5675b38d93bf36bd67d91721c309df184f49bd",
+ "sha256:a2a431ee15799d6db9fe80c82b055bae5a752bef645bba795e8e52687c69efe3",
+ "sha256:bf7a1932ac4176486eab36a19ed4c0492da5d97123f1406cf15e41b05e787d2e",
+ "sha256:c2e6fc39c4ab499049df3bdf567f768a723a5e8464816e8f009f121a5a9f4405",
+ "sha256:cbeb489927bd7af4aa98d4b261af9a5bc025bd87f0e3547e11584be9e9427be2",
+ "sha256:d03b5621a135bffecad2c73e9f4deb1a0f977b9a8ffe6f8e002bf6c9d07b918c",
+ "sha256:d56e96520b1020449bbace2b78b603442e7e378a9b3bd68de65c782db1507995",
+ "sha256:df6b6c6d742395dd77a23ea3728ab62f98379eff8fb61be2744d4679ab678f73",
+ "sha256:e1be4655c7ef6e1bbe6b5d0403526601323420bcf414598955968c9ef3eb7d16",
+ "sha256:f18c716be16bc1fea8e95def49edf46b82fccaa88587a45f8dc0ff6ab5d8e0a7",
+ "sha256:f46304d6f0c6ab8e52770addfa2fc41e6629495548862279641972b6215451cd",
+ "sha256:f7b178f11ed3664fd0e995a47ed2b5ff0a12d893e41dd0494f406d1cf555cab7"
],
"markers": "python_version >= '3.7'",
- "version": "==43.0.0"
+ "version": "==43.0.3"
},
"distlib": {
"hashes": [
@@ -1118,6 +1118,48 @@
"markers": "python_version >= '3.7'",
"version": "==0.11.1"
},
+ "types-cffi": {
+ "hashes": [
+ "sha256:a363e5ea54a4eb6a4a105d800685fde596bc318089b025b27dee09849fe41ff0",
+ "sha256:b8b20d23a2b89cfed5f8c5bc53b0cb8677c3aac6d970dbc771e28b9c698f5dee"
+ ],
+ "markers": "python_version >= '3.8'",
+ "version": "==1.16.0.20240331"
+ },
+ "types-pyopenssl": {
+ "hashes": [
+ "sha256:47913b4678a01d879f503a12044468221ed8576263c1540dcb0484ca21b08c39",
+ "sha256:6a7a5d2ec042537934cfb4c9d4deb0e16c4c6250b09358df1f083682fe6fda54"
+ ],
+ "markers": "python_version >= '3.8'",
+ "version": "==24.1.0.20240722"
+ },
+ "types-pytz": {
+ "hashes": [
+ "sha256:3e22df1336c0c6ad1d29163c8fda82736909eb977281cb823c57f8bae07118b7",
+ "sha256:575dc38f385a922a212bac00a7d6d2e16e141132a3c955078f4a4fd13ed6cb44"
+ ],
+ "index": "pypi",
+ "markers": "python_version >= '3.8'",
+ "version": "==2024.2.0.20241003"
+ },
+ "types-redis": {
+ "hashes": [
+ "sha256:5f17d2b3f9091ab75384153bfa276619ffa1cf6a38da60e10d5e6749cc5b902e",
+ "sha256:ef5da68cb827e5f606c8f9c0b49eeee4c2669d6d97122f301d3a55dc6a63f6ed"
+ ],
+ "index": "pypi",
+ "markers": "python_version >= '3.8'",
+ "version": "==4.6.0.20241004"
+ },
+ "types-setuptools": {
+ "hashes": [
+ "sha256:78cb5fef4a6056d2f37114d27da90f4655a306e4e38042d7034a8a880bc3f5dd",
+ "sha256:f9e1ebd17a56f606e16395c4ee4efa1cdc394b9a2a0ee898a624058b4b62ef8f"
+ ],
+ "markers": "python_version >= '3.8'",
+ "version": "==75.3.0.20241112"
+ },
"typing-extensions": {
"hashes": [
"sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d",
@@ -1265,89 +1307,78 @@
},
"coverage": {
"hashes": [
- "sha256:06a737c882bd26d0d6ee7269b20b12f14a8704807a01056c80bb881a4b2ce6ca",
- "sha256:07e2ca0ad381b91350c0ed49d52699b625aab2b44b65e1b4e02fa9df0e92ad2d",
- "sha256:0c0420b573964c760df9e9e86d1a9a622d0d27f417e1a949a8a66dd7bcee7bc6",
- "sha256:0dbde0f4aa9a16fa4d754356a8f2e36296ff4d83994b2c9d8398aa32f222f989",
- "sha256:1125ca0e5fd475cbbba3bb67ae20bd2c23a98fac4e32412883f9bcbaa81c314c",
- "sha256:13b0a73a0896988f053e4fbb7de6d93388e6dd292b0d87ee51d106f2c11b465b",
- "sha256:166811d20dfea725e2e4baa71fffd6c968a958577848d2131f39b60043400223",
- "sha256:170d444ab405852903b7d04ea9ae9b98f98ab6d7e63e1115e82620807519797f",
- "sha256:1f4aa8219db826ce6be7099d559f8ec311549bfc4046f7f9fe9b5cea5c581c56",
- "sha256:225667980479a17db1048cb2bf8bfb39b8e5be8f164b8f6628b64f78a72cf9d3",
- "sha256:260933720fdcd75340e7dbe9060655aff3af1f0c5d20f46b57f262ab6c86a5e8",
- "sha256:2bdb062ea438f22d99cba0d7829c2ef0af1d768d1e4a4f528087224c90b132cb",
- "sha256:2c09f4ce52cb99dd7505cd0fc8e0e37c77b87f46bc9c1eb03fe3bc9991085388",
- "sha256:3115a95daa9bdba70aea750db7b96b37259a81a709223c8448fa97727d546fe0",
- "sha256:3e0cadcf6733c09154b461f1ca72d5416635e5e4ec4e536192180d34ec160f8a",
- "sha256:3f1156e3e8f2872197af3840d8ad307a9dd18e615dc64d9ee41696f287c57ad8",
- "sha256:4421712dbfc5562150f7554f13dde997a2e932a6b5f352edcce948a815efee6f",
- "sha256:44df346d5215a8c0e360307d46ffaabe0f5d3502c8a1cefd700b34baf31d411a",
- "sha256:502753043567491d3ff6d08629270127e0c31d4184c4c8d98f92c26f65019962",
- "sha256:547f45fa1a93154bd82050a7f3cddbc1a7a4dd2a9bf5cb7d06f4ae29fe94eaf8",
- "sha256:5621a9175cf9d0b0c84c2ef2b12e9f5f5071357c4d2ea6ca1cf01814f45d2391",
- "sha256:609b06f178fe8e9f89ef676532760ec0b4deea15e9969bf754b37f7c40326dbc",
- "sha256:645786266c8f18a931b65bfcefdbf6952dd0dea98feee39bd188607a9d307ed2",
- "sha256:6878ef48d4227aace338d88c48738a4258213cd7b74fd9a3d4d7582bb1d8a155",
- "sha256:6a89ecca80709d4076b95f89f308544ec8f7b4727e8a547913a35f16717856cb",
- "sha256:6db04803b6c7291985a761004e9060b2bca08da6d04f26a7f2294b8623a0c1a0",
- "sha256:6e2cd258d7d927d09493c8df1ce9174ad01b381d4729a9d8d4e38670ca24774c",
- "sha256:6e81d7a3e58882450ec4186ca59a3f20a5d4440f25b1cff6f0902ad890e6748a",
- "sha256:702855feff378050ae4f741045e19a32d57d19f3e0676d589df0575008ea5004",
- "sha256:78b260de9790fd81e69401c2dc8b17da47c8038176a79092a89cb2b7d945d060",
- "sha256:7bb65125fcbef8d989fa1dd0e8a060999497629ca5b0efbca209588a73356232",
- "sha256:7dea0889685db8550f839fa202744652e87c60015029ce3f60e006f8c4462c93",
- "sha256:8284cf8c0dd272a247bc154eb6c95548722dce90d098c17a883ed36e67cdb129",
- "sha256:877abb17e6339d96bf08e7a622d05095e72b71f8afd8a9fefc82cf30ed944163",
- "sha256:8929543a7192c13d177b770008bc4e8119f2e1f881d563fc6b6305d2d0ebe9de",
- "sha256:8ae539519c4c040c5ffd0632784e21b2f03fc1340752af711f33e5be83a9d6c6",
- "sha256:8f59d57baca39b32db42b83b2a7ba6f47ad9c394ec2076b084c3f029b7afca23",
- "sha256:9054a0754de38d9dbd01a46621636689124d666bad1936d76c0341f7d71bf569",
- "sha256:953510dfb7b12ab69d20135a0662397f077c59b1e6379a768e97c59d852ee51d",
- "sha256:95cae0efeb032af8458fc27d191f85d1717b1d4e49f7cb226cf526ff28179778",
- "sha256:9bc572be474cafb617672c43fe989d6e48d3c83af02ce8de73fff1c6bb3c198d",
- "sha256:9c56863d44bd1c4fe2abb8a4d6f5371d197f1ac0ebdee542f07f35895fc07f36",
- "sha256:9e0b2df163b8ed01d515807af24f63de04bebcecbd6c3bfeff88385789fdf75a",
- "sha256:a09ece4a69cf399510c8ab25e0950d9cf2b42f7b3cb0374f95d2e2ff594478a6",
- "sha256:a1ac0ae2b8bd743b88ed0502544847c3053d7171a3cff9228af618a068ed9c34",
- "sha256:a318d68e92e80af8b00fa99609796fdbcdfef3629c77c6283566c6f02c6d6704",
- "sha256:a4acd025ecc06185ba2b801f2de85546e0b8ac787cf9d3b06e7e2a69f925b106",
- "sha256:a6d3adcf24b624a7b778533480e32434a39ad8fa30c315208f6d3e5542aeb6e9",
- "sha256:a78d169acd38300060b28d600344a803628c3fd585c912cacc9ea8790fe96862",
- "sha256:a95324a9de9650a729239daea117df21f4b9868ce32e63f8b650ebe6cef5595b",
- "sha256:abd5fd0db5f4dc9289408aaf34908072f805ff7792632250dcb36dc591d24255",
- "sha256:b06079abebbc0e89e6163b8e8f0e16270124c154dc6e4a47b413dd538859af16",
- "sha256:b43c03669dc4618ec25270b06ecd3ee4fa94c7f9b3c14bae6571ca00ef98b0d3",
- "sha256:b48f312cca9621272ae49008c7f613337c53fadca647d6384cc129d2996d1133",
- "sha256:b5d7b556859dd85f3a541db6a4e0167b86e7273e1cdc973e5b175166bb634fdb",
- "sha256:b9f222de8cded79c49bf184bdbc06630d4c58eec9459b939b4a690c82ed05657",
- "sha256:c3c02d12f837d9683e5ab2f3d9844dc57655b92c74e286c262e0fc54213c216d",
- "sha256:c44fee9975f04b33331cb8eb272827111efc8930cfd582e0320613263ca849ca",
- "sha256:cf4b19715bccd7ee27b6b120e7e9dd56037b9c0681dcc1adc9ba9db3d417fa36",
- "sha256:d0c212c49b6c10e6951362f7c6df3329f04c2b1c28499563d4035d964ab8e08c",
- "sha256:d3296782ca4eab572a1a4eca686d8bfb00226300dcefdf43faa25b5242ab8a3e",
- "sha256:d85f5e9a5f8b73e2350097c3756ef7e785f55bd71205defa0bfdaf96c31616ff",
- "sha256:da511e6ad4f7323ee5702e6633085fb76c2f893aaf8ce4c51a0ba4fc07580ea7",
- "sha256:e05882b70b87a18d937ca6768ff33cc3f72847cbc4de4491c8e73880766718e5",
- "sha256:e61c0abb4c85b095a784ef23fdd4aede7a2628478e7baba7c5e3deba61070a02",
- "sha256:e6a08c0be454c3b3beb105c0596ebdc2371fab6bb90c0c0297f4e58fd7e1012c",
- "sha256:e9a6e0eb86070e8ccaedfbd9d38fec54864f3125ab95419970575b42af7541df",
- "sha256:ed37bd3c3b063412f7620464a9ac1314d33100329f39799255fb8d3027da50d3",
- "sha256:f1adfc8ac319e1a348af294106bc6a8458a0f1633cc62a1446aebc30c5fa186a",
- "sha256:f5796e664fe802da4f57a168c85359a8fbf3eab5e55cd4e4569fbacecc903959",
- "sha256:fc5a77d0c516700ebad189b587de289a20a78324bc54baee03dd486f0855d234",
- "sha256:fd21f6ae3f08b41004dfb433fa895d858f3f5979e7762d052b12aef444e29afc"
+ "sha256:00a1d69c112ff5149cabe60d2e2ee948752c975d95f1e1096742e6077affd376",
+ "sha256:023bf8ee3ec6d35af9c1c6ccc1d18fa69afa1cb29eaac57cb064dbb262a517f9",
+ "sha256:0294ca37f1ba500667b1aef631e48d875ced93ad5e06fa665a3295bdd1d95111",
+ "sha256:06babbb8f4e74b063dbaeb74ad68dfce9186c595a15f11f5d5683f748fa1d172",
+ "sha256:0809082ee480bb8f7416507538243c8863ac74fd8a5d2485c46f0f7499f2b491",
+ "sha256:0b3fb02fe73bed561fa12d279a417b432e5b50fe03e8d663d61b3d5990f29546",
+ "sha256:0b58c672d14f16ed92a48db984612f5ce3836ae7d72cdd161001cc54512571f2",
+ "sha256:0bcd1069e710600e8e4cf27f65c90c7843fa8edfb4520fb0ccb88894cad08b11",
+ "sha256:1032e178b76a4e2b5b32e19d0fd0abbce4b58e77a1ca695820d10e491fa32b08",
+ "sha256:11a223a14e91a4693d2d0755c7a043db43d96a7450b4f356d506c2562c48642c",
+ "sha256:12394842a3a8affa3ba62b0d4ab7e9e210c5e366fbac3e8b2a68636fb19892c2",
+ "sha256:182e6cd5c040cec0a1c8d415a87b67ed01193ed9ad458ee427741c7d8513d963",
+ "sha256:1d5b8007f81b88696d06f7df0cb9af0d3b835fe0c8dbf489bad70b45f0e45613",
+ "sha256:1f76846299ba5c54d12c91d776d9605ae33f8ae2b9d1d3c3703cf2db1a67f2c0",
+ "sha256:27fb4a050aaf18772db513091c9c13f6cb94ed40eacdef8dad8411d92d9992db",
+ "sha256:29155cd511ee058e260db648b6182c419422a0d2e9a4fa44501898cf918866cf",
+ "sha256:29fc0f17b1d3fea332f8001d4558f8214af7f1d87a345f3a133c901d60347c73",
+ "sha256:2b6b4c83d8e8ea79f27ab80778c19bc037759aea298da4b56621f4474ffeb117",
+ "sha256:2fdef0d83a2d08d69b1f2210a93c416d54e14d9eb398f6ab2f0a209433db19e1",
+ "sha256:3c65d37f3a9ebb703e710befdc489a38683a5b152242664b973a7b7b22348a4e",
+ "sha256:4f704f0998911abf728a7783799444fcbbe8261c4a6c166f667937ae6a8aa522",
+ "sha256:51b44306032045b383a7a8a2c13878de375117946d68dcb54308111f39775a25",
+ "sha256:53d202fd109416ce011578f321460795abfe10bb901b883cafd9b3ef851bacfc",
+ "sha256:58809e238a8a12a625c70450b48e8767cff9eb67c62e6154a642b21ddf79baea",
+ "sha256:5915fcdec0e54ee229926868e9b08586376cae1f5faa9bbaf8faf3561b393d52",
+ "sha256:5beb1ee382ad32afe424097de57134175fea3faf847b9af002cc7895be4e2a5a",
+ "sha256:5f8ae553cba74085db385d489c7a792ad66f7f9ba2ee85bfa508aeb84cf0ba07",
+ "sha256:5fbd612f8a091954a0c8dd4c0b571b973487277d26476f8480bfa4b2a65b5d06",
+ "sha256:6bd818b7ea14bc6e1f06e241e8234508b21edf1b242d49831831a9450e2f35fa",
+ "sha256:6f01ba56b1c0e9d149f9ac85a2f999724895229eb36bd997b61e62999e9b0901",
+ "sha256:73d2b73584446e66ee633eaad1a56aad577c077f46c35ca3283cd687b7715b0b",
+ "sha256:7bb92c539a624cf86296dd0c68cd5cc286c9eef2d0c3b8b192b604ce9de20a17",
+ "sha256:8165b796df0bd42e10527a3f493c592ba494f16ef3c8b531288e3d0d72c1f6f0",
+ "sha256:862264b12ebb65ad8d863d51f17758b1684560b66ab02770d4f0baf2ff75da21",
+ "sha256:8902dd6a30173d4ef09954bfcb24b5d7b5190cf14a43170e386979651e09ba19",
+ "sha256:8cf717ee42012be8c0cb205dbbf18ffa9003c4cbf4ad078db47b95e10748eec5",
+ "sha256:8ed9281d1b52628e81393f5eaee24a45cbd64965f41857559c2b7ff19385df51",
+ "sha256:99b41d18e6b2a48ba949418db48159d7a2e81c5cc290fc934b7d2380515bd0e3",
+ "sha256:9cb7fa111d21a6b55cbf633039f7bc2749e74932e3aa7cb7333f675a58a58bf3",
+ "sha256:a181e99301a0ae128493a24cfe5cfb5b488c4e0bf2f8702091473d033494d04f",
+ "sha256:a413a096c4cbac202433c850ee43fa326d2e871b24554da8327b01632673a076",
+ "sha256:a6b1e54712ba3474f34b7ef7a41e65bd9037ad47916ccb1cc78769bae324c01a",
+ "sha256:ade3ca1e5f0ff46b678b66201f7ff477e8fa11fb537f3b55c3f0568fbfe6e718",
+ "sha256:b0ac3d42cb51c4b12df9c5f0dd2f13a4f24f01943627120ec4d293c9181219ba",
+ "sha256:b369ead6527d025a0fe7bd3864e46dbee3aa8f652d48df6174f8d0bac9e26e0e",
+ "sha256:b57b768feb866f44eeed9f46975f3d6406380275c5ddfe22f531a2bf187eda27",
+ "sha256:b8d3a03d9bfcaf5b0141d07a88456bb6a4c3ce55c080712fec8418ef3610230e",
+ "sha256:bc66f0bf1d7730a17430a50163bb264ba9ded56739112368ba985ddaa9c3bd09",
+ "sha256:bf20494da9653f6410213424f5f8ad0ed885e01f7e8e59811f572bdb20b8972e",
+ "sha256:c48167910a8f644671de9f2083a23630fbf7a1cb70ce939440cd3328e0919f70",
+ "sha256:c481b47f6b5845064c65a7bc78bc0860e635a9b055af0df46fdf1c58cebf8e8f",
+ "sha256:c7c8b95bf47db6d19096a5e052ffca0a05f335bc63cef281a6e8fe864d450a72",
+ "sha256:c9b8e184898ed014884ca84c70562b4a82cbc63b044d366fedc68bc2b2f3394a",
+ "sha256:cc8ff50b50ce532de2fa7a7daae9dd12f0a699bfcd47f20945364e5c31799fef",
+ "sha256:d541423cdd416b78626b55f123412fcf979d22a2c39fce251b350de38c15c15b",
+ "sha256:dab4d16dfef34b185032580e2f2f89253d302facba093d5fa9dbe04f569c4f4b",
+ "sha256:dacbc52de979f2823a819571f2e3a350a7e36b8cb7484cdb1e289bceaf35305f",
+ "sha256:df57bdbeffe694e7842092c5e2e0bc80fff7f43379d465f932ef36f027179806",
+ "sha256:ed8fe9189d2beb6edc14d3ad19800626e1d9f2d975e436f84e19efb7fa19469b",
+ "sha256:f3ddf056d3ebcf6ce47bdaf56142af51bb7fad09e4af310241e9db7a3a8022e1",
+ "sha256:f8fe4984b431f8621ca53d9380901f62bfb54ff759a1348cd140490ada7b693c",
+ "sha256:fe439416eb6380de434886b00c859304338f8b19f6f54811984f3420a2e03858"
],
"index": "pypi",
- "markers": "python_version >= '3.8'",
- "version": "==7.6.1"
+ "markers": "python_version >= '3.9'",
+ "version": "==7.6.4"
},
"django": {
"hashes": [
"sha256:56ab63a105e8bb06ee67381d7b65fe6774f057e41a8bab06c8020c8882d8ecd4",
"sha256:b5bb1d11b2518a5f91372a282f24662f58f66749666b0a286ab057029f728080"
],
- "index": "pypi",
"markers": "python_version >= '3.10'",
"version": "==5.0.2"
},
@@ -1571,11 +1602,11 @@
},
"packaging": {
"hashes": [
- "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002",
- "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"
+ "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759",
+ "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"
],
"markers": "python_version >= '3.8'",
- "version": "==24.1"
+ "version": "==24.2"
},
"pathspec": {
"hashes": [
@@ -1587,11 +1618,11 @@
},
"platformdirs": {
"hashes": [
- "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee",
- "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"
+ "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907",
+ "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"
],
"markers": "python_version >= '3.8'",
- "version": "==4.2.2"
+ "version": "==4.3.6"
},
"pluggy": {
"hashes": [
@@ -1619,12 +1650,12 @@
},
"pytest": {
"hashes": [
- "sha256:4ba08f9ae7dcf84ded419494d229b48d0903ea6407b030eaec46df5e6a73bba5",
- "sha256:c132345d12ce551242c87269de812483f5bcc87cdbb4722e48487ba194f9fdce"
+ "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181",
+ "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2"
],
"index": "pypi",
"markers": "python_version >= '3.8'",
- "version": "==8.3.2"
+ "version": "==8.3.3"
},
"pytoolconfig": {
"extras": [
diff --git a/backend/dining/api_wrapper.py b/backend/dining/api_wrapper.py
index 3c55cf18..7ba539d2 100644
--- a/backend/dining/api_wrapper.py
+++ b/backend/dining/api_wrapper.py
@@ -65,6 +65,8 @@ def get_venues(self) -> list[dict[str, Any]]:
for key, value in venues.items():
# Cleaning up json response
venue = Venue.objects.filter(venue_id=key).first()
+ if venue is None:
+ continue
value["name"] = venue.name
value["image"] = venue.image_url if venue else None
diff --git a/backend/dining/migrations/0001_initial.py b/backend/dining/migrations/0001_initial.py
index 51b3bc0f..5cc33ee2 100644
--- a/backend/dining/migrations/0001_initial.py
+++ b/backend/dining/migrations/0001_initial.py
@@ -9,9 +9,7 @@ class Migration(migrations.Migration):
initial = True
- dependencies = [
- ("user", "0002_profile_laundry_preferences"),
- ]
+ dependencies = [("user", "0002_profile_laundry_preferences")]
operations = [
migrations.CreateModel(
diff --git a/backend/dining/migrations/0002_diningitem_diningstation_diningmenu.py b/backend/dining/migrations/0002_diningitem_diningstation_diningmenu.py
index 0caaa2be..51874ad9 100644
--- a/backend/dining/migrations/0002_diningitem_diningstation_diningmenu.py
+++ b/backend/dining/migrations/0002_diningitem_diningstation_diningmenu.py
@@ -7,9 +7,7 @@
class Migration(migrations.Migration):
- dependencies = [
- ("dining", "0001_initial"),
- ]
+ dependencies = [("dining", "0001_initial")]
operations = [
migrations.CreateModel(
diff --git a/backend/dining/migrations/0003_venue_name.py b/backend/dining/migrations/0003_venue_name.py
index 40dada7b..9eddb770 100644
--- a/backend/dining/migrations/0003_venue_name.py
+++ b/backend/dining/migrations/0003_venue_name.py
@@ -5,14 +5,10 @@
class Migration(migrations.Migration):
- dependencies = [
- ("dining", "0002_diningitem_diningstation_diningmenu"),
- ]
+ dependencies = [("dining", "0002_diningitem_diningstation_diningmenu")]
operations = [
migrations.AddField(
- model_name="venue",
- name="name",
- field=models.CharField(max_length=255, null=True),
- ),
+ model_name="venue", name="name", field=models.CharField(max_length=255, null=True)
+ )
]
diff --git a/backend/dining/migrations/0004_remove_diningtransaction_profile_and_more.py b/backend/dining/migrations/0004_remove_diningtransaction_profile_and_more.py
index a0f90009..db86346a 100644
--- a/backend/dining/migrations/0004_remove_diningtransaction_profile_and_more.py
+++ b/backend/dining/migrations/0004_remove_diningtransaction_profile_and_more.py
@@ -5,29 +5,16 @@
class Migration(migrations.Migration):
- dependencies = [
- ("dining", "0003_venue_name"),
- ]
+ dependencies = [("dining", "0003_venue_name")]
operations = [
- migrations.RemoveField(
- model_name="diningtransaction",
- name="profile",
- ),
+ migrations.RemoveField(model_name="diningtransaction", name="profile"),
migrations.AlterField(
- model_name="diningitem",
- name="description",
- field=models.CharField(max_length=1000),
+ model_name="diningitem", name="description", field=models.CharField(max_length=1000)
),
migrations.AlterField(
- model_name="diningitem",
- name="ingredients",
- field=models.CharField(max_length=1000),
- ),
- migrations.DeleteModel(
- name="DiningBalance",
- ),
- migrations.DeleteModel(
- name="DiningTransaction",
+ model_name="diningitem", name="ingredients", field=models.CharField(max_length=1000)
),
+ migrations.DeleteModel(name="DiningBalance"),
+ migrations.DeleteModel(name="DiningTransaction"),
]
diff --git a/backend/dining/migrations/0005_diningitem_allergens_diningitem_nutrition_info_and_more.py b/backend/dining/migrations/0005_diningitem_allergens_diningitem_nutrition_info_and_more.py
index 401ece69..f98c3631 100644
--- a/backend/dining/migrations/0005_diningitem_allergens_diningitem_nutrition_info_and_more.py
+++ b/backend/dining/migrations/0005_diningitem_allergens_diningitem_nutrition_info_and_more.py
@@ -6,9 +6,7 @@
class Migration(migrations.Migration):
- dependencies = [
- ("dining", "0004_remove_diningtransaction_profile_and_more"),
- ]
+ dependencies = [("dining", "0004_remove_diningtransaction_profile_and_more")]
operations = [
migrations.AddField(
diff --git a/backend/dining/migrations/0006_remove_diningmenu_stations_and_more.py b/backend/dining/migrations/0006_remove_diningmenu_stations_and_more.py
index 35e63881..a6eec85d 100644
--- a/backend/dining/migrations/0006_remove_diningmenu_stations_and_more.py
+++ b/backend/dining/migrations/0006_remove_diningmenu_stations_and_more.py
@@ -6,15 +6,10 @@
class Migration(migrations.Migration):
- dependencies = [
- ("dining", "0005_diningitem_allergens_diningitem_nutrition_info_and_more"),
- ]
+ dependencies = [("dining", "0005_diningitem_allergens_diningitem_nutrition_info_and_more")]
operations = [
- migrations.RemoveField(
- model_name="diningmenu",
- name="stations",
- ),
+ migrations.RemoveField(model_name="diningmenu", name="stations"),
migrations.AlterField(
model_name="diningitem",
name="allergens",
diff --git a/backend/dining/models.py b/backend/dining/models.py
index dbb5064a..ae5b7b96 100644
--- a/backend/dining/models.py
+++ b/backend/dining/models.py
@@ -3,37 +3,43 @@
class Venue(models.Model):
- venue_id = models.IntegerField(primary_key=True)
- name = models.CharField(max_length=255, null=True)
- image_url = models.URLField()
+ venue_id: models.IntegerField = models.IntegerField(primary_key=True)
+ name: models.CharField = models.CharField(max_length=255, null=True)
+ image_url: models.URLField = models.URLField()
- def __str__(self):
+ def __str__(self) -> str:
return f"{self.name}-{str(self.venue_id)}"
class DiningItem(models.Model):
- item_id = models.IntegerField(primary_key=True)
- name = models.CharField(max_length=255)
- description = models.CharField(max_length=1000, blank=True)
- ingredients = models.CharField(max_length=1000, blank=True) # comma separated list
- allergens = models.CharField(max_length=1000, blank=True) # comma separated list
- nutrition_info = models.CharField(max_length=1000, blank=True) # json string.
+ item_id: models.IntegerField = models.IntegerField(primary_key=True)
+ name: models.CharField = models.CharField(max_length=255)
+ description: models.CharField = models.CharField(max_length=1000, blank=True)
+ ingredients: models.CharField = models.CharField(
+ max_length=1000, blank=True
+ ) # comma separated list
+ allergens: models.CharField = models.CharField(
+ max_length=1000, blank=True
+ ) # comma separated list
+ nutrition_info: models.CharField = models.CharField(max_length=1000, blank=True) # json string.
# Technically, postgres supports json fields but that involves local postgres
# instead of sqlite AND we don't need to query on this field
- def __str__(self):
+ def __str__(self) -> str:
return f"{self.name}"
class DiningStation(models.Model):
- name = models.CharField(max_length=255)
- items = models.ManyToManyField(DiningItem)
- menu = models.ForeignKey("DiningMenu", on_delete=models.CASCADE, related_name="stations")
+ name: models.CharField = models.CharField(max_length=255)
+ items: models.ManyToManyField = models.ManyToManyField(DiningItem)
+ menu: models.ForeignKey = models.ForeignKey(
+ "DiningMenu", on_delete=models.CASCADE, related_name="stations"
+ )
class DiningMenu(models.Model):
- venue = models.ForeignKey(Venue, on_delete=models.CASCADE)
- date = models.DateField(default=timezone.now)
- start_time = models.DateTimeField()
- end_time = models.DateTimeField()
- service = models.CharField(max_length=255)
+ venue: models.ForeignKey = models.ForeignKey(Venue, on_delete=models.CASCADE)
+ date: models.DateField = models.DateField(default=timezone.now)
+ start_time: models.DateTimeField = models.DateTimeField()
+ end_time: models.DateTimeField = models.DateTimeField()
+ service: models.CharField = models.CharField(max_length=255)
diff --git a/backend/dining/views.py b/backend/dining/views.py
index 8b1d1ff5..35c5240c 100644
--- a/backend/dining/views.py
+++ b/backend/dining/views.py
@@ -1,7 +1,7 @@
import datetime
from django.core.cache import cache
-from django.db.models import Count, QuerySet
+from django.db.models import Count, Manager, QuerySet
from django.shortcuts import get_object_or_404
from django.utils import timezone
from django.utils.timezone import make_aware
@@ -15,6 +15,7 @@
from dining.models import DiningMenu, Venue
from dining.serializers import DiningMenuSerializer
from utils.cache import Cache
+from utils.types import get_user
d = DiningAPIWrapper()
@@ -40,7 +41,7 @@ class Menus(generics.ListAPIView):
serializer_class = DiningMenuSerializer
- def get_queryset(self) -> QuerySet[DiningMenu]:
+ def get_queryset(self) -> QuerySet[DiningMenu, Manager[DiningMenu]]:
# TODO: We only have data for the next week, so we should 404
# if date_param is out of bounds
if date_param := self.kwargs.get("date"):
@@ -63,18 +64,19 @@ class Preferences(APIView):
key = "dining_preferences:{user_id}"
def get(self, request: Request) -> Response:
- key = self.key.format(user_id=request.user.id)
+ key = self.key.format(user_id=get_user(request).id)
cached_preferences = cache.get(key)
if cached_preferences is None:
- preferences = request.user.profile.dining_preferences
+ preferences = get_user(request).profile.dining_preferences
# aggregates venues and puts it in form {"venue_id": x, "count": x}
cached_preferences = preferences.values("venue_id").annotate(count=Count("venue_id"))
cache.set(key, cached_preferences, Cache.MONTH)
return Response({"preferences": cached_preferences})
def post(self, request: Request) -> Response:
- key = self.key.format(user_id=request.user.id)
- profile = request.user.profile
+ user = get_user(request)
+ key = self.key.format(user_id=user.id)
+ profile = user.profile
preferences = profile.dining_preferences
venue_ids = set(request.data["venues"])
diff --git a/backend/gsr_booking/admin.py b/backend/gsr_booking/admin.py
index 5c1586d0..494f2886 100644
--- a/backend/gsr_booking/admin.py
+++ b/backend/gsr_booking/admin.py
@@ -1,8 +1,11 @@
+from typing import Any, cast
+
from django.contrib import admin
-from django.db.models import QuerySet
-from rest_framework.request import Request
+from django.db.models import Manager, QuerySet
+from django.http import HttpRequest
from gsr_booking.models import GSR, Group, GroupMembership, GSRBooking, Reservation
+from utils.types import UserType
class GroupMembershipInline(admin.TabularInline):
@@ -12,12 +15,13 @@ class GroupMembershipInline(admin.TabularInline):
readonly_fields = ["name"]
def name(self, obj: GroupMembership) -> str:
- return obj.user.get_full_name()
+ user = cast(UserType, obj.user)
+ return str(user.get_full_name())
- def get_fields(self, request, obj=None) -> list[str]:
+ def get_fields(self, request: HttpRequest, obj: Any = None) -> list[str]:
fields = super().get_fields(request, obj)
to_remove = ["user", "name"]
- return ["name"] + [f for f in fields if f not in to_remove]
+ return ["name"] + [str(f) for f in fields if f not in to_remove]
class GroupAdmin(admin.ModelAdmin):
@@ -33,7 +37,7 @@ class GroupMembershipAdmin(admin.ModelAdmin):
class GSRAdmin(admin.ModelAdmin):
- def get_queryset(self, request: Request) -> QuerySet[GSR]:
+ def get_queryset(self, request: HttpRequest) -> QuerySet[GSR, Manager[GSR]]:
return GSR.all_objects.all()
list_display = ["name", "kind", "lid", "gid", "in_use"]
diff --git a/backend/gsr_booking/api_wrapper.py b/backend/gsr_booking/api_wrapper.py
index f4f2b1e1..2f639fdc 100644
--- a/backend/gsr_booking/api_wrapper.py
+++ b/backend/gsr_booking/api_wrapper.py
@@ -2,7 +2,7 @@
from abc import ABC, abstractmethod
from enum import Enum
from random import randint
-from typing import TYPE_CHECKING, Any
+from typing import Any, Optional, TypedDict, cast
import requests
from bs4 import BeautifulSoup
@@ -17,15 +17,9 @@
from gsr_booking.models import GSR, Group, GroupMembership, GSRBooking, Reservation
from gsr_booking.serializers import GSRBookingSerializer, GSRSerializer
from utils.errors import APIError
+from utils.types import UserType
-if TYPE_CHECKING:
- from django.contrib.auth.models import AbstractUser
-
- UserType = AbstractUser
-else:
- UserType = Any
-
User = get_user_model()
BASE_URL = "https://libcal.library.upenn.edu"
@@ -46,23 +40,40 @@ class CreditType(Enum):
ARB = "ARB"
+class AvailabilityTime(TypedDict):
+ start_time: str
+ end_time: str
+
+
+class RoomInfo(TypedDict):
+ room_name: str
+ id: int
+ availability: list[AvailabilityTime]
+
+
+class AvailabilityResponse(TypedDict):
+ name: str
+ gid: int
+ rooms: list[RoomInfo]
+
+
class AbstractBookingWrapper(ABC):
@abstractmethod
- def book_room(self, rid: int, start: str, end: str, user: "UserType") -> dict[str, Any]:
+ def book_room(self, rid: int, start: str, end: str, user: UserType) -> dict[str, Any]:
raise NotImplementedError # pragma: no cover
@abstractmethod
- def cancel_room(self, booking_id: str, user: "UserType") -> dict[str, Any]:
+ def cancel_room(self, booking_id: str, user: UserType) -> dict[str, Any]:
raise NotImplementedError # pragma: no cover
@abstractmethod
def get_availability(
- self, lid: int, start: str | None, end: str | None, user: "UserType"
- ) -> list[dict[str, Any]]:
+ self, lid: str | int, start: str | None, end: str | None, user: UserType
+ ) -> list[RoomInfo]:
raise NotImplementedError # pragma: no cover
@abstractmethod
- def get_reservations(self, user: "UserType") -> list[dict[str, Any]]:
+ def get_reservations(self, user: UserType) -> list[dict[str, Any]]:
raise NotImplementedError # pragma: no cover
@@ -82,21 +93,16 @@ def request(self, *args: Any, **kwargs: Any) -> requests.Response:
return response
- def book_room(self, rid: int, start: str, end: str, user: "UserType") -> dict[str, Any]:
+ def book_room(self, rid: int, start: str, end: str, user: UserType) -> dict[str, Any]:
"""Books room if pennkey is valid"""
- payload = {
- "start": start,
- "end": end,
- "pennkey": user.username,
- "room": rid,
- }
+ payload = {"start": start, "end": end, "pennkey": user.username, "room": rid}
url = f"{WHARTON_URL}{user.username}/student_reserve"
response = self.request("POST", url, json=payload).json()
if "error" in response:
raise APIError("Wharton: " + response["error"])
return response
- def cancel_room(self, booking_id: str, user: "UserType") -> dict[str, Any]:
+ def cancel_room(self, booking_id: str, user: UserType) -> dict[str, Any]:
"""Cancels reservation given booking id"""
url = f"{WHARTON_URL}{user.username}/reservations/{booking_id}/cancel"
response = self.request("DELETE", url).json()
@@ -105,8 +111,8 @@ def cancel_room(self, booking_id: str, user: "UserType") -> dict[str, Any]:
return response
def get_availability(
- self, lid: int, start: str | None, end: str | None, user: "UserType"
- ) -> list[dict[str, Any]]:
+ self, lid: str | int, start: str | None, end: str | None, user: UserType
+ ) -> list[RoomInfo]:
"""Returns a list of rooms and their availabilities"""
current_time = timezone.localtime()
search_date = (
@@ -144,7 +150,7 @@ def get_availability(
room["availability"] = valid_slots
return rooms
- def get_reservations(self, user: "UserType") -> list[dict[str, Any]]:
+ def get_reservations(self, user: UserType) -> list[dict[str, Any]]:
url = f"{WHARTON_URL}{user.username}/reservations"
bookings = self.request("GET", url).json()["bookings"]
@@ -162,7 +168,7 @@ def get_reservations(self, user: "UserType") -> list[dict[str, Any]]:
if datetime.datetime.strptime(booking["end"], "%Y-%m-%dT%H:%M:%S%z") >= now
]
- def is_wharton(self, user: "UserType") -> bool | None:
+ def is_wharton(self, user: UserType) -> bool | None:
url = f"{WHARTON_URL}{user.username}/privileges"
try:
response = self.request("GET", url)
@@ -175,7 +181,7 @@ def is_wharton(self, user: "UserType") -> bool | None:
class LibCalBookingWrapper(AbstractBookingWrapper):
- def __init__(self):
+ def __init__(self) -> None:
self.token = None
self.expiration = timezone.localtime()
@@ -213,7 +219,7 @@ def request(self, *args: Any, **kwargs: Any) -> requests.Response:
except (ConnectTimeout, ReadTimeout, ConnectionError):
raise APIError("LibCal: Connection timeout")
- def book_room(self, rid: int, start: str, end: str, user: "UserType") -> dict[str, Any]:
+ def book_room(self, rid: int, start: str, end: str, user: UserType) -> dict[str, Any]:
"""
Books room if pennkey is valid
@@ -258,10 +264,10 @@ def book_room(self, rid: int, start: str, end: str, user: "UserType") -> dict[st
raise APIError("LibCal: " + res_json["error"])
return res_json
- def get_reservations(self, user: "UserType") -> list[dict[str, Any]]:
- pass
+ def get_reservations(self, user: UserType) -> list[dict[str, Any]]:
+ return []
- def cancel_room(self, booking_id: str, user: "UserType") -> dict[str, Any]:
+ def cancel_room(self, booking_id: str, user: UserType) -> dict[str, Any]:
"""Cancels room"""
response = self.request("POST", f"{API_URL}/1.1/space/cancel/{booking_id}").json()
if "error" in response[0]:
@@ -269,8 +275,8 @@ def cancel_room(self, booking_id: str, user: "UserType") -> dict[str, Any]:
return response
def get_availability(
- self, gid: int, start: str | None, end: str | None, user: "UserType"
- ) -> list[dict[str, Any]]:
+ self, gid: str | int, start: str | None, end: str | None, user: UserType
+ ) -> list[RoomInfo]:
"""Returns a list of rooms and their availabilities"""
# adjusts url based on start and end times
@@ -295,7 +301,7 @@ def get_availability(
if response.status_code != 200:
raise APIError(f"GSR Reserve: Error {response.status_code} when reserving data")
- rooms = [
+ rooms: list[dict[str, Any]] = [
{"room_name": room["name"], "id": room["id"], "availability": room["availability"]}
for room in response.json()
if room["id"] not in ROOM_BLACKLIST
@@ -313,7 +319,7 @@ def get_availability(
or datetime.datetime.strptime(time["from"][:-6], "%Y-%m-%dT%H:%M:%S")
>= start_datetime
]
- return rooms
+ return cast(list[RoomInfo], rooms)
def get_affiliation(self, email: str) -> str:
"""Gets school from email"""
@@ -328,13 +334,15 @@ def get_affiliation(self, email: str) -> str:
class BookingHandler:
- def __init__(self, WBW=None, LBW=None):
+ def __init__(
+ self,
+ WBW: Optional[AbstractBookingWrapper] = None,
+ LBW: Optional[AbstractBookingWrapper] = None,
+ ) -> None:
self.WBW = WBW or WhartonBookingWrapper()
self.LBW = LBW or LibCalBookingWrapper()
- def format_members(
- self, members: QuerySet[GroupMembership]
- ) -> list[tuple["UserType", datetime.timedelta]]:
+ def format_members(self, members: QuerySet) -> list[tuple[UserType, datetime.timedelta]]:
PREFIX = "user__"
return [
(
@@ -348,7 +356,7 @@ def format_members(
def get_wharton_members(
self, group: Group, gsr_id: int
- ) -> list[tuple["UserType", datetime.timedelta]]:
+ ) -> list[tuple[UserType, datetime.timedelta]]:
now = timezone.localtime()
ninty_min = datetime.timedelta(minutes=90)
zero_min = datetime.timedelta(minutes=0)
@@ -359,15 +367,18 @@ def get_wharton_members(
.values("user")
.annotate(
credits=ninty_min
- - Coalesce(
- Sum(
- F("user__gsrbooking__end") - F("user__gsrbooking__start"),
- filter=Q(user__gsrbooking__gsr__gid=gsr_id)
- & Q(user__gsrbooking__is_cancelled=False)
- & Q(user__gsrbooking__end__gte=now),
+ - cast(
+ datetime.timedelta,
+ Coalesce(
+ Sum(
+ F("user__gsrbooking__end") - F("user__gsrbooking__start"),
+ filter=Q(user__gsrbooking__gsr__gid=gsr_id)
+ & Q(user__gsrbooking__is_cancelled=False)
+ & Q(user__gsrbooking__end__gte=now),
+ ),
+ zero_min,
),
- zero_min,
- )
+ ),
)
.filter(Q(credits__gt=zero_min))
.values("user__id", "user__username", "credits")
@@ -375,7 +386,7 @@ def get_wharton_members(
)
return self.format_members(ret)
- def get_libcal_members(self, group: Group) -> list[tuple["UserType", datetime.timedelta]]:
+ def get_libcal_members(self, group: Group) -> list[tuple[UserType, datetime.timedelta]]:
day_start = timezone.localtime().replace(hour=0, minute=0, second=0, microsecond=0)
day_end = day_start + datetime.timedelta(days=1)
two_hours = datetime.timedelta(hours=2)
@@ -387,15 +398,18 @@ def get_libcal_members(self, group: Group) -> list[tuple["UserType", datetime.ti
.values("user")
.annotate(
credits=two_hours
- - Coalesce(
- Sum(
- F("user__gsrbooking__end") - F("user__gsrbooking__start"),
- filter=Q(user__gsrbooking__gsr__kind=GSR.KIND_LIBCAL)
- & Q(user__gsrbooking__is_cancelled=False)
- & Q(user__gsrbooking__start__gte=day_start)
- & Q(user__gsrbooking__end__lte=day_end),
+ - cast(
+ datetime.timedelta,
+ Coalesce(
+ Sum(
+ F("user__gsrbooking__end") - F("user__gsrbooking__start"),
+ filter=Q(user__gsrbooking__gsr__kind=GSR.KIND_LIBCAL)
+ & Q(user__gsrbooking__is_cancelled=False)
+ & Q(user__gsrbooking__start__gte=day_start)
+ & Q(user__gsrbooking__end__lte=day_end),
+ ),
+ zero_min,
),
- zero_min,
)
)
.filter(Q(credits__gt=zero_min))
@@ -418,14 +432,14 @@ def book_room(
room_name: str,
start: str,
end: str,
- user: "UserType",
- group: Group | None = None,
+ user: UserType,
+ group: Optional[Group] = None,
) -> Reservation:
# NOTE when booking with a group, we are only querying our db for existing bookings,
# so users in a group who book through wharton may screw up the query
gsr = get_object_or_404(GSR, gid=gid)
- start = datetime.datetime.strptime(start, "%Y-%m-%dT%H:%M:%S%z")
- end = datetime.datetime.strptime(end, "%Y-%m-%dT%H:%M:%S%z")
+ start_dt = datetime.datetime.strptime(start, "%Y-%m-%dT%H:%M:%S%z")
+ end_dt = datetime.datetime.strptime(end, "%Y-%m-%dT%H:%M:%S%z")
book_func = self.WBW.book_room if gsr.kind == GSR.KIND_WHARTON else self.LBW.book_room
members = (
@@ -442,15 +456,17 @@ def book_room(
[time_available for _, time_available in members], datetime.timedelta(minutes=0)
)
- if (end - start) >= total_time_available:
+ if (end_dt - start_dt) >= total_time_available:
raise APIError("Error: Not enough credits")
- reservation = Reservation.objects.create(start=start, end=end, creator=user, group=group)
+ reservation = Reservation.objects.create(
+ start=start_dt, end=end_dt, creator=user, group=group
+ )
- curr_start = start
+ curr_start = start_dt
try:
for curr_user, time_available in members:
- curr_end = curr_start + min(time_available, end - curr_start)
+ curr_end = curr_start + min(time_available, end_dt - curr_start)
booking_id = book_func(
rid,
@@ -470,17 +486,17 @@ def book_room(
booking.reservation = reservation
booking.save()
- if (curr_start := curr_end) >= end:
+ if (curr_start := curr_end) >= end_dt:
break
except APIError as e:
raise APIError(
- f"{str(e)}. Was only able to book {start.strftime('%H:%M')}"
+ f"{str(e)}. Was only able to book {start_dt.strftime('%H:%M')}"
f" - {curr_start.strftime('%H:%M')}"
)
return reservation
- def cancel_room(self, booking_id: str, user: "UserType") -> None:
+ def cancel_room(self, booking_id: str, user: UserType) -> None | APIError:
if (
gsr_booking := GSRBooking.objects.filter(booking_id=booking_id)
.prefetch_related(Prefetch("reservation__gsrbooking_set"), Prefetch("gsr"))
@@ -504,20 +520,20 @@ def cancel_room(self, booking_id: str, user: "UserType") -> None:
for service in [self.WBW, self.LBW]:
try:
service.cancel_room(booking_id, user)
- return
+ return None
except APIError:
- pass
- raise APIError("Error: Unknown booking id")
+ raise APIError("Error: Unknown booking id")
+ return None
def get_availability(
self,
- lid: int,
- gid: int,
+ lid: str | int,
+ gid: str | int,
start: str | None,
end: str | None,
- user: "UserType",
- group: Group | None = None,
- ) -> list[dict[str, Any]]:
+ user: UserType,
+ group: Optional[Group] = None,
+ ) -> AvailabilityResponse:
gsr = get_object_or_404(GSR, gid=gid)
# select a random user from the group if booking wharton gsr
@@ -535,7 +551,7 @@ def get_availability(
return {"name": gsr.name, "gid": gsr.gid, "rooms": rooms}
def get_reservations(
- self, user: "UserType", group: Group | None = None
+ self, user: UserType, group: Optional[Group] = None
) -> list[dict[str, Any]]:
q = Q(user=user) | Q(reservation__creator=user) if group else Q(user=user)
bookings = GSRBooking.objects.filter(
@@ -543,16 +559,16 @@ def get_reservations(
).prefetch_related(Prefetch("reservation"))
if group:
- ret = []
+ ret: list[dict[str, Any]] = []
for booking in bookings:
- data = GSRBookingSerializer(booking).data
+ data = cast(dict[str, Any], GSRBookingSerializer(booking).data)
if booking.reservation.creator == user:
data["room_name"] = f"[Me] {data['room_name']}"
else:
data["room_name"] = f"[{group.name}] {data['room_name']}"
ret.append(data)
else:
- ret = GSRBookingSerializer(bookings, many=True).data
+ ret = cast(list[dict[str, Any]], GSRBookingSerializer(bookings, many=True).data)
# deal with bookings made directly through wharton (not us)
try:
diff --git a/backend/gsr_booking/management/commands/change_group.py b/backend/gsr_booking/management/commands/change_group.py
index b2832087..28eeb7f2 100644
--- a/backend/gsr_booking/management/commands/change_group.py
+++ b/backend/gsr_booking/management/commands/change_group.py
@@ -1,3 +1,4 @@
+from argparse import ArgumentParser
from typing import Any
from django.contrib.auth import get_user_model
@@ -15,7 +16,7 @@ class Command(BaseCommand):
Adds/remove users to a group.
"""
- def add_arguments(self, parser) -> None:
+ def add_arguments(self, parser: ArgumentParser) -> None:
parser.add_argument("usernames", type=str, help="list of pennkeys")
parser.add_argument("group", type=str, help="group name")
parser.add_argument("mode", type=str, help="mode of operation (add/remove/reset)")
diff --git a/backend/gsr_booking/management/commands/get_reservations.py b/backend/gsr_booking/management/commands/get_reservations.py
index 609478da..9e644b0e 100644
--- a/backend/gsr_booking/management/commands/get_reservations.py
+++ b/backend/gsr_booking/management/commands/get_reservations.py
@@ -1,3 +1,4 @@
+from argparse import ArgumentParser
from datetime import datetime
from typing import Any
@@ -28,7 +29,7 @@ class Command(BaseCommand):
Note: --start/--end and --current are mutually exclusive
"""
- def add_arguments(self, parser) -> None:
+ def add_arguments(self, parser: ArgumentParser) -> None:
# optional flags
parser.add_argument("--group", type=str, default=None)
parser.add_argument("--start", type=str, default=None)
diff --git a/backend/gsr_booking/management/commands/individual_usage.py b/backend/gsr_booking/management/commands/individual_usage.py
index 6b27fbaf..80dbf32c 100644
--- a/backend/gsr_booking/management/commands/individual_usage.py
+++ b/backend/gsr_booking/management/commands/individual_usage.py
@@ -1,3 +1,4 @@
+from argparse import ArgumentParser
from typing import Any
from django.core.management.base import BaseCommand
@@ -9,7 +10,7 @@
class Command(BaseCommand):
help = "Provides usage stats for a given user."
- def add_arguments(self, parser) -> None:
+ def add_arguments(self, parser: ArgumentParser) -> None:
parser.add_argument("pennkey", type=str, help="Pennkey of user to check")
def handle(self, *args: Any, **kwargs: Any) -> None:
diff --git a/backend/gsr_booking/migrations/0001_initial.py b/backend/gsr_booking/migrations/0001_initial.py
index 7ff4fead..d3b8d594 100644
--- a/backend/gsr_booking/migrations/0001_initial.py
+++ b/backend/gsr_booking/migrations/0001_initial.py
@@ -18,10 +18,7 @@ class Migration(migrations.Migration):
(
"id",
models.AutoField(
- auto_created=True,
- primary_key=True,
- serialize=False,
- verbose_name="ID",
+ auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
),
),
("name", models.CharField(max_length=255)),
@@ -36,22 +33,15 @@ class Migration(migrations.Migration):
(
"id",
models.AutoField(
- auto_created=True,
- primary_key=True,
- serialize=False,
- verbose_name="ID",
+ auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
),
),
("accepted", models.BooleanField(default=False)),
- (
- "type",
- models.CharField(choices=[("A", "Admin"), ("M", "M")], max_length=10),
- ),
+ ("type", models.CharField(choices=[("A", "Admin"), ("M", "M")], max_length=10)),
(
"group",
models.ForeignKey(
- on_delete=django.db.models.deletion.CASCADE,
- to="gsr_booking.Group",
+ on_delete=django.db.models.deletion.CASCADE, to="gsr_booking.Group"
),
),
(
diff --git a/backend/gsr_booking/migrations/0001_squashed_0011_merge_20200418_2009.py b/backend/gsr_booking/migrations/0001_squashed_0011_merge_20200418_2009.py
index a6f36cba..c3fa6e9c 100644
--- a/backend/gsr_booking/migrations/0001_squashed_0011_merge_20200418_2009.py
+++ b/backend/gsr_booking/migrations/0001_squashed_0011_merge_20200418_2009.py
@@ -34,10 +34,7 @@ class Migration(migrations.Migration):
(
"id",
models.AutoField(
- auto_created=True,
- primary_key=True,
- serialize=False,
- verbose_name="ID",
+ auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
),
),
("name", models.CharField(max_length=255)),
@@ -52,22 +49,15 @@ class Migration(migrations.Migration):
(
"id",
models.AutoField(
- auto_created=True,
- primary_key=True,
- serialize=False,
- verbose_name="ID",
+ auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
),
),
("accepted", models.BooleanField(default=False)),
- (
- "type",
- models.CharField(choices=[("A", "Admin"), ("M", "M")], max_length=10),
- ),
+ ("type", models.CharField(choices=[("A", "Admin"), ("M", "M")], max_length=10)),
(
"group",
models.ForeignKey(
- on_delete=django.db.models.deletion.CASCADE,
- to="gsr_booking.Group",
+ on_delete=django.db.models.deletion.CASCADE, to="gsr_booking.Group"
),
),
(
@@ -82,10 +72,7 @@ class Migration(migrations.Migration):
),
("notifications", models.BooleanField(default=True)),
("pennkey_allow", models.BooleanField(default=False)),
- (
- "username",
- models.CharField(blank=True, default=None, max_length=127, null=True),
- ),
+ ("username", models.CharField(blank=True, default=None, max_length=127, null=True)),
],
options={"verbose_name": "Group Membership"},
),
@@ -114,10 +101,7 @@ class Migration(migrations.Migration):
(
"id",
models.AutoField(
- auto_created=True,
- primary_key=True,
- serialize=False,
- verbose_name="ID",
+ auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
),
),
("full_name", models.CharField(db_index=True, max_length=255)),
@@ -125,8 +109,7 @@ class Migration(migrations.Migration):
(
"user",
models.ForeignKey(
- on_delete=django.db.models.deletion.CASCADE,
- to=settings.AUTH_USER_MODEL,
+ on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL
),
),
],
@@ -137,10 +120,7 @@ class Migration(migrations.Migration):
(
"id",
models.AutoField(
- auto_created=True,
- primary_key=True,
- serialize=False,
- verbose_name="ID",
+ auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
),
),
(
@@ -156,17 +136,13 @@ class Migration(migrations.Migration):
(
"email",
models.CharField(
- max_length=255,
- null=True,
- unique=True,
- verbose_name="school email",
+ max_length=255, null=True, unique=True, verbose_name="school email"
),
),
(
"user",
models.OneToOneField(
- on_delete=django.db.models.deletion.CASCADE,
- to=settings.AUTH_USER_MODEL,
+ on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL
),
),
(
diff --git a/backend/gsr_booking/migrations/0002_auto_20210129_1527.py b/backend/gsr_booking/migrations/0002_auto_20210129_1527.py
index 0411d13d..68848799 100644
--- a/backend/gsr_booking/migrations/0002_auto_20210129_1527.py
+++ b/backend/gsr_booking/migrations/0002_auto_20210129_1527.py
@@ -13,14 +13,8 @@ class Migration(migrations.Migration):
]
operations = [
- migrations.RemoveField(
- model_name="gsrbookingcredentials",
- name="email",
- ),
- migrations.RemoveField(
- model_name="gsrbookingcredentials",
- name="id",
- ),
+ migrations.RemoveField(model_name="gsrbookingcredentials", name="email"),
+ migrations.RemoveField(model_name="gsrbookingcredentials", name="id"),
migrations.AlterField(
model_name="gsrbookingcredentials",
name="date_updated",
diff --git a/backend/gsr_booking/migrations/0004_alter_gsr_lid.py b/backend/gsr_booking/migrations/0004_alter_gsr_lid.py
index b9955207..84bf6c65 100644
--- a/backend/gsr_booking/migrations/0004_alter_gsr_lid.py
+++ b/backend/gsr_booking/migrations/0004_alter_gsr_lid.py
@@ -5,14 +5,8 @@
class Migration(migrations.Migration):
- dependencies = [
- ("gsr_booking", "0003_gsr_gsrbooking"),
- ]
+ dependencies = [("gsr_booking", "0003_gsr_gsrbooking")]
operations = [
- migrations.AlterField(
- model_name="gsr",
- name="lid",
- field=models.CharField(max_length=255),
- ),
+ migrations.AlterField(model_name="gsr", name="lid", field=models.CharField(max_length=255))
]
diff --git a/backend/gsr_booking/migrations/0005_reservation.py b/backend/gsr_booking/migrations/0005_reservation.py
index 14928fbc..b6b71649 100644
--- a/backend/gsr_booking/migrations/0005_reservation.py
+++ b/backend/gsr_booking/migrations/0005_reservation.py
@@ -44,5 +44,5 @@ class Migration(migrations.Migration):
),
),
],
- ),
+ )
]
diff --git a/backend/gsr_booking/migrations/0005_usersearchindex.py b/backend/gsr_booking/migrations/0005_usersearchindex.py
index 61345e3b..a64bc4c6 100644
--- a/backend/gsr_booking/migrations/0005_usersearchindex.py
+++ b/backend/gsr_booking/migrations/0005_usersearchindex.py
@@ -19,10 +19,7 @@ class Migration(migrations.Migration):
(
"id",
models.AutoField(
- auto_created=True,
- primary_key=True,
- serialize=False,
- verbose_name="ID",
+ auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
),
),
("full_name", models.CharField(db_index=True, max_length=255)),
@@ -30,8 +27,7 @@ class Migration(migrations.Migration):
(
"user",
models.ForeignKey(
- on_delete=django.db.models.deletion.CASCADE,
- to=settings.AUTH_USER_MODEL,
+ on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL
),
),
],
diff --git a/backend/gsr_booking/migrations/0006_auto_20200207_2126.py b/backend/gsr_booking/migrations/0006_auto_20200207_2126.py
index 378edc5b..69704727 100644
--- a/backend/gsr_booking/migrations/0006_auto_20200207_2126.py
+++ b/backend/gsr_booking/migrations/0006_auto_20200207_2126.py
@@ -6,9 +6,7 @@
class Migration(migrations.Migration):
- dependencies = [
- ("gsr_booking", "0005_usersearchindex"),
- ]
+ dependencies = [("gsr_booking", "0005_usersearchindex")]
operations = [
migrations.AlterField(
diff --git a/backend/gsr_booking/migrations/0006_auto_20211024_1231.py b/backend/gsr_booking/migrations/0006_auto_20211024_1231.py
index 05297a76..6c636569 100644
--- a/backend/gsr_booking/migrations/0006_auto_20211024_1231.py
+++ b/backend/gsr_booking/migrations/0006_auto_20211024_1231.py
@@ -8,21 +8,13 @@ def create_single_user_group(apps, schema_editor):
Group = apps.get_model("gsr_booking", "Group")
GroupMembership = apps.get_model("gsr_booking", "GroupMembership")
for user in User.objects.all():
- group, created = Group.objects.get_or_create(
- owner=user,
- name="Me",
- color="#14f7d1",
- )
+ group, created = Group.objects.get_or_create(owner=user, name="Me", color="#14f7d1")
if created:
GroupMembership.objects.get_or_create(group=group, user=user, type="A", accepted=True)
class Migration(migrations.Migration):
- dependencies = [
- ("gsr_booking", "0005_reservation"),
- ]
+ dependencies = [("gsr_booking", "0005_reservation")]
- operations = [
- migrations.RunPython(create_single_user_group),
- ]
+ operations = [migrations.RunPython(create_single_user_group)]
diff --git a/backend/gsr_booking/migrations/0006_gsrbookingcredentials.py b/backend/gsr_booking/migrations/0006_gsrbookingcredentials.py
index da854a7d..9214c372 100644
--- a/backend/gsr_booking/migrations/0006_gsrbookingcredentials.py
+++ b/backend/gsr_booking/migrations/0006_gsrbookingcredentials.py
@@ -19,10 +19,7 @@ class Migration(migrations.Migration):
(
"id",
models.AutoField(
- auto_created=True,
- primary_key=True,
- serialize=False,
- verbose_name="ID",
+ auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
),
),
(
@@ -42,19 +39,15 @@ class Migration(migrations.Migration):
(
"email",
models.CharField(
- max_length=255,
- null=True,
- unique=True,
- verbose_name="school email",
+ max_length=255, null=True, unique=True, verbose_name="school email"
),
),
(
"user",
models.ForeignKey(
- on_delete=django.db.models.deletion.CASCADE,
- to=settings.AUTH_USER_MODEL,
+ on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL
),
),
],
- ),
+ )
]
diff --git a/backend/gsr_booking/migrations/0007_delete_gsrbookingcredentials.py b/backend/gsr_booking/migrations/0007_delete_gsrbookingcredentials.py
index 4a089e57..af5a2a01 100644
--- a/backend/gsr_booking/migrations/0007_delete_gsrbookingcredentials.py
+++ b/backend/gsr_booking/migrations/0007_delete_gsrbookingcredentials.py
@@ -5,12 +5,6 @@
class Migration(migrations.Migration):
- dependencies = [
- ("gsr_booking", "0006_auto_20211024_1231"),
- ]
+ dependencies = [("gsr_booking", "0006_auto_20211024_1231")]
- operations = [
- migrations.DeleteModel(
- name="GSRBookingCredentials",
- ),
- ]
+ operations = [migrations.DeleteModel(name="GSRBookingCredentials")]
diff --git a/backend/gsr_booking/migrations/0008_auto_20200202_1218.py b/backend/gsr_booking/migrations/0008_auto_20200202_1218.py
index 74069e66..239eed6d 100644
--- a/backend/gsr_booking/migrations/0008_auto_20200202_1218.py
+++ b/backend/gsr_booking/migrations/0008_auto_20200202_1218.py
@@ -19,5 +19,5 @@ class Migration(migrations.Migration):
field=models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL
),
- ),
+ )
]
diff --git a/backend/gsr_booking/migrations/0008_auto_20211112_1657.py b/backend/gsr_booking/migrations/0008_auto_20211112_1657.py
index bedd8c7d..5a9ea923 100644
--- a/backend/gsr_booking/migrations/0008_auto_20211112_1657.py
+++ b/backend/gsr_booking/migrations/0008_auto_20211112_1657.py
@@ -30,15 +30,10 @@ def create_reservation_for_booking(apps, schema_editor):
class Migration(migrations.Migration):
- dependencies = [
- ("gsr_booking", "0007_delete_gsrbookingcredentials"),
- ]
+ dependencies = [("gsr_booking", "0007_delete_gsrbookingcredentials")]
operations = [
- migrations.RemoveField(
- model_name="reservation",
- name="gsr",
- ),
+ migrations.RemoveField(model_name="reservation", name="gsr"),
migrations.AddField(
model_name="gsrbooking",
name="reservation",
@@ -47,12 +42,8 @@ class Migration(migrations.Migration):
),
),
migrations.AddField(
- model_name="reservation",
- name="is_cancelled",
- field=models.BooleanField(default=False),
- ),
- migrations.DeleteModel(
- name="UserSearchIndex",
+ model_name="reservation", name="is_cancelled", field=models.BooleanField(default=False)
),
+ migrations.DeleteModel(name="UserSearchIndex"),
migrations.RunPython(create_reservation_for_booking),
]
diff --git a/backend/gsr_booking/migrations/0009_auto_20200202_1232.py b/backend/gsr_booking/migrations/0009_auto_20200202_1232.py
index d45a3235..d6c9519e 100644
--- a/backend/gsr_booking/migrations/0009_auto_20200202_1232.py
+++ b/backend/gsr_booking/migrations/0009_auto_20200202_1232.py
@@ -5,14 +5,12 @@
class Migration(migrations.Migration):
- dependencies = [
- ("gsr_booking", "0008_auto_20200202_1218"),
- ]
+ dependencies = [("gsr_booking", "0008_auto_20200202_1218")]
operations = [
migrations.AlterField(
model_name="gsrbookingcredentials",
name="expiration_date",
field=models.DateTimeField(null=True, verbose_name="session ID expiration date"),
- ),
+ )
]
diff --git a/backend/gsr_booking/migrations/0009_remove_groupmembership_username_and_more.py b/backend/gsr_booking/migrations/0009_remove_groupmembership_username_and_more.py
index 9a9e70c0..f7f6d857 100644
--- a/backend/gsr_booking/migrations/0009_remove_groupmembership_username_and_more.py
+++ b/backend/gsr_booking/migrations/0009_remove_groupmembership_username_and_more.py
@@ -5,15 +5,10 @@
class Migration(migrations.Migration):
- dependencies = [
- ("gsr_booking", "0008_auto_20211112_1657"),
- ]
+ dependencies = [("gsr_booking", "0008_auto_20211112_1657")]
operations = [
- migrations.RemoveField(
- model_name="groupmembership",
- name="username",
- ),
+ migrations.RemoveField(model_name="groupmembership", name="username"),
migrations.AddField(
model_name="groupmembership",
name="is_wharton",
diff --git a/backend/gsr_booking/migrations/0010_remove_gsrbooking_reminder_sent_and_more.py b/backend/gsr_booking/migrations/0010_remove_gsrbooking_reminder_sent_and_more.py
index 2ef9af6e..96ee1c7f 100644
--- a/backend/gsr_booking/migrations/0010_remove_gsrbooking_reminder_sent_and_more.py
+++ b/backend/gsr_booking/migrations/0010_remove_gsrbooking_reminder_sent_and_more.py
@@ -5,18 +5,11 @@
class Migration(migrations.Migration):
- dependencies = [
- ("gsr_booking", "0009_remove_groupmembership_username_and_more"),
- ]
+ dependencies = [("gsr_booking", "0009_remove_groupmembership_username_and_more")]
operations = [
- migrations.RemoveField(
- model_name="gsrbooking",
- name="reminder_sent",
- ),
+ migrations.RemoveField(model_name="gsrbooking", name="reminder_sent"),
migrations.AddField(
- model_name="reservation",
- name="reminder_sent",
- field=models.BooleanField(default=False),
+ model_name="reservation", name="reminder_sent", field=models.BooleanField(default=False)
),
]
diff --git a/backend/gsr_booking/migrations/0011_alter_reservation_group.py b/backend/gsr_booking/migrations/0011_alter_reservation_group.py
index df639bad..9b7422a7 100644
--- a/backend/gsr_booking/migrations/0011_alter_reservation_group.py
+++ b/backend/gsr_booking/migrations/0011_alter_reservation_group.py
@@ -6,9 +6,7 @@
class Migration(migrations.Migration):
- dependencies = [
- ("gsr_booking", "0010_remove_gsrbooking_reminder_sent_and_more"),
- ]
+ dependencies = [("gsr_booking", "0010_remove_gsrbooking_reminder_sent_and_more")]
operations = [
migrations.AlterField(
@@ -20,5 +18,5 @@ class Migration(migrations.Migration):
on_delete=django.db.models.deletion.CASCADE,
to="gsr_booking.group",
),
- ),
+ )
]
diff --git a/backend/gsr_booking/migrations/0012_gsr_in_use.py b/backend/gsr_booking/migrations/0012_gsr_in_use.py
index d1e3879f..522505b9 100644
--- a/backend/gsr_booking/migrations/0012_gsr_in_use.py
+++ b/backend/gsr_booking/migrations/0012_gsr_in_use.py
@@ -5,14 +5,10 @@
class Migration(migrations.Migration):
- dependencies = [
- ("gsr_booking", "0011_alter_reservation_group"),
- ]
+ dependencies = [("gsr_booking", "0011_alter_reservation_group")]
operations = [
migrations.AddField(
- model_name="gsr",
- name="in_use",
- field=models.BooleanField(default=True),
- ),
+ model_name="gsr", name="in_use", field=models.BooleanField(default=True)
+ )
]
diff --git a/backend/gsr_booking/models.py b/backend/gsr_booking/models.py
index 003832c3..e2ef5b8f 100644
--- a/backend/gsr_booking/models.py
+++ b/backend/gsr_booking/models.py
@@ -1,17 +1,12 @@
-from typing import TYPE_CHECKING, Any
+from typing import Any
from django.contrib.auth import get_user_model
from django.db import models
from django.db.models import QuerySet
from django.utils import timezone
+from utils.types import UserType
-if TYPE_CHECKING:
- from django.contrib.auth.models import AbstractUser
-
- UserType = AbstractUser
-else:
- UserType = Any
User = get_user_model()
@@ -19,54 +14,65 @@
class GroupMembership(models.Model):
# INVARIANT: either user or username should always be set. if user is not None, then the
# username should the be username of the associated user.
- user = models.ForeignKey(
+ user: models.ForeignKey = models.ForeignKey(
User, on_delete=models.CASCADE, related_name="memberships", blank=True, null=True
)
- group = models.ForeignKey("Group", on_delete=models.CASCADE, related_name="memberships")
+ group: models.ForeignKey = models.ForeignKey(
+ "Group", on_delete=models.CASCADE, related_name="memberships"
+ )
+ group_id: int
# When accepted is False, this is a request, otherwise this is an active membership.
- accepted = models.BooleanField(default=False)
+ accepted: models.BooleanField = models.BooleanField(default=False)
ADMIN = "A"
MEMBER = "M"
- type = models.CharField(max_length=10, choices=[(ADMIN, "Admin"), (MEMBER, "Member")])
+ type: models.CharField = models.CharField(
+ max_length=10, choices=[(ADMIN, "Admin"), (MEMBER, "Member")]
+ )
- pennkey_allow = models.BooleanField(default=False)
+ pennkey_allow: models.BooleanField = models.BooleanField(default=False)
- notifications = models.BooleanField(default=True)
+ notifications: models.BooleanField = models.BooleanField(default=True)
- is_wharton = models.BooleanField(blank=True, null=True, default=None)
+ is_wharton: models.BooleanField = models.BooleanField(blank=True, null=True, default=None)
@property
- def is_invite(self):
+ def is_invite(self) -> bool:
return not self.accepted
- def __str__(self):
+ def __str__(self) -> str:
return f"{self.user}<->{self.group}"
- def save(self, *args, **kwargs):
+ def save(self, *args: Any, **kwargs: Any) -> None:
# determines whether user is wharton or not
if self.is_wharton is None:
self.is_wharton = self.check_wharton()
super().save(*args, **kwargs)
- def check_wharton(self):
- return WhartonGSRBooker.is_wharton(self.user)
+ def check_wharton(self) -> bool:
+ if check := WhartonGSRBooker.is_wharton(self.user):
+ return check
+ return False
class Meta:
verbose_name = "Group Membership"
class Group(models.Model):
- owner = models.ForeignKey(User, on_delete=models.CASCADE)
- members = models.ManyToManyField(User, through=GroupMembership, related_name="booking_groups")
+ id: int
+ owner: models.ForeignKey = models.ForeignKey(User, on_delete=models.CASCADE)
+ members: models.ManyToManyField = models.ManyToManyField(
+ User, through=GroupMembership, related_name="booking_groups"
+ )
+ memberships: QuerySet
- name = models.CharField(max_length=255)
- color = models.CharField(max_length=255)
+ name: models.CharField = models.CharField(max_length=255)
+ color: models.CharField = models.CharField(max_length=255)
- created_at = models.DateTimeField(auto_now_add=True)
- updated_at = models.DateTimeField(auto_now=True)
+ created_at: models.DateTimeField = models.DateTimeField(auto_now_add=True)
+ updated_at: models.DateTimeField = models.DateTimeField(auto_now=True)
ADMIN = "A"
MEMBER = "M"
@@ -74,15 +80,15 @@ class Group(models.Model):
def __str__(self) -> str:
return f"{self.name}-{self.pk}"
- def has_member(self, user: "UserType") -> bool:
+ def has_member(self, user: UserType) -> bool:
memberships = GroupMembership.objects.filter(group=self, user=user)
return memberships.all().exists()
- def has_admin(self, user: "UserType") -> bool:
+ def has_admin(self, user: UserType) -> bool:
memberships = GroupMembership.objects.filter(group=self, accepted=True)
return memberships.all().filter(type="A").filter(user=user).exists()
- def get_pennkey_active_members(self):
+ def get_pennkey_active_members(self) -> list[UserType]:
memberships = GroupMembership.objects.filter(group=self, accepted=True)
pennkey_active_members_list = memberships.all().filter(pennkey_allow=True).all()
return [member for member in pennkey_active_members_list]
@@ -95,7 +101,7 @@ def save(self, *args: Any, **kwargs: Any) -> None:
class GSRManager(models.Manager):
- def get_queryset(self) -> QuerySet[Any]:
+ def get_queryset(self) -> QuerySet:
return super().get_queryset().filter(in_use=True)
@@ -105,41 +111,51 @@ class GSR(models.Model):
KIND_LIBCAL = "LIBCAL"
KIND_OPTIONS = ((KIND_WHARTON, "Wharton"), (KIND_LIBCAL, "Libcal"))
- kind = models.CharField(max_length=7, choices=KIND_OPTIONS, default=KIND_LIBCAL)
- lid = models.CharField(max_length=255)
- gid = models.IntegerField(null=True)
- name = models.CharField(max_length=255)
- image_url = models.URLField()
+ kind: models.CharField = models.CharField(
+ max_length=7, choices=KIND_OPTIONS, default=KIND_LIBCAL
+ )
+ id: int
+ lid: models.CharField = models.CharField(max_length=255)
+ gid: models.IntegerField = models.IntegerField(null=True)
+ name: models.CharField = models.CharField(max_length=255)
+ image_url: models.URLField = models.URLField()
- in_use = models.BooleanField(default=True)
+ in_use: models.BooleanField = models.BooleanField(default=True)
objects = GSRManager()
- all_objects = models.Manager() # for admin page
+ all_objects: models.Manager = models.Manager() # for admin page
def __str__(self) -> str:
return f"{self.name}: {self.lid}-{self.gid}"
class Reservation(models.Model):
- start = models.DateTimeField(default=timezone.now)
- end = models.DateTimeField(default=timezone.now)
- creator = models.ForeignKey(User, on_delete=models.CASCADE)
- group = models.ForeignKey(Group, on_delete=models.CASCADE, null=True, blank=True)
- is_cancelled = models.BooleanField(default=False)
- reminder_sent = models.BooleanField(default=False)
+ id: int
+ start: models.DateTimeField = models.DateTimeField(default=timezone.now)
+ end: models.DateTimeField = models.DateTimeField(default=timezone.now)
+ creator: models.ForeignKey = models.ForeignKey(User, on_delete=models.CASCADE)
+ group: models.ForeignKey = models.ForeignKey(
+ Group, on_delete=models.CASCADE, null=True, blank=True
+ )
+ is_cancelled: models.BooleanField = models.BooleanField(default=False)
+ reminder_sent: models.BooleanField = models.BooleanField(default=False)
+
+ gsrbooking_set: QuerySet
class GSRBooking(models.Model):
# TODO: change to non-null after reservations are created for current bookings
- reservation = models.ForeignKey(Reservation, on_delete=models.CASCADE, null=True)
- user = models.ForeignKey(User, on_delete=models.CASCADE, null=True)
- booking_id = models.CharField(max_length=255, null=True, blank=True)
- gsr = models.ForeignKey(GSR, on_delete=models.CASCADE)
- room_id = models.IntegerField()
- room_name = models.CharField(max_length=255)
- start = models.DateTimeField(default=timezone.now)
- end = models.DateTimeField(default=timezone.now)
- is_cancelled = models.BooleanField(default=False)
+ reservation: models.ForeignKey = models.ForeignKey(
+ Reservation, on_delete=models.CASCADE, null=True
+ )
+ user: models.ForeignKey = models.ForeignKey(User, on_delete=models.CASCADE, null=True)
+ booking_id: models.CharField = models.CharField(max_length=255, null=True, blank=True)
+ gsr: models.ForeignKey = models.ForeignKey(GSR, on_delete=models.CASCADE)
+ room_id: models.IntegerField = models.IntegerField()
+ room_name: models.CharField = models.CharField(max_length=255)
+ start: models.DateTimeField = models.DateTimeField(default=timezone.now)
+ end: models.DateTimeField = models.DateTimeField(default=timezone.now)
+ is_cancelled: models.BooleanField = models.BooleanField(default=False)
def __str__(self) -> str:
return f"{self.user} - {self.gsr.name} - {self.start} - {self.end}"
diff --git a/backend/gsr_booking/serializers.py b/backend/gsr_booking/serializers.py
index da1139a7..206ab816 100644
--- a/backend/gsr_booking/serializers.py
+++ b/backend/gsr_booking/serializers.py
@@ -1,4 +1,4 @@
-from typing import TYPE_CHECKING, Any, TypeAlias
+from typing import Any, TypeAlias
from django.contrib.auth import get_user_model
from rest_framework import serializers
@@ -6,13 +6,6 @@
from gsr_booking.models import GSR, Group, GroupMembership, GSRBooking
-if TYPE_CHECKING:
- from django.contrib.auth.models import AbstractUser
-
- UserType = AbstractUser
-else:
- UserType = Any
-
ValidatedData: TypeAlias = dict[str, Any]
User = get_user_model()
@@ -30,32 +23,21 @@ def get_is_wharton(self, obj: ValidatedData) -> bool:
return obj["lid"] == 1
-class MiniUserSerializer(serializers.ModelSerializer):
- class Meta:
- model = User
- fields = ["username", "first_name", "last_name"]
-
-
class GroupMembershipSerializer(serializers.ModelSerializer):
- user = MiniUserSerializer(read_only=True)
- group = serializers.SlugRelatedField(slug_field="name", queryset=Group.objects.all())
- color = serializers.SlugRelatedField(slug_field="color", read_only=True, source="group")
+ group: serializers.SlugRelatedField = serializers.SlugRelatedField(
+ slug_field="name", queryset=Group.objects.all()
+ )
+ color: serializers.SlugRelatedField = serializers.SlugRelatedField(
+ slug_field="color", read_only=True, source="group"
+ )
class Meta:
model = GroupMembership
- fields = [
- "user",
- "group",
- "type",
- "pennkey_allow",
- "notifications",
- "id",
- "color",
- ]
+ fields = ["group", "type", "pennkey_allow", "notifications", "id", "color"]
class GroupSerializer(serializers.ModelSerializer):
- owner = serializers.SlugRelatedField(
+ owner: serializers.SlugRelatedField = serializers.SlugRelatedField(
slug_field="username", queryset=User.objects.all(), required=False
)
memberships = GroupMembershipSerializer(many=True, read_only=True)
@@ -83,29 +65,6 @@ def to_internal_value(self, data: ValidatedData) -> None:
return None # TODO: If you want to update based on BookingField, implement this.
-class UserSerializer(serializers.ModelSerializer):
- booking_groups = serializers.SerializerMethodField()
-
- def get_booking_groups(self, obj: "UserType") -> list[dict[str, Any]]:
- result = []
- for membership in GroupMembership.objects.filter(accepted=True, user=obj):
- result.append(
- {
- "name": membership.group.name,
- "id": membership.group.id,
- "color": membership.group.color,
- "pennkey_allow": membership.pennkey_allow,
- "notifications": membership.notifications,
- }
- )
-
- return result
-
- class Meta:
- model = User
- fields = ["username", "booking_groups"]
-
-
class GSRSerializer(serializers.ModelSerializer):
class Meta:
model = GSR
diff --git a/backend/gsr_booking/urls.py b/backend/gsr_booking/urls.py
index 7f6e2b01..a54a1e12 100644
--- a/backend/gsr_booking/urls.py
+++ b/backend/gsr_booking/urls.py
@@ -10,16 +10,16 @@
GroupMembershipViewSet,
GroupViewSet,
Locations,
+ MyMembershipViewSet,
RecentGSRs,
ReservationsView,
- UserViewSet,
)
from utils.cache import Cache
router = routers.DefaultRouter()
-router.register(r"users", UserViewSet)
+router.register(r"mymemberships", MyMembershipViewSet, "mymemberships")
router.register(r"membership", GroupMembershipViewSet)
router.register(r"groups", GroupViewSet)
diff --git a/backend/gsr_booking/views.py b/backend/gsr_booking/views.py
index bf82d6c8..f5f2df5b 100644
--- a/backend/gsr_booking/views.py
+++ b/backend/gsr_booking/views.py
@@ -1,6 +1,5 @@
-from typing import TYPE_CHECKING, Any, Optional, cast
+from typing import Optional
-from django.contrib.auth import get_user_model
from django.db.models import Manager, Prefetch, Q, QuerySet
from django.http import HttpResponseForbidden
from django.shortcuts import get_object_or_404
@@ -14,74 +13,32 @@
from gsr_booking.api_wrapper import GSRBooker, WhartonGSRBooker
from gsr_booking.models import GSR, Group, GroupMembership, GSRBooking
-from gsr_booking.serializers import (
- GroupMembershipSerializer,
- GroupSerializer,
- GSRSerializer,
- UserSerializer,
-)
+from gsr_booking.serializers import GroupMembershipSerializer, GroupSerializer, GSRSerializer
from pennmobile.analytics import Metric, record_analytics
from utils.errors import APIError
+from utils.types import get_user
-if TYPE_CHECKING:
- from django.contrib.auth.models import AbstractUser
-
- UserType = AbstractUser
-else:
- UserType = Any
-
-User = get_user_model()
-
-
-# TODO: user model doesn't have a `booking_groups` attribute, so placing Any type for now
-class UserViewSet(viewsets.ReadOnlyModelViewSet["UserType"]):
- """
- Can specify `me` instead of the `username` to retrieve details on the current user.
- """
-
- queryset = User.objects.all().prefetch_related(
- Prefetch("booking_groups", Group.objects.filter(memberships__accepted=True))
- )
+class MyMembershipViewSet(viewsets.ReadOnlyModelViewSet):
permission_classes = [IsAuthenticated]
- serializer_class = UserSerializer
- lookup_field = "username"
- filter_backends = [DjangoFilterBackend]
- filterset_fields = ["username", "first_name", "last_name"]
-
- def get_object(self) -> "UserType":
- lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field
- param = self.kwargs[lookup_url_kwarg]
- if param == "me":
- return cast("UserType", self.request.user)
- return super().get_object()
-
- def get_queryset(self) -> QuerySet["UserType", Manager["UserType"]]:
- if not self.request.user.is_authenticated:
- return User.objects.none()
-
- user: Any = self.request.user
- queryset = User.objects.all()
- queryset = queryset.prefetch_related(
- Prefetch(
- "memberships",
- GroupMembership.objects.filter(group__in=user.booking_groups.all(), accepted=True),
- )
- )
- return queryset
+ serializer_class = GroupMembershipSerializer
+
+ def get_queryset(self) -> QuerySet[GroupMembership, Manager[GroupMembership]]:
+ return GroupMembership.objects.filter(user=self.request.user, accepted=True)
- @action(detail=True, methods=["get"])
- def invites(self, request: Request, username: Optional[str] = None) -> Response:
+ @action(detail=False, methods=["get"])
+ def invites(self, request: Request) -> Response:
"""
Retrieve all invites for a given user.
"""
- user = get_object_or_404(User, username=username)
- request_user: Any = self.request.user
+ request_user = get_user(self.request)
return Response(
GroupMembershipSerializer(
GroupMembership.objects.filter(
- user=user, accepted=False, group__in=request_user.booking_groups.all()
+ user=get_user(request),
+ accepted=False,
+ group__in=request_user.booking_groups.all(),
),
many=True,
).data
@@ -96,7 +53,7 @@ class GroupMembershipViewSet(viewsets.ModelViewSet[GroupMembership]):
serializer_class = GroupMembershipSerializer
def get_queryset(self) -> QuerySet[GroupMembership, Manager[GroupMembership]]:
- user: Any = self.request.user
+ user = get_user(self.request)
if not user.is_authenticated:
return GroupMembership.objects.none()
@@ -118,7 +75,7 @@ def invite(self, request: Request) -> Response | HttpResponseForbidden:
group = get_object_or_404(Group, pk=group_id)
# don't invite when user already in group
- if group.has_member(cast(Any, request.user)):
+ if group.has_member(get_user(request)):
return HttpResponseForbidden()
return Response({"message": "invite(s) sent."})
@@ -128,7 +85,7 @@ def accept(
self, request: Request, pk: Optional[int] = None
) -> Response | HttpResponseForbidden:
membership = get_object_or_404(GroupMembership, pk=pk, accepted=False)
- user = cast("UserType", request.user)
+ user = get_user(request)
if membership.user is None or membership.user != user:
return HttpResponseForbidden()
@@ -150,7 +107,7 @@ def decline(
self, request: Request, pk: Optional[int] = None
) -> Response | HttpResponseForbidden:
membership = get_object_or_404(GroupMembership, pk=pk, accepted=False)
- if membership.user is None or membership.user != cast("UserType", request.user):
+ if membership.user is None or membership.user != get_user(request):
return HttpResponseForbidden()
if not membership.is_invite:
return Response({"message": "cannot decline an invite that has been accepted."}, 400)
@@ -170,7 +127,7 @@ class GroupViewSet(viewsets.ModelViewSet[Group]):
permission_classes = [IsAuthenticated]
def get_queryset(self) -> QuerySet[Group, Manager[Group]]:
- user = cast("UserType", self.request.user)
+ user = get_user(self.request)
if not user.is_authenticated:
return Group.objects.none()
return (
@@ -197,9 +154,8 @@ class RecentGSRs(generics.ListAPIView[GSR]):
permission_classes = [IsAuthenticated]
def get_queryset(self) -> QuerySet[GSR, Manager[GSR]]:
- user = cast("UserType", self.request.user)
return GSR.objects.filter(
- id__in=GSRBooking.objects.filter(user=user, is_cancelled=False)
+ id__in=GSRBooking.objects.filter(user=get_user(self.request), is_cancelled=False)
.distinct()
.order_by("-end")[:2]
.values_list("gsr", flat=True)
@@ -211,7 +167,7 @@ class CheckWharton(APIView):
permission_classes = [IsAuthenticated]
def get(self, request: Request) -> Response:
- user: Any = request.user
+ user = get_user(request)
return Response(
{
"is_wharton": user.booking_groups.filter(name="Penn Labs").exists()
@@ -231,13 +187,13 @@ class Availability(APIView):
permission_classes = [IsAuthenticated]
- def get(self, request: Request, lid: int, gid: int) -> Response:
+ def get(self, request: Request, lid: int, gid: str) -> Response:
start = request.GET.get("start", None)
end = request.GET.get("end", None)
try:
- user: Any = request.user
+ user = get_user(request)
return Response(
GSRBooker.get_availability(
lid,
@@ -263,7 +219,7 @@ def post(self, request: Request) -> Response:
gid = request.data["gid"]
room_id = request.data["id"]
room_name = request.data["room_name"]
- user: Any = request.user
+ user = get_user(request)
try:
GSRBooker.book_room(
@@ -292,7 +248,7 @@ class CancelRoom(APIView):
def post(self, request: Request) -> Response:
booking_id = request.data["booking_id"]
- user: Any = request.user
+ user = get_user(request)
try:
GSRBooker.cancel_room(booking_id, user)
return Response({"detail": "success"})
@@ -308,7 +264,7 @@ class ReservationsView(APIView):
permission_classes = [IsAuthenticated]
def get(self, request: Request) -> Response:
- user: Any = request.user
+ user = get_user(request)
return Response(
GSRBooker.get_reservations(user, user.booking_groups.filter(name="Penn Labs").first())
)
diff --git a/backend/laundry/api_wrapper.py b/backend/laundry/api_wrapper.py
index 938e9a18..16321d9f 100644
--- a/backend/laundry/api_wrapper.py
+++ b/backend/laundry/api_wrapper.py
@@ -48,7 +48,7 @@ def parse_a_hall(hall_link: str) -> dict[str, Any]:
washers = {"open": 0, "running": 0, "out_of_order": 0, "offline": 0, "time_remaining": []}
dryers = {"open": 0, "running": 0, "out_of_order": 0, "offline": 0, "time_remaining": []}
- detailed = []
+ detailed: list[dict[str, Any]] = []
try:
page = requests.get(
diff --git a/backend/laundry/migrations/0002_auto_20210321_1105.py b/backend/laundry/migrations/0002_auto_20210321_1105.py
index eed312d7..9497a9d9 100644
--- a/backend/laundry/migrations/0002_auto_20210321_1105.py
+++ b/backend/laundry/migrations/0002_auto_20210321_1105.py
@@ -6,14 +6,12 @@
class Migration(migrations.Migration):
- dependencies = [
- ("laundry", "0001_initial"),
- ]
+ dependencies = [("laundry", "0001_initial")]
operations = [
migrations.AlterField(
model_name="laundrysnapshot",
name="date",
field=models.DateTimeField(default=django.utils.timezone.now),
- ),
+ )
]
diff --git a/backend/laundry/models.py b/backend/laundry/models.py
index 4af9bd2a..148936e3 100644
--- a/backend/laundry/models.py
+++ b/backend/laundry/models.py
@@ -5,25 +5,25 @@
class LaundryRoom(models.Model):
- hall_id = models.IntegerField(default=0)
- name = models.CharField(max_length=255)
- location = models.CharField(max_length=255)
- total_washers = models.IntegerField(default=0)
- total_dryers = models.IntegerField(default=0)
+ hall_id: models.IntegerField = models.IntegerField(default=0)
+ name: models.CharField = models.CharField(max_length=255)
+ location: models.CharField = models.CharField(max_length=255)
+ total_washers: models.IntegerField = models.IntegerField(default=0)
+ total_dryers: models.IntegerField = models.IntegerField(default=0)
# Each Laundry Room has a UUID that we need to
# access Penn API laundry data
- uuid = models.UUIDField(default=uuid.uuid4)
+ uuid: models.UUIDField = models.UUIDField(default=uuid.uuid4)
- def __str__(self):
+ def __str__(self) -> str:
return f"Hall No. {self.hall_id} | {self.name}"
class LaundrySnapshot(models.Model):
- room = models.ForeignKey(LaundryRoom, on_delete=models.CASCADE, null=True)
- date = models.DateTimeField(default=timezone.now)
- available_washers = models.IntegerField()
- available_dryers = models.IntegerField()
+ room: models.ForeignKey = models.ForeignKey(LaundryRoom, on_delete=models.CASCADE, null=True)
+ date: models.DateTimeField = models.DateTimeField(default=timezone.now)
+ available_washers: models.IntegerField = models.IntegerField()
+ available_dryers: models.IntegerField = models.IntegerField()
- def __str__(self):
- return f"Hall No. {self.room.hall_id} | {self.date.date()}"
+ def __str__(self) -> str:
+ return f"Hall No. {self.room.hall_id} | {self.date.date()}" # ignore: type[attr-defined]
diff --git a/backend/laundry/views.py b/backend/laundry/views.py
index 85d8698c..eccf04a1 100644
--- a/backend/laundry/views.py
+++ b/backend/laundry/views.py
@@ -1,8 +1,9 @@
import calendar
import datetime
+from typing import Any, Optional, cast
from django.core.cache import cache
-from django.db.models import Q, QuerySet
+from django.db.models import Manager, Q, QuerySet
from django.shortcuts import get_object_or_404
from django.utils import timezone
from requests.exceptions import HTTPError
@@ -16,6 +17,7 @@
from laundry.serializers import LaundryRoomSerializer
from pennmobile.analytics import Metric, record_analytics
from utils.cache import Cache
+from utils.types import get_user
class Ids(APIView):
@@ -46,7 +48,7 @@ class MultipleHallInfo(APIView):
def get(self, request: Request, hall_ids: str) -> Response:
halls = [int(x) for x in hall_ids.split(",")]
- output = {"rooms": []}
+ output: dict[str, Any] = {"rooms": []}
for hall_id in halls:
hall_data = hall_status(get_object_or_404(LaundryRoom, hall_id=hall_id))
@@ -54,7 +56,7 @@ def get(self, request: Request, hall_ids: str) -> Response:
hall_data["usage_data"] = HallUsage.compute_usage(hall_id)
output["rooms"].append(hall_data)
- record_analytics(Metric.LAUNDRY_VIEWED, request.user.username)
+ record_analytics(Metric.LAUNDRY_VIEWED, get_user(request).username)
return Response(output)
@@ -64,10 +66,16 @@ class HallUsage(APIView):
GET: returns usage data for dryers and washers of a particular hall
"""
- def safe_division(a: int | None, b: int | None) -> float | None:
- return round(a / float(b), 3) if b > 0 else 0
+ @staticmethod
+ def safe_division(a: Optional[int] = None, b: Optional[int] = None) -> float | None:
+ if a is None or b is None or b <= 0:
+ return 0.0
+ return round(a / float(b), 3)
- def get_snapshot_info(hall_id: int) -> tuple[LaundryRoom, QuerySet[LaundrySnapshot]]:
+ @staticmethod
+ def get_snapshot_info(
+ hall_id: int,
+ ) -> tuple[LaundryRoom, QuerySet[LaundrySnapshot, Manager[LaundrySnapshot]]]:
# filters for LaundrySnapshots within timeframe
room = get_object_or_404(LaundryRoom, hall_id=hall_id)
@@ -84,7 +92,8 @@ def get_snapshot_info(hall_id: int) -> tuple[LaundryRoom, QuerySet[LaundrySnapsh
snapshots = LaundrySnapshot.objects.filter(filter).order_by("-date")
return (room, snapshots)
- def compute_usage(hall_id: int) -> Response:
+ @staticmethod
+ def compute_usage(hall_id: int) -> dict[str, Any] | Response:
try:
(room, snapshots) = HallUsage.get_snapshot_info(hall_id)
except ValueError:
@@ -97,7 +106,8 @@ def compute_usage(hall_id: int) -> Response:
min_date = timezone.localtime()
max_date = timezone.localtime() - datetime.timedelta(days=30)
- for snapshot in snapshots:
+ for snapshot_obj in snapshots.iterator():
+ snapshot = cast(LaundrySnapshot, snapshot_obj)
date = snapshot.date.astimezone()
min_date = min(min_date, date)
max_date = max(max_date, date)
@@ -152,18 +162,20 @@ class Preferences(APIView):
key = "laundry_preferences:{user_id}"
def get(self, request: Request) -> Response:
- key = self.key.format(user_id=request.user.id)
+ user = get_user(request)
+ key = self.key.format(user_id=user.id)
cached_preferences = cache.get(key)
if cached_preferences is None:
- preferences = request.user.profile.laundry_preferences.all()
+ preferences = user.profile.laundry_preferences.all()
cached_preferences = preferences.values_list("hall_id", flat=True)
cache.set(key, cached_preferences, Cache.MONTH)
return Response({"rooms": cached_preferences})
def post(self, request: Request) -> Response:
- key = self.key.format(user_id=request.user.id)
- profile = request.user.profile
+ user = get_user(request)
+ key = self.key.format(user_id=user.id)
+ profile = user.profile
preferences = profile.laundry_preferences
if "rooms" not in request.data:
return Response({"success": False, "error": "No rooms provided"}, status=400)
diff --git a/backend/manage.py b/backend/manage.py
index 4d8cecf0..8b4e3280 100755
--- a/backend/manage.py
+++ b/backend/manage.py
@@ -4,7 +4,7 @@
import sys
-def main():
+def main() -> None:
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "pennmobile.settings.development")
try:
from django.core.management import execute_from_command_line
diff --git a/backend/penndata/admin.py b/backend/penndata/admin.py
index 2ba02fc3..4219fcf1 100644
--- a/backend/penndata/admin.py
+++ b/backend/penndata/admin.py
@@ -1,6 +1,6 @@
from django.contrib import admin
-from django.utils.html import escape, mark_safe
-from django.utils.safestring import SafeText
+from django.utils.html import escape
+from django.utils.safestring import SafeText, mark_safe
from penndata.models import (
AnalyticsEvent,
@@ -16,7 +16,7 @@ class FitnessRoomAdmin(admin.ModelAdmin):
def image_tag(self, instance: FitnessRoom) -> SafeText:
return mark_safe('
' % escape(instance.image_url))
- image_tag.short_description = "Fitness Room Image"
+ image_tag.short_description = "Fitness Room Image" # type: ignore[attr-defined]
readonly_fields = ("image_tag",)
diff --git a/backend/penndata/management/commands/get_college_house_events.py b/backend/penndata/management/commands/get_college_house_events.py
index 7773f91b..b0c2aaba 100644
--- a/backend/penndata/management/commands/get_college_house_events.py
+++ b/backend/penndata/management/commands/get_college_house_events.py
@@ -51,7 +51,7 @@ def scrape_details(self, event_url: str) -> tuple[
resp = requests.get(event_url)
except ConnectionError:
print("Error:", ConnectionError)
- return None
+ return None, None, None, None, None
soup = BeautifulSoup(resp.text, "html.parser")
location = (
diff --git a/backend/penndata/management/commands/get_fitness_snapshot.py b/backend/penndata/management/commands/get_fitness_snapshot.py
index 09036cc0..a684c8cd 100644
--- a/backend/penndata/management/commands/get_fitness_snapshot.py
+++ b/backend/penndata/management/commands/get_fitness_snapshot.py
@@ -1,5 +1,5 @@
import datetime
-from typing import Any
+from typing import Any, Optional
import requests
from bs4 import BeautifulSoup
@@ -14,7 +14,7 @@ def cap_string(s: str) -> str:
return " ".join([word[0].upper() + word[1:] for word in s.split()])
-def get_usages() -> tuple[dict[str, dict[str, int | float]], datetime.datetime]:
+def get_usages() -> tuple[Optional[dict[str, dict[str, int | float]]], datetime.datetime]:
# count/capacities default to 0 since spreadsheet number appears blank if no one there
locations = [
@@ -28,7 +28,9 @@ def get_usages() -> tuple[dict[str, dict[str, int | float]], datetime.datetime]:
"Pool-Shallow",
"Pool-Deep",
]
- usages = {location: {"count": 0, "capacity": 0} for location in locations}
+ usages: dict[str, dict[str, int | float]] = {
+ location: {"count": 0, "capacity": 0} for location in locations
+ }
date = timezone.localtime() # default if can't get date from spreadsheet
@@ -42,12 +44,12 @@ def get_usages() -> tuple[dict[str, dict[str, int | float]], datetime.datetime]:
)
)
except ConnectionError:
- return None
+ return None, date
html = resp.content.decode("utf8")
soup = BeautifulSoup(html, "html5lib")
if not (embedded_spreadsheet := soup.find("tbody")):
- return None
+ return None, date
table_rows = embedded_spreadsheet.findChildren("tr")
for i, row in enumerate(table_rows):
@@ -77,6 +79,10 @@ def handle(self, *args: Any, **kwargs: Any) -> None:
self.stdout.write("FitnessSnapshots already exist for this date!")
return
+ if not usage_by_location:
+ self.stdout.write("Failed to get usages from spreadsheet!")
+ return
+
FitnessSnapshot.objects.bulk_create(
[
FitnessSnapshot(
diff --git a/backend/penndata/management/commands/get_penn_today_events.py b/backend/penndata/management/commands/get_penn_today_events.py
index 8076bd1d..3f88259f 100644
--- a/backend/penndata/management/commands/get_penn_today_events.py
+++ b/backend/penndata/management/commands/get_penn_today_events.py
@@ -2,7 +2,7 @@
from typing import Any, Optional
from urllib.parse import urljoin
-from bs4 import BeautifulSoup
+from bs4 import BeautifulSoup, Tag
from django.core.management.base import BaseCommand
from django.utils import timezone
from selenium import webdriver
@@ -55,60 +55,24 @@ def handle(self, *args: Any, **kwargs: Any) -> None:
"p", class_="tease__meta--sm", string=lambda x: "Through" in str(x)
)
- if start_date_str == "02/29" or start_date_str == "2/29":
- # If it's February 29th
- start_date = datetime.datetime.strptime("02/28", "%m/%d").replace(year=current_year)
- if start_date.month < current_month:
- # If scraped month is before current month, increment year
- start_date = start_date.replace(year=current_year + 1)
- start_date = start_date + datetime.timedelta(days=1)
- else:
- start_date = datetime.datetime.strptime(start_date_str, "%m/%d").replace(
- year=current_year
- )
- if start_date.month < current_month:
- # If scraped month is before current month, increment year
- start_date = start_date.replace(year=current_year + 1)
- print(start_date_str)
- if ALL_DAY in start_time_str.lower():
- start_time = datetime.time(0, 0)
- else:
- start_time = datetime.datetime.strptime(start_time_str, "%I:%M%p").time()
- start_date = datetime.datetime.combine(start_date, start_time)
+ start_date = self._parse_start_date(start_date_str, current_month, current_year)
+ start_time = self._parse_start_time(start_time_str)
+ start_datetime = datetime.datetime.combine(start_date, start_time)
- if start_date > now + datetime.timedelta(days=31):
+ if start_datetime > now + datetime.timedelta(days=31):
continue
event_url = urljoin(PENN_TODAY_WEBSITE, article.find("a", class_="tease__link")["href"])
-
- end_time = self.get_end_time(event_url)
- if end_time is not None:
- if end_date_elem: # end date and end time
- end_date_str = end_date_elem.text.strip().split(" ")[-1]
- end_date = datetime.datetime.strptime(end_date_str, "%m/%d/%Y")
- end_time = datetime.datetime.strptime(end_time, "%I:%M %p").time()
- end_date = datetime.datetime.combine(end_date, end_time)
- else: # no end date but end time
- end_time = datetime.datetime.strptime(end_time, "%I:%M %p").time()
- end_date = datetime.datetime.combine(start_date, end_time)
- else:
- end_of_day = datetime.time(23, 59, 59)
- if end_date_elem: # end date but no end time
- end_date_str = end_date_elem.text.strip().split(" ")[-1]
- end_date = datetime.datetime.combine(
- datetime.datetime.strptime(end_date_str, "%m/%d/%Y"), end_of_day
- )
-
- else: # no end date or end time
- end_date = datetime.datetime.combine(start_date, end_of_day)
+ end_time_str = self.get_end_time(event_url)
+ end_datetime = self._calculate_end_datetime(end_time_str, end_date_elem, start_datetime)
Event.objects.update_or_create(
name=name,
defaults={
"event_type": Event.TYPE_PENN_TODAY,
"image_url": None,
- "start": timezone.make_aware(start_date),
- "end": timezone.make_aware(end_date),
+ "start": timezone.make_aware(start_datetime),
+ "end": timezone.make_aware(end_datetime),
"location": location,
"website": event_url,
"description": description,
@@ -118,7 +82,7 @@ def handle(self, *args: Any, **kwargs: Any) -> None:
self.stdout.write("Uploaded Penn Today Events!")
- def connect_and_parse_html(self, event_url: str, condition: EC) -> Optional[str]:
+ def connect_and_parse_html(self, event_url: str, condition: EC) -> Optional[BeautifulSoup]:
try:
options = Options()
options.add_argument("--headless")
@@ -143,9 +107,14 @@ def get_end_time(self, event_url: str) -> Optional[str]:
event_url, EC.presence_of_element_located((By.CLASS_NAME, "event__topper-content"))
)
- end_time_range_str = (
- end_time_soup.find("p", class_="event__meta event__time").text.strip().replace(".", "")
- )
+ if not end_time_soup:
+ return None
+
+ time_element = end_time_soup.find("p", class_="event__meta event__time")
+ if not time_element:
+ return None
+
+ end_time_range_str = time_element.text.strip().replace(".", "")
if (
not end_time_range_str
@@ -155,3 +124,45 @@ def get_end_time(self, event_url: str) -> Optional[str]:
return None # No end time if the event is all day
return times[1]
+
+ def _parse_start_date(
+ self, date_str: str, current_month: int, current_year: int
+ ) -> datetime.date:
+ if date_str in ("02/29", "2/29"):
+ start_date = datetime.datetime.strptime("02/28", "%m/%d").replace(year=current_year)
+ if start_date.month < current_month:
+ start_date = start_date.replace(year=current_year + 1)
+ return (start_date + datetime.timedelta(days=1)).date()
+
+ start_date = datetime.datetime.strptime(date_str, "%m/%d").replace(year=current_year)
+ if start_date.month < current_month:
+ start_date = start_date.replace(year=current_year + 1)
+ return start_date.date()
+
+ def _parse_start_time(self, time_str: str) -> datetime.time:
+ if ALL_DAY in time_str.lower():
+ return datetime.time(0, 0)
+ return datetime.datetime.strptime(time_str, "%I:%M%p").time()
+
+ def _calculate_end_datetime(
+ self,
+ end_time_str: Optional[str],
+ end_date_elem: Optional[Tag],
+ start_datetime: datetime.datetime,
+ ) -> datetime.datetime:
+ end_of_day = datetime.time(23, 59, 59)
+
+ if end_time_str:
+ end_time = datetime.datetime.strptime(end_time_str, "%I:%M %p").time()
+ if end_date_elem:
+ end_date_str = end_date_elem.text.strip().split(" ")[-1]
+ end_date = datetime.datetime.strptime(end_date_str, "%m/%d/%Y").date()
+ return datetime.datetime.combine(end_date, end_time)
+ return datetime.datetime.combine(start_datetime.date(), end_time)
+
+ if end_date_elem:
+ end_date_str = end_date_elem.text.strip().split(" ")[-1]
+ end_date = datetime.datetime.strptime(end_date_str, "%m/%d/%Y").date()
+ return datetime.datetime.combine(end_date, end_of_day)
+
+ return datetime.datetime.combine(start_datetime.date(), end_of_day)
diff --git a/backend/penndata/management/commands/get_venture_events.py b/backend/penndata/management/commands/get_venture_events.py
index 78cdf00d..ef9e852a 100644
--- a/backend/penndata/management/commands/get_venture_events.py
+++ b/backend/penndata/management/commands/get_venture_events.py
@@ -77,7 +77,7 @@ def handle(self, *args: Any, **kwargs: Any) -> None:
)
# events are ordered from future to past, so break once we find a past event
- if event_start_datetime < now.replace(tzinfo=None):
+ if event_start_datetime and event_start_datetime < now.replace(tzinfo=None):
break
if title := event.find("div", class_="PromoSearchResultEvent-title"):
diff --git a/backend/penndata/management/commands/get_wharton_events.py b/backend/penndata/management/commands/get_wharton_events.py
index 360cbb5e..81a46f0d 100644
--- a/backend/penndata/management/commands/get_wharton_events.py
+++ b/backend/penndata/management/commands/get_wharton_events.py
@@ -1,6 +1,6 @@
import datetime
import re
-from typing import Any
+from typing import Any, Optional
import pytz
import requests
@@ -36,8 +36,12 @@ def handle(self, *args: Any, **kwargs: Any) -> None:
match = re.match(r"(\w+\s+\d+) \| (\d{1,2}:\d{2} [AP]M) - (\d{1,2}:\d{2} [AP]M)", info)
if match:
_, start_time, end_time = match.groups()
- start_time_obj = datetime.datetime.strptime(start_time, "%I:%M %p")
- end_time_obj = datetime.datetime.strptime(end_time, "%I:%M %p")
+ start_time_obj: Optional[datetime.datetime] = datetime.datetime.strptime(
+ start_time, "%I:%M %p"
+ )
+ end_time_obj: Optional[datetime.datetime] = datetime.datetime.strptime(
+ end_time, "%I:%M %p"
+ )
else:
# event has start and end times on different dates
match = re.match(
diff --git a/backend/penndata/migrations/0001_initial.py b/backend/penndata/migrations/0001_initial.py
index 97305072..c2551529 100644
--- a/backend/penndata/migrations/0001_initial.py
+++ b/backend/penndata/migrations/0001_initial.py
@@ -29,5 +29,5 @@ class Migration(migrations.Migration):
("website", models.URLField(max_length=255, null=True)),
("facebook", models.URLField(max_length=255, null=True)),
],
- ),
+ )
]
diff --git a/backend/penndata/migrations/0002_homepageorder.py b/backend/penndata/migrations/0002_homepageorder.py
index 639435e2..83abb355 100644
--- a/backend/penndata/migrations/0002_homepageorder.py
+++ b/backend/penndata/migrations/0002_homepageorder.py
@@ -5,9 +5,7 @@
class Migration(migrations.Migration):
- dependencies = [
- ("penndata", "0001_initial"),
- ]
+ dependencies = [("penndata", "0001_initial")]
operations = [
migrations.CreateModel(
@@ -22,5 +20,5 @@ class Migration(migrations.Migration):
("cell", models.CharField(max_length=255)),
("rank", models.IntegerField()),
],
- ),
+ )
]
diff --git a/backend/penndata/migrations/0003_analyticsevent.py b/backend/penndata/migrations/0003_analyticsevent.py
index 7ba82e52..ec59efa7 100644
--- a/backend/penndata/migrations/0003_analyticsevent.py
+++ b/backend/penndata/migrations/0003_analyticsevent.py
@@ -47,5 +47,5 @@ class Migration(migrations.Migration):
),
),
],
- ),
+ )
]
diff --git a/backend/penndata/migrations/0004_analyticsevent_data.py b/backend/penndata/migrations/0004_analyticsevent_data.py
index dc3aa7cd..04cc6ba5 100644
--- a/backend/penndata/migrations/0004_analyticsevent_data.py
+++ b/backend/penndata/migrations/0004_analyticsevent_data.py
@@ -5,14 +5,12 @@
class Migration(migrations.Migration):
- dependencies = [
- ("penndata", "0003_analyticsevent"),
- ]
+ dependencies = [("penndata", "0003_analyticsevent")]
operations = [
migrations.AddField(
model_name="analyticsevent",
name="data",
field=models.CharField(max_length=255, null=True),
- ),
+ )
]
diff --git a/backend/penndata/migrations/0005_fitnessroom_fitnesssnapshot.py b/backend/penndata/migrations/0005_fitnessroom_fitnesssnapshot.py
index d671cc13..a5f8118f 100644
--- a/backend/penndata/migrations/0005_fitnessroom_fitnesssnapshot.py
+++ b/backend/penndata/migrations/0005_fitnessroom_fitnesssnapshot.py
@@ -7,9 +7,7 @@
class Migration(migrations.Migration):
- dependencies = [
- ("penndata", "0004_analyticsevent_data"),
- ]
+ dependencies = [("penndata", "0004_analyticsevent_data")]
operations = [
migrations.CreateModel(
diff --git a/backend/penndata/migrations/0006_fitnesssnapshot_capacity.py b/backend/penndata/migrations/0006_fitnesssnapshot_capacity.py
index fa144d61..ce1e12f0 100644
--- a/backend/penndata/migrations/0006_fitnesssnapshot_capacity.py
+++ b/backend/penndata/migrations/0006_fitnesssnapshot_capacity.py
@@ -5,14 +5,10 @@
class Migration(migrations.Migration):
- dependencies = [
- ("penndata", "0005_fitnessroom_fitnesssnapshot"),
- ]
+ dependencies = [("penndata", "0005_fitnessroom_fitnesssnapshot")]
operations = [
migrations.AddField(
- model_name="fitnesssnapshot",
- name="capacity",
- field=models.FloatField(null=True),
- ),
+ model_name="fitnesssnapshot", name="capacity", field=models.FloatField(null=True)
+ )
]
diff --git a/backend/penndata/migrations/0007_fitnessroom_image_url.py b/backend/penndata/migrations/0007_fitnessroom_image_url.py
index 44f89470..3874cff3 100644
--- a/backend/penndata/migrations/0007_fitnessroom_image_url.py
+++ b/backend/penndata/migrations/0007_fitnessroom_image_url.py
@@ -5,9 +5,7 @@
class Migration(migrations.Migration):
- dependencies = [
- ("penndata", "0006_fitnesssnapshot_capacity"),
- ]
+ dependencies = [("penndata", "0006_fitnesssnapshot_capacity")]
operations = [
migrations.AddField(
@@ -15,5 +13,5 @@ class Migration(migrations.Migration):
name="image_url",
field=models.URLField(default=""),
preserve_default=False,
- ),
+ )
]
diff --git a/backend/penndata/migrations/0008_calendarevent.py b/backend/penndata/migrations/0008_calendarevent.py
index e067e9bb..0d13f565 100644
--- a/backend/penndata/migrations/0008_calendarevent.py
+++ b/backend/penndata/migrations/0008_calendarevent.py
@@ -5,9 +5,7 @@
class Migration(migrations.Migration):
- dependencies = [
- ("penndata", "0007_fitnessroom_image_url"),
- ]
+ dependencies = [("penndata", "0007_fitnessroom_image_url")]
operations = [
migrations.CreateModel(
@@ -23,5 +21,5 @@ class Migration(migrations.Migration):
("date", models.CharField(blank=True, max_length=50, null=True)),
("date_obj", models.DateTimeField(blank=True, null=True)),
],
- ),
+ )
]
diff --git a/backend/penndata/migrations/0009_auto_20240223_1820.py b/backend/penndata/migrations/0009_auto_20240223_1820.py
index c615f12f..8327da1f 100644
--- a/backend/penndata/migrations/0009_auto_20240223_1820.py
+++ b/backend/penndata/migrations/0009_auto_20240223_1820.py
@@ -5,48 +5,25 @@
class Migration(migrations.Migration):
- dependencies = [
- ("penndata", "0008_calendarevent"),
- ]
+ dependencies = [("penndata", "0008_calendarevent")]
operations = [
- migrations.RenameField(
- model_name="event",
- old_name="start_time",
- new_name="start",
- ),
- migrations.RemoveField(
- model_name="event",
- name="end_time",
- ),
- migrations.AddField(
- model_name="event",
- name="end",
- field=models.DateTimeField(null=True),
- ),
+ migrations.RenameField(model_name="event", old_name="start_time", new_name="start"),
+ migrations.RemoveField(model_name="event", name="end_time"),
+ migrations.AddField(model_name="event", name="end", field=models.DateTimeField(null=True)),
migrations.AddField(
- model_name="event",
- name="location",
- field=models.CharField(max_length=255, null=True),
+ model_name="event", name="location", field=models.CharField(max_length=255, null=True)
),
migrations.AlterField(
- model_name="event",
- name="description",
- field=models.TextField(null=True),
+ model_name="event", name="description", field=models.TextField(null=True)
),
migrations.AlterField(
- model_name="event",
- name="email",
- field=models.CharField(max_length=255, null=True),
+ model_name="event", name="email", field=models.CharField(max_length=255, null=True)
),
migrations.AlterField(
- model_name="event",
- name="event_type",
- field=models.CharField(max_length=255, null=True),
+ model_name="event", name="event_type", field=models.CharField(max_length=255, null=True)
),
migrations.AlterField(
- model_name="event",
- name="image_url",
- field=models.URLField(null=True),
+ model_name="event", name="image_url", field=models.URLField(null=True)
),
]
diff --git a/backend/penndata/migrations/0010_auto_20240228_0150.py b/backend/penndata/migrations/0010_auto_20240228_0150.py
index 9e756298..9cf74601 100644
--- a/backend/penndata/migrations/0010_auto_20240228_0150.py
+++ b/backend/penndata/migrations/0010_auto_20240228_0150.py
@@ -5,19 +5,12 @@
class Migration(migrations.Migration):
- dependencies = [
- ("penndata", "0009_auto_20240223_1820"),
- ]
+ dependencies = [("penndata", "0009_auto_20240223_1820")]
operations = [
- migrations.RemoveField(
- model_name="event",
- name="facebook",
- ),
+ migrations.RemoveField(model_name="event", name="facebook"),
migrations.AlterField(
- model_name="event",
- name="description",
- field=models.TextField(blank=True, null=True),
+ model_name="event", name="description", field=models.TextField(blank=True, null=True)
),
migrations.AlterField(
model_name="event",
@@ -25,9 +18,7 @@ class Migration(migrations.Migration):
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AlterField(
- model_name="event",
- name="end",
- field=models.DateTimeField(blank=True, null=True),
+ model_name="event", name="end", field=models.DateTimeField(blank=True, null=True)
),
migrations.AlterField(
model_name="event",
@@ -35,9 +26,7 @@ class Migration(migrations.Migration):
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AlterField(
- model_name="event",
- name="image_url",
- field=models.URLField(blank=True, null=True),
+ model_name="event", name="image_url", field=models.URLField(blank=True, null=True)
),
migrations.AlterField(
model_name="event",
diff --git a/backend/penndata/migrations/0011_alter_event_event_type_alter_event_start.py b/backend/penndata/migrations/0011_alter_event_event_type_alter_event_start.py
index e9e00be9..105b4335 100644
--- a/backend/penndata/migrations/0011_alter_event_event_type_alter_event_start.py
+++ b/backend/penndata/migrations/0011_alter_event_event_type_alter_event_start.py
@@ -5,9 +5,7 @@
class Migration(migrations.Migration):
- dependencies = [
- ("penndata", "0010_auto_20240228_0150"),
- ]
+ dependencies = [("penndata", "0010_auto_20240228_0150")]
operations = [
migrations.AlterField(
@@ -41,8 +39,6 @@ class Migration(migrations.Migration):
),
),
migrations.AlterField(
- model_name="event",
- name="start",
- field=models.DateTimeField(blank=True, null=True),
+ model_name="event", name="start", field=models.DateTimeField(blank=True, null=True)
),
]
diff --git a/backend/penndata/migrations/0012_alter_event_event_type.py b/backend/penndata/migrations/0012_alter_event_event_type.py
index a211bce8..f0c97461 100644
--- a/backend/penndata/migrations/0012_alter_event_event_type.py
+++ b/backend/penndata/migrations/0012_alter_event_event_type.py
@@ -5,9 +5,7 @@
class Migration(migrations.Migration):
- dependencies = [
- ("penndata", "0011_alter_event_event_type_alter_event_start"),
- ]
+ dependencies = [("penndata", "0011_alter_event_event_type_alter_event_start")]
operations = [
migrations.AlterField(
@@ -40,5 +38,5 @@ class Migration(migrations.Migration):
max_length=63,
null=True,
),
- ),
+ )
]
diff --git a/backend/penndata/models.py b/backend/penndata/models.py
index f1d9122e..9d9b41c3 100644
--- a/backend/penndata/models.py
+++ b/backend/penndata/models.py
@@ -51,65 +51,66 @@ class Event(models.Model):
(TYPE_STOUFFER_COLLEGE_HOUSE, "Stouffer College House"),
)
- event_type = models.CharField(
+ event_type: models.CharField = models.CharField(
max_length=63, choices=TYPE_CHOICES, default=None, null=True, blank=True
)
- name = models.CharField(max_length=255)
- description = models.TextField(null=True, blank=True)
- image_url = models.URLField(null=True, blank=True)
- start = models.DateTimeField(null=True, blank=True)
- end = models.DateTimeField(null=True, blank=True)
- location = models.CharField(max_length=255, null=True, blank=True)
- email = models.CharField(max_length=255, null=True, blank=True)
- website = models.URLField(max_length=255, null=True, blank=True)
+ name: models.CharField = models.CharField(max_length=255)
+ description: models.TextField = models.TextField(null=True, blank=True)
+ image_url: models.URLField = models.URLField(null=True, blank=True)
+ start: models.DateTimeField = models.DateTimeField(null=True, blank=True)
+ end: models.DateTimeField = models.DateTimeField(null=True, blank=True)
+ location: models.CharField = models.CharField(max_length=255, null=True, blank=True)
+ email: models.CharField = models.CharField(max_length=255, null=True, blank=True)
+ website: models.URLField = models.URLField(max_length=255, null=True, blank=True)
class HomePageOrder(models.Model):
- cell = models.CharField(max_length=255)
- rank = models.IntegerField()
+ cell: models.CharField = models.CharField(max_length=255)
+ rank: models.IntegerField = models.IntegerField()
- def __str__(self):
+ def __str__(self) -> str:
return self.cell
class FitnessRoom(models.Model):
- name = models.CharField(max_length=255)
- image_url = models.URLField()
+ id: int
+ name: models.CharField = models.CharField(max_length=255)
+ image_url: models.URLField = models.URLField()
- def __str__(self):
+ def __str__(self) -> str:
return str(self.name)
class FitnessSnapshot(models.Model):
- room = models.ForeignKey(FitnessRoom, on_delete=models.CASCADE, null=True)
- date = models.DateTimeField(default=timezone.now)
- count = models.IntegerField()
- capacity = models.FloatField(null=True)
+ room: models.ForeignKey = models.ForeignKey(FitnessRoom, on_delete=models.CASCADE, null=True)
+ date: models.DateTimeField = models.DateTimeField(default=timezone.now)
+ count: models.IntegerField = models.IntegerField()
+ capacity: models.FloatField = models.FloatField(null=True)
- def __str__(self):
+ def __str__(self) -> str:
return f"Room Name: {self.room.name} | {self.date.date()}"
class AnalyticsEvent(models.Model):
- user = models.ForeignKey(User, on_delete=models.CASCADE)
- created_at = models.DateTimeField(default=timezone.now)
- cell_type = models.CharField(max_length=255)
- index = models.IntegerField(default=0)
- post = models.ForeignKey(Post, on_delete=models.CASCADE, null=True)
- poll = models.ForeignKey(Poll, on_delete=models.CASCADE, null=True)
- is_interaction = models.BooleanField(default=False)
- data = models.CharField(max_length=255, null=True)
-
- def __str__(self):
+ user: models.ForeignKey = models.ForeignKey(User, on_delete=models.CASCADE)
+ created_at: models.DateTimeField = models.DateTimeField(default=timezone.now)
+ cell_type: models.CharField = models.CharField(max_length=255)
+ index: models.IntegerField = models.IntegerField(default=0)
+ post: models.ForeignKey = models.ForeignKey(Post, on_delete=models.CASCADE, null=True)
+ poll: models.ForeignKey = models.ForeignKey(Poll, on_delete=models.CASCADE, null=True)
+ is_interaction: models.BooleanField = models.BooleanField(default=False)
+ data: models.CharField = models.CharField(max_length=255, null=True)
+
+ def __str__(self) -> str:
return f"{self.cell_type}-{self.user.username}"
class CalendarEvent(models.Model):
- event = models.CharField(max_length=255)
- date = models.CharField(max_length=50, null=True, blank=True)
+ event: models.CharField = models.CharField(max_length=255)
+ date: models.CharField = models.CharField(max_length=50, null=True, blank=True)
# NOTE: This is bad practice, though is necessary for the time being
# since frontends use the string date field
- date_obj = models.DateTimeField(null=True, blank=True)
+ date_obj: models.DateTimeField = models.DateTimeField(null=True, blank=True)
- def __str__(self):
+ def __str__(self) -> str:
return f"{self.date}-{self.event}"
diff --git a/backend/penndata/serializers.py b/backend/penndata/serializers.py
index 51933e26..9dca83c0 100644
--- a/backend/penndata/serializers.py
+++ b/backend/penndata/serializers.py
@@ -1,4 +1,4 @@
-from typing import Any, TypeAlias
+from typing import Any
from rest_framework import serializers
@@ -12,9 +12,6 @@
)
-ValidatedData: TypeAlias = dict[str, Any]
-
-
class EventSerializer(serializers.ModelSerializer):
class Meta:
model = Event
@@ -63,7 +60,7 @@ class Meta:
model = AnalyticsEvent
fields = ("created_at", "cell_type", "index", "post", "poll", "is_interaction")
- def create(self, validated_data: ValidatedData) -> AnalyticsEvent:
+ def create(self, validated_data: dict[str, Any]) -> AnalyticsEvent:
validated_data["user"] = self.context["request"].user
if validated_data["poll"] and validated_data["post"]:
raise serializers.ValidationError(
diff --git a/backend/penndata/views.py b/backend/penndata/views.py
index 8b722f68..b8a70d96 100644
--- a/backend/penndata/views.py
+++ b/backend/penndata/views.py
@@ -1,6 +1,6 @@
import datetime
from datetime import timedelta
-from typing import Any, TypeAlias
+from typing import Any, Optional, Sequence, TypeAlias, cast
import requests
from bs4 import BeautifulSoup
@@ -29,9 +29,9 @@
FitnessRoomSerializer,
HomePageOrderSerializer,
)
+from utils.types import get_user
-ValidatedData: TypeAlias = dict[str, Any]
CalendarEventList: TypeAlias = QuerySet[CalendarEvent, Manager[CalendarEvent]]
EventList: TypeAlias = QuerySet[Event, Manager[Event]]
HomePageOrderList: TypeAlias = QuerySet[HomePageOrder, Manager[HomePageOrder]]
@@ -43,7 +43,7 @@ class News(APIView):
"""
def get_article(self) -> dict[str, Any] | None:
- article = {"source": "The Daily Pennsylvanian"}
+ article: dict[str, Any] = {"source": "The Daily Pennsylvanian"}
try:
headers = {
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_5) AppleWebKit/537.36 "
@@ -161,7 +161,7 @@ class HomePage(APIView):
class Cell:
def __init__(
- self, myType: str, myInfo: ValidatedData | None = None, myWeight: int = 0
+ self, myType: str, myInfo: Optional[dict[str, Any]] = None, myWeight: int = 0
) -> None:
self.type = myType
self.info = myInfo
@@ -174,7 +174,7 @@ def get(self, request: Request) -> Response:
# NOTE: accept arguments: ?version=
- profile = request.user.profile
+ profile = get_user(request).profile
# TODO: add user's GSR reservations to Response
# TODO: add user's courses to Response
@@ -216,7 +216,7 @@ def get(self, request: Request) -> Response:
# cells.append(self.Cell("calendar", {"calendar": Calendar.get_calendar(self)}, 40))
# adds front page article of DP
- cells.append(self.Cell("news", {"article": News.get_article(self)}, 50))
+ cells.append(self.Cell("news", {"article": News().get_article()}, 50))
# sorts by cell weight
cells.sort(key=lambda x: x.weight, reverse=True)
@@ -243,7 +243,7 @@ class FitnessRoomView(generics.ListAPIView):
}
def get(self, request: Request) -> Response:
- response = super().get(self, request)
+ response = super().get(request)
# also add last_updated and open/close times to each room in response
for room in response.data:
ss = FitnessSnapshot.objects.filter(room__id=room["id"]).order_by("-date").first()
@@ -263,17 +263,17 @@ def get(self, request: Request) -> Response:
class FitnessUsage(APIView):
- def safe_add(self, a: int | None, b: int | None) -> int | None:
+ def safe_add(self, a: Optional[float] = None, b: Optional[float] = None) -> Optional[float]:
return None if a is None and b is None else (a or 0) + (b or 0)
def linear_interpolate(
self,
- before_val: int | None,
- after_val: int | None,
+ before_val: int | float,
+ after_val: int | float,
before_date: datetime.datetime,
current_date: datetime.datetime,
after_date: datetime.datetime,
- ) -> int | None:
+ ) -> int | float:
return (
before_val
+ (after_val - before_val)
@@ -283,7 +283,7 @@ def linear_interpolate(
def get_usage_on_date(
self, room: FitnessRoom, date: datetime.date, field: str
- ) -> list[int | None]:
+ ) -> Sequence[Optional[int | float]]:
"""
Returns the number of people in the fitness center on a given date per hour
"""
@@ -298,7 +298,7 @@ def get_usage_on_date(
snapshots = FitnessSnapshot.objects.filter(room=room, date__date=date)
# For usage, None represents no data
- usage = [0] * 24
+ usage: list[Optional[float]] = [0] * 24
for hour in range(open, close + 1):
# consider the :30 mark of each hour
hour_date = timezone.make_aware(datetime.datetime.combine(date, datetime.time(hour)))
@@ -307,8 +307,8 @@ def get_usage_on_date(
before = snapshots.filter(date__lte=hour_date).order_by("-date").first()
after = snapshots.filter(date__gte=hour_date).order_by("date").first()
- before_date, before_val = getattr(before, "date", None), getattr(before, field, None)
- after_date, after_val = getattr(after, "date", None), getattr(after, field, None)
+ before_date, before_val = getattr(before, "date", None), getattr(before, field, 0)
+ after_date, after_val = getattr(after, "date", None), getattr(after, field, 0)
# This condition should only activate during morning times
if before is None:
@@ -322,7 +322,7 @@ def get_usage_on_date(
if date == timezone.localtime().date():
# Set value to None if the last retrieved data was
# over 2 hours old to avoid extrapolation
- if hour_date - datetime.timedelta(hours=1) > before_date:
+ if before_date and hour_date - datetime.timedelta(hours=1) > before_date:
for i in range(hour, 24):
usage[i] = None
break
@@ -334,21 +334,29 @@ def get_usage_on_date(
+ datetime.timedelta(minutes=30),
0,
)
-
- usage[hour] = (
- self.linear_interpolate(before_val, after_val, before_date, hour_date, after_date)
- if before_date != after_date
- else after_val
- )
+ if before_date and after_date:
+ usage[hour] = (
+ self.linear_interpolate(
+ before_val,
+ after_val,
+ cast(datetime.datetime, before_date),
+ hour_date,
+ cast(datetime.datetime, after_date),
+ )
+ if before_date != after_date
+ else (after_val)
+ )
if all(amt == 0 for amt in usage): # location probably closed - don't count in aggregate
return [None] * 24
- return usage
+ return cast(Sequence[Optional[int | float]], usage)
def get_usage(
self, room: FitnessRoom, date: datetime.date, num_samples: int, group_by: str, field: str
- ) -> tuple[list[int | None], datetime.date, datetime.date]:
+ ) -> tuple[list[Optional[int | float]], datetime.date, datetime.date]:
unit = 1 if group_by == "day" else 7 # skip by 1 or 7 days
- usage_aggs = [(None, 0)] * 24 # (sum, count) for each hour
+ usage_aggs: list[tuple[Optional[float], int]] = [
+ (None, 0)
+ ] * 24 # (sum, count) for each hour
min_date = timezone.localtime().date()
max_date = date - datetime.timedelta(days=unit * (num_samples - 1))
@@ -361,10 +369,10 @@ def get_usage(
for (sum, count), val in zip(usage_aggs, usage)
]
# update min and max date if any data was logged
- if any(usage):
+ if any(u is not None for u in usage):
min_date = min(min_date, curr)
max_date = max(max_date, curr)
- ret = [(sum / count) if count else None for (sum, count) in usage_aggs]
+ ret = [(sum / count if count and sum is not None else None) for (sum, count) in usage_aggs]
return ret, min_date, max_date
def get(self, request: Request, room_id: int) -> Response:
@@ -397,6 +405,7 @@ def get(self, request: Request, room_id: int) -> Response:
usage_per_hour, min_date, max_date = self.get_usage(
room, date, num_samples, group_by, field
)
+
return Response(
{
"room_name": room.name,
@@ -419,7 +428,7 @@ class FitnessPreferences(APIView):
def get(self, request: Request) -> Response:
- preferences = request.user.profile.fitness_preferences.all()
+ preferences = get_user(request).profile.fitness_preferences.all()
# returns all ids in a person's preferences
return Response({"rooms": preferences.values_list("id", flat=True)})
@@ -429,7 +438,7 @@ def post(self, request: Request) -> Response:
if "rooms" not in request.data:
return Response({"success": False, "error": "No rooms provided"})
- profile = request.user.profile
+ profile = get_user(request).profile
ids = request.data["rooms"]
@@ -454,14 +463,14 @@ class UniqueCounterView(APIView):
permission_classes = [IsAuthenticated]
def get(self, request: Request) -> Response:
- query = dict()
+ query: dict[str, Any] = {}
if "post_id" in request.query_params:
query["post__id"] = request.query_params["post_id"]
if "poll_id" in request.query_params:
query["poll__id"] = request.query_params["poll_id"]
if len(query) != 1:
return Response({"detail": "require 1 id out of post_id or poll_id"}, status=400)
- query["is_interaction"] = (
- request.query_params.get("is_interaction", "false").lower() == "true"
+ query["is_interaction"] = bool(
+ str(request.query_params.get("is_interaction", "false")).lower() == "true"
)
return Response({"count": AnalyticsEvent.objects.filter(**query).count()})
diff --git a/backend/pennmobile/admin.py b/backend/pennmobile/admin.py
index 2da5a734..45aa6711 100644
--- a/backend/pennmobile/admin.py
+++ b/backend/pennmobile/admin.py
@@ -1,21 +1,31 @@
# CUSTOM ADMIN SETTUP FOR PENN MOBILE
-from typing import Any, Dict, Optional, Type, TypeAlias
+from typing import Any, Dict, Optional, Type, TypeAlias, cast
from django.contrib import admin, messages
from django.contrib.admin.apps import AdminConfig
from django.db.models import Model
from django.http import HttpRequest
+from django.template.response import TemplateResponse
from django.urls import reverse
from django.utils.html import format_html
-ModelType: TypeAlias = Type[Model]
AdminContext: TypeAlias = Dict[str, Any]
MessageText: TypeAlias = str
-def add_post_poll_message(request: HttpRequest, model: ModelType) -> None:
- if (count := model.objects.filter(model.ACTION_REQUIRED_CONDITION).count()) > 0:
+def add_post_poll_message(request: HttpRequest, model: Type[Model]) -> None:
+ from portal.models import Poll, Post
+
+ model_obj: Poll | Post
+ if model == Poll:
+ model_obj = cast(Poll, model)
+ elif model == Post:
+ model_obj = cast(Post, model)
+ else:
+ raise ValueError(f"Invalid model: {model}")
+
+ if (count := model_obj.objects.filter(model_obj.ACTION_REQUIRED_CONDITION).count()) > 0:
link = reverse(f"admin:{model._meta.app_label}_{model._meta.model_name}_changelist")
messages.info(
request,
@@ -30,7 +40,9 @@ def add_post_poll_message(request: HttpRequest, model: ModelType) -> None:
class CustomAdminSite(admin.AdminSite):
site_header = "Penn Mobile Backend Admin"
- def index(self, request: HttpRequest, extra_context: Optional[AdminContext] = None) -> Any:
+ def index(
+ self, request: HttpRequest, extra_context: Optional[AdminContext] = None
+ ) -> TemplateResponse:
from portal.models import Poll, Post
add_post_poll_message(request, Post)
@@ -43,4 +55,5 @@ class PennMobileAdminConfig(AdminConfig):
default_site = "pennmobile.admin.CustomAdminSite"
-admin.AdminSite = CustomAdminSite # anything else that overrides default admin should override ours
+# anything else that overrides default admin should override ours
+admin.site = CustomAdminSite()
diff --git a/backend/pennmobile/celery.py b/backend/pennmobile/celery.py
index 7b81bab0..18ddcd1d 100644
--- a/backend/pennmobile/celery.py
+++ b/backend/pennmobile/celery.py
@@ -1,4 +1,5 @@
import os
+from typing import Any
from celery import Celery
@@ -21,5 +22,5 @@
@app.task(bind=True)
-def debug_task(self) -> None:
+def debug_task(self: Any) -> None:
print(f"Request: {self.request!r}")
diff --git a/backend/pennmobile/settings/base.py b/backend/pennmobile/settings/base.py
index cf285a89..1cc083bd 100644
--- a/backend/pennmobile/settings/base.py
+++ b/backend/pennmobile/settings/base.py
@@ -89,7 +89,7 @@
# Database
# https://docs.djangoproject.com/en/2.1/ref/settings/#databases
DATABASES = {
- "default": dj_database_url.config(default="sqlite:///" + os.path.join(BASE_DIR, "db.sqlite3")),
+ "default": dj_database_url.config(default="sqlite:///" + os.path.join(BASE_DIR, "db.sqlite3"))
}
DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
@@ -148,7 +148,7 @@
"rest_framework.authentication.SessionAuthentication",
"rest_framework.authentication.BasicAuthentication",
"accounts.authentication.PlatformAuthentication",
- ],
+ ]
}
# Redis for Celery & Caching
diff --git a/backend/pennmobile/settings/production.py b/backend/pennmobile/settings/production.py
index 066012aa..87996116 100644
--- a/backend/pennmobile/settings/production.py
+++ b/backend/pennmobile/settings/production.py
@@ -17,7 +17,7 @@
ALLOWED_HOSTS = DOMAINS
# Make sure SECRET_KEY is set to a secret in production
-SECRET_KEY = os.environ.get("SECRET_KEY", None)
+SECRET_KEY = os.environ.get("SECRET_KEY", "")
# Sentry settings
SENTRY_URL = os.environ.get("SENTRY_URL", "")
diff --git a/backend/pennmobile/test_runner.py b/backend/pennmobile/test_runner.py
index 55937401..cd4cd55d 100644
--- a/backend/pennmobile/test_runner.py
+++ b/backend/pennmobile/test_runner.py
@@ -1,31 +1,32 @@
+from typing import Any
from unittest import mock
from django.test.runner import DiscoverRunner
from xmlrunner.extra.djangotestrunner import XMLTestRunner
-def check_wharton(*args) -> bool:
+def check_wharton(*args: Any) -> bool:
return False
class MockLabsAnalytics:
- def __init__(self):
+ def __init__(self) -> None:
pass
- def submit(self, txn):
+ def submit(self, txn: Any) -> None:
pass
class MobileTestCIRunner(XMLTestRunner):
@mock.patch("analytics.analytics.LabsAnalytics", MockLabsAnalytics)
@mock.patch("gsr_booking.models.GroupMembership.check_wharton", check_wharton)
- def run_tests(self, test_labels, **kwargs) -> None:
+ def run_tests(self, test_labels: list[str], **kwargs: Any) -> int:
return super().run_tests(test_labels, **kwargs)
class MobileTestLocalRunner(DiscoverRunner):
@mock.patch("analytics.analytics.LabsAnalytics", MockLabsAnalytics)
@mock.patch("gsr_booking.models.GroupMembership.check_wharton", check_wharton)
- def run_tests(self, test_labels, **kwargs) -> None:
+ def run_tests(self, test_labels: list[str], **kwargs: Any) -> int:
return super().run_tests(test_labels, **kwargs)
diff --git a/backend/pennmobile/urls.py b/backend/pennmobile/urls.py
index 5e94960e..1ea3b6d2 100644
--- a/backend/pennmobile/urls.py
+++ b/backend/pennmobile/urls.py
@@ -1,11 +1,15 @@
+from typing import List, Union
+
from django.conf import settings
from django.contrib import admin
-from django.urls import include, path
+from django.urls import URLPattern, URLResolver, include, path
from django.views.generic import TemplateView
from rest_framework.schemas import get_schema_view
-urlpatterns = [
+URLPatternList = List[Union[URLPattern, URLResolver]]
+
+urlpatterns: URLPatternList = [
path("gsr/", include("gsr_booking.urls")),
path("portal/", include("portal.urls")),
path("admin/", admin.site.urls),
@@ -29,7 +33,7 @@
path("sublet/", include("sublet.urls")),
]
-urlpatterns = [
+urlpatterns: URLPatternList = [ # type: ignore[no-redef]
path("api/", include(urlpatterns)),
path("", include((urlpatterns, "apex"))),
]
@@ -37,4 +41,6 @@
if settings.DEBUG:
import debug_toolbar
- urlpatterns = [path("__debug__/", include(debug_toolbar.urls))] + urlpatterns
+ urlpatterns: URLPatternList = [ # type: ignore[no-redef]
+ path("__debug__/", include(debug_toolbar.urls))
+ ] + urlpatterns
diff --git a/backend/portal/admin.py b/backend/portal/admin.py
index a92c248b..a39cfb05 100644
--- a/backend/portal/admin.py
+++ b/backend/portal/admin.py
@@ -1,51 +1,57 @@
+from typing import Any
+
from django.contrib import admin
-from django.utils.html import escape, mark_safe
+from django.db.models import QuerySet
+from django.utils.html import escape
+from django.utils.safestring import SafeString, mark_safe
from portal.models import Content, Poll, PollOption, PollVote, Post, TargetPopulation
class ContentAdmin(admin.ModelAdmin):
@admin.action(description="Set status to Approved")
- def action_approved(modeladmin, request, queryset):
+ def action_approved(modeladmin: Any, request: Any, queryset: QuerySet) -> None:
queryset.update(status=Content.STATUS_APPROVED)
@admin.action(description="Set status to Draft")
- def action_draft(modeladmin, request, queryset):
+ def action_draft(modeladmin: Any, request: Any, queryset: QuerySet) -> None:
queryset.update(status=Content.STATUS_DRAFT)
@admin.action(description="Set status to Revision")
- def action_revision(modeladmin, request, queryset):
+ def action_revision(modeladmin: Any, request: Any, queryset: QuerySet) -> None:
queryset.update(status=Content.STATUS_REVISION)
actions = [action_approved, action_draft, action_revision]
- def get_queryset(self, request):
+ def get_queryset(self, request: Any) -> QuerySet:
queryset = super().get_queryset(request)
return queryset.annotate(ar=Content.ACTION_REQUIRED_CONDITION).order_by(
"-ar", "-created_date"
)
- def ar(self, obj):
- return obj.ar
+ # Using any for the ar property since it comes from a queryset annotation
+ def ar(self, obj: Any) -> bool:
+ return bool(obj.ar)
- ar.boolean = True
+ ar.boolean = True # type: ignore[attr-defined]
class PostAdmin(ContentAdmin):
- def image_tag(instance, height):
+ @staticmethod
+ def image_tag(instance: Post, height: int) -> SafeString:
return mark_safe(
f'
' % escape(instance.image and instance.image.url)
)
- def small_image(self, instance):
+ def small_image(self, instance: Post) -> SafeString:
return PostAdmin.image_tag(instance, 100)
- small_image.short_description = "Post Image"
+ small_image.short_description = "Post Image" # type: ignore[attr-defined]
- def large_image(self, instance):
+ def large_image(self, instance: Post) -> SafeString:
return PostAdmin.image_tag(instance, 300)
- large_image.short_description = "Post Image"
+ large_image.short_description = "Post Image" # type: ignore[attr-defined]
readonly_fields = ("large_image",)
list_display = (
diff --git a/backend/portal/logic.py b/backend/portal/logic.py
index 8234a246..bd6d546b 100644
--- a/backend/portal/logic.py
+++ b/backend/portal/logic.py
@@ -1,25 +1,15 @@
import json
from collections import defaultdict
-from typing import TYPE_CHECKING, Any
+from typing import Any, Optional
from accounts.ipc import authenticated_request
-from django.contrib.auth import get_user_model
from rest_framework.exceptions import PermissionDenied
from portal.models import Poll, PollOption, PollVote, TargetPopulation
+from utils.types import UserType
-if TYPE_CHECKING:
- from django.contrib.auth.models import AbstractUser
-
- UserType = AbstractUser
-else:
- UserType = Any
-
-User = get_user_model()
-
-
-def get_user_info(user: "UserType") -> dict[str, Any]:
+def get_user_info(user: UserType) -> dict[str, Any]:
"""Returns Platform user information"""
response = authenticated_request(user, "GET", "https://platform.pennlabs.org/accounts/me/")
if response.status_code == 403:
@@ -27,7 +17,7 @@ def get_user_info(user: "UserType") -> dict[str, Any]:
return json.loads(response.content)
-def get_user_clubs(user: "UserType") -> list[dict[str, Any]]:
+def get_user_clubs(user: UserType) -> list[dict[str, Any]]:
"""Returns list of clubs that user is a member of"""
response = authenticated_request(user, "GET", "https://pennclubs.com/api/memberships/")
if response.status_code == 403:
@@ -36,7 +26,7 @@ def get_user_clubs(user: "UserType") -> list[dict[str, Any]]:
return res_json
-def get_club_info(user: "UserType", club_code: str) -> dict[str, Any]:
+def get_club_info(user: UserType, club_code: str) -> dict[str, Any]:
"""Returns club information based on club code"""
response = authenticated_request(user, "GET", f"https://pennclubs.com/api/clubs/{club_code}/")
if response.status_code == 403:
@@ -45,12 +35,12 @@ def get_club_info(user: "UserType", club_code: str) -> dict[str, Any]:
return {"name": res_json["name"], "image": res_json["image_url"], "club_code": club_code}
-def get_user_populations(user: "UserType") -> list[TargetPopulation]:
+def get_user_populations(user: UserType) -> list[list[TargetPopulation]]:
"""Returns the target populations that the user belongs to"""
user_info = get_user_info(user)
- year = (
+ year: list[TargetPopulation] = (
[
TargetPopulation.objects.get(
kind=TargetPopulation.KIND_YEAR, population=user_info["student"]["graduation_year"]
@@ -60,7 +50,7 @@ def get_user_populations(user: "UserType") -> list[TargetPopulation]:
else []
)
- school = (
+ school: list[TargetPopulation] = (
[
TargetPopulation.objects.get(kind=TargetPopulation.KIND_SCHOOL, population=x["name"])
for x in user_info["student"]["school"]
@@ -69,7 +59,7 @@ def get_user_populations(user: "UserType") -> list[TargetPopulation]:
else []
)
- major = (
+ major: list[TargetPopulation] = (
[
TargetPopulation.objects.get(kind=TargetPopulation.KIND_MAJOR, population=x["name"])
for x in user_info["student"]["major"]
@@ -78,7 +68,7 @@ def get_user_populations(user: "UserType") -> list[TargetPopulation]:
else []
)
- degree = (
+ degree: list[TargetPopulation] = (
[
TargetPopulation.objects.get(
kind=TargetPopulation.KIND_DEGREE, population=x["degree_type"]
@@ -92,29 +82,30 @@ def get_user_populations(user: "UserType") -> list[TargetPopulation]:
return [year, school, major, degree]
-def check_targets(obj: Poll, user: "UserType") -> bool:
+def check_targets(obj: Poll, user: UserType) -> bool:
"""
Check if user aligns with target populations of poll or post
"""
- populations = get_user_populations(user)
+ population_groups = get_user_populations(user)
- year = set(obj.target_populations.filter(kind=TargetPopulation.KIND_YEAR))
- school = set(obj.target_populations.filter(kind=TargetPopulation.KIND_SCHOOL))
- major = set(obj.target_populations.filter(kind=TargetPopulation.KIND_MAJOR))
- degree = set(obj.target_populations.filter(kind=TargetPopulation.KIND_DEGREE))
+ year_targets = set(obj.target_populations.filter(kind=TargetPopulation.KIND_YEAR))
+ school_targets = set(obj.target_populations.filter(kind=TargetPopulation.KIND_SCHOOL))
+ major_targets = set(obj.target_populations.filter(kind=TargetPopulation.KIND_MAJOR))
+ degree_targets = set(obj.target_populations.filter(kind=TargetPopulation.KIND_DEGREE))
- return (
- set(populations[0]).issubset(year)
- and set(populations[1]).issubset(school)
- and set(populations[2]).issubset(major)
- and set(populations[3]).issubset(degree)
+ return all(
+ set(group).issubset(targets)
+ for group, targets in zip(
+ population_groups, [year_targets, school_targets, major_targets, degree_targets]
+ )
)
-def get_demographic_breakdown(poll_id: int) -> list[dict[str, Any]]:
+def get_demographic_breakdown(poll_id: Optional[int] = None) -> list[dict[str, Any]]:
"""Collects Poll statistics on school and graduation year demographics"""
-
+ if poll_id is None:
+ raise ValueError("poll_id is required")
# passing in id is necessary because
# poll info is already serialized
poll = Poll.objects.get(id=poll_id)
diff --git a/backend/portal/management/commands/load_target_populations.py b/backend/portal/management/commands/load_target_populations.py
index 82ecf247..6244e73a 100644
--- a/backend/portal/management/commands/load_target_populations.py
+++ b/backend/portal/management/commands/load_target_populations.py
@@ -1,3 +1,6 @@
+from argparse import ArgumentParser
+from typing import Any, Optional
+
import requests
from django.core.management.base import BaseCommand
from django.utils import timezone
@@ -6,7 +9,7 @@
class Command(BaseCommand):
- def add_arguments(self, parser):
+ def add_arguments(self, parser: ArgumentParser) -> None:
parser.add_argument(
"--years",
type=str,
@@ -14,7 +17,7 @@ def add_arguments(self, parser):
"This is only used for testing currently.",
)
- def handle(self, *args, **kwargs):
+ def handle(self, *args: Any, **kwargs: Any) -> None:
# loads majors, years, schools, and degrees onto TargetPopulations
# runs get_or_create to ensure no duplicates
majors = requests.get("https://platform.pennlabs.org/accounts/majors/").json()
@@ -36,10 +39,10 @@ def handle(self, *args, **kwargs):
TargetPopulation.objects.get_or_create(kind=TargetPopulation.KIND_YEAR, population=year)
self.stdout.write("Uploaded Target Populations!")
- def get_degrees(self):
+ def get_degrees(self) -> list[str]:
return ["BACHELORS", "MASTERS", "PHD", "PROFESSIONAL"]
- def get_years(self, years):
+ def get_years(self, years: Optional[str] = None) -> list[int]:
# creates new class year in August in preparation for upcoming school year
if years is None:
return (
diff --git a/backend/portal/management/commands/polls_populate.py b/backend/portal/management/commands/polls_populate.py
index 1414ab37..e5939f87 100644
--- a/backend/portal/management/commands/polls_populate.py
+++ b/backend/portal/management/commands/polls_populate.py
@@ -1,19 +1,29 @@
import datetime
+from typing import Any
-from django.contrib.auth import get_user_model
from django.core.management import call_command
from django.core.management.base import BaseCommand
from django.utils import timezone
from portal.models import Poll, PollOption, PollVote, TargetPopulation
from user.models import Profile
-
-
-User = get_user_model()
+from utils.types import DjangoUserModel, UserType
class Command(BaseCommand):
- def handle(self, *args, **kwargs):
+ def _create_user(
+ self, username: str, email: str, password: str, graduation_date: datetime.date
+ ) -> UserType:
+ """Helper to create a user with profile"""
+ if not DjangoUserModel.objects.filter(username=username).exists():
+ user = DjangoUserModel.objects.create_user(username, email, password)
+ profile = Profile.objects.get(user=user)
+ setattr(profile, "expected_graduation", graduation_date)
+ profile.save()
+ return user
+ return DjangoUserModel.objects.get(username=username)
+
+ def handle(self, *args: Any, **kwargs: Any) -> None:
# Define graduation years
df_2022 = datetime.date(2022, 5, 15)
@@ -22,55 +32,12 @@ def handle(self, *args, **kwargs):
df_2025 = datetime.date(2025, 5, 17)
# Create users and set graduation years
- if not User.objects.filter(username="user1").first():
- user1 = User.objects.create_user("user1", "user@seas.upenn.edu", "user")
- user1_profile = Profile.objects.get(user=user1)
- user1_profile.expected_graduation = df_2022
- user1_profile.save()
- else:
- user1 = User.objects.get(username="user1")
-
- if not User.objects.filter(username="user2").first():
- user2 = User.objects.create_user("user2", "user2@seas.upenn.edu", "user2")
- user2_profile = Profile.objects.get(user=user2)
- user2_profile.expected_graduation = df_2023
- user2_profile.save()
- else:
- user2 = User.objects.get(username="user2")
-
- if not User.objects.filter(username="user3").first():
- user3 = User.objects.create_user("user3", "user3@seas.upenn.edu", "user3")
- user3_profile = Profile.objects.get(user=user3)
- user3_profile.expected_graduation = df_2024
- user3_profile.save()
- else:
- user3 = User.objects.get(username="user3")
-
- if not User.objects.filter(username="user_cas").first():
- user_cas = User.objects.create_user("user_cas", "user@sas.upenn.edu", "user_cas")
- user_cas_profile = Profile.objects.get(user=user_cas)
- user_cas_profile.expected_graduation = df_2025
- user_cas_profile.save()
- else:
- user_cas = User.objects.get(username="user_cas")
-
- if not User.objects.filter(username="user_wh").first():
- user_wh = User.objects.create_user("user_wh", "user@wharton.upenn.edu", "user_wh")
- user_wh_profile = Profile.objects.get(user=user_wh)
- user_wh_profile.expected_graduation = df_2024
- user_wh_profile.save()
- else:
- user_wh = User.objects.get(username="user_wh")
-
- if not User.objects.filter(username="user_nursing").first():
- user_nursing = User.objects.create_user(
- "user_nursing", "user@nursing.upenn.edu", "user_nursing"
- )
- user_nursing_profile = Profile.objects.get(user=user_nursing)
- user_nursing_profile.expected_graduation = df_2023
- user_nursing_profile.save()
- else:
- user_nursing = User.objects.get(username="user_nursing")
+ self._create_user("user1", "user@seas.upenn.edu", "user", df_2022)
+ self._create_user("user2", "user2@seas.upenn.edu", "user2", df_2023)
+ self._create_user("user3", "user3@seas.upenn.edu", "user3", df_2024)
+ self._create_user("user_cas", "user@sas.upenn.edu", "user_cas", df_2025)
+ self._create_user("user_wh", "user@wharton.upenn.edu", "user_wh", df_2024)
+ self._create_user("user_nursing", "user@nursing.upenn.edu", "user_nursing", df_2023)
# Create target populations
call_command("load_target_populations", "--years", "2022, 2023, 2024, 2025")
diff --git a/backend/portal/migrations/0001_initial.py b/backend/portal/migrations/0001_initial.py
index c7ed66f3..ec955f2c 100644
--- a/backend/portal/migrations/0001_initial.py
+++ b/backend/portal/migrations/0001_initial.py
@@ -10,9 +10,7 @@ class Migration(migrations.Migration):
initial = True
- dependencies = [
- migrations.swappable_dependency(settings.AUTH_USER_MODEL),
- ]
+ dependencies = [migrations.swappable_dependency(settings.AUTH_USER_MODEL)]
operations = [
migrations.CreateModel(
diff --git a/backend/portal/migrations/0002_auto_20211003_2225.py b/backend/portal/migrations/0002_auto_20211003_2225.py
index 06a6a1b6..33fa66d9 100644
--- a/backend/portal/migrations/0002_auto_20211003_2225.py
+++ b/backend/portal/migrations/0002_auto_20211003_2225.py
@@ -6,24 +6,17 @@
class Migration(migrations.Migration):
- dependencies = [
- ("portal", "0001_initial"),
- ]
+ dependencies = [("portal", "0001_initial")]
operations = [
- migrations.RemoveField(
- model_name="poll",
- name="image_url",
- ),
+ migrations.RemoveField(model_name="poll", name="image_url"),
migrations.AddField(
model_name="poll",
name="start_date",
field=models.DateTimeField(default=django.utils.timezone.now),
),
migrations.AddField(
- model_name="polloption",
- name="vote_count",
- field=models.IntegerField(default=0),
+ model_name="polloption", name="vote_count", field=models.IntegerField(default=0)
),
migrations.AddField(
model_name="targetpopulation",
diff --git a/backend/portal/migrations/0003_alter_targetpopulation_kind.py b/backend/portal/migrations/0003_alter_targetpopulation_kind.py
index 6d89a215..2868d863 100644
--- a/backend/portal/migrations/0003_alter_targetpopulation_kind.py
+++ b/backend/portal/migrations/0003_alter_targetpopulation_kind.py
@@ -5,9 +5,7 @@
class Migration(migrations.Migration):
- dependencies = [
- ("portal", "0002_auto_20211003_2225"),
- ]
+ dependencies = [("portal", "0002_auto_20211003_2225")]
operations = [
migrations.AlterField(
@@ -23,5 +21,5 @@ class Migration(migrations.Migration):
default="SCHOOL",
max_length=10,
),
- ),
+ )
]
diff --git a/backend/portal/migrations/0004_post.py b/backend/portal/migrations/0004_post.py
index c80fd9dd..cdeb467a 100644
--- a/backend/portal/migrations/0004_post.py
+++ b/backend/portal/migrations/0004_post.py
@@ -45,5 +45,5 @@ class Migration(migrations.Migration):
),
),
],
- ),
+ )
]
diff --git a/backend/portal/migrations/0005_auto_20211231_1558.py b/backend/portal/migrations/0005_auto_20211231_1558.py
index 2f35eba8..6df6356a 100644
--- a/backend/portal/migrations/0005_auto_20211231_1558.py
+++ b/backend/portal/migrations/0005_auto_20211231_1558.py
@@ -6,36 +6,16 @@
class Migration(migrations.Migration):
- dependencies = [
- ("portal", "0004_post"),
- ]
+ dependencies = [("portal", "0004_post")]
operations = [
- migrations.RenameField(
- model_name="poll",
- old_name="user_comment",
- new_name="club_comment",
- ),
- migrations.RemoveField(
- model_name="poll",
- name="approved",
- ),
- migrations.RemoveField(
- model_name="poll",
- name="source",
- ),
- migrations.RemoveField(
- model_name="poll",
- name="user",
- ),
- migrations.RemoveField(
- model_name="pollvote",
- name="user",
- ),
+ migrations.RenameField(model_name="poll", old_name="user_comment", new_name="club_comment"),
+ migrations.RemoveField(model_name="poll", name="approved"),
+ migrations.RemoveField(model_name="poll", name="source"),
+ migrations.RemoveField(model_name="poll", name="user"),
+ migrations.RemoveField(model_name="pollvote", name="user"),
migrations.AddField(
- model_name="poll",
- name="club_code",
- field=models.CharField(blank=True, max_length=255),
+ model_name="poll", name="club_code", field=models.CharField(blank=True, max_length=255)
),
migrations.AddField(
model_name="poll",
diff --git a/backend/portal/migrations/0006_auto_20220112_1529.py b/backend/portal/migrations/0006_auto_20220112_1529.py
index 3dc42228..44bfd28c 100644
--- a/backend/portal/migrations/0006_auto_20220112_1529.py
+++ b/backend/portal/migrations/0006_auto_20220112_1529.py
@@ -5,36 +5,15 @@
class Migration(migrations.Migration):
- dependencies = [
- ("portal", "0005_auto_20211231_1558"),
- ]
+ dependencies = [("portal", "0005_auto_20211231_1558")]
operations = [
- migrations.RenameField(
- model_name="post",
- old_name="user_comment",
- new_name="club_comment",
- ),
- migrations.RenameField(
- model_name="post",
- old_name="created_at",
- new_name="created_date",
- ),
- migrations.RemoveField(
- model_name="post",
- name="approved",
- ),
- migrations.RemoveField(
- model_name="post",
- name="source",
- ),
- migrations.RemoveField(
- model_name="post",
- name="user",
- ),
+ migrations.RenameField(model_name="post", old_name="user_comment", new_name="club_comment"),
+ migrations.RenameField(model_name="post", old_name="created_at", new_name="created_date"),
+ migrations.RemoveField(model_name="post", name="approved"),
+ migrations.RemoveField(model_name="post", name="source"),
+ migrations.RemoveField(model_name="post", name="user"),
migrations.AddField(
- model_name="post",
- name="club_code",
- field=models.CharField(blank=True, max_length=255),
+ model_name="post", name="club_code", field=models.CharField(blank=True, max_length=255)
),
]
diff --git a/backend/portal/migrations/0007_post_status.py b/backend/portal/migrations/0007_post_status.py
index ae126767..7cfba2bf 100644
--- a/backend/portal/migrations/0007_post_status.py
+++ b/backend/portal/migrations/0007_post_status.py
@@ -5,9 +5,7 @@
class Migration(migrations.Migration):
- dependencies = [
- ("portal", "0006_auto_20220112_1529"),
- ]
+ dependencies = [("portal", "0006_auto_20220112_1529")]
operations = [
migrations.AddField(
@@ -18,5 +16,5 @@ class Migration(migrations.Migration):
default="DRAFT",
max_length=30,
),
- ),
+ )
]
diff --git a/backend/portal/migrations/0008_alter_post_image_url.py b/backend/portal/migrations/0008_alter_post_image_url.py
index 2249e1d0..92ba5a3a 100644
--- a/backend/portal/migrations/0008_alter_post_image_url.py
+++ b/backend/portal/migrations/0008_alter_post_image_url.py
@@ -5,14 +5,12 @@
class Migration(migrations.Migration):
- dependencies = [
- ("portal", "0007_post_status"),
- ]
+ dependencies = [("portal", "0007_post_status")]
operations = [
migrations.AlterField(
model_name="post",
name="image_url",
field=models.ImageField(blank=True, null=True, upload_to="portal/images"),
- ),
+ )
]
diff --git a/backend/portal/migrations/0009_rename_image_url_post_image.py b/backend/portal/migrations/0009_rename_image_url_post_image.py
index 64028b4f..cbfca6f1 100644
--- a/backend/portal/migrations/0009_rename_image_url_post_image.py
+++ b/backend/portal/migrations/0009_rename_image_url_post_image.py
@@ -5,14 +5,6 @@
class Migration(migrations.Migration):
- dependencies = [
- ("portal", "0008_alter_post_image_url"),
- ]
+ dependencies = [("portal", "0008_alter_post_image_url")]
- operations = [
- migrations.RenameField(
- model_name="post",
- old_name="image_url",
- new_name="image",
- ),
- ]
+ operations = [migrations.RenameField(model_name="post", old_name="image_url", new_name="image")]
diff --git a/backend/portal/migrations/0010_remove_post_image.py b/backend/portal/migrations/0010_remove_post_image.py
index ede76232..22759b5c 100644
--- a/backend/portal/migrations/0010_remove_post_image.py
+++ b/backend/portal/migrations/0010_remove_post_image.py
@@ -5,13 +5,6 @@
class Migration(migrations.Migration):
- dependencies = [
- ("portal", "0009_rename_image_url_post_image"),
- ]
+ dependencies = [("portal", "0009_rename_image_url_post_image")]
- operations = [
- migrations.RemoveField(
- model_name="post",
- name="image",
- ),
- ]
+ operations = [migrations.RemoveField(model_name="post", name="image")]
diff --git a/backend/portal/migrations/0011_post_image.py b/backend/portal/migrations/0011_post_image.py
index b7a03d12..b2d9b737 100644
--- a/backend/portal/migrations/0011_post_image.py
+++ b/backend/portal/migrations/0011_post_image.py
@@ -5,14 +5,12 @@
class Migration(migrations.Migration):
- dependencies = [
- ("portal", "0010_remove_post_image"),
- ]
+ dependencies = [("portal", "0010_remove_post_image")]
operations = [
migrations.AddField(
model_name="post",
name="image",
field=models.ImageField(blank=True, null=True, upload_to="portal/images"),
- ),
+ )
]
diff --git a/backend/portal/migrations/0012_remove_post_image.py b/backend/portal/migrations/0012_remove_post_image.py
index 2ae2a531..32a1013b 100644
--- a/backend/portal/migrations/0012_remove_post_image.py
+++ b/backend/portal/migrations/0012_remove_post_image.py
@@ -5,13 +5,6 @@
class Migration(migrations.Migration):
- dependencies = [
- ("portal", "0011_post_image"),
- ]
+ dependencies = [("portal", "0011_post_image")]
- operations = [
- migrations.RemoveField(
- model_name="post",
- name="image",
- ),
- ]
+ operations = [migrations.RemoveField(model_name="post", name="image")]
diff --git a/backend/portal/migrations/0013_post_image.py b/backend/portal/migrations/0013_post_image.py
index 44a7ac6c..cf67dfee 100644
--- a/backend/portal/migrations/0013_post_image.py
+++ b/backend/portal/migrations/0013_post_image.py
@@ -5,14 +5,12 @@
class Migration(migrations.Migration):
- dependencies = [
- ("portal", "0012_remove_post_image"),
- ]
+ dependencies = [("portal", "0012_remove_post_image")]
operations = [
migrations.AddField(
model_name="post",
name="image",
field=models.ImageField(blank=True, null=True, upload_to="portal/images"),
- ),
+ )
]
diff --git a/backend/portal/migrations/0014_alter_post_post_url.py b/backend/portal/migrations/0014_alter_post_post_url.py
index b2690d59..bd0df256 100644
--- a/backend/portal/migrations/0014_alter_post_post_url.py
+++ b/backend/portal/migrations/0014_alter_post_post_url.py
@@ -5,14 +5,12 @@
class Migration(migrations.Migration):
- dependencies = [
- ("portal", "0013_post_image"),
- ]
+ dependencies = [("portal", "0013_post_image")]
operations = [
migrations.AlterField(
model_name="post",
name="post_url",
field=models.CharField(blank=True, max_length=255, null=True),
- ),
+ )
]
diff --git a/backend/portal/migrations/0015_auto_20240226_2236.py b/backend/portal/migrations/0015_auto_20240226_2236.py
index 7a9c7108..722d7083 100644
--- a/backend/portal/migrations/0015_auto_20240226_2236.py
+++ b/backend/portal/migrations/0015_auto_20240226_2236.py
@@ -5,24 +5,14 @@
class Migration(migrations.Migration):
- dependencies = [
- ("portal", "0014_alter_post_post_url"),
- ]
+ dependencies = [("portal", "0014_alter_post_post_url")]
operations = [
migrations.AddField(
- model_name="poll",
- name="priority",
- field=models.IntegerField(default=0),
+ model_name="poll", name="priority", field=models.IntegerField(default=0)
),
migrations.AddField(
- model_name="post",
- name="priority",
- field=models.IntegerField(default=0),
- ),
- migrations.AlterField(
- model_name="poll",
- name="expire_date",
- field=models.DateTimeField(),
+ model_name="post", name="priority", field=models.IntegerField(default=0)
),
+ migrations.AlterField(model_name="poll", name="expire_date", field=models.DateTimeField()),
]
diff --git a/backend/portal/models.py b/backend/portal/models.py
index b15aeee1..6e11f0a9 100644
--- a/backend/portal/models.py
+++ b/backend/portal/models.py
@@ -1,3 +1,5 @@
+from typing import Any
+
from django.contrib.auth import get_user_model
from django.db import models
from django.db.models import Q
@@ -6,9 +8,6 @@
from utils.email import get_backend_manager_emails, send_automated_email
-User = get_user_model()
-
-
class TargetPopulation(models.Model):
KIND_SCHOOL = "SCHOOL"
KIND_YEAR = "YEAR"
@@ -21,10 +20,13 @@ class TargetPopulation(models.Model):
(KIND_DEGREE, "Degree"),
)
- kind = models.CharField(max_length=10, choices=KIND_OPTIONS, default=KIND_SCHOOL)
- population = models.CharField(max_length=255)
+ id: int
+ kind: models.CharField = models.CharField(
+ max_length=10, choices=KIND_OPTIONS, default=KIND_SCHOOL
+ )
+ population: models.CharField = models.CharField(max_length=255)
- def __str__(self):
+ def __str__(self) -> str:
return self.population
@@ -41,34 +43,46 @@ class Content(models.Model):
ACTION_REQUIRED_CONDITION = Q(expire_date__gt=timezone.now()) & Q(status=STATUS_DRAFT)
- club_code = models.CharField(max_length=255, blank=True)
- created_date = models.DateTimeField(default=timezone.now)
- start_date = models.DateTimeField(default=timezone.now)
- expire_date = models.DateTimeField()
- status = models.CharField(max_length=30, choices=STATUS_OPTIONS, default=STATUS_DRAFT)
- club_comment = models.CharField(max_length=255, null=True, blank=True)
- admin_comment = models.CharField(max_length=255, null=True, blank=True)
- target_populations = models.ManyToManyField(TargetPopulation, blank=True)
- priority = models.IntegerField(default=0)
- creator = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True)
+ id: int
+ club_code: models.CharField = models.CharField(max_length=255, blank=True)
+ created_date: models.DateTimeField = models.DateTimeField(default=timezone.now)
+ start_date: models.DateTimeField = models.DateTimeField(default=timezone.now)
+ expire_date: models.DateTimeField = models.DateTimeField()
+ status: models.CharField = models.CharField(
+ max_length=30, choices=STATUS_OPTIONS, default=STATUS_DRAFT
+ )
+ club_comment: models.CharField = models.CharField(max_length=255, null=True, blank=True)
+ admin_comment: models.CharField = models.CharField(max_length=255, null=True, blank=True)
+ target_populations: models.ManyToManyField = models.ManyToManyField(
+ TargetPopulation, blank=True
+ )
+ priority: models.IntegerField = models.IntegerField(default=0)
+ creator: models.ForeignKey = models.ForeignKey(
+ get_user_model(), on_delete=models.SET_NULL, null=True, blank=True
+ )
class Meta:
abstract = True
- def _get_email_subject(self):
- return f"[Portal] {self.__class__._meta.model_name.capitalize()} #{self.id}"
+ def _get_email_subject(self) -> str:
+ model_name = (
+ self.__class__._meta.model_name.capitalize()
+ if self.__class__._meta.model_name is not None
+ else ""
+ )
+ return f"[Portal] {model_name} #{self.id}"
- def _on_create(self):
+ def _on_create(self) -> None:
send_automated_email.delay_on_commit(
self._get_email_subject(),
get_backend_manager_emails(),
(
- f"A new {self.__class__._meta.model_name} for {self.club_code}"
+ f"A new {self.__class__._meta.model_name} for {self.club_code} "
f"has been created by {self.creator}."
),
)
- def _on_status_change(self):
+ def _on_status_change(self) -> None:
if email := getattr(self.creator, "email", None):
send_automated_email.delay_on_commit(
self._get_email_subject(),
@@ -82,7 +96,7 @@ def _on_status_change(self):
),
)
- def save(self, *args, **kwargs):
+ def save(self, *args: Any, **kwargs: Any) -> None:
prev = self.__class__.objects.filter(id=self.id).first()
super().save(*args, **kwargs)
if prev is None:
@@ -93,35 +107,39 @@ def save(self, *args, **kwargs):
class Poll(Content):
- question = models.CharField(max_length=255)
- multiselect = models.BooleanField(default=False)
+ question: models.CharField = models.CharField(max_length=255)
+ multiselect: models.BooleanField = models.BooleanField(default=False)
- def __str__(self):
+ def __str__(self) -> str:
return self.question
class PollOption(models.Model):
- poll = models.ForeignKey(Poll, on_delete=models.CASCADE)
- choice = models.CharField(max_length=255)
- vote_count = models.IntegerField(default=0)
+ id: int
+ poll: models.ForeignKey = models.ForeignKey(Poll, on_delete=models.CASCADE)
+ choice: models.CharField = models.CharField(max_length=255)
+ vote_count: models.IntegerField = models.IntegerField(default=0)
- def __str__(self):
+ def __str__(self) -> str:
return f"{self.poll.id} - Option - {self.choice}"
class PollVote(models.Model):
- id_hash = models.CharField(max_length=255, blank=True)
- poll = models.ForeignKey(Poll, on_delete=models.CASCADE)
- poll_options = models.ManyToManyField(PollOption)
- created_date = models.DateTimeField(default=timezone.now)
- target_populations = models.ManyToManyField(TargetPopulation, blank=True)
+ id: int
+ id_hash: models.CharField = models.CharField(max_length=255, blank=True)
+ poll: models.ForeignKey = models.ForeignKey(Poll, on_delete=models.CASCADE)
+ poll_options: models.ManyToManyField = models.ManyToManyField(PollOption)
+ created_date: models.DateTimeField = models.DateTimeField(default=timezone.now)
+ target_populations: models.ManyToManyField = models.ManyToManyField(
+ TargetPopulation, blank=True
+ )
class Post(Content):
- title = models.CharField(max_length=255)
- subtitle = models.CharField(max_length=255)
- post_url = models.CharField(max_length=255, null=True, blank=True)
- image = models.ImageField(upload_to="portal/images", null=True, blank=True)
+ title: models.CharField = models.CharField(max_length=255)
+ subtitle: models.CharField = models.CharField(max_length=255)
+ post_url: models.CharField = models.CharField(max_length=255, null=True, blank=True)
+ image: models.ImageField = models.ImageField(upload_to="portal/images", null=True, blank=True)
- def __str__(self):
+ def __str__(self) -> str:
return self.title
diff --git a/backend/portal/permissions.py b/backend/portal/permissions.py
index adc319ba..c8b09364 100644
--- a/backend/portal/permissions.py
+++ b/backend/portal/permissions.py
@@ -1,7 +1,11 @@
+from typing import Any, cast
+
from rest_framework import permissions
+from rest_framework.request import Request
from portal.logic import get_user_clubs
-from portal.models import Poll
+from portal.models import Poll, PollOption
+from utils.types import get_auth_user
class IsSuperUser(permissions.BasePermission):
@@ -9,66 +13,79 @@ class IsSuperUser(permissions.BasePermission):
Grants permission if the current user is a superuser.
"""
- def has_object_permission(self, request, view, obj):
- return request.user.is_superuser
+ def has_object_permission(self, request: Request, view: Any, obj: Any) -> bool:
+ return get_auth_user(request).is_superuser
- def has_permission(self, request, view):
- return request.user.is_superuser
+ def has_permission(self, request: Request, view: Any) -> bool:
+ return get_auth_user(request).is_superuser
class PollOwnerPermission(permissions.BasePermission):
"""Permission that checks authentication and only permits owner to update/destroy objects"""
- def has_object_permission(self, request, view, obj):
+ def _get_club_code(self, obj: Any) -> str:
+ """Helper to get club_code from either Poll or PollOption object"""
+ if isinstance(obj, Poll):
+ return obj.club_code
+ elif isinstance(obj, PollOption):
+ poll = cast(Poll, obj.poll)
+ return poll.club_code
+ raise ValueError(f"Unexpected object type: {type(obj)}")
+
+ def has_object_permission(self, request: Request, view: Any, obj: Any) -> bool:
# only creator can edit
+ user = get_auth_user(request)
if view.action in ["partial_update", "update", "destroy"]:
- return obj.club_code in [x["club"]["code"] for x in get_user_clubs(request.user)]
- return request.user.is_authenticated
+ club_code = self._get_club_code(obj)
+ return club_code in [x["club"]["code"] for x in get_user_clubs(user)]
+ return user.is_authenticated
- def has_permission(self, request, view):
- return request.user.is_authenticated
+ def has_permission(self, request: Request, view: Any) -> bool:
+ return get_auth_user(request).is_authenticated
class OptionOwnerPermission(permissions.BasePermission):
"""Permission that checks authentication and only permits owner of Poll to update
corresponding Option objects"""
- def has_object_permission(self, request, view, obj):
+ def has_object_permission(self, request: Request, view: Any, obj: Any) -> bool:
# only creator can edit
+ user = get_auth_user(request)
if view.action in ["partial_update", "update", "destroy"]:
- return obj.poll.club_code in [x["club"]["code"] for x in get_user_clubs(request.user)]
+ return obj.poll.club_code in [x["club"]["code"] for x in get_user_clubs(user)]
return True
- def has_permission(self, request, view):
+ def has_permission(self, request: Request, view: Any) -> bool:
# only creator of poll can create poll option
+ user = get_auth_user(request)
if view.action == "create" and request.data:
poll = Poll.objects.get(id=request.data["poll"])
- return poll.club_code in [x["club"]["code"] for x in get_user_clubs(request.user)]
- return request.user.is_authenticated
+ return poll.club_code in [x["club"]["code"] for x in get_user_clubs(user)]
+ return user.is_authenticated
class TimeSeriesPermission(permissions.BasePermission):
"""Permission that checks for Time Series access (only creator of Poll and admins)"""
- def has_permission(self, request, view):
- poll = Poll.objects.filter(id=view.kwargs["poll_id"])
+ def has_permission(self, request: Request, view: Any) -> bool:
+ poll = Poll.objects.filter(id=view.kwargs["poll_id"]).first()
+ user = get_auth_user(request)
# checks if poll exists
- if poll.exists():
+ if poll is not None:
# only poll creator and admin can access
- return poll.first().club_code in [
- x["club"]["code"] for x in get_user_clubs(request.user)
- ]
+ return poll.club_code in [x["club"]["code"] for x in get_user_clubs(user)]
return False
class PostOwnerPermission(permissions.BasePermission):
"""checks authentication and only permits owner to update/destroy posts"""
- def has_object_permission(self, request, view, obj):
+ def has_object_permission(self, request: Request, view: Any, obj: Any) -> bool:
# only creator can edit
+ user = get_auth_user(request)
if view.action in ["partial_update", "update", "destroy"]:
- return obj.club_code in [x["club"]["code"] for x in get_user_clubs(request.user)]
+ return obj.club_code in [x["club"]["code"] for x in get_user_clubs(user)]
return True
- def has_permission(self, request, view):
+ def has_permission(self, request: Request, view: Any) -> bool:
return request.user.is_authenticated
diff --git a/backend/portal/serializers.py b/backend/portal/serializers.py
index d9a65e44..b384d90e 100644
--- a/backend/portal/serializers.py
+++ b/backend/portal/serializers.py
@@ -1,5 +1,6 @@
-from typing import Any, Dict, TypeAlias
+from typing import Any, ClassVar, Type, cast
+from django.db.models import Model
from django.http.request import QueryDict
from rest_framework import serializers
@@ -7,10 +8,6 @@
from portal.models import Content, Poll, PollOption, PollVote, Post, TargetPopulation
-ClubCode: TypeAlias = str
-ValidationData: TypeAlias = Dict[str, Any]
-
-
class TargetPopulationSerializer(serializers.ModelSerializer):
class Meta:
model = TargetPopulation
@@ -19,7 +16,8 @@ class Meta:
class ContentSerializer(serializers.ModelSerializer):
class Meta:
- fields = (
+ model: ClassVar[Type[Model]]
+ fields: tuple[str, ...] = (
"id",
"club_code",
"created_date",
@@ -30,10 +28,10 @@ class Meta:
"status",
"target_populations",
)
- read_only_fields = ("id", "created_date")
+ read_only_fields: tuple[str, ...] = ("id", "created_date")
abstract = True
- def _auto_add_target_population(self, validated_data: ValidationData) -> None:
+ def _auto_add_target_population(self, validated_data: dict[str, Any]) -> None:
# auto add all target populations of a kind if not specified
if target_populations := validated_data.get("target_populations"):
auto_add_kind = [
@@ -47,15 +45,19 @@ def _auto_add_target_population(self, validated_data: ValidationData) -> None:
else:
validated_data["target_populations"] = list(TargetPopulation.objects.all())
- def create(self, validated_data: ValidationData) -> Poll:
- club_code: ClubCode = validated_data["club_code"]
+ def create(self, validated_data: dict[str, Any]) -> Poll:
+ club_code: str = validated_data["club_code"]
user = self.context["request"].user
# ensures user is part of club
if not any([x["club"]["code"] == club_code for x in get_user_clubs(user)]):
+ model_name = (
+ self.Meta.model._meta.model_name.capitalize()
+ if self.Meta.model._meta.model_name is not None
+ else "content"
+ )
raise serializers.ValidationError(
detail={
- "detail": "You do not have access to create a "
- + f"{self.Meta.model._meta.model_name.capitalize()} under this club."
+ "detail": f"You do not have access to create a {model_name} under this club."
}
)
@@ -69,7 +71,7 @@ def create(self, validated_data: ValidationData) -> Poll:
return super().create(validated_data)
- def update(self, instance: Content, validated_data: ValidationData) -> Content:
+ def update(self, instance: Content, validated_data: dict[str, Any]) -> Content:
# if Content is updated, then approve should be false
if not self.context["request"].user.is_superuser:
validated_data["status"] = Content.STATUS_DRAFT
@@ -82,25 +84,16 @@ def update(self, instance: Content, validated_data: ValidationData) -> Content:
class PollSerializer(ContentSerializer):
class Meta(ContentSerializer.Meta):
model = Poll
- fields = (
- *ContentSerializer.Meta.fields,
- "question",
- "multiselect",
- )
+ fields: tuple[str, ...] = (*ContentSerializer.Meta.fields, "question", "multiselect")
class PollOptionSerializer(serializers.ModelSerializer):
class Meta:
model = PollOption
- fields = (
- "id",
- "poll",
- "choice",
- "vote_count",
- )
- read_only_fields = ("id", "vote_count")
+ fields: tuple[str, ...] = ("id", "poll", "choice", "vote_count")
+ read_only_fields: tuple[str, ...] = ("id", "vote_count")
- def create(self, validated_data: ValidationData) -> PollOption:
+ def create(self, validated_data: dict[str, Any]) -> PollOption:
poll_options_count = PollOption.objects.filter(poll=validated_data["poll"]).count()
if poll_options_count >= 5:
raise serializers.ValidationError(
@@ -108,10 +101,11 @@ def create(self, validated_data: ValidationData) -> PollOption:
)
return super().create(validated_data)
- def update(self, instance, validated_data):
+ def update(self, instance: PollOption, validated_data: dict[str, Any]) -> PollOption:
# if Poll Option is updated, then corresponding Poll approval should be false
- instance.poll.status = Poll.STATUS_DRAFT
- instance.poll.save()
+ poll = cast(Poll, instance.poll)
+ poll.status = Poll.STATUS_DRAFT
+ poll.save()
return super().update(instance, validated_data)
@@ -122,7 +116,7 @@ class RetrievePollSerializer(serializers.ModelSerializer):
class Meta:
model = Poll
- fields = (
+ fields: tuple[str, ...] = (
"id",
"club_code",
"question",
@@ -140,13 +134,10 @@ class Meta:
class PollVoteSerializer(serializers.ModelSerializer):
class Meta:
model = PollVote
- fields = ("id", "id_hash", "poll_options", "created_date")
- read_only_fields = (
- "id",
- "created_date",
- )
+ fields: tuple[str, ...] = ("id", "id_hash", "poll_options", "created_date")
+ read_only_fields: tuple[str, ...] = ("id", "created_date")
- def create(self, validated_data: ValidationData) -> PollVote:
+ def create(self, validated_data: dict[str, Any]) -> PollVote:
options = validated_data["poll_options"]
id_hash = validated_data["id_hash"]
@@ -201,11 +192,8 @@ class RetrievePollVoteSerializer(serializers.ModelSerializer):
class Meta:
model = PollVote
- fields = ("id", "id_hash", "poll", "poll_options", "created_date")
- read_only_fields = (
- "id",
- "created_date",
- )
+ fields: tuple[str, ...] = ("id", "id_hash", "poll", "poll_options", "created_date")
+ read_only_fields: tuple[str, ...] = ("id", "created_date")
class PostSerializer(ContentSerializer):
@@ -229,7 +217,7 @@ def get_image_url(self, obj: Post) -> str | None:
class Meta(ContentSerializer.Meta):
model = Post
- fields = (
+ fields: tuple[str, ...] = (
*ContentSerializer.Meta.fields,
"title",
"subtitle",
@@ -240,12 +228,13 @@ class Meta(ContentSerializer.Meta):
def is_valid(self, *args: Any, **kwargs: Any) -> bool:
if isinstance(self.initial_data, QueryDict):
- self.initial_data = self.initial_data.dict()
- self.initial_data["target_populations"] = list(
- (
- map(int, self.initial_data["target_populations"].split(","))
- if "target_populations" in self.initial_data
+ data = self.initial_data.dict()
+ target_populations = data.get("target_populations", "")
+ if isinstance(target_populations, str):
+ data["target_populations"] = (
+ list(map(int, target_populations.split(",")))
+ if target_populations.strip()
else []
- ),
- )
+ )
+ self.initial_data = data
return super().is_valid(*args, **kwargs)
diff --git a/backend/portal/views.py b/backend/portal/views.py
index 61ac2413..b87b058c 100644
--- a/backend/portal/views.py
+++ b/backend/portal/views.py
@@ -1,6 +1,5 @@
-from typing import Any, Dict, List, TypeAlias
+from typing import Any, List, Optional, TypeAlias
-from django.contrib.auth import get_user_model
from django.db.models import Count, Manager, Q, QuerySet
from django.db.models.functions import Trunc
from django.utils import timezone
@@ -36,17 +35,13 @@
RetrievePollVoteSerializer,
TargetPopulationSerializer,
)
+from utils.types import AuthRequest, get_auth_user
PollQuerySet: TypeAlias = QuerySet[Poll, Manager[Poll]]
PostQuerySet: TypeAlias = QuerySet[Post, Manager[Post]]
PollVoteQuerySet: TypeAlias = QuerySet[PollVote, Manager[PollVote]]
-ClubData: TypeAlias = List[Dict[str, Any]]
PollOptionQuerySet: TypeAlias = QuerySet[PollOption, Manager[PollOption]]
-TimeSeriesData: TypeAlias = Dict[str, Any]
-VoteStatistics: TypeAlias = Dict[str, Any]
-
-User = get_user_model()
class UserInfo(APIView):
@@ -54,7 +49,7 @@ class UserInfo(APIView):
permission_classes = [IsAuthenticated]
- def get(self, request: Request) -> Response:
+ def get(self, request: AuthRequest) -> Response:
return Response({"user": get_user_info(request.user)})
@@ -63,8 +58,8 @@ class UserClubs(APIView):
permission_classes = [IsAuthenticated]
- def get(self, request: Request) -> Response:
- club_data: ClubData = [
+ def get(self, request: AuthRequest) -> Response:
+ club_data = [
get_club_info(request.user, club["club"]["code"])
for club in get_user_clubs(request.user)
]
@@ -104,21 +99,23 @@ class Polls(viewsets.ModelViewSet[Poll]):
def get_queryset(self) -> PollQuerySet:
# all polls if superuser, polls corresponding to club for regular user
+ user = get_auth_user(self.request)
return (
Poll.objects.all()
- if self.request.user.is_superuser
+ if user.is_superuser
else Poll.objects.filter(
- club_code__in=[x["club"]["code"] for x in get_user_clubs(self.request.user)]
+ club_code__in=[x["club"]["code"] for x in get_user_clubs(user)]
)
)
@action(detail=False, methods=["post"])
- def browse(self, request: Request) -> Response:
+ def browse(self, request: AuthRequest) -> Response:
"""Returns list of all possible polls user can answer but has yet to
For admins, returns list of all polls they have not voted for and have yet to expire
"""
id_hash = request.data["id_hash"]
+ user = get_auth_user(request)
# unvoted polls in draft/approaved mode for superuser
# unvoted and approved polls within time frame for regular user
@@ -128,7 +125,7 @@ def browse(self, request: Request) -> Response:
Q(status=Poll.STATUS_DRAFT) | Q(status=Poll.STATUS_APPROVED),
expire_date__gte=timezone.localtime(),
)
- if request.user.is_superuser
+ if user.is_superuser
else Poll.objects.filter(
~Q(id__in=PollVote.objects.filter(id_hash=id_hash).values_list("poll_id")),
status=Poll.STATUS_APPROVED,
@@ -140,9 +137,9 @@ def browse(self, request: Request) -> Response:
# list of polls where user doesn't identify with
# target populations
bad_polls = []
- if not request.user.is_superuser:
+ if not user.is_superuser:
for unfiltered_poll in unfiltered_polls:
- if not check_targets(unfiltered_poll, request.user):
+ if not check_targets(unfiltered_poll, user):
bad_polls.append(unfiltered_poll.id)
# excludes the bad polls
@@ -175,7 +172,7 @@ def review(self, request: Request) -> Response:
)
@action(detail=True, methods=["get"])
- def option_view(self, request: Request, pk: int = None) -> Response:
+ def option_view(self, request: Request, pk: Optional[int] = None) -> Response:
"""Returns information on specific poll, including options and vote counts"""
return Response(RetrievePollSerializer(Poll.objects.filter(id=pk).first(), many=False).data)
@@ -199,12 +196,13 @@ class PollOptions(viewsets.ModelViewSet[PollOption]):
def get_queryset(self) -> PollOptionQuerySet:
# if user is admin, they can update anything
# if user is not admin, they can only update their own options
+ user = get_auth_user(self.request)
return (
PollOption.objects.all()
- if self.request.user.is_superuser
+ if user.is_superuser
else PollOption.objects.filter(
poll__in=Poll.objects.filter(
- club_code__in=[x["club"]["code"] for x in get_user_clubs(self.request.user)]
+ club_code__in=[x["club"]["code"] for x in get_user_clubs(user)]
)
)
)
@@ -239,7 +237,8 @@ def all(self, request: Request) -> Response:
return Response(RetrievePollVoteSerializer(poll_votes, many=True).data)
def create(self, request: Request, *args: Any, **kwargs: Any) -> Response:
- record_analytics(Metric.PORTAL_POLL_VOTED, request.user.username)
+ user = get_auth_user(request)
+ record_analytics(Metric.PORTAL_POLL_VOTED, user.username)
return super().create(request, *args, **kwargs)
@@ -257,7 +256,7 @@ def get(self, request: Request, poll_id: int) -> Response:
.order_by("date")
)
- statistics: VoteStatistics = {
+ statistics: dict[str, Any] = {
"time_series": time_series,
"poll_statistics": get_demographic_breakdown(poll_id),
}
@@ -286,26 +285,28 @@ class Posts(viewsets.ModelViewSet[Post]):
serializer_class = PostSerializer
def get_queryset(self) -> PostQuerySet:
+ user = get_auth_user(self.request)
return (
Post.objects.all()
- if self.request.user.is_superuser
+ if user.is_superuser
else Post.objects.filter(
- club_code__in=[x["club"]["code"] for x in get_user_clubs(self.request.user)]
+ club_code__in=[x["club"]["code"] for x in get_user_clubs(user)]
)
)
@action(detail=False, methods=["get"])
- def browse(self, request: Request) -> Response:
+ def browse(self, request: AuthRequest) -> Response:
"""
Returns a list of all posts that are targeted at the current user
For admins, returns list of posts that they have not approved and have yet to expire
"""
+ user = get_auth_user(request)
unfiltered_posts = (
Post.objects.filter(
Q(status=Post.STATUS_DRAFT) | Q(status=Post.STATUS_APPROVED),
expire_date__gte=timezone.localtime(),
)
- if request.user.is_superuser
+ if user.is_superuser
else Post.objects.filter(
status=Post.STATUS_APPROVED,
start_date__lte=timezone.localtime(),
diff --git a/backend/setup.cfg b/backend/setup.cfg
index 278639ed..afca0145 100644
--- a/backend/setup.cfg
+++ b/backend/setup.cfg
@@ -2,7 +2,8 @@
max-line-length = 100
exclude = .venv, migrations
inline-quotes = double
-ignore = E203, W503
+ignore = E203, W503, E704
+extend-select = F841, F401
[isort]
default_section = THIRDPARTY
diff --git a/backend/sublet/admin.py b/backend/sublet/admin.py
index 0dbdd521..8e7fa78a 100644
--- a/backend/sublet/admin.py
+++ b/backend/sublet/admin.py
@@ -1,15 +1,15 @@
from django.contrib import admin
-from django.utils.html import mark_safe
+from django.utils.safestring import SafeText, mark_safe
from sublet.models import Amenity, Offer, Sublet, SubletImage
class SubletAdmin(admin.ModelAdmin):
- def image_tag(self, instance):
+ def image_tag(self, instance: Sublet) -> SafeText:
images = ['
' for image in instance.images.all()]
return mark_safe("
".join(images))
- image_tag.short_description = "Sublet Images"
+ image_tag.short_description = "Sublet Images" # type: ignore[attr-defined]
readonly_fields = ("image_tag",)
diff --git a/backend/sublet/migrations/0001_initial.py b/backend/sublet/migrations/0001_initial.py
index 5245debc..7e7f7948 100644
--- a/backend/sublet/migrations/0001_initial.py
+++ b/backend/sublet/migrations/0001_initial.py
@@ -10,16 +10,12 @@ class Migration(migrations.Migration):
initial = True
- dependencies = [
- migrations.swappable_dependency(settings.AUTH_USER_MODEL),
- ]
+ dependencies = [migrations.swappable_dependency(settings.AUTH_USER_MODEL)]
operations = [
migrations.CreateModel(
name="Amenity",
- fields=[
- ("name", models.CharField(max_length=255, primary_key=True, serialize=False)),
- ],
+ fields=[("name", models.CharField(max_length=255, primary_key=True, serialize=False))],
),
migrations.CreateModel(
name="Offer",
diff --git a/backend/sublet/migrations/0002_auto_20240209_1649.py b/backend/sublet/migrations/0002_auto_20240209_1649.py
index 35943aab..85d431b6 100644
--- a/backend/sublet/migrations/0002_auto_20240209_1649.py
+++ b/backend/sublet/migrations/0002_auto_20240209_1649.py
@@ -5,23 +5,12 @@
class Migration(migrations.Migration):
- dependencies = [
- ("sublet", "0001_initial"),
- ]
+ dependencies = [("sublet", "0001_initial")]
operations = [
- migrations.RenameField(
- model_name="sublet",
- old_name="max_price",
- new_name="price",
- ),
- migrations.RemoveField(
- model_name="sublet",
- name="min_price",
- ),
+ migrations.RenameField(model_name="sublet", old_name="max_price", new_name="price"),
+ migrations.RemoveField(model_name="sublet", name="min_price"),
migrations.AddField(
- model_name="sublet",
- name="negotiable",
- field=models.BooleanField(default=True),
+ model_name="sublet", name="negotiable", field=models.BooleanField(default=True)
),
]
diff --git a/backend/sublet/migrations/0003_alter_sublet_baths.py b/backend/sublet/migrations/0003_alter_sublet_baths.py
index 7f8b65b9..bc0e7280 100644
--- a/backend/sublet/migrations/0003_alter_sublet_baths.py
+++ b/backend/sublet/migrations/0003_alter_sublet_baths.py
@@ -5,14 +5,12 @@
class Migration(migrations.Migration):
- dependencies = [
- ("sublet", "0002_auto_20240209_1649"),
- ]
+ dependencies = [("sublet", "0002_auto_20240209_1649")]
operations = [
migrations.AlterField(
model_name="sublet",
name="baths",
field=models.DecimalField(blank=True, decimal_places=1, max_digits=3, null=True),
- ),
+ )
]
diff --git a/backend/sublet/migrations/0004_alter_sublet_external_link.py b/backend/sublet/migrations/0004_alter_sublet_external_link.py
index 5e01a6ce..01a30b39 100644
--- a/backend/sublet/migrations/0004_alter_sublet_external_link.py
+++ b/backend/sublet/migrations/0004_alter_sublet_external_link.py
@@ -5,14 +5,12 @@
class Migration(migrations.Migration):
- dependencies = [
- ("sublet", "0003_alter_sublet_baths"),
- ]
+ dependencies = [("sublet", "0003_alter_sublet_baths")]
operations = [
migrations.AlterField(
model_name="sublet",
name="external_link",
field=models.URLField(blank=True, max_length=255, null=True),
- ),
+ )
]
diff --git a/backend/sublet/models.py b/backend/sublet/models.py
index e391053a..4447b9d4 100644
--- a/backend/sublet/models.py
+++ b/backend/sublet/models.py
@@ -1,5 +1,6 @@
from django.contrib.auth import get_user_model
from django.db import models
+from django.db.models import QuerySet
from phonenumber_field.modelfields import PhoneNumberField
@@ -10,49 +11,64 @@ class Offer(models.Model):
class Meta:
constraints = [models.UniqueConstraint(fields=["user", "sublet"], name="unique_offer")]
- user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="offers_made")
- sublet = models.ForeignKey("Sublet", on_delete=models.CASCADE, related_name="offers")
- email = models.EmailField(max_length=255, null=True, blank=True)
- phone_number = PhoneNumberField(null=True, blank=True)
- message = models.CharField(max_length=255, blank=True)
- created_date = models.DateTimeField(auto_now_add=True)
+ id: int
+ user: models.ForeignKey = models.ForeignKey(
+ User, on_delete=models.CASCADE, related_name="offers_made"
+ )
+ sublet: models.ForeignKey = models.ForeignKey(
+ "Sublet", on_delete=models.CASCADE, related_name="offers"
+ )
+ email: models.EmailField = models.EmailField(max_length=255, null=True, blank=True)
+ phone_number: PhoneNumberField = PhoneNumberField(null=True, blank=True)
+ message: models.CharField = models.CharField(max_length=255, blank=True)
+ created_date: models.DateTimeField = models.DateTimeField(auto_now_add=True)
- def __str__(self):
+ def __str__(self) -> str:
return f"Offer for {self.sublet} made by {self.user}"
class Amenity(models.Model):
- name = models.CharField(max_length=255, primary_key=True)
+ name: models.CharField = models.CharField(max_length=255, primary_key=True)
- def __str__(self):
+ def __str__(self) -> str:
return self.name
class Sublet(models.Model):
- subletter = models.ForeignKey(User, on_delete=models.CASCADE)
- sublettees = models.ManyToManyField(
+ id: int
+ subletter: models.ForeignKey = models.ForeignKey(User, on_delete=models.CASCADE)
+ sublettees: models.ManyToManyField = models.ManyToManyField(
User, through=Offer, related_name="sublets_offered", blank=True
)
- favorites = models.ManyToManyField(User, related_name="sublets_favorited", blank=True)
- amenities = models.ManyToManyField(Amenity, blank=True)
-
- title = models.CharField(max_length=255)
- address = models.CharField(max_length=255, null=True, blank=True)
- beds = models.IntegerField(null=True, blank=True)
- baths = models.DecimalField(max_digits=3, decimal_places=1, null=True, blank=True)
- description = models.TextField(null=True, blank=True)
- external_link = models.URLField(max_length=255, null=True, blank=True)
- price = models.IntegerField()
- negotiable = models.BooleanField(default=True)
- created_at = models.DateTimeField(auto_now_add=True)
- expires_at = models.DateTimeField()
- start_date = models.DateField()
- end_date = models.DateField()
-
- def __str__(self):
+ favorites: models.ManyToManyField = models.ManyToManyField(
+ User, related_name="sublets_favorited", blank=True
+ )
+ amenities: models.ManyToManyField = models.ManyToManyField(Amenity, blank=True)
+
+ title: models.CharField = models.CharField(max_length=255)
+ address: models.CharField = models.CharField(max_length=255, null=True, blank=True)
+ beds: models.IntegerField = models.IntegerField(null=True, blank=True)
+ baths: models.DecimalField = models.DecimalField(
+ max_digits=3, decimal_places=1, null=True, blank=True
+ )
+ description: models.TextField = models.TextField(null=True, blank=True)
+ external_link: models.URLField = models.URLField(max_length=255, null=True, blank=True)
+ price: models.IntegerField = models.IntegerField()
+ negotiable: models.BooleanField = models.BooleanField(default=True)
+ created_at: models.DateTimeField = models.DateTimeField(auto_now_add=True)
+ expires_at: models.DateTimeField = models.DateTimeField()
+ start_date: models.DateField = models.DateField()
+ end_date: models.DateField = models.DateField()
+
+ images: QuerySet
+
+ def __str__(self) -> str:
return f"{self.title} by {self.subletter}"
class SubletImage(models.Model):
- sublet = models.ForeignKey(Sublet, on_delete=models.CASCADE, related_name="images")
- image = models.ImageField(upload_to="sublet/images")
+ id: int
+ sublet: models.ForeignKey = models.ForeignKey(
+ Sublet, on_delete=models.CASCADE, related_name="images"
+ )
+ image: models.ImageField = models.ImageField(upload_to="sublet/images")
diff --git a/backend/sublet/permissions.py b/backend/sublet/permissions.py
index c1aeb314..69a5f3de 100644
--- a/backend/sublet/permissions.py
+++ b/backend/sublet/permissions.py
@@ -1,4 +1,9 @@
+from typing import Any
+
from rest_framework import permissions
+from rest_framework.request import Request
+
+from utils.types import get_auth_user
class IsSuperUser(permissions.BasePermission):
@@ -6,11 +11,11 @@ class IsSuperUser(permissions.BasePermission):
Grants permission if the current user is a superuser.
"""
- def has_object_permission(self, request, view, obj):
- return request.user.is_superuser
+ def has_object_permission(self, request: Request, view: Any, obj: Any) -> bool:
+ return get_auth_user(request).is_superuser
- def has_permission(self, request, view):
- return request.user.is_superuser
+ def has_permission(self, request: Request, view: Any) -> bool:
+ return get_auth_user(request).is_superuser
class SubletOwnerPermission(permissions.BasePermission):
@@ -18,14 +23,14 @@ class SubletOwnerPermission(permissions.BasePermission):
Custom permission to allow the owner of a Sublet to edit or delete it.
"""
- def has_permission(self, request, view):
- return request.user.is_authenticated
+ def has_permission(self, request: Request, view: Any) -> bool:
+ return get_auth_user(request).is_authenticated
- def has_object_permission(self, request, view, obj):
+ def has_object_permission(self, request: Request, view: Any, obj: Any) -> bool:
# Check if the user is the owner of the Sublet.
if request.method in permissions.SAFE_METHODS:
return True
- return obj.subletter == request.user
+ return obj.subletter == get_auth_user(request)
class SubletImageOwnerPermission(permissions.BasePermission):
@@ -33,12 +38,14 @@ class SubletImageOwnerPermission(permissions.BasePermission):
Custom permission to allow the owner of a SubletImage to edit or delete it.
"""
- def has_permission(self, request, view):
- return request.user.is_authenticated
+ def has_permission(self, request: Request, view: Any) -> bool:
+ return get_auth_user(request).is_authenticated
- def has_object_permission(self, request, view, obj):
+ def has_object_permission(self, request: Request, view: Any, obj: Any) -> bool:
# Check if the user is the owner of the Sublet.
- return request.method in permissions.SAFE_METHODS or obj.sublet.subletter == request.user
+ return request.method in permissions.SAFE_METHODS or obj.sublet.subletter == get_auth_user(
+ request
+ )
class OfferOwnerPermission(permissions.BasePermission):
@@ -46,12 +53,12 @@ class OfferOwnerPermission(permissions.BasePermission):
Custom permission to allow owner of an offer to delete it.
"""
- def has_permission(self, request, view):
- return request.user.is_authenticated
+ def has_permission(self, request: Request, view: Any) -> bool:
+ return get_auth_user(request).is_authenticated
- def has_object_permission(self, request, view, obj):
+ def has_object_permission(self, request: Request, view: Any, obj: Any) -> bool:
if request.method in permissions.SAFE_METHODS:
# Check if the user owns the sublet when getting list
- return obj.subletter == request.user
+ return obj.subletter == get_auth_user(request)
# This is redundant, here for safety
- return obj.user == request.user
+ return obj.user == get_auth_user(request)
diff --git a/backend/sublet/serializers.py b/backend/sublet/serializers.py
index 5622ae07..e211b675 100644
--- a/backend/sublet/serializers.py
+++ b/backend/sublet/serializers.py
@@ -1,12 +1,13 @@
-from datetime import datetime
from typing import Any, Optional, cast
from phonenumber_field.serializerfields import PhoneNumberField
from profanity_check import predict
from rest_framework import serializers
+from rest_framework.relations import PrimaryKeyRelatedField
from rest_framework.request import Request
from sublet.models import Amenity, Offer, Sublet, SubletImage
+from utils.types import get_auth_user
class BaseModelSerializer(serializers.ModelSerializer):
@@ -15,7 +16,7 @@ def get_request(self) -> Request:
class AmenitySerializer(BaseModelSerializer):
- name: str = serializers.CharField(max_length=255)
+ name = serializers.CharField(max_length=255)
class Meta:
model = Amenity
@@ -23,10 +24,10 @@ class Meta:
class OfferSerializer(BaseModelSerializer):
- phone_number: str = PhoneNumberField()
- email: Optional[str] = serializers.EmailField(allow_null=True)
- message: str = serializers.CharField(max_length=255)
- created_date: datetime = serializers.DateTimeField(read_only=True)
+ phone_number = PhoneNumberField()
+ email = serializers.EmailField(allow_null=True)
+ message = serializers.CharField(max_length=255)
+ created_date = serializers.DateTimeField(read_only=True)
class Meta:
model = Offer
@@ -51,7 +52,7 @@ class Meta:
class SubletImageURLSerializer(BaseModelSerializer):
image_url = serializers.SerializerMethodField("get_image_url")
- def get_image_url(self, obj) -> Optional[str]:
+ def get_image_url(self, obj: SubletImage) -> Optional[str]:
if not obj.image:
return None
@@ -71,7 +72,7 @@ class Meta:
class SubletSerializer(BaseModelSerializer):
# amenities = AmenitySerializer(many=True, required=False)
# images = SubletImageURLSerializer(many=True, required=False)
- amenities: list[Amenity] = serializers.PrimaryKeyRelatedField(
+ amenities: PrimaryKeyRelatedField = serializers.PrimaryKeyRelatedField(
many=True, queryset=Amenity.objects.all(), required=False
)
@@ -126,7 +127,7 @@ def create(self, validated_data: dict[str, Any]) -> Sublet:
# delete_images is a list of image ids to delete
def update(self, instance: Sublet, validated_data: dict[str, Any]) -> Sublet:
# Check if the user is the subletter before allowing the update
- user = self.get_request().user
+ user = get_auth_user(self.get_request())
if user == instance.subletter or user.is_superuser:
instance = super().update(instance, validated_data)
instance.save()
@@ -135,7 +136,7 @@ def update(self, instance: Sublet, validated_data: dict[str, Any]) -> Sublet:
def destroy(self, instance: Sublet) -> None:
# Check if the user is the subletter before allowing the delete
- user = self.get_request().user
+ user = get_auth_user(self.get_request())
if user == instance.subletter or user.is_superuser:
instance.delete()
else:
@@ -143,10 +144,10 @@ def destroy(self, instance: Sublet) -> None:
class SubletSerializerRead(BaseModelSerializer):
- amenities: list[Amenity] = serializers.PrimaryKeyRelatedField(
+ amenities: PrimaryKeyRelatedField = serializers.PrimaryKeyRelatedField(
many=True, queryset=Amenity.objects.all(), required=False
)
- images: list[SubletImage] = SubletImageURLSerializer(many=True, required=False)
+ images = SubletImageURLSerializer(many=True, required=False)
class Meta:
model = Sublet
@@ -178,10 +179,10 @@ def to_representation(self, instance: Sublet) -> dict[str, Any]:
# simple sublet serializer for use when pulling all serializers/etc
class SubletSerializerSimple(BaseModelSerializer):
- amenities: list[Amenity] = serializers.PrimaryKeyRelatedField(
+ amenities: PrimaryKeyRelatedField = serializers.PrimaryKeyRelatedField(
many=True, queryset=Amenity.objects.all(), required=False
)
- images: list[SubletImage] = SubletImageURLSerializer(many=True, required=False)
+ images = SubletImageURLSerializer(many=True, required=False)
class Meta:
model = Sublet
diff --git a/backend/sublet/views.py b/backend/sublet/views.py
index 3509a1aa..c749514c 100644
--- a/backend/sublet/views.py
+++ b/backend/sublet/views.py
@@ -1,7 +1,7 @@
-from typing import Any, TypeAlias
+from typing import Any, Type, TypeAlias, cast
-from django.contrib.auth import get_user_model
from django.db.models import Manager, QuerySet, prefetch_related_objects
+from django.http import QueryDict
from django.utils import timezone
from django.utils.dateparse import parse_date
from rest_framework import exceptions, generics, mixins, status, viewsets
@@ -21,6 +21,7 @@
)
from sublet.serializers import (
AmenitySerializer,
+ BaseModelSerializer,
OfferSerializer,
SubletImageSerializer,
SubletImageURLSerializer,
@@ -28,16 +29,15 @@
SubletSerializerRead,
SubletSerializerSimple,
)
+from utils.types import get_user
SubletQuerySet: TypeAlias = QuerySet[Sublet, Manager[Sublet]]
OfferQuerySet: TypeAlias = QuerySet[Offer, Manager[Offer]]
+AmenityQuerySet: TypeAlias = QuerySet[Amenity, Manager[Amenity]]
ImageList: TypeAlias = QuerySet[SubletImage, Manager[SubletImage]]
-FavoriteQuerySet: TypeAlias = QuerySet[Sublet, Manager[Sublet]]
UserOfferQuerySet: TypeAlias = QuerySet[Offer, Manager[Offer]]
-User = get_user_model()
-
class Amenities(generics.ListAPIView):
serializer_class = AmenitySerializer
@@ -52,9 +52,8 @@ class UserFavorites(generics.ListAPIView):
serializer_class = SubletSerializerSimple
permission_classes = [IsAuthenticated]
- def get_queryset(self) -> FavoriteQuerySet:
- user = self.request.user
- return user.sublets_favorited
+ def get_queryset(self) -> SubletQuerySet:
+ return get_user(self.request).sublets_favorited
class UserOffers(generics.ListAPIView):
@@ -62,8 +61,7 @@ class UserOffers(generics.ListAPIView):
permission_classes = [IsAuthenticated]
def get_queryset(self) -> UserOfferQuerySet:
- user = self.request.user
- return Offer.objects.filter(user=user)
+ return Offer.objects.filter(user=get_user(self.request))
class Properties(viewsets.ModelViewSet):
@@ -83,7 +81,7 @@ class Properties(viewsets.ModelViewSet):
permission_classes = [SubletOwnerPermission | IsSuperUser]
- def get_serializer_class(self):
+ def get_serializer_class(self) -> Type[BaseModelSerializer]:
return SubletSerializerRead if self.action == "retrieve" else SubletSerializer
def get_queryset(self) -> SubletQuerySet:
@@ -95,7 +93,7 @@ def create(self, request: Request, *args: Any, **kwargs: Any) -> Response:
instance = serializer.save() # Create the Sublet
instance_serializer = SubletSerializerRead(instance=instance, context={"request": request})
- record_analytics(Metric.SUBLET_CREATED, request.user.username)
+ record_analytics(Metric.SUBLET_CREATED, get_user(request).username)
return Response(instance_serializer.data, status=status.HTTP_201_CREATED)
@@ -108,7 +106,7 @@ def update(self, request: Request, *args: Any, **kwargs: Any) -> Response:
queryset = self.filter_queryset(self.get_queryset())
# no clue what this does but I copied it from the DRF source code
- if queryset._prefetch_related_lookups:
+ if hasattr(queryset, "_prefetch_related_lookups"):
# If 'prefetch_related' has been applied to a queryset, we need to
# forcibly invalidate the prefetch cache on the instance,
# and then re-prefetch related objects
@@ -142,7 +140,7 @@ def list(self, request: Request, *args: Any, **kwargs: Any) -> Response:
queryset: SubletQuerySet = self.get_queryset()
if params.get("subletter", "false").lower() == "true":
- queryset = queryset.filter(subletter=request.user)
+ queryset = queryset.filter(subletter=get_user(request))
else:
queryset = queryset.filter(expires_at__gte=timezone.now())
@@ -188,7 +186,7 @@ def list(self, request: Request, *args: Any, **kwargs: Any) -> Response:
for amenity in amenities:
queryset = queryset.filter(amenities__name=amenity)
- record_analytics(Metric.SUBLET_BROWSE, request.user.username)
+ record_analytics(Metric.SUBLET_BROWSE, get_user(request).username)
serializer = SubletSerializerSimple(queryset, many=True)
return Response(serializer.data)
@@ -198,10 +196,7 @@ class CreateImages(generics.CreateAPIView):
serializer_class = SubletImageSerializer
http_method_names = ["post"]
permission_classes = [SubletImageOwnerPermission | IsSuperUser]
- parser_classes = (
- MultiPartParser,
- FormParser,
- )
+ parser_classes = (MultiPartParser, FormParser)
def get_queryset(self, *args: Any, **kwargs: Any) -> ImageList:
sublet = get_object_or_404(Sublet, id=int(self.kwargs["sublet_id"]))
@@ -209,7 +204,8 @@ def get_queryset(self, *args: Any, **kwargs: Any) -> ImageList:
# takes an image multipart form data and creates a new image object
def post(self, request: Request, *args: Any, **kwargs: Any) -> Response:
- images = request.data.getlist("images")
+ data = cast(QueryDict, request.data)
+ images = data.getlist("images")
sublet_id = int(self.kwargs["sublet_id"])
self.get_queryset() # check if sublet exists
img_serializers = []
@@ -218,8 +214,10 @@ def post(self, request: Request, *args: Any, **kwargs: Any) -> Response:
img_serializer.is_valid(raise_exception=True)
img_serializers.append(img_serializer)
instances = [img_serializer.save() for img_serializer in img_serializers]
- data = [SubletImageURLSerializer(instance=instance).data for instance in instances]
- return Response(data, status=status.HTTP_201_CREATED)
+ serialized_data = [
+ SubletImageURLSerializer(instance=instance).data for instance in instances
+ ]
+ return Response(serialized_data, status=status.HTTP_201_CREATED)
class DeleteImage(generics.DestroyAPIView):
@@ -231,7 +229,7 @@ class DeleteImage(generics.DestroyAPIView):
def destroy(self, request: Request, *args: Any, **kwargs: Any) -> Response:
queryset = self.get_queryset()
filter = {"id": self.kwargs["image_id"]}
- obj = get_object_or_404(queryset, **filter)
+ obj: SubletImage = get_object_or_404(queryset, **filter)
# checking permissions here is kind of redundant
self.check_object_permissions(self.request, obj)
self.perform_destroy(obj)
@@ -243,8 +241,8 @@ class Favorites(mixins.DestroyModelMixin, mixins.CreateModelMixin, viewsets.Gene
http_method_names = ["post", "delete"]
permission_classes = [IsAuthenticated | IsSuperUser]
- def get_queryset(self) -> FavoriteQuerySet:
- user = self.request.user
+ def get_queryset(self) -> SubletQuerySet:
+ user = get_user(self.request)
return user.sublets_favorited
def create(self, request: Request, *args: Any, **kwargs: Any) -> Response:
@@ -253,16 +251,16 @@ def create(self, request: Request, *args: Any, **kwargs: Any) -> Response:
if queryset.filter(id=sublet_id).exists():
raise exceptions.NotAcceptable("Favorite already exists")
sublet = get_object_or_404(Sublet, id=sublet_id)
- self.get_queryset().add(sublet)
+ get_user(self.request).sublets_favorited.add(sublet)
- record_analytics(Metric.SUBLET_FAVORITED, request.user.username)
+ record_analytics(Metric.SUBLET_FAVORITED, get_user(request).username)
return Response(status=status.HTTP_201_CREATED)
def destroy(self, request: Request, *args: Any, **kwargs: Any) -> Response:
- queryset = self.get_queryset()
- sublet = get_object_or_404(queryset, pk=int(self.kwargs["sublet_id"]))
- self.get_queryset().remove(sublet)
+ queryset = cast(QuerySet, self.get_queryset())
+ sublet: Sublet = get_object_or_404(queryset, pk=int(self.kwargs["sublet_id"]))
+ get_user(self.request).sublets_favorited.remove(sublet)
return Response(status=status.HTTP_204_NO_CONTENT)
@@ -288,22 +286,23 @@ def get_queryset(self) -> OfferQuerySet:
def create(self, request: Request, *args: Any, **kwargs: Any) -> Response:
data = request.data
- request.POST._mutable = True
- if self.get_queryset().filter(user=self.request.user).exists():
+ if isinstance(data, QueryDict):
+ data._mutable = True
+ if self.get_queryset().filter(user=get_user(self.request)).exists():
raise exceptions.NotAcceptable("Offer already exists")
data["sublet"] = int(self.kwargs["sublet_id"])
- data["user"] = self.request.user.id
+ data["user"] = get_user(self.request).id
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save()
- record_analytics(Metric.SUBLET_OFFER, request.user.username)
+ record_analytics(Metric.SUBLET_OFFER, get_user(request).username)
return Response(serializer.data, status=status.HTTP_201_CREATED)
def destroy(self, request: Request, *args: Any, **kwargs: Any) -> Response:
queryset = self.get_queryset()
- filter = {"user": self.request.user.id, "sublet": int(self.kwargs["sublet_id"])}
+ filter = {"user": get_user(self.request).id, "sublet": int(self.kwargs["sublet_id"])}
obj: Offer = get_object_or_404(queryset, **filter)
# checking permissions here is kind of redundant
self.check_object_permissions(self.request, obj)
diff --git a/backend/tests/dining/test_load_venues.py b/backend/tests/dining/test_load_venues.py
index 596027ab..91b66586 100644
--- a/backend/tests/dining/test_load_venues.py
+++ b/backend/tests/dining/test_load_venues.py
@@ -7,13 +7,13 @@
class TestLoadVenues(TestCase):
- def test(self):
+ def test(self) -> None:
out = StringIO()
call_command("load_venues", stdout=out)
self.assertEqual(len(Venue.objects.all()), 16)
- list_of_ids = []
+ list_of_ids: list[int] = []
for venue in Venue.objects.all():
self.assertNotIn(venue.venue_id, list_of_ids)
list_of_ids.append(venue.venue_id)
diff --git a/backend/tests/dining/test_views.py b/backend/tests/dining/test_views.py
index 2ab0c474..82d18049 100644
--- a/backend/tests/dining/test_views.py
+++ b/backend/tests/dining/test_views.py
@@ -1,8 +1,8 @@
import datetime
import json
+from typing import Any
from unittest import mock
-from django.contrib.auth import get_user_model
from django.core.management import call_command
from django.test import TestCase
from django.urls import reverse
@@ -10,18 +10,16 @@
from dining.api_wrapper import APIError, DiningAPIWrapper
from dining.models import DiningMenu, Venue
+from utils.types import DjangoUserModel, UserType
-User = get_user_model()
-
-
-def mock_dining_requests(url, *args, **kwargs):
+def mock_dining_requests(url: str, *args: Any, **kwargs: Any) -> Any:
class Mock:
- def __init__(self, json_data, status_code):
+ def __init__(self, json_data: dict, status_code: int) -> None:
self.json_data = json_data
self.status_code = status_code
- def json(self):
+ def json(self) -> dict:
return self.json_data
if "token" in url:
@@ -37,23 +35,23 @@ def json(self):
return Mock(json.load(data), 200)
-def mock_request_raise_error(*args, **kwargs):
+def mock_request_raise_error(*args: Any, **kwargs: Any) -> None:
raise ConnectionError
-def mock_request_post_error(*args, **kwargs):
+def mock_request_post_error(*args: Any, **kwargs: Any) -> Any:
class Mock:
- def json(self):
+ def json(self) -> dict:
return {"error": None}
return Mock()
class TestTokenAndRequest(TestCase):
- def setUp(self):
+ def setUp(self) -> None:
self.wrapper = DiningAPIWrapper()
- def test_expired_token(self):
+ def test_expired_token(self) -> None:
self.wrapper.expiration += datetime.timedelta(days=1)
prev_token = self.wrapper.token
prev_expiration = self.wrapper.expiration
@@ -65,19 +63,19 @@ def test_expired_token(self):
self.assertEqual(prev_expiration, self.wrapper.expiration)
@mock.patch("requests.post", mock_request_post_error)
- def test_update_token_error(self):
+ def test_update_token_error(self) -> None:
with self.assertRaises(APIError):
self.wrapper.update_token()
@mock.patch("requests.post", mock_dining_requests)
@mock.patch("requests.request", lambda **kwargs: None)
- def test_request_headers_update(self):
+ def test_request_headers_update(self) -> None:
res = self.wrapper.request(headers=dict())
self.assertIsNone(res)
@mock.patch("requests.post", mock_dining_requests)
@mock.patch("requests.request", mock_request_raise_error)
- def test_request_api_error(self):
+ def test_request_api_error(self) -> None:
with self.assertRaises(APIError):
self.wrapper.request()
@@ -85,10 +83,10 @@ def test_request_api_error(self):
@mock.patch("requests.post", mock_dining_requests)
@mock.patch("requests.request", mock_dining_requests)
class TestVenues(TestCase):
- def setUp(self):
+ def setUp(self) -> None:
call_command("load_venues")
- def test_get(self):
+ def test_get(self) -> None:
response = self.client.get(reverse("venues"))
for entry in response.json():
self.assertIn("name", entry)
@@ -104,7 +102,7 @@ def test_get(self):
class TestMenus(TestCase):
@mock.patch("requests.post", mock_dining_requests)
@mock.patch("requests.request", mock_dining_requests)
- def setUp(self):
+ def setUp(self) -> None:
Venue.objects.create(
venue_id=593,
name="1920 Commons",
@@ -112,7 +110,7 @@ def setUp(self):
)
call_command("load_next_menu")
- def try_structure(self, data):
+ def try_structure(self, data: list[dict]) -> None:
for entry in data:
self.assertIn("venue", entry)
self.assertIn("date", entry)
@@ -131,16 +129,16 @@ def try_structure(self, data):
self.assertIn("allergens", item)
self.assertIn("nutrition_info", item)
- def test_get_default(self):
+ def test_get_default(self) -> None:
response = self.client.get(reverse("menus"))
self.try_structure(response.json())
- def test_get_date(self):
+ def test_get_date(self) -> None:
response = self.client.get("/dining/menus/2022-10-04/")
self.try_structure(response.json())
@mock.patch("requests.request", mock_dining_requests)
- def test_skip_venue(self):
+ def test_skip_venue(self) -> None:
Venue.objects.all().delete()
Venue.objects.create(venue_id=747, name="Skip", image_url="URL")
wrapper = DiningAPIWrapper()
@@ -149,11 +147,11 @@ def test_skip_venue(self):
class TestPreferences(TestCase):
- def setUp(self):
+ def setUp(self) -> None:
call_command("load_venues")
- self.client = APIClient()
+ self.client: APIClient = APIClient()
- self.test_user = User.objects.create_user("user", "user@a.com", "user")
+ self.test_user: UserType = DjangoUserModel.objects.create_user("user", "user@a.com", "user")
preference = self.test_user.profile.dining_preferences
preference.add(Venue.objects.get(venue_id=593))
@@ -163,7 +161,7 @@ def setUp(self):
preference.add(Venue.objects.get(venue_id=636))
preference.add(Venue.objects.get(venue_id=637))
- def test_get(self):
+ def test_get(self) -> None:
self.client.force_authenticate(user=self.test_user)
response = self.client.get(reverse("dining-preferences"))
@@ -177,7 +175,7 @@ def test_get(self):
else:
self.assertEqual(item["count"], 1)
- def test_post(self):
+ def test_post(self) -> None:
self.client.force_authenticate(user=self.test_user)
self.client.post(
reverse("dining-preferences"),
diff --git a/backend/tests/gsr_booking/test_gsr_booking.py b/backend/tests/gsr_booking/test_gsr_booking.py
index 1559dbbd..89b0bdb0 100644
--- a/backend/tests/gsr_booking/test_gsr_booking.py
+++ b/backend/tests/gsr_booking/test_gsr_booking.py
@@ -1,89 +1,87 @@
-from django.contrib.auth import get_user_model
from django.test import TestCase
from rest_framework.test import APIClient
from gsr_booking.models import Group, GroupMembership
+from utils.types import DjangoUserModel, UserType
-User = get_user_model()
-
-
-class UserViewTestCase(TestCase):
- def setUp(self):
- self.user1 = User.objects.create_user(
+class MyMembershipViewTestCase(TestCase):
+ def setUp(self) -> None:
+ self.user1: UserType = DjangoUserModel.objects.create_user(
username="user1", password="password", first_name="user", last_name="one"
)
- self.user2 = User.objects.create_user(
+ self.user2: UserType = DjangoUserModel.objects.create_user(
username="user2", password="password", first_name="user", last_name="two"
)
- self.group = Group.objects.create(owner=self.user1, name="g1", color="blue")
- self.group.members.add(self.user1)
- memship = self.group.memberships.all()[0]
- memship.accepted = True
- memship.save()
+ Group.objects.create(
+ owner=self.user1, name="g1", color="blue"
+ ) # creating group also adds user
+ group2 = Group.objects.create(owner=self.user2, name="g2", color="blue")
+ GroupMembership.objects.create(user=self.user1, group=group2, accepted=True)
+ group3 = Group.objects.create(owner=self.user2, name="g3", color="blue")
+ GroupMembership.objects.create(user=self.user1, group=group3)
self.client = APIClient()
self.client.login(username="user1", password="password")
- def test_user_list(self):
- response = self.client.get("/gsr/users/")
+ def test_user_memberships(self) -> None:
+ response = self.client.get("/gsr/mymemberships/")
self.assertTrue(200, response.status_code)
- self.assertEqual(2, len(response.data))
+ self.assertEqual(2, len(response.data)) # type: ignore[attr-defined]
- def test_user_detail_in_group(self):
- response = self.client.get("/gsr/users/user1/")
+ def test_user_invites(self) -> None:
+ response = self.client.get("/gsr/mymemberships/invites/")
self.assertTrue(200, response.status_code)
- self.assertEqual(2, len(response.data["booking_groups"]))
-
- def test_me_user_detail_in_group(self):
- response = self.client.get("/gsr/users/me/")
- self.assertTrue(200, response.status_code)
- self.assertEqual(2, len(response.data["booking_groups"]))
+ self.assertEqual(1, len(response.data)) # type: ignore[attr-defined]
class MembershipViewTestCase(TestCase):
- def setUp(self):
- self.user1 = User.objects.create_user(username="user1", password="password")
- self.user2 = User.objects.create_user(username="user2", password="password")
+ def setUp(self) -> None:
+ self.user1: UserType = DjangoUserModel.objects.create_user(
+ username="user1", password="password"
+ )
+ self.user2: UserType = DjangoUserModel.objects.create_user(
+ username="user2", password="password"
+ )
self.group = Group.objects.create(owner=self.user1, name="g1", color="blue")
self.group2 = Group.objects.create(owner=self.user2, name="g2", color="white")
- self.client = APIClient()
+ self.client: APIClient = APIClient()
self.client.login(username="user1", password="password")
- def test_invite_single(self):
+ def test_invite_single(self) -> None:
self.client.login(username="user2", password="password")
response = self.client.post(
"/gsr/membership/invite/", {"user": "user2", "group": self.group.pk}
)
self.assertEqual(200, response.status_code)
- def test_bulk_invite(self):
- User.objects.create_user(username="user3", password="password")
+ def test_bulk_invite(self) -> None:
+ DjangoUserModel.objects.create_user(username="user3", password="password")
self.client.login(username="user2", password="password")
response = self.client.post(
"/gsr/membership/invite/", {"user": "user2,user3", "group": self.group.pk}
)
self.assertEqual(200, response.status_code)
- def test_invite_no_permission(self):
+ def test_invite_no_permission(self) -> None:
self.client.login(username="user2", password="password")
response = self.client.post(
"/gsr/membership/invite/", {"user": "user2", "group": self.group.pk}
)
self.assertEqual(200, response.status_code)
- def test_invite_logged_out_fails(self):
+ def test_invite_logged_out_fails(self) -> None:
self.client.logout()
response = self.client.post(
"/gsr/membership/invite/", {"user": "user2", "group": self.group.pk}
)
self.assertEqual(403, response.status_code)
- def test_invite_bad_group_fails(self):
+ def test_invite_bad_group_fails(self) -> None:
response = self.client.post("/gsr/membership/invite/", {"user": "user2", "group": 300})
self.assertEqual(404, response.status_code)
- def test_duplicate_invite_fails(self):
+ def test_duplicate_invite_fails(self) -> None:
GroupMembership.objects.create(user=self.user2, group=self.group, accepted=False)
self.client.force_authenticate(user=self.user2)
response = self.client.post(
@@ -91,7 +89,7 @@ def test_duplicate_invite_fails(self):
)
self.assertEqual(403, response.status_code)
- def test_already_member_invite_fails(self):
+ def test_already_member_invite_fails(self) -> None:
GroupMembership.objects.create(user=self.user2, group=self.group, accepted=True)
self.client.force_authenticate(user=self.user2)
response = self.client.post(
@@ -99,44 +97,44 @@ def test_already_member_invite_fails(self):
)
self.assertEqual(403, response.status_code)
- def test_accept_invite(self):
+ def test_accept_invite(self) -> None:
mem = GroupMembership.objects.create(user=self.user1, group=self.group2, accepted=False)
response = self.client.post(f"/gsr/membership/{mem.pk}/accept/")
self.assertEqual(200, response.status_code)
self.assertTrue(GroupMembership.objects.get(pk=mem.pk).accepted)
- def test_wrong_user_accept_invite_fails(self):
- user3 = User.objects.create_user(username="user3", password="password")
+ def test_wrong_user_accept_invite_fails(self) -> None:
+ user3: UserType = DjangoUserModel.objects.create_user(username="user3", password="password")
mem = GroupMembership.objects.create(user=user3, group=self.group2, accepted=False)
response = self.client.post(f"/gsr/membership/{mem.pk}/accept/")
self.assertEqual(403, response.status_code)
self.assertFalse(GroupMembership.objects.get(pk=mem.pk).accepted)
- def test_accept_invite_again_fails(self):
+ def test_accept_invite_again_fails(self) -> None:
mem = GroupMembership.objects.create(user=self.user1, group=self.group2, accepted=True)
response = self.client.post(f"/gsr/membership/{mem.pk}/accept/")
self.assertEqual(404, response.status_code)
- def test_decline_invite(self):
+ def test_decline_invite(self) -> None:
mem = GroupMembership.objects.create(user=self.user1, group=self.group2, accepted=False)
response = self.client.post(f"/gsr/membership/{mem.pk}/decline/")
self.assertEqual(200, response.status_code)
self.assertFalse(GroupMembership.objects.filter(pk=mem.pk).exists())
- def test_wrong_user_decline_invite_fails(self):
- user3 = User.objects.create_user(username="user3", password="password")
+ def test_wrong_user_decline_invite_fails(self) -> None:
+ user3: UserType = DjangoUserModel.objects.create_user(username="user3", password="password")
mem = GroupMembership.objects.create(user=user3, group=self.group2, accepted=False)
response = self.client.post(f"/gsr/membership/{mem.pk}/decline/")
self.assertEqual(403, response.status_code)
self.assertTrue(GroupMembership.objects.filter(pk=mem.pk).exists())
self.assertFalse(GroupMembership.objects.get(pk=mem.pk).accepted)
- def test_decline_invite_again_fails(self):
+ def test_decline_invite_again_fails(self) -> None:
mem = GroupMembership.objects.create(user=self.user1, group=self.group2, accepted=True)
response = self.client.post(f"/gsr/membership/{mem.pk}/decline/")
self.assertEqual(404, response.status_code)
- def test_promote_to_admin(self):
+ def test_promote_to_admin(self) -> None:
GroupMembership.objects.create(user=self.user1, group=self.group, accepted=True, type="A")
mem = GroupMembership.objects.create(
user=self.user2, group=self.group, accepted=True, type="M"
@@ -148,35 +146,39 @@ def test_promote_to_admin(self):
class GroupTestCase(TestCase):
- def setUp(self):
- self.user1 = User.objects.create_user(username="user1", password="password")
- self.user2 = User.objects.create_user(username="user2", password="password")
+ def setUp(self) -> None:
+ self.user1: UserType = DjangoUserModel.objects.create_user(
+ username="user1", password="password"
+ )
+ self.user2: UserType = DjangoUserModel.objects.create_user(
+ username="user2", password="password"
+ )
self.group = Group.objects.create(owner=self.user1, name="g1", color="blue")
self.group2 = Group.objects.create(owner=self.user2, name="g2", color="white")
- self.client = APIClient()
+ self.client: APIClient = APIClient()
self.client.login(username="user1", password="password")
- def test_get_groups(self):
+ def test_get_groups(self) -> None:
response = self.client.get("/gsr/groups/")
self.assertEqual(200, response.status_code)
- self.assertEqual(2, len(response.data))
+ self.assertEqual(1, len(response.data))
- def test_get_groups_includes_invites(self):
+ def test_get_groups_includes_invites(self) -> None:
GroupMembership.objects.create(user=self.user1, group=self.group2, accepted=False)
response = self.client.get(f"/gsr/groups/{self.group2.pk}/")
self.assertEqual(200, response.status_code)
- def test_get_group_not_involved_fails(self):
+ def test_get_group_not_involved_fails(self) -> None:
response = self.client.get(f"/gsr/groups/{self.group2.pk}/")
self.assertEqual(404, response.status_code)
- def test_make_group(self):
+ def test_make_group(self) -> None:
response = self.client.post("/gsr/groups/", {"name": "gx", "color": "blue"})
self.assertEqual(201, response.status_code, response.data)
- self.assertEqual(5, Group.objects.count())
+ self.assertEqual(3, Group.objects.count())
self.assertEqual("user1", Group.objects.get(name="gx").owner.username)
- def test_only_accepted_memberships(self):
+ def test_only_accepted_memberships(self) -> None:
gm = GroupMembership.objects.create(user=self.user2, group=self.group, accepted=False)
response = self.client.get(f"/gsr/groups/{self.group.pk}/")
self.assertEqual(200, response.status_code)
diff --git a/backend/tests/gsr_booking/test_gsr_views.py b/backend/tests/gsr_booking/test_gsr_views.py
index c749d2b9..3dcbb772 100644
--- a/backend/tests/gsr_booking/test_gsr_views.py
+++ b/backend/tests/gsr_booking/test_gsr_views.py
@@ -1,56 +1,56 @@
import json
+from typing import Any
from unittest import mock
-from django.contrib.auth import get_user_model
from django.core.management import call_command
from django.test import TestCase
from django.urls import reverse
from rest_framework.test import APIClient
from gsr_booking.models import GSR, Group, GSRBooking
+from utils.types import DjangoUserModel, UserType
-User = get_user_model()
-
-
-def is_wharton_false(*args):
+def is_wharton_false(*args: Any) -> bool:
return False
-def is_wharton_true(*args):
+def is_wharton_true(*args: Any) -> bool:
return True
-def libcal_availability(*args):
+def libcal_availability(*args: Any) -> list[dict]:
with open("tests/gsr_booking/views_libcal_availability.json") as data:
return json.load(data)
-def wharton_availability(*args):
+def wharton_availability(*args: Any) -> list[dict]:
with open("tests/gsr_booking/views_wharton_availability.json") as data:
return json.load(data)
-def book_cancel_room(*args):
+def book_cancel_room(*args: Any) -> None:
pass
-def reservations(*args):
+def reservations(*args: Any) -> list[dict]:
with open("tests/gsr_booking/views_reservations.json") as data:
return json.load(data)
class TestGSRs(TestCase):
- def setUp(self):
+ def setUp(self) -> None:
call_command("load_gsrs")
- self.user = User.objects.create_user("user", "user@seas.upenn.edu", "user")
- self.client = APIClient()
+ self.user: UserType = DjangoUserModel.objects.create_user(
+ "user", "user@seas.upenn.edu", "user"
+ )
+ self.client: APIClient = APIClient()
self.client.force_authenticate(user=self.user)
- test_user = User.objects.create_user("user1", "user")
+ test_user = DjangoUserModel.objects.create_user("user1", "user")
Group.objects.create(owner=test_user, name="Penn Labs", color="blue")
- def test_get_location(self):
+ def test_get_location(self) -> None:
response = self.client.get(reverse("locations"))
res_json = json.loads(response.content)
for entry in res_json:
@@ -63,16 +63,18 @@ def test_get_location(self):
class TestGSRFunctions(TestCase):
- def setUp(self):
+ def setUp(self) -> None:
call_command("load_gsrs")
- self.user = User.objects.create_user("user", "user@sas.upenn.edu", "user")
- self.client = APIClient()
+ self.user: UserType = DjangoUserModel.objects.create_user(
+ "user", "user@sas.upenn.edu", "user"
+ )
+ self.client: APIClient = APIClient()
self.client.force_authenticate(user=self.user)
- test_user = User.objects.create_user("user1", "user")
+ test_user = DjangoUserModel.objects.create_user("user1", "user")
Group.objects.create(owner=test_user, name="Penn Labs", color="blue")
- def test_recent(self):
+ def test_recent(self) -> None:
gsrs = list(GSR.objects.all())
GSRBooking.objects.create(user=self.user, room_id=1, room_name="Room 1", gsr=gsrs[0])
GSRBooking.objects.create(user=self.user, room_id=3, room_name="Room 3", gsr=gsrs[0])
@@ -93,21 +95,21 @@ def test_recent(self):
self.assertNotEqual(res_json[0]["id"], res_json[1]["id"])
@mock.patch("gsr_booking.api_wrapper.WhartonBookingWrapper.is_wharton", is_wharton_false)
- def test_get_wharton_false(self):
+ def test_get_wharton_false(self) -> None:
response = self.client.get(reverse("is-wharton"))
res_json = json.loads(response.content)
self.assertEqual(1, len(res_json))
self.assertFalse(res_json["is_wharton"])
@mock.patch("gsr_booking.api_wrapper.WhartonBookingWrapper.is_wharton", is_wharton_true)
- def test_get_wharton_true(self):
+ def test_get_wharton_true(self) -> None:
response = self.client.get(reverse("is-wharton"))
res_json = json.loads(response.content)
self.assertEqual(1, len(res_json))
self.assertTrue(res_json["is_wharton"])
@mock.patch("gsr_booking.api_wrapper.BookingHandler.get_availability", libcal_availability)
- def test_availability_libcal(self):
+ def test_availability_libcal(self) -> None:
response = self.client.get(reverse("availability", args=["1086", "1889"]))
res_json = json.loads(response.content)
self.assertEqual(3, len(res_json))
@@ -121,7 +123,7 @@ def test_availability_libcal(self):
self.assertIn("availability", room)
@mock.patch("gsr_booking.api_wrapper.BookingHandler.get_availability", wharton_availability)
- def test_availability_wharton(self):
+ def test_availability_wharton(self) -> None:
response = self.client.get(reverse("availability", args=["JMHH", "1"]))
res_json = json.loads(response.content)
self.assertEqual(3, len(res_json))
@@ -135,7 +137,7 @@ def test_availability_wharton(self):
self.assertIn("availability", room)
@mock.patch("gsr_booking.api_wrapper.BookingHandler.book_room", book_cancel_room)
- def test_book_libcal(self):
+ def test_book_libcal(self) -> None:
payload = {
"start_time": "2021-11-21T18:30:00-05:00",
"end_time": "2021-11-21T19:00:00-05:00",
@@ -151,7 +153,7 @@ def test_book_libcal(self):
self.assertEqual("success", res_json["detail"])
@mock.patch("gsr_booking.api_wrapper.BookingHandler.book_room", book_cancel_room)
- def test_book_wharton(self):
+ def test_book_wharton(self) -> None:
payload = {
"start_time": "2021-11-21T18:30:00-05:00",
"end_time": "2021-11-21T19:00:00-05:00",
@@ -167,7 +169,7 @@ def test_book_wharton(self):
self.assertEqual("success", res_json["detail"])
@mock.patch("gsr_booking.api_wrapper.BookingHandler.cancel_room", book_cancel_room)
- def test_cancel_room(self):
+ def test_cancel_room(self) -> None:
payload = {"booking_id": "booking id"}
response = self.client.post(
reverse("cancel"), json.dumps(payload), content_type="application/json"
@@ -177,7 +179,7 @@ def test_cancel_room(self):
self.assertEqual("success", res_json["detail"])
@mock.patch("gsr_booking.api_wrapper.BookingHandler.get_reservations", reservations)
- def test_reservations(self):
+ def test_reservations(self) -> None:
response = self.client.get(reverse("reservations"))
res_json = json.loads(response.content)
self.assertEqual(6, len(res_json))
diff --git a/backend/tests/gsr_booking/test_gsr_wrapper.py b/backend/tests/gsr_booking/test_gsr_wrapper.py
index aa214b95..23a307c3 100644
--- a/backend/tests/gsr_booking/test_gsr_wrapper.py
+++ b/backend/tests/gsr_booking/test_gsr_wrapper.py
@@ -1,8 +1,8 @@
import json
from datetime import timedelta
+from typing import Any, cast
from unittest import mock
-from django.contrib.auth import get_user_model
from django.core.management import call_command
from django.test import TestCase
from django.utils import timezone
@@ -10,19 +10,17 @@
from gsr_booking.api_wrapper import APIError, GSRBooker, WhartonGSRBooker
from gsr_booking.models import GSR, Group, GroupMembership, GSRBooking, Reservation
+from utils.types import DjangoUserModel, UserType
-User = get_user_model()
-
-
-def mock_requests_get(obj, *args, **kwargs):
+def mock_requests_get(obj: Any, *args: Any, **kwargs: Any) -> Any:
class Mock:
- def __init__(self, json_data, status_code):
+ def __init__(self, json_data: dict, status_code: int) -> None:
self.json_data = json_data
self.status_code = status_code
self.ok = True
- def json(self):
+ def json(self) -> dict:
return self.json_data
url = args[1]
@@ -52,22 +50,24 @@ def json(self):
class TestBookingWrapper(TestCase):
- def setUp(self):
+ def setUp(self) -> None:
call_command("load_gsrs")
- self.user = User.objects.create_user("user", "user@seas.upenn.edu", "user")
- self.group_user = User.objects.create_user(
+ self.user: UserType = DjangoUserModel.objects.create_user(
+ "user", "user@seas.upenn.edu", "user"
+ )
+ self.group_user: UserType = DjangoUserModel.objects.create_user(
"grou_user", "group_user@seas.upenn.edu", "group_user"
)
- self.client = APIClient()
+ self.client: APIClient = APIClient()
self.client.force_authenticate(user=self.user)
self.group = Group.objects.create(owner=self.group_user, name="Penn Labs", color="blue")
@mock.patch("gsr_booking.api_wrapper.WhartonBookingWrapper.request", mock_requests_get)
- def test_is_wharton(self):
+ def test_is_wharton(self) -> None:
self.assertFalse(WhartonGSRBooker.is_wharton(self.user))
@mock.patch("gsr_booking.api_wrapper.WhartonBookingWrapper.request", mock_requests_get)
- def test_wharton_availability(self):
+ def test_wharton_availability(self) -> None:
availability = GSRBooker.get_availability("JMHH", 1, "2021-01-07", "2022-01-08", self.user)
self.assertIn("name", availability)
self.assertIn("gid", availability)
@@ -78,26 +78,28 @@ def test_wharton_availability(self):
# @mock.patch("gsr_booking.api_wrapper.WhartonBookingWrapper.check_credits", mock_check_credits)
@mock.patch("gsr_booking.api_wrapper.WhartonBookingWrapper.request", mock_requests_get)
- def test_book_wharton(self):
+ def test_book_wharton(self) -> None:
book_wharton = GSRBooker.book_room(
1, 94, "241", "2021-12-05T16:00:00-05:00", "2021-12-05T16:30:00-05:00", self.user
)
- self.assertEquals("241", book_wharton.gsrbooking_set.first().room_name)
+ first_booking = book_wharton.gsrbooking_set.first()
+ assert first_booking is not None
+ self.assertEquals("241", first_booking.room_name)
@mock.patch("gsr_booking.api_wrapper.WhartonBookingWrapper.request", mock_requests_get)
- def test_wharton_reservations(self):
+ def test_wharton_reservations(self) -> None:
reservations = WhartonGSRBooker.get_reservations(self.user)
self.assertTrue(isinstance(reservations, list))
self.assertIn("booking_id", reservations[0])
self.assertIn("gid", reservations[0])
@mock.patch("gsr_booking.api_wrapper.WhartonBookingWrapper.request", mock_requests_get)
- def test_cancel_wharton(self):
+ def test_cancel_wharton(self) -> None:
cancel = GSRBooker.cancel_room("987654", self.user)
self.assertIsNone(cancel)
@mock.patch("gsr_booking.api_wrapper.LibCalBookingWrapper.request", mock_requests_get)
- def test_libcal_availability(self):
+ def test_libcal_availability(self) -> None:
availability = GSRBooker.get_availability(
"1086", 1889, "2021-01-07", "2022-01-08", self.user
)
@@ -109,7 +111,7 @@ def test_libcal_availability(self):
self.assertIn("availability", availability["rooms"][0])
@mock.patch("gsr_booking.api_wrapper.LibCalBookingWrapper.request", mock_requests_get)
- def test_book_libcal(self):
+ def test_book_libcal(self) -> None:
book_libcal = GSRBooker.book_room(
1889,
7192,
@@ -118,19 +120,21 @@ def test_book_libcal(self):
"2021-12-05T16:30:00-05:00",
self.user,
)
- self.assertEquals("VP WIC Booth 01", book_libcal.gsrbooking_set.first().room_name)
+ first_booking = book_libcal.gsrbooking_set.first()
+ assert first_booking is not None
+ self.assertEquals("VP WIC Booth 01", first_booking.room_name)
@mock.patch(
"gsr_booking.api_wrapper.WhartonBookingWrapper.request", mock_requests_get
) # purposefully wharton request here
- def test_libcal_reservations(self):
+ def test_libcal_reservations(self) -> None:
reservations = GSRBooker.get_reservations(self.user)
self.assertTrue(isinstance(reservations, list))
self.assertIn("booking_id", reservations[0])
self.assertIn("gsr", reservations[0])
@mock.patch("gsr_booking.api_wrapper.LibCalBookingWrapper.request", mock_requests_get)
- def test_cancel_libcal(self):
+ def test_cancel_libcal(self) -> None:
group = Group.objects.create(owner=self.user)
reservation = Reservation.objects.create(creator=self.user, group=group)
GSRBooking.objects.create(
@@ -145,10 +149,11 @@ def test_cancel_libcal(self):
self.assertIsNone(cancel)
@mock.patch("gsr_booking.api_wrapper.WhartonBookingWrapper.request", mock_requests_get)
- def test_group_book_wharton(self):
+ def test_group_book_wharton(self) -> None:
# make sure group_user is treated as a wharton user so they
# are returned in list of wharton users in gb.book_room
membership1 = GroupMembership.objects.filter(group=self.group).first()
+ assert membership1 is not None
membership1.is_wharton = True
membership1.save()
@@ -180,7 +185,7 @@ def test_group_book_wharton(self):
self.assertIsNotNone(Reservation.objects.get(pk=reservation.id))
@mock.patch("gsr_booking.api_wrapper.LibCalBookingWrapper.request", mock_requests_get)
- def test_group_book_libcal(self):
+ def test_group_book_libcal(self) -> None:
# add user to the group
GroupMembership.objects.create(user=self.user, group=self.group, accepted=True)
@@ -211,7 +216,9 @@ def test_group_book_libcal(self):
self.assertEqual(len(res), 1)
self.assertEqual(res[0]["room_name"], "[Me] VP WIC Booth 01")
- credit_owner = reservation.gsrbooking_set.first().user
+ first_booking = reservation.gsrbooking_set.first()
+ assert first_booking is not None
+ credit_owner = first_booking.user
res = GSRBooker.get_reservations(credit_owner, self.group)
self.assertEqual(len(res), 1)
self.assertEqual(
@@ -220,7 +227,7 @@ def test_group_book_libcal(self):
)
@mock.patch("gsr_booking.api_wrapper.WhartonBookingWrapper.request", mock_requests_get)
- def test_group_wharton_availability(self):
+ def test_group_wharton_availability(self) -> None:
with self.assertRaises(APIError):
GSRBooker.get_availability(
"JMHH", 1, "2021-01-07", "2022-01-08", self.group_user, self.group
@@ -234,6 +241,7 @@ def test_group_wharton_availability(self):
self.assertIn("name", availability)
self.assertIn("gid", availability)
self.assertIn("rooms", availability)
- self.assertIn("room_name", availability["rooms"][0])
- self.assertIn("id", availability["rooms"][0])
- self.assertIn("availability", availability["rooms"][0])
+ room_info = cast(dict[str, Any], availability["rooms"][0])
+ self.assertIn("room_name", room_info)
+ self.assertIn("id", room_info)
+ self.assertIn("availability", room_info)
diff --git a/backend/tests/laundry/test_api_wrapper.py b/backend/tests/laundry/test_api_wrapper.py
index dccca9ea..a5749879 100644
--- a/backend/tests/laundry/test_api_wrapper.py
+++ b/backend/tests/laundry/test_api_wrapper.py
@@ -13,7 +13,7 @@
@mock.patch("requests.get", fakeLaundryGet)
class TestAllStatus(TestCase):
- def setUp(self):
+ def setUp(self) -> None:
LaundryRoom.objects.get_or_create(
hall_id=0, name="Bishop White", location="Quad", total_washers=9, total_dryers=9
)
@@ -27,7 +27,7 @@ def setUp(self):
hall_id=3, name="Craig", location="Quad", total_washers=3, total_dryers=3
)
- def test_all_status(self):
+ def test_all_status(self) -> None:
data = all_status()
@@ -56,7 +56,7 @@ def test_all_status(self):
@mock.patch("requests.get", fakeLaundryGet)
class TestHallStatus(TestCase):
- def setUp(self):
+ def setUp(self) -> None:
LaundryRoom.objects.get_or_create(
hall_id=0, name="Bishop White", location="Quad", total_washers=9, total_dryers=9
)
@@ -70,7 +70,7 @@ def setUp(self):
hall_id=3, name="Craig", location="Quad", total_washers=3, total_dryers=3
)
- def test_all_status(self):
+ def test_all_status(self) -> None:
for room in LaundryRoom.objects.all():
@@ -88,7 +88,7 @@ def test_all_status(self):
@mock.patch("requests.get", fakeLaundryGet)
class TestSaveData(TestCase):
- def setUp(self):
+ def setUp(self) -> None:
LaundryRoom.objects.get_or_create(
hall_id=0, name="Bishop White", location="Quad", total_washers=9, total_dryers=9
)
@@ -102,7 +102,7 @@ def setUp(self):
hall_id=3, name="Craig", location="Quad", total_washers=3, total_dryers=3
)
- def test_save_data(self):
+ def test_save_data(self) -> None:
self.assertEqual(LaundrySnapshot.objects.all().count(), 0)
diff --git a/backend/tests/laundry/test_commands.py b/backend/tests/laundry/test_commands.py
index 79028394..998a3758 100644
--- a/backend/tests/laundry/test_commands.py
+++ b/backend/tests/laundry/test_commands.py
@@ -1,7 +1,9 @@
import csv
from io import StringIO
+from typing import Any
from unittest import mock
+import requests
from django.conf import settings
from django.core.management import call_command
from django.test import TestCase
@@ -9,7 +11,7 @@
from laundry.models import LaundryRoom, LaundrySnapshot
-def fakeLaundryGet(url, *args, **kwargs):
+def fakeLaundryGet(url: str, *args: Any, **kwargs: Any) -> requests.models.Response:
if settings.LAUNDRY_URL in url:
with open("tests/laundry/laundry_snapshot.html", "rb") as f:
m = mock.MagicMock(content=f.read())
@@ -20,7 +22,7 @@ def fakeLaundryGet(url, *args, **kwargs):
@mock.patch("requests.get", fakeLaundryGet)
class TestGetSnapshot(TestCase):
- def setUp(self):
+ def setUp(self) -> None:
# populates database with LaundryRooms
LaundryRoom.objects.get_or_create(
hall_id=0, name="Bishop White", location="Quad", total_washers=9, total_dryers=9
@@ -35,7 +37,7 @@ def setUp(self):
hall_id=3, name="Craig", location="Quad", total_washers=3, total_dryers=3
)
- def test_db_populate(self):
+ def test_db_populate(self) -> None:
out = StringIO()
call_command("get_snapshot", stdout=out)
@@ -48,7 +50,7 @@ def test_db_populate(self):
@mock.patch("requests.get", fakeLaundryGet)
class TestLaundryRoomMigration(TestCase):
- def test_db_populate(self):
+ def test_db_populate(self) -> None:
out = StringIO()
call_command("load_laundry_rooms", stdout=out)
diff --git a/backend/tests/laundry/test_models.py b/backend/tests/laundry/test_models.py
index 5dbd8fbc..bf2fa08b 100644
--- a/backend/tests/laundry/test_models.py
+++ b/backend/tests/laundry/test_models.py
@@ -4,7 +4,7 @@
class LaundrySnapshotTestCase(TestCase):
- def setUp(self):
+ def setUp(self) -> None:
# populates database with LaundryRooms
LaundryRoom.objects.get_or_create(
hall_id=0, name="Bishop White", location="Quad", total_washers=9, total_dryers=9
@@ -23,15 +23,15 @@ def setUp(self):
room=self.laundry_room, available_washers=10, available_dryers=10
)
- def test_str(self):
+ def test_str(self) -> None:
self.assertEqual(
str(self.snapshot),
- f"Hall No. {self.snapshot.room.hall_id} | {self.snapshot.date.date()}",
+ f"Hall No. {self.snapshot.room.hall_id} | {self.snapshot.date.date()}", # ignore: type
)
class LaundryRoomTestCase(TestCase):
- def setUp(self):
+ def setUp(self) -> None:
# populates database with LaundryRooms
LaundryRoom.objects.get_or_create(
hall_id=0, name="Bishop White", location="Quad", total_washers=9, total_dryers=9
@@ -47,5 +47,5 @@ def setUp(self):
)
self.room = LaundryRoom.objects.create(hall_id=1, name="test hall", location="location")
- def test_str(self):
+ def test_str(self) -> None:
self.assertEqual(str(self.room), f"Hall No. {self.room.hall_id} | {self.room.name}")
diff --git a/backend/tests/laundry/test_views.py b/backend/tests/laundry/test_views.py
index d0dae554..78249de9 100644
--- a/backend/tests/laundry/test_views.py
+++ b/backend/tests/laundry/test_views.py
@@ -1,7 +1,6 @@
import json
from unittest import mock
-from django.contrib.auth import get_user_model
from django.test import TestCase
from django.urls import reverse
from django.utils import timezone
@@ -9,14 +8,12 @@
from laundry.models import LaundryRoom, LaundrySnapshot
from tests.laundry.test_commands import fakeLaundryGet
-
-
-User = get_user_model()
+from utils.types import DjangoUserModel, UserType
@mock.patch("requests.get", fakeLaundryGet)
class HallIdViewTestCase(TestCase):
- def setUp(self):
+ def setUp(self) -> None:
LaundryRoom.objects.get_or_create(
hall_id=0, name="Bishop White", location="Quad", total_washers=9, total_dryers=9
)
@@ -29,9 +26,9 @@ def setUp(self):
LaundryRoom.objects.get_or_create(
hall_id=3, name="Craig", location="Quad", total_washers=3, total_dryers=3
)
- self.client = APIClient()
+ self.client: APIClient = APIClient()
- def test_response(self):
+ def test_response(self) -> None:
response = self.client.get(reverse("hall-ids"))
res_json = json.loads(response.content)
for hall in res_json:
@@ -44,7 +41,7 @@ def test_response(self):
@mock.patch("requests.get", fakeLaundryGet)
class HallInfoViewTestCase(TestCase):
- def setUp(self):
+ def setUp(self) -> None:
LaundryRoom.objects.get_or_create(
hall_id=0, name="Bishop White", location="Quad", total_washers=9, total_dryers=9
)
@@ -58,9 +55,9 @@ def setUp(self):
hall_id=3, name="Craig", location="Quad", total_washers=3, total_dryers=3
)
self.laundry_room = LaundryRoom.objects.get(hall_id=0, name="Bishop White", location="Quad")
- self.client = APIClient()
+ self.client: APIClient = APIClient()
- def test_response(self):
+ def test_response(self) -> None:
response = self.client.get(reverse("hall-info", args=[self.laundry_room.hall_id]))
res_json = json.loads(response.content)
if response.status_code == 200:
@@ -69,14 +66,14 @@ def test_response(self):
elif response.status_code == 503:
self.assertEqual("The laundry api is currently unavailable.", res_json["error"])
- def test_hall_error(self):
+ def test_hall_error(self) -> None:
response = self.client.get(reverse("hall-info", args=[1000000]))
self.assertEqual(404, response.status_code)
@mock.patch("requests.get", fakeLaundryGet)
class MultipleHallInfoViewTestCase(TestCase):
- def setUp(self):
+ def setUp(self) -> None:
LaundryRoom.objects.get_or_create(
hall_id=0, name="Bishop White", location="Quad", total_washers=9, total_dryers=9
)
@@ -90,9 +87,9 @@ def setUp(self):
hall_id=3, name="Craig", location="Quad", total_washers=3, total_dryers=3
)
self.laundry_room = LaundryRoom.objects.get(hall_id=0, name="Bishop White", location="Quad")
- self.client = APIClient()
+ self.client: APIClient = APIClient()
- def test_response(self):
+ def test_response(self) -> None:
response = self.client.get(reverse("multiple-hall-info", args=["0,1,2,3"]))
res_json = json.loads(response.content)
if response.status_code == 200:
@@ -111,14 +108,14 @@ def test_response(self):
elif response.status_code == 503:
self.assertEqual("The laundry api is currently unavailable.", res_json["error"])
- def test_hall_error(self):
+ def test_hall_error(self) -> None:
response = self.client.get(reverse("multiple-hall-info", args=["1000000"]))
self.assertEqual(404, response.status_code)
@mock.patch("requests.get", fakeLaundryGet)
class HallUsageViewTestCase(TestCase):
- def setUp(self):
+ def setUp(self) -> None:
LaundryRoom.objects.get_or_create(
hall_id=0, name="Bishop White", location="Quad", total_washers=9, total_dryers=9
)
@@ -135,9 +132,9 @@ def setUp(self):
self.snapshot = LaundrySnapshot.objects.create(
room=self.laundry_room, available_washers=5, available_dryers=10
)
- self.client = APIClient()
+ self.client: APIClient = APIClient()
- def test_response(self):
+ def test_response(self) -> None:
response = self.client.get(reverse("hall-usage", args=[self.laundry_room.hall_id]))
res_json = json.loads(response.content)
@@ -150,7 +147,7 @@ def test_response(self):
@mock.patch("requests.get", fakeLaundryGet)
class PreferencesTestCase(TestCase):
- def setUp(self):
+ def setUp(self) -> None:
LaundryRoom.objects.get_or_create(
hall_id=0, name="Bishop White", location="Quad", total_washers=9, total_dryers=9
)
@@ -163,22 +160,22 @@ def setUp(self):
LaundryRoom.objects.get_or_create(
hall_id=3, name="Craig", location="Quad", total_washers=3, total_dryers=3
)
- self.client = APIClient()
- self.test_user = User.objects.create_user("user", "user@a.com", "user")
+ self.client: APIClient = APIClient()
+ self.test_user: UserType = DjangoUserModel.objects.create_user("user", "user@a.com", "user")
self.laundry_room = LaundryRoom.objects.get(hall_id=0, name="Bishop White", location="Quad")
self.other_laundry_room = LaundryRoom.objects.get(
hall_id=1, name="Chestnut Butcher", location="Quad"
)
self.test_user.profile.laundry_preferences.add(self.laundry_room)
- def test_get(self):
+ def test_get(self) -> None:
self.client.force_authenticate(user=self.test_user)
response = self.client.get(reverse("preferences"))
res_json = json.loads(response.content)
self.assertIn(self.laundry_room.hall_id, res_json["rooms"])
- def test_post(self):
+ def test_post(self) -> None:
self.client.force_authenticate(user=self.test_user)
self.client.post(reverse("preferences"), {"rooms": [self.other_laundry_room.hall_id]})
diff --git a/backend/tests/penndata/test_views.py b/backend/tests/penndata/test_views.py
index fea551a7..67795636 100644
--- a/backend/tests/penndata/test_views.py
+++ b/backend/tests/penndata/test_views.py
@@ -1,8 +1,8 @@
import datetime
import json
+from typing import Any
from unittest import mock
-from django.contrib.auth import get_user_model
from django.core.management import call_command
from django.test import TestCase
from django.urls import reverse
@@ -13,9 +13,10 @@
from laundry.models import LaundryRoom
from penndata.models import AnalyticsEvent, Event, FitnessRoom, FitnessSnapshot
from portal.models import Poll, Post
+from utils.types import DjangoUserModel, UserType
-def fakeFitnessGet(url, *args, **kwargs):
+def fakeFitnessGet(url: str, *args: Any, **kwargs: Any) -> mock.MagicMock:
if "docs.google.com/spreadsheets/" in url:
with open("tests/penndata/fitness_snapshot.html", "rb") as f:
m = mock.MagicMock(content=f.read())
@@ -24,11 +25,8 @@ def fakeFitnessGet(url, *args, **kwargs):
raise NotImplementedError
-User = get_user_model()
-
-
class TestNews(TestCase):
- def test_response(self):
+ def test_response(self) -> None:
response = self.client.get(reverse("news"))
res_json = json.loads(response.content)
self.assertEqual(len(res_json), 6)
@@ -40,10 +38,10 @@ def test_response(self):
class TestCalender(TestCase):
- def setUp(self):
+ def setUp(self) -> None:
call_command("get_calendar")
- def test_response(self):
+ def test_response(self) -> None:
response = self.client.get(reverse("calendar"))
res_json = json.loads(response.content)
@@ -54,8 +52,8 @@ def test_response(self):
class TestEvent(TestCase):
- def setUp(self):
- self.client = APIClient()
+ def setUp(self) -> None:
+ self.client: APIClient = APIClient()
self.event1 = Event.objects.create(
event_type="type1",
name="Event 1",
@@ -75,7 +73,7 @@ def setUp(self):
website="https://pennlabs.org/",
)
- def test_get_all_events(self):
+ def test_get_all_events(self) -> None:
"""Test GET request to retrieve all events (no type)"""
url = reverse("events")
response = self.client.get(url)
@@ -83,7 +81,7 @@ def test_get_all_events(self):
res_json = json.loads(response.content)
self.assertEqual(len(events), len(res_json))
- def test_get_events_by_type(self):
+ def test_get_events_by_type(self) -> None:
"""Test GET request to retrieve events by type"""
url = reverse("events-type", kwargs={"type": "type1"})
response = self.client.get(url)
@@ -95,13 +93,13 @@ def test_get_events_by_type(self):
class TestHomePage(TestCase):
- def setUp(self):
+ def setUp(self) -> None:
call_command("load_venues")
call_command("load_laundry_rooms")
- self.client = APIClient()
- self.test_user = User.objects.create_user("user", "user@a.com", "user")
+ self.client: APIClient = APIClient()
+ self.test_user: UserType = DjangoUserModel.objects.create_user("user", "user@a.com", "user")
- def test_first_response(self):
+ def test_first_response(self) -> None:
self.client.force_authenticate(user=self.test_user)
response = self.client.get(reverse("homepage"))
res_json = json.loads(response.content)["cells"]
@@ -138,10 +136,10 @@ def test_first_response(self):
class TestGetRecentFitness(TestCase):
- def setUp(self):
+ def setUp(self) -> None:
call_command("load_fitness_rooms")
- self.client = APIClient()
- self.test_user = User.objects.create_user("user", "user@a.com", "user")
+ self.client: APIClient = APIClient()
+ self.test_user: UserType = DjangoUserModel.objects.create_user("user", "user@a.com", "user")
self.client.force_authenticate(user=self.test_user)
self.fitness_room = FitnessRoom.objects.first()
@@ -156,13 +154,14 @@ def setUp(self):
room=self.fitness_room, date=self.new_time, count=self.new_count
)
- def test_get_recent(self):
+ def test_get_recent(self) -> None:
response = self.client.get(reverse("fitness"))
res_json = json.loads(response.content)
for room_obj in res_json:
room_obj.pop("open")
room_obj.pop("close")
+ assert self.fitness_room is not None
expected = [
{
"id": room.id,
@@ -196,10 +195,10 @@ def test_get_recent(self):
@mock.patch("requests.get", fakeFitnessGet)
class TestGetFitnessSnapshot(TestCase):
- def setUp(self):
+ def setUp(self) -> None:
call_command("load_fitness_rooms")
- def test_get_fitness_snapshot(self):
+ def test_get_fitness_snapshot(self) -> None:
self.assertEqual(FitnessSnapshot.objects.all().count(), 0)
call_command("get_fitness_snapshot")
@@ -219,7 +218,7 @@ def test_get_fitness_snapshot(self):
class TestFitnessUsage(TestCase):
- def load_snapshots_1(self, date):
+ def load_snapshots_1(self, date: datetime.datetime) -> None:
# 6:00, 0
FitnessSnapshot.objects.create(
room=self.room, date=date + datetime.timedelta(hours=6), count=0, capacity=0.0
@@ -254,7 +253,7 @@ def load_snapshots_1(self, date):
capacity=0.0,
)
- def load_snapshots_2(self, date):
+ def load_snapshots_2(self, date: datetime.datetime) -> None:
# 7:30, 3
FitnessSnapshot.objects.create(
room=self.room,
@@ -270,12 +269,12 @@ def load_snapshots_2(self, date):
capacity=93.0,
)
- def setUp(self):
- self.client = APIClient()
+ def setUp(self) -> None:
+ self.client: APIClient = APIClient()
self.date = timezone.make_aware(datetime.datetime(2023, 1, 19, 0, 0, 0))
self.room = FitnessRoom.objects.create(name="test")
- def test_get_fitness_usage_1(self):
+ def test_get_fitness_usage_1(self) -> None:
self.load_snapshots_1(self.date)
response = self.client.get(
reverse("fitness-usage", args=[self.room.id]),
@@ -319,7 +318,7 @@ def test_get_fitness_usage_1(self):
self.assertEqual(res_json, expected)
- def test_get_fitness_usage_2(self):
+ def test_get_fitness_usage_2(self) -> None:
self.load_snapshots_2(self.date)
response = self.client.get(
reverse("fitness-usage", args=[self.room.id]),
@@ -362,14 +361,14 @@ def test_get_fitness_usage_2(self):
self.assertEqual(res_json, expected)
- def test_get_usage_2_samples_week(self):
+ def test_get_usage_2_samples_week(self) -> None:
self.load_snapshots_1(self.date)
self.load_snapshots_2(self.date - datetime.timedelta(days=7))
response = self.client.get(
reverse("fitness-usage", args=[self.room.id]),
{
"date": (self.date).strftime("%Y-%m-%d"),
- "num_samples": 2,
+ "num_samples": "2",
"group_by": "week",
"field": "capacity",
},
@@ -438,12 +437,12 @@ def test_get_usage_2_samples_week(self):
self.assertEqual(res_json, expected)
- def test_get_usage_2_samples_day(self):
+ def test_get_usage_2_samples_day(self) -> None:
self.load_snapshots_2(self.date)
self.load_snapshots_1(self.date - datetime.timedelta(days=1))
response = self.client.get(
reverse("fitness-usage", args=[self.room.id]),
- {"date": (self.date).strftime("%Y-%m-%d"), "num_samples": 2},
+ {"date": (self.date).strftime("%Y-%m-%d"), "num_samples": "2"},
)
res_json = json.loads(response.content)
@@ -510,7 +509,7 @@ def test_get_usage_2_samples_day(self):
self.assertEqual(res_json, expected)
- def test_day_closed(self):
+ def test_day_closed(self) -> None:
self.load_snapshots_1(self.date - datetime.timedelta(days=1))
response = self.client.get(
reverse("fitness-usage", args=[self.room.id]),
@@ -565,7 +564,7 @@ def test_day_closed(self):
}
self.assertEqual(res_json, expected)
- def test_get_fitness_usage_error(self):
+ def test_get_fitness_usage_error(self) -> None:
response = self.client.get(reverse("fitness-usage", args=[self.room.id + 1]))
self.assertEqual(response.status_code, 404)
@@ -576,25 +575,25 @@ def test_get_fitness_usage_error(self):
@mock.patch("requests.get", fakeFitnessGet)
class FitnessPreferencesTestCase(TestCase):
- def setUp(self):
+ def setUp(self) -> None:
FitnessRoom.objects.get_or_create(id=0, name="1st Floor Fitness")
FitnessRoom.objects.get_or_create(id=1, name="MPR")
FitnessRoom.objects.get_or_create(id=2, name="Pool-Deep")
FitnessRoom.objects.get_or_create(id=3, name="4th Floor Fitness")
- self.client = APIClient()
- self.test_user = User.objects.create_user("user", "user@a.com", "user")
+ self.client: APIClient = APIClient()
+ self.test_user: UserType = DjangoUserModel.objects.create_user("user", "user@a.com", "user")
self.fitness_room = FitnessRoom.objects.get(id=0, name="1st Floor Fitness")
self.other_fitness_room = FitnessRoom.objects.get(id=1, name="MPR")
self.test_user.profile.fitness_preferences.add(self.fitness_room)
- def test_get(self):
+ def test_get(self) -> None:
self.client.force_authenticate(user=self.test_user)
response = self.client.get(reverse("fitness-preferences"))
res_json = json.loads(response.content)
self.assertIn(self.fitness_room.id, res_json["rooms"])
- def test_post(self):
+ def test_post(self) -> None:
self.client.force_authenticate(user=self.test_user)
self.client.post(reverse("fitness-preferences"), {"rooms": [self.other_fitness_room.id]})
@@ -605,12 +604,12 @@ def test_post(self):
class TestAnalytics(TestCase):
- def setUp(self):
- self.client = APIClient()
- self.test_user = User.objects.create_user("user", "user@a.com", "user")
+ def setUp(self) -> None:
+ self.client: APIClient = APIClient()
+ self.test_user: UserType = DjangoUserModel.objects.create_user("user", "user@a.com", "user")
self.client.force_authenticate(user=self.test_user)
- def test_create_regular_analytics(self):
+ def test_create_regular_analytics(self) -> None:
payload = {
"cell_type": "dining",
"index": 0,
@@ -624,7 +623,7 @@ def test_create_regular_analytics(self):
self.assertIsNone(res_json["post"])
self.assertIsNone(res_json["poll"])
- def test_create_poll_analytics(self):
+ def test_create_poll_analytics(self) -> None:
poll = Poll.objects.create(
club_code="pennlabs",
question="hello?",
@@ -645,7 +644,7 @@ def test_create_poll_analytics(self):
self.assertIsNone(res_json["post"])
self.assertTrue(res_json["is_interaction"])
- def test_create_post_analytics(self):
+ def test_create_post_analytics(self) -> None:
post = Post.objects.create(
club_code="notpennlabs",
title="Test title 2",
@@ -667,7 +666,7 @@ def test_create_post_analytics(self):
self.assertIsNotNone(res_json["post"])
self.assertFalse(res_json["is_interaction"])
- def test_fail_post_poll_analytics(self):
+ def test_fail_post_poll_analytics(self) -> None:
poll = Poll.objects.create(
club_code="pennlabs",
question="hello?",
@@ -693,12 +692,12 @@ def test_fail_post_poll_analytics(self):
class TestUniqueCounterView(TestCase):
- def setUp(self):
- self.client = APIClient()
- self.test_user = User.objects.create_user("user", "user@a.com", "user")
+ def setUp(self) -> None:
+ self.client: APIClient = APIClient()
+ self.test_user: UserType = DjangoUserModel.objects.create_user("user", "user@a.com", "user")
self.client.force_authenticate(user=self.test_user)
- def test_get_unique_counter(self):
+ def test_get_unique_counter(self) -> None:
post = Post.objects.create(
club_code="pennlabs",
title="Test title",
@@ -728,6 +727,6 @@ def test_get_unique_counter(self):
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()["count"], 0)
- def test_get_unique_counter_no_id(self):
+ def test_get_unique_counter_no_id(self) -> None:
response = self.client.get(reverse("eventcounter"))
self.assertEqual(response.status_code, 400)
diff --git a/backend/tests/portal/test_permissions.py b/backend/tests/portal/test_permissions.py
index a3081b76..b0bee144 100644
--- a/backend/tests/portal/test_permissions.py
+++ b/backend/tests/portal/test_permissions.py
@@ -1,8 +1,8 @@
import datetime
import json
+from typing import Any
from unittest import mock
-from django.contrib.auth import get_user_model
from django.core.management import call_command
from django.test import TestCase
from django.urls import reverse
@@ -10,55 +10,60 @@
from rest_framework.test import APIClient
from portal.models import Poll, PollOption, PollVote
+from utils.types import DjangoUserModel, UserType
-User = get_user_model()
-
-
-def mock_get_user_clubs(*args, **kwargs):
+def mock_get_user_clubs(*args: Any, **kwargs: Any) -> list[dict[str, Any]]:
with open("tests/portal/get_user_clubs.json") as data:
return json.load(data)
class PollPermissions(TestCase):
- def setUp(self):
+ def setUp(self) -> None:
call_command("load_target_populations", "--years", "2022, 2023, 2024, 2025")
- self.client = APIClient()
- self.admin = User.objects.create_superuser("admin@example.com", "admin", "admin")
- self.user1 = User.objects.create_user("user1", "user@seas.upenn.edu", "user")
- self.user2 = User.objects.create_user("user2", "user@seas.upenn.edu", "user")
+ self.client: APIClient = APIClient()
+ self.admin: UserType = DjangoUserModel.objects.create_superuser(
+ "admin@example.com", "admin", "admin"
+ )
+ self.user1: UserType = DjangoUserModel.objects.create_user(
+ "user1", "user@seas.upenn.edu", "user"
+ )
+ self.user2: UserType = DjangoUserModel.objects.create_user(
+ "user2", "user@seas.upenn.edu", "user"
+ )
- self.poll_1 = Poll.objects.create(
+ self.poll_1: Poll = Poll.objects.create(
club_code="pennlabs",
question="poll question 1",
expire_date=timezone.now() + datetime.timedelta(days=1),
status=Poll.STATUS_APPROVED,
)
- self.poll_option_1 = PollOption.objects.create(poll=self.poll_1, choice="hello!")
- self.poll_option_2 = PollOption.objects.create(poll=self.poll_1, choice="hello!!!!")
- self.poll_option_3 = PollOption.objects.create(poll=self.poll_1, choice="hello!!!!!!!")
+ self.poll_option_1: PollOption = PollOption.objects.create(
+ poll=self.poll_1, choice="hello!"
+ )
+ self.poll_option_2: PollOption = PollOption.objects.create(
+ poll=self.poll_1, choice="hello!!!!"
+ )
+ self.poll_option_3: PollOption = PollOption.objects.create(
+ poll=self.poll_1, choice="hello!!!!!!!"
+ )
- self.poll_2 = Poll.objects.create(
+ self.poll_2: Poll = Poll.objects.create(
club_code="pennlabs",
question="poll question 2",
expire_date=timezone.now() + datetime.timedelta(days=1),
status=Poll.STATUS_APPROVED,
)
- self.poll_vote = PollVote.objects.create(id_hash="2", poll=self.poll_1)
+ self.poll_vote: PollVote = PollVote.objects.create(id_hash="2", poll=self.poll_1)
self.poll_vote.poll_options.add(self.poll_option_1)
@mock.patch("portal.permissions.get_user_clubs", mock_get_user_clubs)
- def test_authentication(self):
+ def test_authentication(self) -> None:
# asserts that anonymous users cannot access any route
- list_urls = [
- "poll-list",
- "polloption-list",
- "pollvote-list",
- "target-populations",
- ]
+ list_urls = ["poll-list", "polloption-list", "pollvote-list", "target-populations"]
for url in list_urls:
response_1 = self.client.get(reverse(f"portal:{url}"))
self.assertEqual(response_1.status_code, 403)
@@ -70,7 +75,7 @@ def test_authentication(self):
@mock.patch("portal.permissions.get_user_clubs", mock_get_user_clubs)
@mock.patch("portal.views.get_user_clubs", mock_get_user_clubs)
- def test_update_poll(self):
+ def test_update_poll(self) -> None:
# users in same club can edit
self.client.force_authenticate(user=self.user2)
payload_1 = {"status": Poll.STATUS_REVISION}
@@ -90,7 +95,7 @@ def test_update_poll(self):
@mock.patch("portal.permissions.get_user_clubs", mock_get_user_clubs)
@mock.patch("portal.views.get_user_clubs", mock_get_user_clubs)
- def test_create_update_options(self):
+ def test_create_update_options(self) -> None:
# users in same club can edit poll option
self.client.force_authenticate(user=self.user2)
payload_1 = {"poll": self.poll_1.id, "choice": "hello"}
diff --git a/backend/tests/portal/test_polls.py b/backend/tests/portal/test_polls.py
index 1002d803..5d67be25 100644
--- a/backend/tests/portal/test_polls.py
+++ b/backend/tests/portal/test_polls.py
@@ -1,8 +1,8 @@
import datetime
import json
+from typing import Any
from unittest import mock
-from django.contrib.auth import get_user_model
from django.core.management import call_command
from django.test import TestCase
from django.utils import timezone
@@ -10,27 +10,25 @@
from portal.models import Poll, PollOption, PollVote, TargetPopulation
from utils.email import get_backend_manager_emails
+from utils.types import DjangoUserModel, UserType
-User = get_user_model()
-
-
-def mock_get_user_clubs(*args, **kwargs):
+def mock_get_user_clubs(*args: Any, **kwargs: Any) -> list[dict[str, Any]]:
with open("tests/portal/get_user_clubs.json") as data:
return json.load(data)
-def mock_get_user_info(*args, **kwargs):
+def mock_get_user_info(*args: Any, **kwargs: Any) -> dict[str, Any]:
with open("tests/portal/get_user_info.json") as data:
return json.load(data)
-def mock_get_null_user_info(*args, **kwargs):
+def mock_get_null_user_info(*args: Any, **kwargs: Any) -> dict[str, Any]:
with open("tests/portal/get_null_user_info.json") as data:
return json.load(data)
-def mock_get_club_info(*args, **kwargs):
+def mock_get_club_info(*args: Any, **kwargs: Any) -> dict[str, Any]:
with open("tests/portal/get_club_info.json") as data:
return json.load(data)
@@ -38,22 +36,28 @@ def mock_get_club_info(*args, **kwargs):
class TestUserClubs(TestCase):
"""Test User and Club information"""
- def setUp(self):
- self.client = APIClient()
- self.test_user = User.objects.create_user("user", "user@seas.upenn.edu", "user")
+ def setUp(self) -> None:
+ self.client: APIClient = APIClient()
+ self.test_user: UserType = DjangoUserModel.objects.create_user(
+ "user", "user@seas.upenn.edu", "user"
+ )
self.client.force_authenticate(user=self.test_user)
@mock.patch("portal.views.get_user_info", mock_get_user_info)
- def test_user_info(self):
+ def test_user_info(self) -> None:
response = self.client.get("/portal/user/")
res_json = json.loads(response.content)
+ assert isinstance(res_json, dict)
+ assert isinstance(res_json["user"], dict)
self.assertEqual(12345678, res_json["user"]["pennid"])
@mock.patch("portal.views.get_club_info", mock_get_club_info)
@mock.patch("portal.views.get_user_clubs", mock_get_user_clubs)
- def test_user_clubs(self):
+ def test_user_clubs(self) -> None:
response = self.client.get("/portal/clubs/")
res_json = json.loads(response.content)
+ assert isinstance(res_json, dict)
+ assert isinstance(res_json["clubs"], list)
self.assertEqual("pennlabs", res_json["clubs"][0]["code"])
@@ -61,17 +65,21 @@ class TestPolls(TestCase):
"""Tests Create/Update/Retrieve for Polls and Poll Options"""
@mock.patch("portal.serializers.get_user_clubs", mock_get_user_clubs)
- def setUp(self):
+ def setUp(self) -> None:
call_command("load_target_populations", "--years", "2022, 2023, 2024, 2025")
- self.target_id = TargetPopulation.objects.get(population="2024").id
- year = TargetPopulation.objects.get(population="2024").id
+ target = TargetPopulation.objects.get(population="2024")
+ self.target_id = target.id
+ year = target.id
major = TargetPopulation.objects.get(population="Computer Science, BSE").id
school = TargetPopulation.objects.get(
population="School of Engineering and Applied Science"
).id
degree = TargetPopulation.objects.get(population="BACHELORS").id
- self.client = APIClient()
- self.test_user = User.objects.create_user("user", "user@seas.upenn.edu", "user")
+
+ self.client: APIClient = APIClient()
+ self.test_user: UserType = DjangoUserModel.objects.create_user(
+ "user", "user@seas.upenn.edu", "user"
+ )
self.client.force_authenticate(user=self.test_user)
# creates an approved poll to work with
payload = {
@@ -98,10 +106,10 @@ def setUp(self):
poll.save()
poll_1 = Poll.objects.get(question="How is your day")
- self.id = poll_1.id
+ self.poll_id = poll_1.id
@mock.patch("portal.serializers.get_user_clubs", mock_get_user_clubs)
- def test_create_poll(self):
+ def test_create_poll(self) -> None:
# creates an unapproved poll
payload = {
"club_code": "pennlabs",
@@ -119,19 +127,17 @@ def test_create_poll(self):
@mock.patch("portal.views.get_user_clubs", mock_get_user_clubs)
@mock.patch("portal.permissions.get_user_clubs", mock_get_user_clubs)
- def test_update_poll(self):
- payload = {
- "question": "New question",
- }
- response = self.client.patch(f"/portal/polls/{self.id}/", payload)
+ def test_update_poll(self) -> None:
+ payload = {"question": "New question"}
+ response = self.client.patch(f"/portal/polls/{self.poll_id}/", payload)
res_json = json.loads(response.content)
# asserts that the update worked
- self.assertEqual(self.id, res_json["id"])
- self.assertEqual("New question", Poll.objects.get(id=self.id).question)
+ self.assertEqual(self.poll_id, res_json["id"])
+ self.assertEqual("New question", Poll.objects.get(id=self.poll_id).question)
@mock.patch("portal.serializers.get_user_clubs", mock_get_user_clubs)
@mock.patch("portal.logic.get_user_info", mock_get_user_info)
- def test_browse(self):
+ def test_browse(self) -> None:
payload = {
"club_code": "pennlabs",
"question": "How is this question? 2",
@@ -148,7 +154,7 @@ def test_browse(self):
@mock.patch("portal.serializers.get_user_clubs", mock_get_user_clubs)
@mock.patch("portal.logic.get_user_info", mock_get_null_user_info)
- def test_null_user_info_browse(self):
+ def test_null_user_info_browse(self) -> None:
# Asserts that a user with empty user info can access all available polls
response = self.client.post("/portal/polls/browse/", {"id_hash": 1})
res_json = json.loads(response.content)
@@ -158,38 +164,38 @@ def test_null_user_info_browse(self):
@mock.patch("portal.serializers.get_user_clubs", mock_get_user_clubs)
@mock.patch("portal.permissions.get_user_clubs", mock_get_user_clubs)
@mock.patch("portal.logic.get_user_info", mock_get_user_info)
- def test_create_option(self):
- payload_1 = {"poll": self.id, "choice": "yes!"}
- payload_2 = {"poll": self.id, "choice": "no!"}
+ def test_create_option(self) -> None:
+ payload_1 = {"poll": self.poll_id, "choice": "yes!"}
+ payload_2 = {"poll": self.poll_id, "choice": "no!"}
self.client.post("/portal/options/", payload_1)
self.client.post("/portal/options/", payload_2)
self.assertEqual(2, PollOption.objects.all().count())
# asserts options were created and were placed to right poll
for poll_option in PollOption.objects.all():
- self.assertEqual(Poll.objects.get(id=self.id), poll_option.poll)
+ self.assertEqual(Poll.objects.get(id=self.poll_id), poll_option.poll)
response = self.client.post("/portal/polls/browse/", {"id_hash": 1})
res_json = json.loads(response.content)
self.assertEqual(2, len(res_json[0]["options"]))
@mock.patch("portal.permissions.get_user_clubs", mock_get_user_clubs)
@mock.patch("portal.views.get_user_clubs", mock_get_user_clubs)
- def test_update_option(self):
- payload_1 = {"poll": self.id, "choice": "yes!"}
+ def test_update_option(self) -> None:
+ payload_1 = {"poll": self.poll_id, "choice": "yes!"}
response = self.client.post("/portal/options/", payload_1)
res_json = json.loads(response.content)
self.assertEqual("yes!", PollOption.objects.get(id=res_json["id"]).choice)
- payload_2 = {"poll": self.id, "choice": "no!"}
+ payload_2 = {"poll": self.poll_id, "choice": "no!"}
# checks that poll's option was changed
self.client.patch(f'/portal/options/{res_json["id"]}/', payload_2)
self.assertEqual("no!", PollOption.objects.get(id=res_json["id"]).choice)
- def test_review_poll(self):
+ def test_review_poll(self) -> None:
Poll.objects.create(
club_code="pennlabs",
question="hello?",
expire_date=timezone.now() + datetime.timedelta(days=3),
)
- admin = User.objects.create_superuser("admin@example.com", "admin", "admin")
+ admin = DjangoUserModel.objects.create_superuser("admin@example.com", "admin", "admin")
self.client.force_authenticate(user=admin)
response = self.client.get("/portal/polls/review/")
res_json = json.loads(response.content)
@@ -200,12 +206,12 @@ def test_review_poll(self):
@mock.patch("portal.serializers.get_user_clubs", mock_get_user_clubs)
@mock.patch("portal.permissions.get_user_clubs", mock_get_user_clubs)
@mock.patch("portal.logic.get_user_info", mock_get_user_info)
- def test_more_than_five_options(self):
- payload_1 = {"poll": self.id, "choice": "1"}
- payload_2 = {"poll": self.id, "choice": "2"}
- payload_3 = {"poll": self.id, "choice": "3"}
- payload_4 = {"poll": self.id, "choice": "4"}
- payload_5 = {"poll": self.id, "choice": "5"}
+ def test_more_than_five_options(self) -> None:
+ payload_1 = {"poll": self.poll_id, "choice": "1"}
+ payload_2 = {"poll": self.poll_id, "choice": "2"}
+ payload_3 = {"poll": self.poll_id, "choice": "3"}
+ payload_4 = {"poll": self.poll_id, "choice": "4"}
+ payload_5 = {"poll": self.poll_id, "choice": "5"}
self.client.post("/portal/options/", payload_1)
self.client.post("/portal/options/", payload_2)
self.client.post("/portal/options/", payload_3)
@@ -214,17 +220,17 @@ def test_more_than_five_options(self):
self.assertEqual(5, PollOption.objects.all().count())
# asserts options were created and were placed to right poll
for poll_option in PollOption.objects.all():
- self.assertEqual(Poll.objects.get(id=self.id), poll_option.poll)
+ self.assertEqual(Poll.objects.get(id=self.poll_id), poll_option.poll)
response = self.client.post("/portal/polls/browse/", {"id_hash": 1})
res_json = json.loads(response.content)
self.assertEqual(5, len(res_json[0]["options"]))
# adding more than 5 options to same poll should not be allowed
- payload_6 = {"poll": self.id, "choice": "6"}
+ payload_6 = {"poll": self.poll_id, "choice": "6"}
response = self.client.post("/portal/options/", payload_6)
self.assertEqual(5, PollOption.objects.all().count())
- def test_option_vote_view(self):
- response = self.client.get(f"/portal/polls/{self.id}/option_view/")
+ def test_option_vote_view(self) -> None:
+ response = self.client.get(f"/portal/polls/{self.poll_id}/option_view/")
res_json = json.loads(response.content)
self.assertEqual("pennlabs", res_json["club_code"])
# test that options key is in response
@@ -233,7 +239,7 @@ def test_option_vote_view(self):
@mock.patch("portal.serializers.get_user_clubs", mock_get_user_clubs)
@mock.patch("portal.permissions.get_user_clubs", mock_get_user_clubs)
@mock.patch("utils.email.send_automated_email.delay_on_commit")
- def test_send_email_on_create(self, mock_send_email):
+ def test_send_email_on_create(self, mock_send_email: mock.Mock) -> None:
payload = {
"club_code": "pennlabs",
"question": "How is this question? 2",
@@ -249,7 +255,7 @@ def test_send_email_on_create(self, mock_send_email):
@mock.patch("portal.serializers.get_user_clubs", mock_get_user_clubs)
@mock.patch("portal.permissions.get_user_clubs", mock_get_user_clubs)
@mock.patch("utils.email.send_automated_email.delay_on_commit")
- def test_send_email_on_status_change(self, mock_send_email):
+ def test_send_email_on_status_change(self, mock_send_email: mock.Mock) -> None:
payload = {
"club_code": "pennlabs",
"question": "How is this question? 2",
@@ -262,6 +268,7 @@ def test_send_email_on_status_change(self, mock_send_email):
mock_send_email.assert_called_once()
poll = Poll.objects.last()
+ assert poll is not None
poll.status = Poll.STATUS_REVISION
poll.save()
@@ -272,12 +279,14 @@ def test_send_email_on_status_change(self, mock_send_email):
class TestPollVotes(TestCase):
"""Tests Create/Update Polls and History"""
- def setUp(self):
+ def setUp(self) -> None:
call_command("load_target_populations", "--years", "2022, 2023, 2024, 2025")
self.target_id = TargetPopulation.objects.get(population="2024").id
- self.client = APIClient()
- self.test_user = User.objects.create_user("user", "user@seas.upenn.edu", "user")
+ self.client: APIClient = APIClient()
+ self.test_user: UserType = DjangoUserModel.objects.create_user(
+ "user", "user@seas.upenn.edu", "user"
+ )
self.client.force_authenticate(user=self.test_user)
# creates 4 polls, each with 3 options
@@ -331,27 +340,29 @@ def setUp(self):
PollOption.objects.create(poll=p4, choice="choice 12")
@mock.patch("portal.logic.get_user_info", mock_get_user_info)
- def test_create_vote(self):
+ def test_create_vote(self) -> None:
payload_1 = {"id_hash": 1, "poll_options": [self.p1_op1_id]}
response = self.client.post("/portal/votes/", payload_1)
res_json = json.loads(response.content)
+ assert isinstance(res_json, dict)
# tests that voting works
self.assertIn(self.p1_op1_id, res_json["poll_options"])
+ vote = PollVote.objects.first()
+ assert vote is not None
self.assertEqual(1, PollVote.objects.all().count())
- self.assertEqual("1", PollVote.objects.all().first().id_hash)
+ self.assertEqual("1", vote.id_hash)
self.assertIn(
- TargetPopulation.objects.get(id=self.target_id),
- PollVote.objects.all().first().target_populations.all(),
+ TargetPopulation.objects.get(id=self.target_id), vote.target_populations.all()
)
- def test_recent_poll_empty(self):
+ def test_recent_poll_empty(self) -> None:
response = self.client.post("/portal/votes/recent/", {"id_hash": 1})
res_json = json.loads(response.content)
self.assertIsNone(res_json["created_date"])
self.assertIsNone(res_json["poll"]["created_date"])
@mock.patch("portal.logic.get_user_info", mock_get_user_info)
- def test_recent_poll(self):
+ def test_recent_poll(self) -> None:
# answer poll
payload_1 = {"id_hash": 1, "poll_options": [self.p1_op1_id]}
self.client.post("/portal/votes/", payload_1)
@@ -368,7 +379,7 @@ def test_recent_poll(self):
self.assertEquals(self.p4_id, res_json2["poll"]["id"])
@mock.patch("portal.logic.get_user_info", mock_get_user_info)
- def test_all_votes(self):
+ def test_all_votes(self) -> None:
payload_1 = {"id_hash": 1, "poll_options": [self.p1_op1_id]}
self.client.post("/portal/votes/", payload_1)
payload_2 = {"id_hash": 1, "poll_options": [self.p4_op1_id]}
@@ -382,7 +393,7 @@ def test_all_votes(self):
@mock.patch("portal.logic.get_user_info", mock_get_user_info)
@mock.patch("portal.permissions.get_user_clubs", mock_get_user_clubs)
- def test_demographic_breakdown(self):
+ def test_demographic_breakdown(self) -> None:
# plugging in votes for breakdown
payload_1 = {"id_hash": 1, "poll_options": [self.p1_op1_id]}
self.client.post("/portal/votes/", payload_1)
diff --git a/backend/tests/portal/test_posts.py b/backend/tests/portal/test_posts.py
index 4a2b5bae..8b1cf513 100644
--- a/backend/tests/portal/test_posts.py
+++ b/backend/tests/portal/test_posts.py
@@ -1,8 +1,8 @@
import datetime
import json
+from typing import Any, cast
from unittest import mock
-from django.contrib.auth import get_user_model
from django.core.management import call_command
from django.test import TestCase
from django.utils import timezone
@@ -10,26 +10,24 @@
from portal.models import Post, TargetPopulation
from utils.email import get_backend_manager_emails
+from utils.types import DjangoUserModel, UserType
-User = get_user_model()
-
-
-def mock_get_user_clubs(*args, **kwargs):
+def mock_get_user_clubs(*args: Any, **kwargs: Any) -> list[dict]:
with open("tests/portal/get_user_clubs.json") as data:
return json.load(data)
-def mock_get_no_clubs(*args, **kwargs):
+def mock_get_no_clubs(*args: Any, **kwargs: Any) -> list[dict]:
return []
-def mock_get_user_info(*args, **kwargs):
+def mock_get_user_info(*args: Any, **kwargs: Any) -> list[dict]:
with open("tests/portal/get_user_info.json") as data:
return json.load(data)
-def mock_get_club_info(*args, **kwargs):
+def mock_get_club_info(*args: Any, **kwargs: Any) -> list[dict]:
with open("tests/portal/get_club_info.json") as data:
return json.load(data)
@@ -38,11 +36,13 @@ class TestPosts(TestCase):
"""Tests Created/Update/Retrieve for Posts"""
@mock.patch("portal.serializers.get_user_clubs", mock_get_user_clubs)
- def setUp(self):
+ def setUp(self) -> None:
call_command("load_target_populations", "--years", "2022, 2023, 2024, 2025")
- self.target_id = TargetPopulation.objects.get(population="2024").id
- self.client = APIClient()
- self.test_user = User.objects.create_user("user", "user@seas.upenn.edu", "user")
+ self.target_id: int = TargetPopulation.objects.get(population="2024").id
+ self.client: APIClient = APIClient()
+ self.test_user: UserType = DjangoUserModel.objects.create_user(
+ "user", "user@seas.upenn.edu", "user"
+ )
self.client.force_authenticate(user=self.test_user)
payload = {
@@ -57,12 +57,13 @@ def setUp(self):
}
self.client.post("/portal/posts/", payload)
post_1 = Post.objects.all().first()
+ assert post_1 is not None
post_1.status = Post.STATUS_APPROVED
post_1.save()
- self.id = post_1.id
+ self.post_id = post_1.id
@mock.patch("portal.serializers.get_user_clubs", mock_get_user_clubs)
- def test_create_post(self):
+ def test_create_post(self) -> None:
# Creates an unapproved post
payload = {
"club_code": "pennlabs",
@@ -81,7 +82,7 @@ def test_create_post(self):
self.assertEqual(None, Post.objects.get(id=res_json["id"]).admin_comment)
@mock.patch("portal.serializers.get_user_clubs", mock_get_no_clubs)
- def test_fail_post(self):
+ def test_fail_post(self) -> None:
# Creates an unapproved post
payload = {
"club_code": "pennlabs",
@@ -101,29 +102,31 @@ def test_fail_post(self):
@mock.patch("portal.views.get_user_clubs", mock_get_user_clubs)
@mock.patch("portal.permissions.get_user_clubs", mock_get_user_clubs)
- def test_update_post(self):
+ def test_update_post(self) -> None:
payload = {"title": "New Test Title 3"}
- response = self.client.patch(f"/portal/posts/{self.id}/", payload)
+ response = self.client.patch(f"/portal/posts/{self.post_id}/", payload)
res_json = json.loads(response.content)
- self.assertEqual(self.id, res_json["id"])
- self.assertEqual("New Test Title 3", Post.objects.get(id=self.id).title)
+ self.assertEqual(self.post_id, res_json["id"])
+ self.assertEqual("New Test Title 3", Post.objects.get(id=self.post_id).title)
# since the user is not an admin, approved should be set to false after update
self.assertEqual(Post.STATUS_DRAFT, res_json["status"])
@mock.patch("portal.views.get_user_clubs", mock_get_user_clubs)
@mock.patch("portal.permissions.get_user_clubs", mock_get_user_clubs)
- def test_update_post_admin(self):
- admin = User.objects.create_superuser("admin@upenn.edu", "admin", "admin")
+ def test_update_post_admin(self) -> None:
+ admin: UserType = DjangoUserModel.objects.create_superuser(
+ "admin@upenn.edu", "admin", "admin"
+ )
self.client.force_authenticate(user=admin)
payload = {"title": "New Test Title 3"}
- response = self.client.patch(f"/portal/posts/{self.id}/", payload)
+ response = self.client.patch(f"/portal/posts/{self.post_id}/", payload)
res_json = json.loads(response.content)
- self.assertEqual(self.id, res_json["id"])
+ self.assertEqual(self.post_id, res_json["id"])
self.assertEqual(Post.STATUS_APPROVED, res_json["status"])
@mock.patch("portal.serializers.get_user_clubs", mock_get_user_clubs)
@mock.patch("portal.logic.get_user_info", mock_get_user_info)
- def test_browse(self):
+ def test_browse(self) -> None:
payload = {
"club_code": "pennlabs",
"title": "Test Title 2",
@@ -139,7 +142,7 @@ def test_browse(self):
self.assertEqual(1, len(res_json))
self.assertEqual(2, Post.objects.all().count())
- def test_review_post_no_admin_comment(self):
+ def test_review_post_no_admin_comment(self) -> None:
# No admin comment
Post.objects.create(
club_code="notpennlabs",
@@ -147,7 +150,9 @@ def test_review_post_no_admin_comment(self):
subtitle="Test subtitle 2",
expire_date=timezone.localtime() + datetime.timedelta(days=1),
)
- admin = User.objects.create_superuser("admin@upenn.edu", "admin", "admin")
+ admin: UserType = DjangoUserModel.objects.create_superuser(
+ "admin@upenn.edu", "admin", "admin"
+ )
self.client.force_authenticate(user=admin)
response = self.client.get("/portal/posts/review/")
res_json = json.loads(response.content)
@@ -158,7 +163,7 @@ def test_review_post_no_admin_comment(self):
@mock.patch("portal.serializers.get_user_clubs", mock_get_user_clubs)
@mock.patch("portal.permissions.get_user_clubs", mock_get_user_clubs)
@mock.patch("utils.email.send_automated_email.delay_on_commit")
- def test_send_email_on_create(self, mock_send_email):
+ def test_send_email_on_create(self, mock_send_email: mock.Mock) -> None:
payload = {
"club_code": "pennlabs",
"title": "Test Title 2",
@@ -175,7 +180,7 @@ def test_send_email_on_create(self, mock_send_email):
@mock.patch("portal.serializers.get_user_clubs", mock_get_user_clubs)
@mock.patch("portal.permissions.get_user_clubs", mock_get_user_clubs)
@mock.patch("utils.email.send_automated_email.delay_on_commit")
- def test_send_email_on_status_change(self, mock_send_email):
+ def test_send_email_on_status_change(self, mock_send_email: mock.Mock) -> None:
payload = {
"club_code": "pennlabs",
"title": "Test Title 2",
@@ -190,8 +195,10 @@ def test_send_email_on_status_change(self, mock_send_email):
mock_send_email.assert_called_once()
post = Post.objects.last()
+ assert post is not None
+ creator = cast(UserType, post.creator)
post.status = Post.STATUS_APPROVED
post.save()
self.assertEqual(mock_send_email.call_count, 2)
- self.assertEqual(mock_send_email.call_args[0][1], [post.creator.email])
+ self.assertEqual(mock_send_email.call_args[0][1], [creator.email])
diff --git a/backend/tests/sublet/test_permissions.py b/backend/tests/sublet/test_permissions.py
index 39a26c5b..3be61b79 100644
--- a/backend/tests/sublet/test_permissions.py
+++ b/backend/tests/sublet/test_permissions.py
@@ -1,13 +1,10 @@
import json
-from django.contrib.auth import get_user_model
from django.test import TestCase
from rest_framework.test import APIClient
from sublet.models import Amenity, Sublet
-
-
-User = get_user_model()
+from utils.types import DjangoUserModel, UserType
class SubletPermissions(TestCase):
@@ -16,28 +13,34 @@ class SubletPermissions(TestCase):
class OfferPermissions(TestCase):
- def setUp(self):
- self.client = APIClient()
- self.admin = User.objects.create_superuser("admin", "admin@example.com", "admin")
- self.user1 = User.objects.create_user("user1", "user1@seas.upenn.edu", "user1")
- self.user2 = User.objects.create_user("user2", "user2@seas.upenn.edu", "user2")
+ def setUp(self) -> None:
+ self.client: APIClient = APIClient()
+ self.admin: UserType = DjangoUserModel.objects.create_superuser(
+ "admin", "admin@example.com", "admin"
+ )
+ self.user1: UserType = DjangoUserModel.objects.create_user(
+ "user1", "user1@seas.upenn.edu", "user1"
+ )
+ self.user2: UserType = DjangoUserModel.objects.create_user(
+ "user2", "user2@seas.upenn.edu", "user2"
+ )
for i in range(1, 6):
Amenity.objects.create(name=f"Amenity{str(i)}")
# TODO: Add amenities
with open("tests/sublet/mock_sublets.json") as data:
- data = json.load(data)
- self.sublet1 = Sublet.objects.create(subletter=self.admin, **data[0])
- self.sublet2 = Sublet.objects.create(subletter=self.user1, **data[0])
- self.sublet3 = Sublet.objects.create(subletter=self.user2, **data[1])
+ mock_data = json.load(data)
+ self.sublet1 = Sublet.objects.create(subletter=self.admin, **mock_data[0])
+ self.sublet2 = Sublet.objects.create(subletter=self.user1, **mock_data[0])
+ self.sublet3 = Sublet.objects.create(subletter=self.user2, **mock_data[1])
- def test_authentication(self):
+ def test_authentication(self) -> None:
prop_url = f"/sublet/properties/{str(self.sublet1.id)}/offers/"
self.assertEqual(self.client.get(prop_url).status_code, 403)
self.assertEqual(self.client.post(prop_url).status_code, 403)
self.assertEqual(self.client.delete(prop_url).status_code, 403)
self.assertEqual(self.client.get("/sublet/offers/").status_code, 403)
- def create_create_offer(self):
+ def test_create_offer(self) -> None:
prop_url = f"/sublet/properties/{str(self.sublet1.id)}/offers/"
payload = {
"email": "offer@seas.upenn.edu",
@@ -50,7 +53,7 @@ def create_create_offer(self):
self.client.force_authenticate(user=u)
self.assertEqual(self.client.post(prop_url, payload).status_code, 201)
- def test_delete_offer(self):
+ def test_delete_offer(self) -> None:
prop_url = f"/sublet/properties/{str(self.sublet1.id)}/offers/"
payload = {
"email": "offer@seas.upenn.edu",
@@ -64,7 +67,7 @@ def test_delete_offer(self):
self.client.post(prop_url, payload)
self.assertEqual(self.client.delete(prop_url).status_code, 204)
- def test_get_offers_property(self):
+ def test_get_offers_property(self) -> None:
prop_url = f"/sublet/properties/{str(self.sublet2.id)}/offers/"
payload = {
"email": "offer@seas.upenn.edu",
@@ -81,40 +84,46 @@ def test_get_offers_property(self):
self.client.force_authenticate(user=u)
self.assertEqual(self.client.get(prop_url).status_code, c)
- def test_get_offers_user(self):
+ def test_get_offers_user(self) -> None:
self.client.force_authenticate(user=self.user1)
self.assertEqual(self.client.get("/sublet/offers/").status_code, 200)
class FavoritePermissions(TestCase):
- def setUp(self):
- self.client = APIClient()
- self.admin = User.objects.create_superuser("admin", "admin@example.com", "admin")
- self.user1 = User.objects.create_user("user1", "user1@seas.upenn.edu", "user1")
- self.user2 = User.objects.create_user("user2", "user2@seas.upenn.edu", "user2")
+ def setUp(self) -> None:
+ self.client: APIClient = APIClient()
+ self.admin: UserType = DjangoUserModel.objects.create_superuser(
+ "admin", "admin@example.com", "admin"
+ )
+ self.user1: UserType = DjangoUserModel.objects.create_user(
+ "user1", "user1@seas.upenn.edu", "user1"
+ )
+ self.user2: UserType = DjangoUserModel.objects.create_user(
+ "user2", "user2@seas.upenn.edu", "user2"
+ )
for i in range(1, 6):
Amenity.objects.create(name=f"Amenity{str(i)}")
# TODO: Add amenities
with open("tests/sublet/mock_sublets.json") as data:
- data = json.load(data)
- self.sublet1 = Sublet.objects.create(subletter=self.admin, **data[0])
- self.sublet2 = Sublet.objects.create(subletter=self.user1, **data[0])
- self.sublet3 = Sublet.objects.create(subletter=self.user2, **data[1])
+ mock_data = json.load(data)
+ self.sublet1 = Sublet.objects.create(subletter=self.admin, **mock_data[0])
+ self.sublet2 = Sublet.objects.create(subletter=self.user1, **mock_data[0])
+ self.sublet3 = Sublet.objects.create(subletter=self.user2, **mock_data[1])
- def test_authentication(self):
+ def test_authentication(self) -> None:
prop_url = f"/sublet/properties/{str(self.sublet1.id)}/favorites/"
self.assertEqual(self.client.post(prop_url).status_code, 403)
self.assertEqual(self.client.delete(prop_url).status_code, 403)
self.assertEqual(self.client.get("/sublet/favorites/").status_code, 403)
- def test_create_favorite(self):
+ def test_create_favorite(self) -> None:
prop_url = f"/sublet/properties/{str(self.sublet1.id)}/favorites/"
users = [self.admin, self.user1]
for u in users:
self.client.force_authenticate(user=u)
self.assertEqual(self.client.post(prop_url).status_code, 201)
- def test_delete_favorite(self):
+ def test_delete_favorite(self) -> None:
prop_url = f"/sublet/properties/{str(self.sublet1.id)}/favorites/"
users = [self.admin, self.user1]
for u in users:
@@ -122,6 +131,6 @@ def test_delete_favorite(self):
self.client.post(prop_url)
self.assertEqual(self.client.delete(prop_url).status_code, 204)
- def test_get_favorites_user(self):
+ def test_get_favorites_user(self) -> None:
self.client.force_authenticate(user=self.user1)
self.assertEqual(self.client.get("/sublet/favorites/").status_code, 200)
diff --git a/backend/tests/sublet/test_sublets.py b/backend/tests/sublet/test_sublets.py
index 793fc5f9..a10757e5 100644
--- a/backend/tests/sublet/test_sublets.py
+++ b/backend/tests/sublet/test_sublets.py
@@ -1,40 +1,45 @@
import json
+from typing import cast
from unittest.mock import MagicMock
-from django.contrib.auth import get_user_model
from django.core.files.storage import Storage
+from django.forms import ImageField
from django.test import TestCase
from rest_framework.test import APIClient
from sublet.models import Amenity, Offer, Sublet, SubletImage
-
-
-User = get_user_model()
+from utils.types import DjangoUserModel, UserType
class TestSublets(TestCase):
"""Tests Create/Update/Retrieve/List for sublets"""
- def setUp(self):
- self.user = User.objects.create_user("user", "user@seas.upenn.edu", "user")
- self.client = APIClient()
+ def setUp(self) -> None:
+ self.user: UserType = DjangoUserModel.objects.create_user(
+ "user", "user@seas.upenn.edu", "user"
+ )
+ self.client: APIClient = APIClient()
self.client.force_authenticate(user=self.user)
- test_user = User.objects.create_user("user1", "user1@seas.upenn.edu", "user1")
+ test_user: UserType = DjangoUserModel.objects.create_user(
+ "user1", "user1@seas.upenn.edu", "user1"
+ )
for i in range(1, 6):
Amenity.objects.create(name=f"Amenity{str(i)}")
with open("tests/sublet/mock_sublets.json") as data:
- data = json.load(data)
- self.test_sublet1 = Sublet.objects.create(subletter=self.user, **data[0])
- self.test_sublet2 = Sublet.objects.create(subletter=test_user, **data[1])
+ mock_data = json.load(data)
+ self.test_sublet1 = Sublet.objects.create(subletter=self.user, **mock_data[0])
+ self.test_sublet2 = Sublet.objects.create(subletter=test_user, **mock_data[1])
storage_mock = MagicMock(spec=Storage, name="StorageMock")
storage_mock.generate_filename = lambda filename: filename
storage_mock.save = MagicMock(side_effect=lambda name, *args, **kwargs: name)
storage_mock.url = MagicMock(name="url")
storage_mock.url.return_value = "http://penn-mobile.com/mock-image.png"
- SubletImage._meta.get_field("image").storage = storage_mock
- def test_create_sublet(self):
+ image_field = cast(ImageField, SubletImage._meta.get_field("image"))
+ image_field.storage = storage_mock # type: ignore
+
+ def test_create_sublet(self) -> None:
# Create a new sublet using the serializer
payload = {
"title": "Test Sublet1",
@@ -66,13 +71,14 @@ def test_create_sublet(self):
"end_date",
"amenities",
]
- [self.assertEqual(payload[key], res_json[key]) for key in match_keys]
+ for key in match_keys:
+ self.assertEqual(payload[key], res_json[key])
self.assertIn("id", res_json)
self.assertEqual(self.user.id, res_json["subletter"])
self.assertEqual(2, len(res_json["amenities"]))
self.assertIn("images", res_json)
- def test_create_sublet_with_profanity(self):
+ def test_create_sublet_with_profanity(self) -> None:
# Payload with profanity in the title and description
payload_with_profanity = {
"title": "fuck",
@@ -101,7 +107,7 @@ def test_create_sublet_with_profanity(self):
res_json["description"][0], "The description contains inappropriate language."
)
- def test_update_sublet(self):
+ def test_update_sublet(self) -> None:
# Create a sublet to be updated
payload = {
"title": "Test Sublet2",
@@ -124,12 +130,14 @@ def test_update_sublet(self):
response = self.client.patch(f"/sublet/properties/{str(old_id)}/", data)
res_json = json.loads(response.content)
self.assertEqual(3, res_json["beds"])
- self.assertEqual(old_id, Sublet.objects.all().last().id)
- self.assertEqual("New Title", Sublet.objects.get(id=old_id).title)
+ last_sublet = Sublet.objects.all().last()
+ assert last_sublet is not None
+ self.assertEqual(old_id, last_sublet.id)
+ self.assertEqual("New Title", last_sublet.title)
self.assertEqual("New Title", res_json["title"])
self.assertEqual(1, len(res_json["amenities"]))
- def test_browse_sublets(self):
+ def test_browse_sublets(self) -> None:
response = self.client.get("/sublet/properties/")
res_json = json.loads(response.content)
first_length = len(res_json)
@@ -158,7 +166,7 @@ def test_browse_sublets(self):
self.assertEqual(sublet.beds, 2)
self.assertEqual(sublet.baths, 1)
- def test_browse_filtered(self):
+ def test_browse_filtered(self) -> None:
payload = {
"title": "Test Sublet2",
"address": "1234 Test Street",
@@ -175,12 +183,12 @@ def test_browse_filtered(self):
}
response = self.client.post("/sublet/properties/", payload)
old_id = json.loads(response.content)["id"]
- payload = {
+ filter_payload = {
"title": "Sublet2",
- "max_price": 999,
- "min_price": 499,
+ "max_price": "999",
+ "min_price": "499",
}
- response = self.client.get("/sublet/properties/", payload)
+ response = self.client.get("/sublet/properties/", filter_payload)
res_json = json.loads(response.content)
sublet = res_json[0]
self.assertEqual(1, len(res_json))
@@ -208,7 +216,7 @@ def test_browse_filtered(self):
res_json = json.loads(response.content)
self.assertEqual(old_length, len(res_json))
- def test_browse_sublet(self):
+ def test_browse_sublet(self) -> None:
# browse single sublet by id
payload = {
"title": "Test Sublet2",
@@ -234,19 +242,19 @@ def test_browse_sublet(self):
self.assertEqual(res_json["baths"], "1.5")
self.assertEqual(res_json["amenities"], ["Amenity1", "Amenity2"])
- def test_delete_sublet(self):
+ def test_delete_sublet(self) -> None:
sublets_count = Sublet.objects.all().count()
self.client.delete(f"/sublet/properties/{str(self.test_sublet1.id)}/")
self.assertEqual(sublets_count - 1, Sublet.objects.all().count())
self.assertFalse(Sublet.objects.filter(id=1).exists())
- def test_amenities(self):
+ def test_amenities(self) -> None:
response = self.client.get("/sublet/amenities/")
res_json = json.loads(response.content)
for i in range(1, 6):
self.assertIn(f"Amenity{i}", res_json)
- def test_create_image(self):
+ def test_create_image(self) -> None:
with open("tests/sublet/mock_image.jpg", "rb") as image:
response = self.client.post(
f"/sublet/properties/{str(self.test_sublet1.id)}/images/", {"images": image}
@@ -254,9 +262,11 @@ def test_create_image(self):
self.assertEqual(response.status_code, 201)
images = Sublet.objects.get(id=self.test_sublet1.id).images.all()
self.assertTrue(images.exists())
- self.assertEqual(self.test_sublet1.id, images.first().sublet.id)
+ first_image = images.first()
+ assert first_image is not None
+ self.assertEqual(self.test_sublet1.id, first_image.sublet.id)
- def test_create_delete_images(self):
+ def test_create_delete_images(self) -> None:
with open("tests/sublet/mock_image.jpg", "rb") as image:
with open("tests/sublet/mock_image.jpg", "rb") as image2:
response = self.client.post(
@@ -266,10 +276,12 @@ def test_create_delete_images(self):
)
self.assertEqual(response.status_code, 201)
images = Sublet.objects.get(id=self.test_sublet1.id).images.all()
- image_id1 = images.first().id
+ first_image = images.first()
+ assert first_image is not None
+ image_id1 = first_image.id
self.assertTrue(images.exists())
self.assertEqual(2, images.count())
- self.assertEqual(self.test_sublet1.id, images.first().sublet.id)
+ self.assertEqual(self.test_sublet1.id, first_image.sublet.id)
response = self.client.delete(f"/sublet/properties/images/{image_id1}/")
self.assertEqual(response.status_code, 204)
self.assertFalse(SubletImage.objects.filter(id=image_id1).exists())
@@ -279,20 +291,22 @@ def test_create_delete_images(self):
class TestOffers(TestCase):
"""Tests Create/Delete/List for offers"""
- def setUp(self):
- self.user = User.objects.create_user("user", "user@seas.upenn.edu", "user")
- self.client = APIClient()
+ def setUp(self) -> None:
+ self.user: UserType = DjangoUserModel.objects.create_user(
+ "user", "user@seas.upenn.edu", "user"
+ )
+ self.client: APIClient = APIClient()
self.client.force_authenticate(user=self.user)
- self.test_user = User.objects.create_user("user1", "user")
+ self.test_user: UserType = DjangoUserModel.objects.create_user("user1", "user")
for i in range(1, 6):
Amenity.objects.create(name=f"Amenity{str(i)}")
# TODO: Not sure how to add these amenities to the sublets, but not important for now
with open("tests/sublet/mock_sublets.json") as data:
- data = json.load(data)
- self.first_sublet = Sublet.objects.create(subletter=self.user, **data[0])
- self.second_sublet = Sublet.objects.create(subletter=self.test_user, **data[1])
+ mock_data = json.load(data)
+ self.first_sublet = Sublet.objects.create(subletter=self.user, **mock_data[0])
+ self.second_sublet = Sublet.objects.create(subletter=self.test_user, **mock_data[1])
- def test_create_offer(self):
+ def test_create_offer(self) -> None:
prop_url = f"/sublet/properties/{str(self.second_sublet.id)}/offers/"
payload = {
"email": "offer@seas.upenn.edu",
@@ -317,7 +331,7 @@ def test_create_offer(self):
self.assertIsNotNone(offer.id)
self.assertIsNotNone(offer.created_date)
- def test_delete_offer(self):
+ def test_delete_offer(self) -> None:
prop_url1 = f"/sublet/properties/{str(self.first_sublet.id)}/offers/"
prop_url2 = f"/sublet/properties/{str(self.second_sublet.id)}/offers/"
payload = {
@@ -334,7 +348,7 @@ def test_delete_offer(self):
self.client.delete(prop_url2)
self.assertFalse(Offer.objects.filter(user=self.user, sublet=self.second_sublet).exists())
- def test_get_offers_property(self):
+ def test_get_offers_property(self) -> None:
response = self.client.get("/sublet/offers/")
res_json = json.loads(response.content)
self.assertEqual(0, len(res_json))
@@ -376,7 +390,7 @@ def test_get_offers_property(self):
self.assertIsNotNone(offer["id"])
self.assertIsNotNone(offer["created_date"])
- def test_get_offer_user(self):
+ def test_get_offer_user(self) -> None:
response = self.client.get("/sublet/offers/")
res_json = json.loads(response.content)
self.assertEqual(0, len(res_json))
@@ -420,20 +434,22 @@ def test_get_offer_user(self):
class TestFavorites(TestCase):
"""Tests Create/Delete/List for favorites"""
- def setUp(self):
- self.user = User.objects.create_user("user", "user@seas.upenn.edu", "user")
- self.client = APIClient()
+ def setUp(self) -> None:
+ self.user: UserType = DjangoUserModel.objects.create_user(
+ "user", "user@seas.upenn.edu", "user"
+ )
+ self.client: APIClient = APIClient()
self.client.force_authenticate(user=self.user)
- test_user = User.objects.create_user("user1", "user")
+ test_user: UserType = DjangoUserModel.objects.create_user("user1", "user")
for i in range(1, 6):
Amenity.objects.create(name=f"Amenity{str(i)}")
# TODO: Not sure how to add these amenities to the sublets, but not important for now
with open("tests/sublet/mock_sublets.json") as data:
- data = json.load(data)
- self.first_sublet = Sublet.objects.create(subletter=self.user, **data[0])
- self.second_sublet = Sublet.objects.create(subletter=test_user, **data[1])
+ mock_data = json.load(data)
+ self.first_sublet = Sublet.objects.create(subletter=self.user, **mock_data[0])
+ self.second_sublet = Sublet.objects.create(subletter=test_user, **mock_data[1])
- def test_create_favorite(self):
+ def test_create_favorite(self) -> None:
prop_url1 = f"/sublet/properties/{str(self.first_sublet.id)}/favorites/"
prop_url2 = f"/sublet/properties/{str(self.second_sublet.id)}/favorites/"
self.client.post(prop_url2)
@@ -444,7 +460,7 @@ def test_create_favorite(self):
self.assertTrue(self.user.sublets_favorited.filter(pk=self.first_sublet.id).exists())
self.assertEqual(self.client.post(prop_url1).status_code, 406)
- def test_delete_favorite(self):
+ def test_delete_favorite(self) -> None:
self.client.post(f"/sublet/properties/{str(self.second_sublet.id)}/favorites/")
self.client.post(f"/sublet/properties/{str(self.first_sublet.id)}/favorites/")
self.client.delete(f"/sublet/properties/{str(self.first_sublet.id)}/favorites/")
@@ -460,7 +476,7 @@ def test_delete_favorite(self):
self.assertFalse(self.user.sublets_favorited.filter(pk=self.second_sublet.id).exists())
self.assertFalse(self.user.sublets_favorited.filter(pk=self.first_sublet.id).exists())
- def test_get_favorite_user(self):
+ def test_get_favorite_user(self) -> None:
response = self.client.get("/sublet/favorites/")
res_json = json.loads(response.content)
self.assertEqual(len(res_json), 0)
diff --git a/backend/tests/user/test_notifs.py b/backend/tests/user/test_notifs.py
index 512b271a..e860ba8d 100644
--- a/backend/tests/user/test_notifs.py
+++ b/backend/tests/user/test_notifs.py
@@ -1,27 +1,25 @@
import datetime
import json
+from typing import Any
from unittest import mock
-from django.contrib.auth import get_user_model
from django.core.management import call_command
from django.test import TestCase
from django.utils import timezone
from identity.identity import attest, container, get_platform_jwks
from rest_framework.test import APIClient
-from gsr_booking.models import GSR, Group, GSRBooking, Reservation
+from gsr_booking.models import GSR, GSRBooking, Reservation
from user.models import NotificationSetting, NotificationToken
+from utils.types import DjangoUserModel, UserType
-User = get_user_model()
-
-
-def initialize_b2b():
+def initialize_b2b() -> None:
get_platform_jwks()
attest()
-def get_b2b_client():
+def get_b2b_client() -> APIClient:
client = APIClient()
client.logout()
client.credentials(HTTP_AUTHORIZATION="Bearer " + container.access_jwt.serialize())
@@ -29,33 +27,37 @@ def get_b2b_client():
class MockAPNsClient:
- def send_notification(self, token, payload, topic):
+ def send_notification(self, token: str, payload: dict, topic: str) -> None:
del token, payload, topic
pass
- def send_notification_batch(self, notifications, topic):
+ def send_notification_batch(self, notifications: list[dict], topic: str) -> None:
del notifications, topic
pass
-def mock_client(is_dev):
+def mock_client(is_dev: bool) -> MockAPNsClient:
return MockAPNsClient()
class TestNotificationToken(TestCase):
"""Tests for CRUD Notification Tokens"""
- def setUp(self):
- self.client = APIClient()
- self.test_user = User.objects.create_user("user", "user@seas.upenn.edu", "user")
+ def setUp(self) -> None:
+ self.client: APIClient = APIClient()
+ self.test_user: UserType = DjangoUserModel.objects.create_user(
+ "user", "user@seas.upenn.edu", "user"
+ )
self.client.force_authenticate(user=self.test_user)
- def test_post_save(self):
+ def test_post_save(self) -> None:
# asserts that post save hook in creating tokens works correctly
self.assertEqual(1, NotificationToken.objects.all().count())
- self.assertEqual(self.test_user, NotificationToken.objects.all().first().user)
+ notification_token = NotificationToken.objects.all().first()
+ assert notification_token is not None
+ self.assertEqual(self.test_user, notification_token.user)
- def test_create_update_token(self):
+ def test_create_update_token(self) -> None:
NotificationToken.objects.all().delete()
# test that creating token returns correct response
@@ -74,13 +76,13 @@ def test_create_update_token(self):
self.assertEqual("newtoken", res_json["token"])
self.assertEqual(1, NotificationToken.objects.all().count())
- def test_create_token_again_fail(self):
+ def test_create_token_again_fail(self) -> None:
# test that creating token returns correct response
payload = {"kind": "IOS", "token": "test123"}
response = self.client.post("/user/notifications/tokens/", payload)
self.assertEqual(response.status_code, 400)
- def test_get_token(self):
+ def test_get_token(self) -> None:
NotificationToken.objects.all().delete()
# create token
@@ -99,13 +101,15 @@ def test_get_token(self):
class TestNotificationSetting(TestCase):
"""Tests for CRUD Notification Settings"""
- def setUp(self):
- self.client = APIClient()
- self.test_user = User.objects.create_user("user", "user@seas.upenn.edu", "user")
+ def setUp(self) -> None:
+ self.client: APIClient = APIClient()
+ self.test_user: UserType = DjangoUserModel.objects.create_user(
+ "user", "user@seas.upenn.edu", "user"
+ )
self.client.force_authenticate(user=self.test_user)
initialize_b2b()
- def test_get_settings(self):
+ def test_get_settings(self) -> None:
# test that settings visible via GET
response = self.client.get("/user/notifications/settings/")
res_json = json.loads(response.content)
@@ -113,9 +117,9 @@ def test_get_settings(self):
for setting in res_json:
self.assertFalse(setting["enabled"])
- def test_invalid_settings_update(self):
+ def test_invalid_settings_update(self) -> None:
NotificationToken.objects.all().delete()
- payload = {"kind": "IOS", "token": "test123"}
+ payload: dict[str, Any] = {"kind": "IOS", "token": "test123"}
response = self.client.post("/user/notifications/tokens/", payload)
res_json = json.loads(response.content)
@@ -128,13 +132,13 @@ def test_invalid_settings_update(self):
self.assertEqual(res_json["service"], "PENN_MOBILE")
self.assertTrue(res_json["enabled"])
- def test_valid_settings_update(self):
+ def test_valid_settings_update(self) -> None:
NotificationToken.objects.all().delete()
response = self.client.get("/user/notifications/settings/PENN_MOBILE/check/")
res_json = json.loads(response.content)
self.assertFalse(res_json["enabled"])
- payload = {"kind": "IOS", "token": "test123"}
+ payload: dict[str, Any] = {"kind": "IOS", "token": "test123"}
response = self.client.post("/user/notifications/tokens/", payload)
res_json = json.loads(response.content)
@@ -145,7 +149,7 @@ def test_valid_settings_update(self):
response = self.client.patch(f"/user/notifications/settings/{settings_id}/", payload)
self.assertEqual(response.status_code, 400)
- def test_create_update_check_settings(self):
+ def test_create_update_check_settings(self) -> None:
# test that invalid settings are rejected
NotificationSetting.objects.filter(service="PENN_MOBILE").delete()
payload = {"service": "Penn Mobile", "enabled": True}
@@ -181,7 +185,7 @@ def test_create_update_check_settings(self):
res_json = json.loads(response.content)
self.assertTrue(res_json["enabled"])
- def test_check_fail(self):
+ def test_check_fail(self) -> None:
# since invalid setting, should return error
response = self.client.get("/user/notifications/settings/PENN_MOBIL/check/")
self.assertEqual(response.status_code, 400)
@@ -205,7 +209,7 @@ def test_check_fail(self):
# self.assertEqual(res_json["service"], "PENN_MOBILE")
# self.assertFalse(res_json["enabled"])
- def test_b2b_auth_fails(self):
+ def test_b2b_auth_fails(self) -> None:
self.client.logout()
response = self.client.get("/user/notifications/settings/PENN_MOBILE/check/?pennkey=user")
self.assertEqual(response.status_code, 403)
@@ -214,18 +218,22 @@ def test_b2b_auth_fails(self):
class TestNotificationAlert(TestCase):
"""Tests for sending Notification Alerts"""
- def setUp(self):
- self.client = APIClient()
+ def setUp(self) -> None:
+ self.client: APIClient = APIClient()
# create user1
- self.test_user = User.objects.create_user("user", "user@seas.upenn.edu", "user")
+ self.test_user: UserType = DjangoUserModel.objects.create_user(
+ "user", "user@seas.upenn.edu", "user"
+ )
self.client.force_authenticate(user=self.test_user)
token_obj = NotificationToken.objects.get(user=self.test_user)
token_obj.token = "test123"
token_obj.save()
# create user2
- self.test_user = User.objects.create_user("user2", "user2@seas.upenn.edu", "user2")
+ self.test_user = DjangoUserModel.objects.create_user(
+ "user2", "user2@seas.upenn.edu", "user2"
+ )
self.client.force_authenticate(user=self.test_user)
token_obj = NotificationToken.objects.get(user=self.test_user)
token_obj.token = "test234"
@@ -235,7 +243,7 @@ def setUp(self):
setting.save()
# create user3
- user3 = User.objects.create_user("user3", "user3@seas.upenn.edu", "user3")
+ user3 = DjangoUserModel.objects.create_user("user3", "user3@seas.upenn.edu", "user3")
token_obj = NotificationToken.objects.get(user=user3)
token_obj.token = "test234"
token_obj.save()
@@ -246,7 +254,7 @@ def setUp(self):
initialize_b2b()
@mock.patch("user.notifications.get_client", mock_client)
- def test_failed_notif(self):
+ def test_failed_notif(self) -> None:
# missing title
payload = {"body": ":D", "service": "PENN_MOBILE"}
response = self.client.post("/user/notifications/alerts/", payload)
@@ -262,7 +270,7 @@ def test_failed_notif(self):
self.assertEqual(response.status_code, 400)
@mock.patch("user.notifications.get_client", mock_client)
- def test_single_notif(self):
+ def test_single_notif(self) -> None:
# test notif fail when setting is false
payload = {"title": "Test", "body": ":D", "service": "OHQ"}
response = self.client.post("/user/notifications/alerts/", payload)
@@ -278,7 +286,7 @@ def test_single_notif(self):
self.assertEqual(0, len(res_json["failed_users"]))
@mock.patch("user.notifications.get_client", mock_client)
- def test_batch_notif(self):
+ def test_batch_notif(self) -> None:
# update all settings to be enabled
NotificationSetting.objects.all().update(enabled=True)
@@ -319,10 +327,12 @@ def test_batch_notif(self):
class TestSendGSRReminders(TestCase):
"""Test Sending GSR Reminders"""
- def setUp(self):
+ def setUp(self) -> None:
call_command("load_gsrs")
- self.client = APIClient()
- self.test_user = User.objects.create_user("user", "user@seas.upenn.edu", "user")
+ self.client: APIClient = APIClient()
+ self.test_user: UserType = DjangoUserModel.objects.create_user(
+ "user", "user@seas.upenn.edu", "user"
+ )
self.client.force_authenticate(user=self.test_user)
# enabling tokens and settings
@@ -350,38 +360,41 @@ def setUp(self):
start=g.start,
end=g.end,
creator=self.test_user,
- group=Group.objects.get(owner=self.test_user),
)
g.reservation = r
g.save()
@mock.patch("user.notifications.get_client", mock_client)
- def test_send_reminder(self):
+ def test_send_reminder(self) -> None:
call_command("send_gsr_reminders")
r = Reservation.objects.all().first()
+ assert r is not None
self.assertTrue(r.reminder_sent)
- def test_send_reminder_no_gsrs(self):
+ def test_send_reminder_no_gsrs(self) -> None:
GSRBooking.objects.all().delete()
call_command("send_gsr_reminders")
r = Reservation.objects.all().first()
+ assert r is not None
self.assertFalse(r.reminder_sent)
class TestSendShadowNotifs(TestCase):
"""Test Sending Shadow Notifications"""
- def setUp(self):
- self.client = APIClient()
- self.test_user = User.objects.create_user("user", "user@seas.upenn.edu", "user")
+ def setUp(self) -> None:
+ self.client: APIClient = APIClient()
+ self.test_user: UserType = DjangoUserModel.objects.create_user(
+ "user", "user@seas.upenn.edu", "user"
+ )
self.client.force_authenticate(user=self.test_user)
token_obj = NotificationToken.objects.get(user=self.test_user)
token_obj.token = "test123"
token_obj.save()
@mock.patch("user.notifications.get_client", mock_client)
- def test_shadow_notifications(self):
+ def test_shadow_notifications(self) -> None:
# call command on every user
call_command("send_shadow_notifs", "yes", '{"test":"test"}')
diff --git a/backend/tests/user/test_user.py b/backend/tests/user/test_user.py
index 27bc8b83..010f9cf2 100644
--- a/backend/tests/user/test_user.py
+++ b/backend/tests/user/test_user.py
@@ -1,16 +1,12 @@
from django.contrib import auth
-from django.contrib.auth import get_user_model
from django.test import TestCase
from rest_framework.test import APIClient
from user.models import Profile
-User = get_user_model()
-
-
class UserTestCase(TestCase):
- def setUp(self):
+ def setUp(self) -> None:
self.user1 = {
"pennid": 1,
"first_name": "First",
@@ -35,21 +31,28 @@ def setUp(self):
"token": {"access_token": "abc", "refresh_token": "123", "expires_in": 100},
}
- self.client = APIClient()
+ self.client: APIClient = APIClient()
self.client.login(username="user1", password="password")
- def test_user1_profile(self):
+ def test_user1_profile(self) -> None:
user = auth.authenticate(remote_user=self.user1)
self.assertEqual(1, Profile.objects.all().count())
- self.assertEqual(user, Profile.objects.all().first().user)
- self.assertEqual("user", str(Profile.objects.all().first()))
+ profile = Profile.objects.all().first()
+ assert profile is not None
+ self.assertEqual(user, profile.user)
+ self.assertEqual("user", str(profile))
- def test_user2_profile(self):
+ def test_user2_profile(self) -> None:
self.assertEqual(0, Profile.objects.all().count())
user = auth.authenticate(remote_user=self.user2)
+ assert user is not None
self.assertEqual(1, Profile.objects.all().count())
- self.assertEqual(user, Profile.objects.all().first().user)
- user.name = "New Name"
+ profile = Profile.objects.all().first()
+ assert profile is not None
+ self.assertEqual(user, profile.user)
+ user.name = "New Name" # type: ignore
user.save()
self.assertEqual(1, Profile.objects.all().count())
- self.assertEqual(user, Profile.objects.all().first().user)
+ profile = Profile.objects.all().first()
+ assert profile is not None
+ self.assertEqual(user, profile.user)
diff --git a/backend/tests/utils/test_email.py b/backend/tests/utils/test_email.py
index 1b033c6c..b1a4a5e8 100644
--- a/backend/tests/utils/test_email.py
+++ b/backend/tests/utils/test_email.py
@@ -1,31 +1,30 @@
from unittest import mock
-from django.contrib.auth import get_user_model
from django.contrib.auth.models import Group
from django.test import TestCase
from utils.email import get_backend_manager_emails, send_automated_email, send_mail
-
-
-User = get_user_model()
+from utils.types import DjangoUserModel, UserType
class EmailTestCase(TestCase):
- def setUp(self):
- self.group = Group.objects.create(name="backend_managers")
- self.user1 = User.objects.create_user(
+ def setUp(self) -> None:
+ self.group: Group = Group.objects.create(name="backend_managers")
+ self.user1: UserType = DjangoUserModel.objects.create_user(
username="user1", password="password", email="user1@domain.com"
)
- self.user2 = User.objects.create_user(
+ self.user2: UserType = DjangoUserModel.objects.create_user(
username="user2", password="password", email="user2@domain.com"
)
- self.user3 = User.objects.create_user(username="user3", password="password")
+ self.user3: UserType = DjangoUserModel.objects.create_user(
+ username="user3", password="password"
+ )
- self.group.user_set.add(self.user1)
- self.group.user_set.add(self.user3)
+ self.group.user_set.add(self.user1) # type: ignore[attr-defined]
+ self.group.user_set.add(self.user3) # type: ignore[attr-defined]
@mock.patch("utils.email.django_send_mail")
- def test_send_mail(self, mock_send_mail):
+ def test_send_mail(self, mock_send_mail: mock.Mock) -> None:
send_mail("testing321", ["test@example.com"], message="test message?!")
mock_send_mail.assert_called_once_with(
subject="testing321",
@@ -36,12 +35,12 @@ def test_send_mail(self, mock_send_mail):
html_message=None,
)
- def test_send_mail_error(self):
+ def test_send_mail_error(self) -> None:
with self.assertRaises(ValueError):
send_mail("testing321", None, message="test message?!")
@mock.patch("utils.email.django_send_mail")
- def test_send_automated_email(self, mock_send_mail):
+ def test_send_automated_email(self, mock_send_mail: mock.Mock) -> None:
send_automated_email("testing123", ["test@example.com"], "test message?!")
html_message = mock_send_mail.call_args[1]["html_message"]
mock_send_mail.assert_called_once_with(
@@ -55,7 +54,7 @@ def test_send_automated_email(self, mock_send_mail):
self.assertIsNotNone(html_message)
self.assertIn("test message?!", html_message)
- def test_get_backend_manager_emails(self):
+ def test_get_backend_manager_emails(self) -> None:
emails = get_backend_manager_emails()
self.assertEqual(emails, ["user1@domain.com"])
diff --git a/backend/tests/utils/test_r_request.py b/backend/tests/utils/test_r_request.py
index 409afa90..02adc4e5 100644
--- a/backend/tests/utils/test_r_request.py
+++ b/backend/tests/utils/test_r_request.py
@@ -1,68 +1,69 @@
from json.decoder import JSONDecodeError
+from unittest import mock
from unittest.mock import patch
from django.test import TestCase
-from utils.r_request import RRequest
+from utils.r_request import Method, RRequest
-def raise_decode_error():
+def raise_decode_error() -> None:
raise JSONDecodeError("Invalid JSON data", "invalid_json", 0)
class RRequestTestCase(TestCase):
- def setUp(self):
+ def setUp(self) -> None:
self.url = "https://pennlabs.org"
self.json = {"data": "data"}
self.rrequest = RRequest()
@patch("requests.request")
- def test_successful_request(self, mock_response):
+ def test_successful_request(self, mock_response: mock.Mock) -> None:
mock_response.return_value.status_code = 200
- response = self.rrequest.request("get", self.url)
+ response = self.rrequest.request(Method.GET, self.url)
self.assertEqual(200, response.status_code)
@patch("requests.request")
- def test_unsuccessful_request(self, mock_response):
+ def test_unsuccessful_request(self, mock_response: mock.Mock) -> None:
mock_response.return_value.status_code = 400
mock_response.return_value.content = "Bad Error"
- response = self.rrequest.request("post", self.url, json=self.json)
+ response = self.rrequest.request(Method.POST, self.url, json=self.json)
self.assertEqual(400, response.status_code)
self.assertEqual("Bad Error", response.content)
@patch("requests.request")
- def test_bad_json(self, mock_response):
+ def test_bad_json(self, mock_response: mock.Mock) -> None:
mock_response.return_value.status_code = 200
mock_response.return_value.json = raise_decode_error
response = self.rrequest.delete(self.url, json=self.json)
self.assertEqual(200, response.status_code)
@patch("requests.request")
- def test_get_request(self, mock_response):
+ def test_get_request(self, mock_response: mock.Mock) -> None:
mock_response.return_value.status_code = 200
response = self.rrequest.get(self.url)
self.assertEqual(200, response.status_code)
@patch("requests.request")
- def test_post_request(self, mock_response):
+ def test_post_request(self, mock_response: mock.Mock) -> None:
mock_response.return_value.status_code = 200
response = self.rrequest.post(self.url, json=self.json)
self.assertEqual(200, response.status_code)
@patch("requests.request")
- def test_patch_request(self, mock_response):
+ def test_patch_request(self, mock_response: mock.Mock) -> None:
mock_response.return_value.status_code = 200
response = self.rrequest.patch(self.url)
self.assertEqual(200, response.status_code)
@patch("requests.request")
- def test_put_request(self, mock_response):
+ def test_put_request(self, mock_response: mock.Mock) -> None:
mock_response.return_value.status_code = 200
response = self.rrequest.put(self.url, json=self.json)
self.assertEqual(200, response.status_code)
@patch("requests.request")
- def test_delete_request(self, mock_response):
+ def test_delete_request(self, mock_response: mock.Mock) -> None:
mock_response.return_value.status_code = 200
response = self.rrequest.delete(self.url, json=self.json)
self.assertEqual(200, response.status_code)
diff --git a/backend/user/management/commands/clear_cache.py b/backend/user/management/commands/clear_cache.py
index b87d15c7..d5e64f55 100644
--- a/backend/user/management/commands/clear_cache.py
+++ b/backend/user/management/commands/clear_cache.py
@@ -1,4 +1,5 @@
import logging
+from typing import Any
import redis
from django.conf import settings
@@ -30,7 +31,7 @@ def clear_cache() -> int:
class Command(BaseCommand):
- def handle(self, *args, **options) -> None:
+ def handle(self, *args: Any, **options: Any) -> None:
root_logger = logging.getLogger("")
root_logger.setLevel(logging.DEBUG)
diff --git a/backend/user/management/commands/profile_info.py b/backend/user/management/commands/profile_info.py
index 667f4da2..d2350c14 100644
--- a/backend/user/management/commands/profile_info.py
+++ b/backend/user/management/commands/profile_info.py
@@ -1,4 +1,5 @@
from argparse import ArgumentParser
+from typing import Any
from django.contrib.auth import get_user_model
from django.core.management.base import BaseCommand
@@ -16,7 +17,7 @@ def add_arguments(self, parser: ArgumentParser) -> None:
parser.add_argument("--pennkey", type=str, help="pennkey")
parser.add_argument("--email", type=str, help="email")
- def handle(self, *args, **kwargs):
+ def handle(self, *args: Any, **kwargs: Any) -> None:
if kwargs["pennkey"] is None and kwargs["email"] is None:
self.stdout.write("Please provide a pennkey or an email.")
return
diff --git a/backend/user/migrations/0002_profile_laundry_preferences.py b/backend/user/migrations/0002_profile_laundry_preferences.py
index f34836f2..eb606b42 100644
--- a/backend/user/migrations/0002_profile_laundry_preferences.py
+++ b/backend/user/migrations/0002_profile_laundry_preferences.py
@@ -5,15 +5,12 @@
class Migration(migrations.Migration):
- dependencies = [
- ("laundry", "0001_initial"),
- ("user", "0001_initial"),
- ]
+ dependencies = [("laundry", "0001_initial"), ("user", "0001_initial")]
operations = [
migrations.AddField(
model_name="profile",
name="laundry_preferences",
field=models.ManyToManyField(blank=True, to="laundry.LaundryRoom"),
- ),
+ )
]
diff --git a/backend/user/migrations/0003_profile_dining_preferences.py b/backend/user/migrations/0003_profile_dining_preferences.py
index 9b6e3503..592460dc 100644
--- a/backend/user/migrations/0003_profile_dining_preferences.py
+++ b/backend/user/migrations/0003_profile_dining_preferences.py
@@ -5,15 +5,12 @@
class Migration(migrations.Migration):
- dependencies = [
- ("dining", "0001_initial"),
- ("user", "0002_profile_laundry_preferences"),
- ]
+ dependencies = [("dining", "0001_initial"), ("user", "0002_profile_laundry_preferences")]
operations = [
migrations.AddField(
model_name="profile",
name="dining_preferences",
field=models.ManyToManyField(to="dining.Venue"),
- ),
+ )
]
diff --git a/backend/user/migrations/0004_auto_20210324_1851.py b/backend/user/migrations/0004_auto_20210324_1851.py
index 37f3f2d4..448a3b4c 100644
--- a/backend/user/migrations/0004_auto_20210324_1851.py
+++ b/backend/user/migrations/0004_auto_20210324_1851.py
@@ -5,15 +5,12 @@
class Migration(migrations.Migration):
- dependencies = [
- ("dining", "0001_initial"),
- ("user", "0003_profile_dining_preferences"),
- ]
+ dependencies = [("dining", "0001_initial"), ("user", "0003_profile_dining_preferences")]
operations = [
migrations.AlterField(
model_name="profile",
name="dining_preferences",
field=models.ManyToManyField(blank=True, to="dining.Venue"),
- ),
+ )
]
diff --git a/backend/user/migrations/0005_auto_20211003_2240.py b/backend/user/migrations/0005_auto_20211003_2240.py
index f9e011a7..c4abcd54 100644
--- a/backend/user/migrations/0005_auto_20211003_2240.py
+++ b/backend/user/migrations/0005_auto_20211003_2240.py
@@ -5,20 +5,10 @@
class Migration(migrations.Migration):
- dependencies = [
- ("user", "0004_auto_20210324_1851"),
- ]
+ dependencies = [("user", "0004_auto_20210324_1851")]
operations = [
- migrations.RemoveField(
- model_name="profile",
- name="degrees",
- ),
- migrations.RemoveField(
- model_name="profile",
- name="expected_graduation",
- ),
- migrations.DeleteModel(
- name="Degree",
- ),
+ migrations.RemoveField(model_name="profile", name="degrees"),
+ migrations.RemoveField(model_name="profile", name="expected_graduation"),
+ migrations.DeleteModel(name="Degree"),
]
diff --git a/backend/user/migrations/0007_alter_notificationsetting_service.py b/backend/user/migrations/0007_alter_notificationsetting_service.py
index 58c68465..ff16cabe 100644
--- a/backend/user/migrations/0007_alter_notificationsetting_service.py
+++ b/backend/user/migrations/0007_alter_notificationsetting_service.py
@@ -37,9 +37,7 @@ def create_settings_for_users(apps, schema_editor):
class Migration(migrations.Migration):
- dependencies = [
- ("user", "0006_alter_notificationtoken_user_notificationsetting"),
- ]
+ dependencies = [("user", "0006_alter_notificationtoken_user_notificationsetting")]
operations = [
migrations.AlterField(
diff --git a/backend/user/migrations/0008_remove_notificationtoken_dev.py b/backend/user/migrations/0008_remove_notificationtoken_dev.py
index 693cce30..c1d6598c 100644
--- a/backend/user/migrations/0008_remove_notificationtoken_dev.py
+++ b/backend/user/migrations/0008_remove_notificationtoken_dev.py
@@ -5,13 +5,6 @@
class Migration(migrations.Migration):
- dependencies = [
- ("user", "0007_alter_notificationsetting_service"),
- ]
+ dependencies = [("user", "0007_alter_notificationsetting_service")]
- operations = [
- migrations.RemoveField(
- model_name="notificationtoken",
- name="dev",
- ),
- ]
+ operations = [migrations.RemoveField(model_name="notificationtoken", name="dev")]
diff --git a/backend/user/migrations/0009_profile_fitness_preferences.py b/backend/user/migrations/0009_profile_fitness_preferences.py
index ca477331..0a298b52 100644
--- a/backend/user/migrations/0009_profile_fitness_preferences.py
+++ b/backend/user/migrations/0009_profile_fitness_preferences.py
@@ -15,5 +15,5 @@ class Migration(migrations.Migration):
model_name="profile",
name="fitness_preferences",
field=models.ManyToManyField(blank=True, to="penndata.FitnessRoom"),
- ),
+ )
]
diff --git a/backend/user/models.py b/backend/user/models.py
index deb3714b..538bd6e2 100644
--- a/backend/user/models.py
+++ b/backend/user/models.py
@@ -1,11 +1,13 @@
+from typing import Any
+
from django.contrib.auth import get_user_model
from django.db import models
from django.db.models.signals import post_save
from django.dispatch import receiver
-from gsr_booking.models import Group
from laundry.models import LaundryRoom
from penndata.models import FitnessRoom
+from utils.types import UserType
User = get_user_model()
@@ -16,9 +18,9 @@ class NotificationToken(models.Model):
KIND_ANDROID = "ANDROID"
KIND_OPTIONS = ((KIND_IOS, "iOS"), (KIND_ANDROID, "Android"))
- user = models.OneToOneField(User, on_delete=models.CASCADE)
- kind = models.CharField(max_length=7, choices=KIND_OPTIONS, default=KIND_IOS)
- token = models.CharField(max_length=255)
+ user: models.OneToOneField = models.OneToOneField(User, on_delete=models.CASCADE)
+ kind: models.CharField = models.CharField(max_length=7, choices=KIND_OPTIONS, default=KIND_IOS)
+ token: models.CharField = models.CharField(max_length=255)
class NotificationSetting(models.Model):
@@ -49,29 +51,32 @@ class NotificationSetting(models.Model):
(SERVICE_LAUNDRY, "Laundry"),
)
- token = models.ForeignKey(NotificationToken, on_delete=models.CASCADE)
- service = models.CharField(max_length=30, choices=SERVICE_OPTIONS, default=SERVICE_PENN_MOBILE)
- enabled = models.BooleanField(default=True)
+ token: models.ForeignKey = models.ForeignKey(NotificationToken, on_delete=models.CASCADE)
+ service: models.CharField = models.CharField(
+ max_length=30, choices=SERVICE_OPTIONS, default=SERVICE_PENN_MOBILE
+ )
+ enabled: models.BooleanField = models.BooleanField(default=True)
class Profile(models.Model):
- user = models.OneToOneField(User, on_delete=models.CASCADE)
- laundry_preferences = models.ManyToManyField(LaundryRoom, blank=True)
- fitness_preferences = models.ManyToManyField(FitnessRoom, blank=True)
- dining_preferences = models.ManyToManyField("dining.Venue", blank=True)
+ user: models.OneToOneField = models.OneToOneField(User, on_delete=models.CASCADE)
+ laundry_preferences: models.ManyToManyField = models.ManyToManyField(LaundryRoom, blank=True)
+ fitness_preferences: models.ManyToManyField = models.ManyToManyField(FitnessRoom, blank=True)
+ dining_preferences: models.ManyToManyField = models.ManyToManyField("dining.Venue", blank=True)
- def __str__(self):
+ def __str__(self) -> str:
return str(self.user.username)
@receiver(post_save, sender=User)
-def create_or_update_user_profile(sender, instance, created, **kwargs):
+def create_or_update_user_profile(
+ sender: UserType, instance: UserType, created: bool, **kwargs: Any
+) -> None:
"""
This post_save hook triggers automatically when a User object is saved, and if no Profile
object exists for that User, it will create one
"""
Profile.objects.get_or_create(user=instance)
- Group.objects.get_or_create(owner=instance, name="Me", color="#14f7d1")
# notifications
token, _ = NotificationToken.objects.get_or_create(user=instance)
diff --git a/backend/user/notifications.py b/backend/user/notifications.py
index eacdea32..a79fe60c 100644
--- a/backend/user/notifications.py
+++ b/backend/user/notifications.py
@@ -1,7 +1,7 @@
import collections
import os
import sys
-from typing import Optional
+from typing import Optional, Tuple
# Monkey Patch for apn2 errors, referenced from:
@@ -12,34 +12,37 @@
were removed in 3.10. Specifically, the error is coming from a dependency
of apns2 named hyper.
"""
- from collections import abc
+ import collections.abc as collections_abc
- collections.Iterable = abc.Iterable
- collections.Mapping = abc.Mapping
- collections.MutableSet = abc.MutableSet
- collections.MutableMapping = abc.MutableMapping
+ setattr(collections, "Iterable", collections_abc.Iterable)
+ setattr(collections, "Mapping", collections_abc.Mapping)
+ setattr(collections, "MutableSet", collections_abc.MutableSet)
+ setattr(collections, "MutableMapping", collections_abc.MutableMapping)
from apns2.client import APNsClient
from apns2.credentials import TokenCredentials
from apns2.payload import Payload
from celery import shared_task
+from django.db.models import Manager, QuerySet
from user.models import NotificationToken
# taken from the apns2 method for batch notifications
Notification = collections.namedtuple("Notification", ["token", "payload"])
+TokenPair = Tuple[str, str] # (username, token)
+NotificationResult = Tuple[list[str], list[str]] # (success_users, failed_users)
def send_push_notifications(
users: Optional[list[str]],
service: Optional[str],
- title: str,
+ title: Optional[str],
body: str,
delay: int = 0,
is_dev: bool = False,
is_shadow: bool = False,
-) -> tuple[list[str], list[str]]:
+) -> NotificationResult:
"""
Sends push notifications.
@@ -55,27 +58,31 @@ def send_push_notifications(
# collect available usernames & their respective device tokens
token_objects = get_tokens(users, service)
if not token_objects:
- return [], users
+ return [], users if users is not None else []
success_users, tokens = zip(*token_objects)
+ success_users_list = list(success_users)
+ tokens_list = list(tokens)
# send notifications
if delay:
- send_delayed_notifications(tokens, title, body, service, is_dev, is_shadow, delay)
+ send_delayed_notifications(
+ tokens_list, title or "", body, service or "", is_dev, is_shadow, delay
+ )
else:
- send_immediate_notifications(tokens, title, body, service, is_dev, is_shadow)
+ send_immediate_notifications(tokens_list, title, body, service or "", is_dev, is_shadow)
if not users: # if to all users, can't be any failed pennkeys
- return success_users, []
- failed_users = list(set(users) - set(success_users))
- return success_users, failed_users
+ return success_users_list, []
+ failed_users = list(set(users) - set(success_users_list))
+ return success_users_list, failed_users
-def get_tokens(
- users: Optional[list[str]] = None, service: Optional[str] = None
-) -> list[tuple[str, str]]:
+def get_tokens(users: Optional[list[str]] = None, service: Optional[str] = None) -> list[TokenPair]:
"""Returns list of token objects (with username & token value) for specified users"""
- token_objs = NotificationToken.objects.select_related("user").filter(
+ token_objs: QuerySet[
+ NotificationToken, Manager[NotificationToken]
+ ] = NotificationToken.objects.select_related("user").filter(
kind=NotificationToken.KIND_IOS # NOTE: until Android implementation
)
if users:
@@ -84,7 +91,7 @@ def get_tokens(
token_objs = token_objs.filter(
notificationsetting__service=service, notificationsetting__enabled=True
)
- return token_objs.exclude(token="").values_list("user__username", "token")
+ return list(token_objs.exclude(token="").values_list("user__username", "token"))
@shared_task(name="notifications.send_immediate_notifications")
diff --git a/backend/user/serializers.py b/backend/user/serializers.py
index fce622da..e6c9ee90 100644
--- a/backend/user/serializers.py
+++ b/backend/user/serializers.py
@@ -56,11 +56,4 @@ class UserSerializer(serializers.ModelSerializer):
class Meta:
model = get_user_model()
- fields = (
- "id",
- "first_name",
- "last_name",
- "email",
- "username",
- "profile",
- )
+ fields = ("id", "first_name", "last_name", "email", "username", "profile")
diff --git a/backend/user/views.py b/backend/user/views.py
index 260d29d9..2b615977 100644
--- a/backend/user/views.py
+++ b/backend/user/views.py
@@ -1,8 +1,7 @@
-from typing import TYPE_CHECKING, Any, Optional
+from typing import Optional
-from django.contrib.auth import get_user_model
-from django.db.models import QuerySet
-from django.http import HttpResponseRedirect
+from django.db.models import Manager, QuerySet
+from django.http import HttpResponse, HttpResponseRedirect
from django.shortcuts import get_object_or_404
from identity.permissions import B2BPermission
from rest_framework import generics, viewsets
@@ -19,16 +18,7 @@
NotificationTokenSerializer,
UserSerializer,
)
-
-
-if TYPE_CHECKING:
- from django.contrib.auth.models import AbstractUser
-
- UserType = AbstractUser
-else:
- UserType = Any
-
-User = get_user_model()
+from utils.types import DjangoUser, UserType, get_user
class UserView(generics.RetrieveUpdateAPIView):
@@ -60,7 +50,7 @@ class NotificationTokenView(viewsets.ModelViewSet):
permission_classes = [IsAuthenticated]
serializer_class = NotificationTokenSerializer
- def get_queryset(self) -> QuerySet[NotificationToken]:
+ def get_queryset(self) -> QuerySet[NotificationToken, Manager[NotificationToken]]:
return NotificationToken.objects.filter(user=self.request.user)
@@ -80,9 +70,9 @@ class NotificationSettingView(viewsets.ModelViewSet):
serializer_class = NotificationSettingSerializer
def is_authorized(self, request: Request) -> bool:
- return request.user and request.user.is_authenticated
+ return request.user is not None and request.user.is_authenticated
- def get_queryset(self) -> QuerySet[NotificationSetting]:
+ def get_queryset(self) -> QuerySet[NotificationSetting, Manager[NotificationSetting]]:
if self.is_authorized(self.request):
return NotificationSetting.objects.filter(token__user=self.request.user)
return NotificationSetting.objects.none()
@@ -99,9 +89,9 @@ def check(self, request: Request, pk: Optional[str] = None) -> Response:
pennkey = request.GET.get("pennkey")
user = (
- request.user
+ get_user(request)
if self.is_authorized(request)
- else get_object_or_404(User, username=pennkey)
+ else get_object_or_404(DjangoUser, username=pennkey)
)
token = NotificationToken.objects.filter(user=user).first()
@@ -121,13 +111,13 @@ class NotificationAlertView(APIView):
def post(self, request: Request) -> Response:
users = (
- [self.request.user.username]
- if request.user and request.user.is_authenticated
+ [get_user(self.request).username]
+ if get_user(request) and get_user(request).is_authenticated
else request.data.get("users", list())
)
service = request.data.get("service")
- title = request.data.get("title")
- body = request.data.get("body")
+ title = request.data.get("title", None)
+ body = request.data.get("body", None)
delay = max(request.data.get("delay", 0), 0)
is_dev = request.data.get("is_dev", False)
@@ -149,7 +139,7 @@ class ClearCookiesView(APIView):
Clears all cookies from the browser
"""
- def get(self, request: Request) -> Response:
+ def get(self, request: Request) -> HttpResponse:
next_url = request.GET.get("next", None)
response = (
HttpResponseRedirect(f"/api/accounts/login/?next={next_url}")
diff --git a/backend/utils/email.py b/backend/utils/email.py
index c8608cc6..b44d2869 100644
--- a/backend/utils/email.py
+++ b/backend/utils/email.py
@@ -1,16 +1,22 @@
+from typing import Optional
+
from celery import shared_task
-from django.contrib.auth.models import Group
from django.core.mail import send_mail as django_send_mail
from django.template.loader import get_template
@shared_task(name="utils.send_mail")
-def send_mail(subject, recipient_list, message=None, html_message=None):
+def send_mail(
+ subject: str,
+ recipient_list: Optional[list[str]] = None,
+ message: Optional[str] = None,
+ html_message: Optional[str] = None,
+) -> int:
if recipient_list is None:
raise ValueError("Recipient list cannot be None")
success = django_send_mail(
subject=subject,
- message=message,
+ message=message, # type: ignore[arg-type]
from_email=None,
recipient_list=recipient_list,
fail_silently=False,
@@ -21,17 +27,24 @@ def send_mail(subject, recipient_list, message=None, html_message=None):
@shared_task(name="utils.send_automated_email")
-def send_automated_email(subject, recipient_list, message):
+def send_automated_email(
+ subject: str, recipient_list: Optional[list[str]] = None, message: Optional[str] = None
+) -> bool:
template = get_template("email.html")
html_message = template.render({"message": message})
return send_mail(subject, recipient_list, html_message=html_message)
-def get_backend_manager_emails():
- if group := Group.objects.filter(name="backend_managers").first():
- return list(
- group.user_set.exclude(email="")
- .exclude(email__isnull=True)
- .values_list("email", flat=True)
- )
- return []
+def get_backend_manager_emails() -> list[str]:
+ from django.contrib.auth.models import Group
+
+ try:
+ if group := Group.objects.filter(name="backend_managers").first():
+ return list(
+ group.user_set.exclude(email="")
+ .exclude(email__isnull=True)
+ .values_list("email", flat=True)
+ )
+ return []
+ except Group.DoesNotExist:
+ return []
diff --git a/backend/utils/r_request.py b/backend/utils/r_request.py
index 3df7e360..0ae4f96f 100644
--- a/backend/utils/r_request.py
+++ b/backend/utils/r_request.py
@@ -60,7 +60,7 @@ def request(
verify: Optional[bool] = None,
cert: Optional[str] = None,
json: Optional[dict] = None,
- ):
+ ) -> Response:
response = self.__default_response()
for _ in range(self.num_retries):
@@ -93,7 +93,7 @@ def request(
return response
if not response.content:
- response.content = "RRequest: Default Error"
+ response._content = b"RRequest: Default Error"
return response
diff --git a/backend/utils/types.py b/backend/utils/types.py
new file mode 100644
index 00000000..893bfdb1
--- /dev/null
+++ b/backend/utils/types.py
@@ -0,0 +1,75 @@
+from typing import Any, Optional, Protocol, Type, TypeAlias, TypeVar, cast, runtime_checkable
+
+from django.contrib.auth import get_user_model
+from django.contrib.auth.models import AbstractBaseUser
+from django.db.models import Manager, QuerySet
+from rest_framework.request import Request
+
+
+# Get the actual User model
+DjangoUser = get_user_model()
+
+
+class UserManager(Protocol):
+ def create_user(
+ self,
+ username: str,
+ email: Optional[str] = None,
+ password: Optional[str] = None,
+ **kwargs: Any
+ ) -> "UserType": ...
+
+ def create_superuser(self, username: str, email: str, password: str) -> "UserType": ...
+
+ def get(self, **kwargs: Any) -> "UserType": ...
+
+ def filter(self, **kwargs: Any) -> QuerySet["UserType", Manager["UserType"]]: ...
+
+ def all(self) -> QuerySet["UserType", Manager["UserType"]]: ...
+
+
+@runtime_checkable
+class DjangoUserType(Protocol):
+ """Protocol defining the interface of our Django User"""
+
+ objects: UserManager
+ is_superuser: bool
+ id: int
+ username: str
+ email: str
+ is_authenticated: bool
+ is_active: bool
+ is_staff: bool
+ date_joined: Any
+ last_login: Any
+ password: str
+
+ def check_password(self, raw_password: str) -> bool: ...
+
+ def set_password(self, raw_password: str) -> None: ...
+
+ def save(self, *args: Any, **kwargs: Any) -> None: ...
+
+ def get_username(self) -> str: ...
+
+
+DjangoUserInstance = TypeVar("DjangoUserInstance", bound=AbstractBaseUser)
+UserType: TypeAlias = DjangoUserInstance
+DjangoUserModel: Type[DjangoUserType] = cast(Type[DjangoUserType], get_user_model())
+
+
+# Type for authenticated Django user requests
+class AuthRequest(Request):
+ user: UserType
+
+
+def get_auth_user(request: Request) -> DjangoUserType:
+ if not request.user.is_authenticated:
+ from rest_framework.exceptions import NotAuthenticated
+
+ raise NotAuthenticated()
+ return cast(DjangoUserType, request.user)
+
+
+def get_user(request: Request) -> UserType:
+ return cast(UserType, request.user)