Skip to content

Commit b477217

Browse files
committed
Report MIT email for student members
We *require* students to provide a valid `@mit.edu` email address in order to pay dues as students -- this is valuable for identifying somebody's Kerberos even if they choose to have, say, a Gmail address as their preferred address.
1 parent d8978f8 commit b477217

File tree

3 files changed

+94
-1
lines changed

3 files changed

+94
-1
lines changed

ws/api_views.py

+2
Original file line numberDiff line numberDiff line change
@@ -646,6 +646,7 @@ class MemberInfo(TypedDict):
646646
affiliation: str
647647
num_rentals: int
648648
# Fields from TripInformation, if found
649+
mit_email: str | None
649650
is_leader: NotRequired[bool]
650651
num_trips_attended: NotRequired[int]
651652
num_trips_led: NotRequired[int]
@@ -669,6 +670,7 @@ def _flat_members_info(
669670
# A bit repetetive, but `_as_dict()` won't satisfy mypy
670671
{
671672
"email": info.trips_information.email,
673+
"mit_email": info.trips_information.mit_email,
672674
"is_leader": info.trips_information.is_leader,
673675
"num_trips_attended": info.trips_information.num_trips_attended,
674676
"num_trips_led": info.trips_information.num_trips_led,

ws/tests/views/test_api_views.py

+74-1
Original file line numberDiff line numberDiff line change
@@ -659,6 +659,15 @@ def test_fetches_async_by_default(self):
659659
verified=True,
660660
primary=False,
661661
)
662+
# Reflect reality -- to be an MIT student, you *must* have a verified mit.edu
663+
self.participant.affiliation = "MU"
664+
self.participant.save()
665+
factories.EmailAddressFactory.create(
666+
user=self.participant.user,
667+
verified=True,
668+
primary=False,
669+
670+
)
662671
factories.SignUpFactory.create(participant=self.participant, on_trip=True)
663672

664673
with mock.patch.object(tasks.update_member_stats, "delay") as update:
@@ -680,6 +689,7 @@ def test_fetches_async_by_default(self):
680689
"num_trips_attended": 1,
681690
"num_trips_led": 0,
682691
"num_discounts": 0,
692+
"mit_email": "[email protected]",
683693
},
684694
],
685695
},
@@ -768,6 +778,7 @@ def test_matches_on_verified_emails_only(self) -> None:
768778
"num_rentals": 0,
769779
"num_trips_attended": 0,
770780
"num_trips_led": 0,
781+
"mit_email": None,
771782
},
772783
# We did not find a matching trips account
773784
{
@@ -777,6 +788,56 @@ def test_matches_on_verified_emails_only(self) -> None:
777788
},
778789
)
779790

