diff --git a/README.md b/README.md index 38479fcd..c33ff463 100644 --- a/README.md +++ b/README.md @@ -22,9 +22,6 @@ there to easily launch an instance. ## Account management ![Email address management][screenshots-email_address_management] -## Member discounts -![MITOC members can receive discounts][screenshots-discounts] - ## Leader application ![Submitted application][screenshots-leader_application_submitted] @@ -78,7 +75,6 @@ trip formats once subject to same problems as Winter School. [screenshots-profile]: https://dcain.me/static/images/mitoc-trips/profile.png [screenshots-email_address_management]: https://dcain.me/static/images/mitoc-trips/email_address_management.png - [screenshots-discounts]: https://dcain.me/static/images/mitoc-trips/discounts.png [screenshots-leader_application_submitted]: https://dcain.me/static/images/mitoc-trips/leader_application_submitted.png [screenshots-leader_application_queue]: https://dcain.me/static/images/mitoc-trips/leader_application_queue.png [screenshots-leader_application]: https://dcain.me/static/images/mitoc-trips/leader_application.png diff --git a/poetry.lock b/poetry.lock index 742bac34..523f8689 100644 --- a/poetry.lock +++ b/poetry.lock @@ -117,17 +117,6 @@ files = [ {file = "billiard-4.2.0.tar.gz", hash = "sha256:9a3c3184cb275aa17a732f93f65b20c525d3d9f253722d26a82194803ade5a2c"}, ] -[[package]] -name = "cachetools" -version = "5.3.3" -description = "Extensible memoizing collections and decorators" -optional = false -python-versions = ">=3.7" -files = [ - {file = "cachetools-5.3.3-py3-none-any.whl", hash = "sha256:0abad1021d3f8325b2fc1d2e9c8b9c9d57b04c3932657a72465447332c24d945"}, - {file = "cachetools-5.3.3.tar.gz", hash = "sha256:ba29e2dfa0b8b556606f097407ed1aa62080ee108ab0dc5ec9d6a723a007d105"}, -] - [[package]] name = "celery" version = "5.4.0" @@ -838,62 +827,6 @@ files = [ [package.dependencies] python-dateutil = ">=2.7" -[[package]] -name = "google-auth" -version = "2.31.0" -description = "Google Authentication Library" -optional = false -python-versions = ">=3.7" -files = [ - {file = "google-auth-2.31.0.tar.gz", hash = "sha256:87805c36970047247c8afe614d4e3af8eceafc1ebba0c679fe75ddd1d575e871"}, - {file = "google_auth-2.31.0-py2.py3-none-any.whl", hash = "sha256:042c4702efa9f7d3c48d3a69341c209381b125faa6dbf3ebe56bc7e40ae05c23"}, -] - -[package.dependencies] -cachetools = ">=2.0.0,<6.0" -pyasn1-modules = ">=0.2.1" -rsa = ">=3.1.4,<5" - -[package.extras] -aiohttp = ["aiohttp (>=3.6.2,<4.0.0.dev0)", "requests (>=2.20.0,<3.0.0.dev0)"] -enterprise-cert = ["cryptography (==36.0.2)", "pyopenssl (==22.0.0)"] -pyopenssl = ["cryptography (>=38.0.3)", "pyopenssl (>=20.0.0)"] -reauth = ["pyu2f (>=0.1.5)"] -requests = ["requests (>=2.20.0,<3.0.0.dev0)"] - -[[package]] -name = "google-auth-oauthlib" -version = "1.2.0" -description = "Google Authentication Library" -optional = false -python-versions = ">=3.6" -files = [ - {file = "google-auth-oauthlib-1.2.0.tar.gz", hash = "sha256:292d2d3783349f2b0734a0a0207b1e1e322ac193c2c09d8f7c613fb7cc501ea8"}, - {file = "google_auth_oauthlib-1.2.0-py2.py3-none-any.whl", hash = "sha256:297c1ce4cb13a99b5834c74a1fe03252e1e499716718b190f56bcb9c4abc4faf"}, -] - -[package.dependencies] -google-auth = ">=2.15.0" -requests-oauthlib = ">=0.7.0" - -[package.extras] -tool = ["click (>=6.0.0)"] - -[[package]] -name = "gspread" -version = "6.1.2" -description = "Google Spreadsheets Python API" -optional = false -python-versions = ">=3.8" -files = [ - {file = "gspread-6.1.2-py3-none-any.whl", hash = "sha256:345996fbb74051ee574e3d330a375ac625774f289459f73cb1f8b6fb3cf4cac5"}, - {file = "gspread-6.1.2.tar.gz", hash = "sha256:b147688b8c7a18c9835d5f998997ec17c97c0470babcab17f65ac2b3a32402b7"}, -] - -[package.dependencies] -google-auth = ">=1.12.0" -google-auth-oauthlib = ">=0.4.1" - [[package]] name = "gunicorn" version = "22.0.0" @@ -1558,31 +1491,6 @@ httpx = "*" docs = ["furo", "sphinx", "sphinx-copybutton", "sphinx-inline-tabs", "sphinx-notfound-page", "sphinxcontrib-django", "sphinxext-opengraph"] tests = ["coverage", "tomli"] -[[package]] -name = "pyasn1" -version = "0.6.0" -description = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208)" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pyasn1-0.6.0-py2.py3-none-any.whl", hash = "sha256:cca4bb0f2df5504f02f6f8a775b6e416ff9b0b3b16f7ee80b5a3153d9b804473"}, - {file = "pyasn1-0.6.0.tar.gz", hash = "sha256:3a35ab2c4b5ef98e17dfdec8ab074046fbda76e281c5a706ccd82328cfc8f64c"}, -] - -[[package]] -name = "pyasn1-modules" -version = "0.4.0" -description = "A collection of ASN.1-based protocols modules" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pyasn1_modules-0.4.0-py3-none-any.whl", hash = "sha256:be04f15b66c206eed667e0bb5ab27e2b1855ea54a842e5037738099e8ca4ae0b"}, - {file = "pyasn1_modules-0.4.0.tar.gz", hash = "sha256:831dbcea1b177b28c9baddf4c6d1013c24c3accd14a1873fffaa6a2e905f17b6"}, -] - -[package.dependencies] -pyasn1 = ">=0.4.6,<0.7.0" - [[package]] name = "pycparser" version = "2.22" @@ -1842,20 +1750,6 @@ urllib3 = ">=1.25.10,<3.0" [package.extras] tests = ["coverage (>=6.0.0)", "flake8", "mypy", "pytest (>=7.0.0)", "pytest-asyncio", "pytest-cov", "pytest-httpserver", "tomli", "tomli-w", "types-PyYAML", "types-requests"] -[[package]] -name = "rsa" -version = "4.9" -description = "Pure-Python RSA implementation" -optional = false -python-versions = ">=3.6,<4" -files = [ - {file = "rsa-4.9-py3-none-any.whl", hash = "sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7"}, - {file = "rsa-4.9.tar.gz", hash = "sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21"}, -] - -[package.dependencies] -pyasn1 = ">=0.1.3" - [[package]] name = "ruff" version = "0.5.1" @@ -2161,4 +2055,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.10.0" -content-hash = "7f6798435fbec82b834c582b9420cf70299b6daba3241e24757d35e1eea904f0" +content-hash = "2f26560f84113e77c032aabcf71349e518e45afc67fb10bf0b74382001df8700" diff --git a/pyproject.toml b/pyproject.toml index 3b39823c..173da295 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -161,7 +161,6 @@ django-phonenumber-field = { version = ">= 2.0", extras = ["phonenumberslite"] } django-pipeline = "*" # TODO: To eventually be replaced by webpack-loader django-smtp-ssl = "*" django-webpack-loader = "^1.1.0" # Should maintain parity with frontend/package.json -gspread = "*" gunicorn = "^22.0.0" # Used to run production worker markdown2 = "*" mitoc-const = "^1.0.0" # (1.0.0 includes type hints) diff --git a/ws/api_views.py b/ws/api_views.py index 7e439b54..10218aa7 100644 --- a/ws/api_views.py +++ b/ws/api_views.py @@ -24,7 +24,7 @@ import ws.utils.membership as membership_utils import ws.utils.perms as perm_utils import ws.utils.signups as signup_utils -from ws import enums, models, tasks +from ws import enums, models from ws.decorators import group_required from ws.middleware import RequestWithParticipant from ws.mixins import JsonTripLeadersOnlyView, TripLeadersOnlyView @@ -568,9 +568,6 @@ def post(self, request, *args, **kwargs): if not participant: # Not in our system, nothing to do return JsonResponse({}) - # Can be true in case of early renewal! - was_already_active = participant.membership_active - keys = ("membership_expires", "waiver_expires") update_fields = { key: date.fromisoformat(self.payload[key]) @@ -579,14 +576,6 @@ def post(self, request, *args, **kwargs): } _membership, created = participant.update_membership(**update_fields) - # If the participant has reactivated a membership, update any discount sheets - # (this will ensure that they do not have to wait for the next daily refresh) - if not was_already_active: - for discount in participant.discounts.all(): - tasks.update_discount_sheet_for_participant.delay( - discount.pk, participant.pk - ) - return JsonResponse({}, status=201 if created else 200) @@ -654,7 +643,6 @@ class MemberInfo(TypedDict): is_leader: NotRequired[bool] num_trips_attended: NotRequired[int] num_trips_led: NotRequired[int] - num_discounts: NotRequired[int] class RawMembershipStatsView(View): @@ -678,7 +666,6 @@ def _flat_members_info( "is_leader": info.trips_information.is_leader, "num_trips_attended": info.trips_information.num_trips_attended, "num_trips_led": info.trips_information.num_trips_led, - "num_discounts": info.trips_information.num_discounts, } ) # If there's a verified MIT email address from the trips site, use it! diff --git a/ws/cleanup.py b/ws/cleanup.py index 3b01715a..6fa7ff53 100644 --- a/ws/cleanup.py +++ b/ws/cleanup.py @@ -45,22 +45,6 @@ def lapsed_participants() -> QuerySet[models.Participant]: return models.Participant.objects.filter(lapsed_update).exclude(active_members) -def purge_non_student_discounts() -> None: - """Purge non-students from student-only discounts. - - Student eligibility is enforced at the API and form level. If somebody was - a student at the time of enrolling but is no longer a student, we should - unenroll them. - """ - stu_discounts = models.Discount.objects.filter(student_required=True) - not_student = ~Q(affiliation__in=models.Participant.STUDENT_AFFILIATIONS) - - # Remove student discounts from all non-students who have them - participants = models.Participant.objects.all() - for par in participants.filter(not_student, discounts__in=stu_discounts): - par.discounts.set(par.discounts.filter(student_required=False)) - - @transaction.atomic def purge_old_medical_data() -> None: """For privacy reasons, purge old medical information. diff --git a/ws/email/renew.py b/ws/email/renew.py index a4730a98..0058f9e1 100644 --- a/ws/email/renew.py +++ b/ws/email/renew.py @@ -44,7 +44,6 @@ def send_email_reminding_to_renew( context = { "participant": participant, - "discounts": participant.discounts.all().order_by("name"), "expiry_if_renewing": membership.membership_expires + timedelta(days=365), "unsubscribe_token": unsubscribe.generate_unsubscribe_token(participant), } diff --git a/ws/forms.py b/ws/forms.py index ab259cad..ae52167f 100644 --- a/ws/forms.py +++ b/ws/forms.py @@ -57,37 +57,6 @@ class RequiredModelForm(forms.ModelForm): error_css_class = "warning" -class DiscountForm(forms.ModelForm): - send_membership_reminder = forms.BooleanField( - label="Email me when it's time to renew my membership", - help_text="Ensure continued access to discounts (not required, but strongly recommended!)", - required=False, - ) - - def clean_discounts(self): - """Ensure the participant meets the requirements for each discount.""" - participant = self.instance - discounts = self.cleaned_data["discounts"] - - if not participant.is_student: - for discount in discounts: - if discount.student_required: - raise ValidationError(f"{discount.name} is a student-only discount") - if not discount.ga_key: - # The UI should prevent "enrolling" in these read-only discounts, but check anyway. - raise ValidationError( - f"{discount.name} does not support sharing your information automatically. " - "See discount terms for instructions." - ) - - return discounts - - class Meta: - model = models.Participant - fields = ["discounts", "send_membership_reminder"] - widgets = {"discounts": forms.CheckboxSelectMultiple} - - class ParticipantForm(forms.ModelForm): class Meta: model = models.Participant diff --git a/ws/merge.py b/ws/merge.py index b28762cf..54b81b77 100644 --- a/ws/merge.py +++ b/ws/merge.py @@ -33,13 +33,11 @@ "ws_leaderrecommendation": ("creator_id", "participant_id"), "ws_lectureattendance": ("participant_id", "creator_id"), "ws_winterschoolsettings": ("last_updated_by_id",), - "ws_discount_administrators": ("participant_id",), "ws_distinctaccounts": ("left_id", "right_id"), # Each of these tables should only have one row for the given person. # (For example, it's possible that two participants representing the same human are on the same trip. # In practice, though, this should never actually be happening. Uniqueness constraints will protect us. "ws_tripinfo_drivers": ("participant_id",), - "ws_participant_discounts": ("participant_id",), "ws_trip_leaders": ("participant_id",), "ws_leadersignup": ("participant_id",), "ws_signup": ("participant_id",), diff --git a/ws/models.py b/ws/models.py index 567f79f7..44459b05 100644 --- a/ws/models.py +++ b/ws/models.py @@ -115,56 +115,6 @@ def get_queryset(self): return leaders.prefetch_related("leaderrating_set") -class Discount(models.Model): - """Discount at another company available to MITOC members.""" - - administrators = models.ManyToManyField( - "ws.Participant", - blank=True, - help_text="Persons selected to administer this discount", - related_name="discounts_administered", - ) - - active = models.BooleanField( - default=True, help_text="Discount is currently open & active" - ) - name = models.CharField(max_length=255) - summary = models.CharField(max_length=255) - terms = models.TextField(max_length=4095) - url = models.URLField(blank=True) - ga_key = models.CharField( - max_length=63, - # If blank, then we don't actually report this information to a spreadsheet - blank=True, - help_text="key for Google spreadsheet with membership information (shared as read-only with the company)", - ) - - time_created = models.DateTimeField(auto_now_add=True) - last_updated = models.DateTimeField(auto_now=True) - - student_required = models.BooleanField( - default=False, help_text="Discount provider requires recipients to be students" - ) - - report_school = models.BooleanField( - default=False, help_text="Report MIT affiliation if participant is a student" - ) - report_student = models.BooleanField( - default=False, - help_text="Report MIT affiliation and student status to discount provider", - ) - report_leader = models.BooleanField( - default=False, help_text="Report MITOC leader status to discount provider" - ) - report_access = models.BooleanField( - default=False, - help_text="Report if participant should have leader, student, or admin level access", - ) - - def __str__(self): # pylint: disable=invalid-str-returned - return self.name - - class MembershipStats(SingletonModel): """Cached response from https://mitoc-gear.mit.edu/api-auth/v1/stats @@ -418,8 +368,6 @@ class Participant(models.Model): } ) - discounts = models.ManyToManyField(Discount, blank=True) - class Meta: ordering = ["name", "email"] diff --git a/ws/privacy.py b/ws/privacy.py index 1e05d797..3f2b87b1 100644 --- a/ws/privacy.py +++ b/ws/privacy.py @@ -24,7 +24,6 @@ def all_data(self): fields = [ "user", "membership", - "discounts", "car", "medical", "lottery_info", @@ -60,12 +59,6 @@ def car(self): """Participant's car information.""" return self.par.car and model_to_dict(self.par.car, exclude="id") - @property - def discounts(self): - """Discounts where the participant elected to share their info.""" - for d in self.par.discounts.all(): - yield model_to_dict(d, fields=["name", "active", "summary", "url"]) - @property def authored_feedback(self): """Feedback supplied by the participant.""" diff --git a/ws/settings.py b/ws/settings.py index f39fa2e1..14568c36 100644 --- a/ws/settings.py +++ b/ws/settings.py @@ -138,18 +138,10 @@ } CELERY_BEAT_SCHEDULE = { - "purge-non-student-discounts": { - "task": "ws.tasks.purge_non_student_discounts", - "schedule": crontab(minute=0, hour=2, day_of_week=1), - }, "purge-old-medical-data": { "task": "ws.tasks.purge_old_medical_data", "schedule": crontab(minute=0, hour=2, day_of_week=2), }, - "refresh-all-discount-spreadsheets": { - "task": "ws.tasks.update_all_discount_sheets", - "schedule": crontab(minute=0, hour=3), - }, "send-trip-summaries-email": { "task": "ws.tasks.send_trip_summaries_email", # Tuesdays around noon (ignore DST) @@ -319,10 +311,6 @@ "recipientEvents": [{"recipientEventStatusCode": "Completed"}], } -# Google Sheet (discount roster) settings -OAUTH_JSON_CREDENTIALS = os.getenv("OAUTH_JSON_CREDENTIALS") -DISABLE_GSHEETS = bool(os.getenv("DISABLE_GSHEETS")) - # Internationalization # https://docs.djangoproject.com/en/2.2/topics/i18n/ LANGUAGE_CODE = "en-us" diff --git a/ws/tasks.py b/ws/tasks.py index 6aee05d9..e7f6ec4f 100644 --- a/ws/tasks.py +++ b/ws/tasks.py @@ -5,18 +5,18 @@ from time import monotonic import requests -from celery import group, shared_task +from celery import shared_task from django.core.cache import cache from django.db import connections, transaction from django.db.utils import IntegrityError -from ws import cleanup, models, settings +from ws import cleanup, models from ws.email import renew from ws.email.sole import send_email_to_funds from ws.email.trips import send_trips_summary from ws.lottery.run import SingleTripLotteryRunner, WinterSchoolLotteryRunner from ws.utils import dates as date_utils -from ws.utils import geardb, member_sheets +from ws.utils import geardb logger = logging.getLogger(__name__) @@ -51,80 +51,6 @@ def exclusive_lock(task_identifier: str) -> Iterator[bool]: cache.delete(task_identifier) -@shared_task -def update_discount_sheet_for_participant( - discount_id: int, - participant_id: int, -) -> None: - """Lock the sheet and add/update a single participant. - - Updating of the sheet should not be done at the same time that we're - updating the sheet for another participant (or for all participants, as we - do nightly). Simultaneous edits are prevented with a Redis lock. - """ - discount = models.Discount.objects.get(pk=discount_id) - if not discount.ga_key: - # Form logic should prevent ever letting participants "enroll" in this type of discount - logger.error("Discount %s does not have a Google Sheet!", discount.name) - return - - participant = models.Participant.objects.get(pk=participant_id) - - if settings.DISABLE_GSHEETS: - logger.warning( - "Google Sheets functionality is disabled, not updating '%s' for %s", - discount.name, - participant.name, - ) - return - - with exclusive_lock(f"update_discount-{discount_id}"): - member_sheets.update_participant(discount, participant) - - -@shared_task -def update_discount_sheet( - discount_id: int, - *, - check_all_lapsed_members: bool = False, -) -> None: - """Overwrite the sheet to include all members desiring the discount. - - This is the only means of removing users if they no longer - wish to share their information, so it should be run periodically. - - This task should not run at the same time that we're updating the sheet for - another participant (or for all participants, as we do nightly). - """ - discount = models.Discount.objects.get(pk=discount_id) - if not discount.ga_key: - # Form logic should prevent ever letting participants "enroll" in this type of discount - logger.error("Discount %s does not have a Google Sheet!", discount.name) - return - - logger.info("Updating the discount sheet for %s", discount.name) - - if settings.DISABLE_GSHEETS: - logger.warning( - "Google Sheets functionality is disabled, not updating sheet for '%s'", - discount.name, - ) - return - - trust_cache = not check_all_lapsed_members - with exclusive_lock(f"update_discount-{discount_id}"): - member_sheets.update_discount_sheet(discount, trust_cache=trust_cache) - - -@shared_task -def update_all_discount_sheets() -> None: - logger.info("Updating the member roster for all discount sheets") - discount_pks = models.Discount.objects.exclude(ga_key="").values_list( - "pk", flat=True - ) - group([update_discount_sheet.s(pk) for pk in discount_pks])() - - @shared_task( autoretry_for=(requests.exceptions.RequestException,), # Account for brief outages by retrying after 1 minute, then 2, then 4, then 8 @@ -287,13 +213,6 @@ def run_ws_lottery() -> None: runner() -@shared_task -def purge_non_student_discounts() -> None: - """Purge non-students from student-only discounts.""" - logger.info("Purging non-students from student-only discounts") - cleanup.purge_non_student_discounts() - - @shared_task def purge_old_medical_data() -> None: """Purge old, dated medical information.""" diff --git a/ws/templates/email/membership/renew.html b/ws/templates/email/membership/renew.html index cce40231..9bdc2159 100644 --- a/ws/templates/email/membership/renew.html +++ b/ws/templates/email/membership/renew.html @@ -11,17 +11,6 @@ Your MITOC membership will expire on {{ participant.membership.membership_expires|date:"F j, Y" }}.
- {% if discounts %} -- Renewing is required to maintain access to your discounts with - {% for discount in discounts %} - {% include 'snippets/oxford_comma.html' %} - {{ discount.name }}{% if forloop.last %}.{% endif %} - {% endfor %} - -
- {% endif %} -Renew today to add another 365 days to your membership. (Renewing any time between now and {{ participant.membership.membership_expires|date:"F jS" }} @@ -32,7 +21,6 @@ Your MITOC membership enables you to:
- You are sharing your name, email address, and membership status with the following companies: -
- -- You can change this at any time by modifying your - discount preferences. -
-{% endif %} diff --git a/ws/templates/preferences/discounts.html b/ws/templates/preferences/discounts.html index 7593106f..6f8dc5dc 100644 --- a/ws/templates/preferences/discounts.html +++ b/ws/templates/preferences/discounts.html @@ -6,82 +6,69 @@ {% block content %} {{ block.super }} -+ Previously, MITOC allowed active members to opt into sharing information with local businesses. +
++ The discount program has been discontinued. Local businesses may extend + discounts (at their own discretion) to MITOCers. Inquire directly at those + businesses; MITOC does not manage these programs in any way. +
-Various companies offer discounts and perks to MITOC members. At your request, we can share your membership information to make you eligible for these discounts.
++ To receive a discount for the 2024-25 season, fill out a brief form. +
++ MITOC members who are also students can get discounted passes: + $879 for the Ikon Pass, + $839 for the Ikon Base Plus Pass, + and $589 for the Ikon Base Pass. +
++ The major distinction between the Ikon (Full) and Base passes are that the + Base passes have some blackout dates, and provide 5 days vs. 7 days at + the non-core resorts. -
People who pay dues and rent gear, but have never been on a trip.
-- People who use MITOC discounts, but - have never rented gear or been on a trip. -
- - {% endblock content %} diff --git a/ws/templatetags/discount_tags.py b/ws/templatetags/discount_tags.py deleted file mode 100644 index 88235cbb..00000000 --- a/ws/templatetags/discount_tags.py +++ /dev/null @@ -1,8 +0,0 @@ -from django import template - -register = template.Library() - - -@register.inclusion_tag("for_templatetags/active_discounts.html") -def active_discounts(participant): - return {"participant": participant} diff --git a/ws/tests/email/test_renew.py b/ws/tests/email/test_renew.py index c38caf67..0a5d1b1b 100644 --- a/ws/tests/email/test_renew.py +++ b/ws/tests/email/test_renew.py @@ -2,17 +2,18 @@ from textwrap import dedent from unittest import mock -from bs4 import BeautifulSoup from django.core import mail from django.test import TestCase from freezegun import freeze_time from ws.email import renew -from ws.tests.factories import DiscountFactory, ParticipantFactory +from ws.tests.factories import ParticipantFactory @freeze_time("2020-01-12 09:00:00 EST") class RenewTest(TestCase): + maxDiff = None + def test_will_not_email_without_membership(self): par = ParticipantFactory.create(membership=None) @@ -61,7 +62,7 @@ def test_will_not_email_before_renewal_date(self): self.assertIn("don't yet recommend renewal", str(cm.exception)) - def test_normal_renewal_no_discounts(self): + def test_normal_renewal(self): par = ParticipantFactory.create( # Exact token depends on the participant's PK pk=881203, @@ -86,7 +87,6 @@ def test_normal_renewal_no_discounts(self): Your MITOC membership enables you to: - rent gear from the MITOC office - - enroll in discounts for club members - go on official trips - stay in MITOC's cabins @@ -105,74 +105,3 @@ def test_normal_renewal_no_discounts(self): """ ) self.assertEqual(msg.body, expected_text) - - def test_participant_with_discounts(self): - """We mention a participant's discounts when offering renewal.""" - par = ParticipantFactory.create( - # Exact token depends on the participant's PK - pk=991838, - membership__membership_expires=date(2020, 2, 5), - ) - par.discounts.add(DiscountFactory.create(name="Zazu's Advisory Services")) - par.discounts.add(DiscountFactory.create(name="Acme Corp")) - - with mock.patch.object(mail.EmailMultiAlternatives, "send") as send: - with self.settings(UNSUBSCRIBE_SECRET_KEY="sooper-secret"): # noqa: S106 - msg = renew.send_email_reminding_to_renew(par) - send.assert_called_once() - - expected_text = dedent( - """ - Your MITOC membership will expire on February 5, 2020. - - Renewing is required to maintain access to your discounts with: - - Acme Corp - - Zazu's Advisory Services - - Renew today to add another 365 days to your membership: - https://mitoc-trips.mit.edu/profile/membership/ - - Renewing any time between now and February 5th - will ensure that your membership is valid until February 4, 2021. - - Your MITOC membership enables you to: - - rent gear from the MITOC office - - enroll in discounts for club members - - go on official trips - - stay in MITOC's cabins - - ------------------------------------------------------ - - You can unsubscribe from membership renewal reminders: - https://mitoc-trips.mit.edu/preferences/email/eyJwayI6OTkxODM4LCJlbWFpbHMiOlswXX0:1iqdma:toxfJebkHgNNKTDNf42EiXTHT32ifB8EsqflXOED7R8/ - - Note that we send at most one reminder per year: - we will not email you again unless you renew. - - You can also manage your email preferences directly: - https://mitoc-trips.mit.edu/preferences/email/ - - Questions? Contact us: https://mitoc-trips.mit.edu/contact/ - """ - ) - self.assertEqual(msg.body, expected_text) - - html, mime_type = msg.alternatives[0] - self.assertEqual(mime_type, "text/html") - soup = BeautifulSoup(html, "html.parser") - self.assertEqual( - [tag.attrs["href"] for tag in soup.find_all("a")], - [ - # We link to the discounts immediately, since that's mentioned up-front - "https://mitoc.mit.edu/preferences/discounts/", - "https://mitoc-trips.mit.edu/profile/membership/", - "https://mitoc.mit.edu/rentals", - # All MITOCers are told about discounts in their renewal email - "https://mitoc.mit.edu/preferences/discounts/", - "https://mitoc-trips.mit.edu/trips/", - "https://mitoc.mit.edu/rentals/cabins", - "https://mitoc-trips.mit.edu/preferences/email/eyJwayI6OTkxODM4LCJlbWFpbHMiOlswXX0:1iqdma:toxfJebkHgNNKTDNf42EiXTHT32ifB8EsqflXOED7R8/", - "https://mitoc-trips.mit.edu/preferences/email/", - "https://mitoc-trips.mit.edu/contact/", - ], - ) diff --git a/ws/tests/factories.py b/ws/tests/factories.py index 7e94ef7e..aaa03a03 100644 --- a/ws/tests/factories.py +++ b/ws/tests/factories.py @@ -18,14 +18,6 @@ class Meta: skip_postgeneration_save = True -class DiscountFactory(BaseFactory): - class Meta: - model = models.Discount - - name = "Local Climbing Gym" - active = True - - class EmergencyContactFactory(BaseFactory): class Meta: model = models.EmergencyContact diff --git a/ws/tests/templatetags/test_discount_tags.py b/ws/tests/templatetags/test_discount_tags.py deleted file mode 100644 index d947f2e1..00000000 --- a/ws/tests/templatetags/test_discount_tags.py +++ /dev/null @@ -1,50 +0,0 @@ -from bs4 import BeautifulSoup -from django.template import Context, Template -from django.test import TestCase - -from ws.tests import factories - - -class DiscountTagsTest(TestCase): - def test_no_discounts(self): - html_template = Template( - "{% load discount_tags %}{% active_discounts participant%}" - ) - context = Context({"participant": factories.ParticipantFactory.create()}) - self.assertFalse(html_template.render(context).strip()) - - def test_discounts(self): - participant = factories.ParticipantFactory.create() - gym = factories.DiscountFactory.create(name="Local Gym", url="example.com/gym") - retailer = factories.DiscountFactory.create( - name="Large Retailer", url="example.com/retail" - ) - factories.DiscountFactory.create(name="Other Outing Club") - - participant.discounts.add(gym) - participant.discounts.add(retailer) - - html_template = Template( - "{% load discount_tags %}{% active_discounts participant%}" - ) - context = Context({"participant": participant}) - raw_html = html_template.render(context) - soup = BeautifulSoup(raw_html, "html.parser") - - self.assertEqual( - soup.find("p").get_text(" ", strip=True), - "You are sharing your name, email address, and membership status with the following companies:", - ) - self.assertEqual( - [str(li) for li in soup.find("ul").find_all("li")], - [ - '