791+
@responses.activate
792+
def test_ignores_possibly_old_mit_edu(self):
793+
with freeze_time("2019-02-22 12:25:00 EST"):
794+
cached = models.MembershipStats.load()
795+
cached.response = [
796+
{
797+
"id": 37,
798+
"affiliation": "MIT alum (former student)",
799+
"alternate_emails": ["[email protected]"],
800+
"email": "[email protected]",
801+
"num_rentals": 3,
802+
}
803+
]
804+
cached.save()
805+
806+
# Simulate an old MIT email they may no longer own!
807+
factories.EmailAddressFactory.create(
808+
user=self.participant.user,
809+
810+
verified=True,
811+
primary=False,
812+
)
813+
# MIT alum -- they *used* to own [email protected]
814+
self.participant.affiliation = "ML"
815+
self.participant.save()
816+
817+
with mock.patch.object(tasks.update_member_stats, "delay"):
818+
response = self.client.get("/stats/membership.json") # No cache_strategy
819+
820+
self.assertEqual(
821+
response.json(),
822+
{
823+
# We used the cached information from the geardb
824+
"retrieved_at": "2019-02-22T12:25:00-05:00",
825+
"members": [
826+
{
827+
"email": self.participant.email,
828+
"affiliation": "MIT alum (former student)",
829+
"num_rentals": 3,
830+
"is_leader": True,
831+
"num_trips_attended": 0,
832+
"num_trips_led": 0,
833+
"num_discounts": 0,
834+
# We do *not* report the mit.edu email -- it may be old
835+
"mit_email": None,
836+
},
837+
],
838+
},
839+
)
840+
780841
@responses.activate
781842
def test_trips_data_included(self):
782843
responses.get(
@@ -812,6 +873,15 @@ def test_trips_data_included(self):
812873
verified=True,
813874
primary=False,
814875
)
876+
# Reflect reality -- to be an MIT student, you *must* have a verified mit.edu
877+
self.participant.affiliation = "MU"
878+
self.participant.save()
879+
factories.EmailAddressFactory.create(
880+
user=self.participant.user,
881+
verified=True,
882+
primary=False,
883+
884+
)
815885
bob = factories.ParticipantFactory.create(email="[email protected]")
816886
factories.EmailAddressFactory.create(
817887
user=bob.user, email="[email protected]", verified=True, primary=False
@@ -849,6 +919,7 @@ def test_trips_data_included(self):
849919
"num_trips_attended": 1,
850920
"num_trips_led": 2,
851921
"num_discounts": 0,
922+
"mit_email": None,
852923
},
853924
{
854925
"email": self.participant.email,
@@ -858,6 +929,7 @@ def test_trips_data_included(self):
858929
"num_trips_attended": 2,
859930
"num_trips_led": 1,
860931
"num_discounts": 1,
932+
"mit_email": "[email protected]",
861933
},
862934
# We did not find a matching trips account
863935
{
@@ -878,5 +950,6 @@ def test_trips_data_included(self):
878950
# 1. Count trips per participant (separate to avoid double-counting)
879951
# 2. Count discounts, trips led, per participant
880952
# 3. Get all emails (lowercased, for mapping back to participant records)
881-
with self.assertNumQueries(3):
953+
# 4. Get MIT email addresses
954+
with self.assertNumQueries(4):
882955
stats.with_trips_information()

ws/utils/member_stats.py

+18
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from allauth.account.models import EmailAddress
99
from django.db.models import Count, Exists, OuterRef
1010
from django.db.models.functions import Lower
11+
from mitoc_const import affiliations
1112
from typing_extensions import assert_never
1213

1314
from ws import models, tasks
@@ -32,6 +33,9 @@ class TripsInformation(NamedTuple):
3233
# Email address as given on Participant object
3334
# (We assume this is their preferred current email)
3435
email: str
36+
# In order to claim MIT student status, you must give a Kerberos.
37+
# We'll only report this for current students (since many others will lack)
38+
mit_email: str | None
3539

3640

3741
class MembershipInformation(NamedTuple):
@@ -161,6 +165,19 @@ def _get_trip_stats_by_user() -> dict[int, TripsInformation]:
161165
.values_list("participant_id", "num_trips")
162166
)
163167

168+
# Technically, yes, one could have multiple verified mit.edu addresses.
169+
# But that's going to be exceptionally rare, and the dict will handle dupes.
170+
mit_email_for_students: dict[str, int] = dict(
171+
models.Participant.objects.filter(
172+
affiliation__in=[
173+
affiliations.MIT_UNDERGRAD.CODE,
174+
affiliations.MIT_GRAD_STUDENT.CODE,
175+
],
176+
user__emailaddress__verified=True,
177+
user__emailaddress__email__iendswith="@mit.edu",
178+
).values_list("pk", "user__emailaddress__email")
179+
)
180+
164181
additional_stats = (
165182
models.Participant.objects.all()
166183
.annotate(
@@ -187,6 +204,7 @@ def _get_trip_stats_by_user() -> dict[int, TripsInformation]:
187204
return {
188205
par["user_id"]: TripsInformation(
189206
email=par["email"],
207+
mit_email=mit_email_for_students.get(par["pk"]),
190208
is_leader=par["is_leader"],
191209
num_trips_attended=trips_per_participant.get(par["pk"], 0),
192210
num_trips_led=par["num_trips_led"],

0 commit comments

Comments
 (0)