Skip to content

Commit

Permalink
Refer to annual payments (dues) not membership
Browse files Browse the repository at this point in the history
SOLE wants us to be more careful about who we term as "members" -- as it
is, MITOC's current membership definition excludes people who pay dues
to the club but then only use the club for renting gear.

We can add some clarity to code/comments/documentation by referring to
payment of dues and *membership* as distinct concepts.

I did my best to update as much user-facing language as possible (as
well as code comments), but `git grep -i member` still has a *lot* of
places. We may need to revisit this later.
  • Loading branch information
DavidCain committed Sep 8, 2024
1 parent a9e0d49 commit d69bd7d
Show file tree
Hide file tree
Showing 47 changed files with 216 additions and 226 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
This is a Django-based trip management system for the [MIT Outing Club][mitoc].

MITOC's volunteer leaders craft trips to take participants climbing, hiking,
biking, skiing, mountaineering, rafting, canoeing, and surfing. All trips are
open to MITOC members - a community of thousands.
biking, skiing, mountaineering, rafting, canoeing, and surfing. Trips are open
to everyone in the club - a community of thousands.


# Deployment
Expand Down
6 changes: 3 additions & 3 deletions frontend/src/components/MembershipStatus.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const MEMBERSHIP_RESPONSE: MembershipResponse = {
email: "[email protected]",
},
waiver: { expires: "2020-02-02", active: true },
status: "Missing Membership",
status: "Missing Dues",
};
let mockAxios: MockAdapter;

Expand Down Expand Up @@ -86,7 +86,7 @@ describe("Expiring Soon", () => {
dateNowSpy.mockRestore();
});

it("Applies a special status for active memberships soon expiring", async () => {
it("Applies a special status for active dues soon expiring", async () => {
respondsWith({
membership: {
// Active, but expiring in a couple days!
Expand All @@ -104,7 +104,7 @@ describe("Expiring Soon", () => {
expect(statusIndicator.props("membershipStatus")).toEqual("Expiring Soon");
});

it("Leaves memberships with plenty of time remaining as 'Active'", async () => {
it("Leaves dues with plenty of time remaining as 'Active'", async () => {
respondsWith({
membership: {
// Active, plenty of time left
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,11 @@ describe("active statuses", () => {
});

describe("inactive statuses", () => {
it("handles missing or expired memberships", () => {
it("handles missing or expired dues payments", () => {
const wrapper = shallowMount(MembershipStatusFaq, {
propsData: { membershipStatus: "Missing" },
});
expect(wrapper.text()).toContain("Why isn't my membership showing up?");
expect(wrapper.text()).toContain("Why isn't my dues payment showing up?");
expect(wrapper.text()).toContain("But I'm positive");
});

Expand Down
20 changes: 7 additions & 13 deletions frontend/src/components/MembershipStatus/MembershipStatusFaq.vue
Original file line number Diff line number Diff line change
@@ -1,18 +1,12 @@
<template>
<div v-if="!membershipIsActive">
<div v-if="missingOrExpired" class="well">
<h5>Why isn't my membership showing up?</h5>
<h5>Why isn't my dues payment showing up?</h5>
<p>
We search for a current MITOC membership under any of your verified
email addresses. If we find a matching membership, we tie that to your
account.
</p>

<p>
If you think you're a current member, but don't see yourself as active
here, you've most likely signed up for a membership under another email
address. Make sure that you add and verify any email address that you
may have signed up with.
If you think you should be current, but don't see yourself as active
here, you've most likely paid annual dues under another email address.
Make sure that you add and verify any email address that you may have
signed up with.
</p>
</div>

Expand All @@ -30,8 +24,8 @@
But I'm positive that my account is under one of these email addresses!
</h5>
<p>
If you've paid your membership dues, signed the waiver, and are still
not seeing that you're an active member, please
If you've paid your annual dues, signed the waiver, and are still not
seeing that you're active, please
<a href="/contact/">contact us</a>.
</p>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,7 @@ it("renders the active state", () => {
});

it("renders warning statuses", () => {
const warningStatuses = [
"Waiver Expired",
"Missing Waiver",
"Missing Membership",
];
const warningStatuses = ["Waiver Expired", "Missing Waiver", "Missing Dues"];
warningStatuses.forEach((membershipStatus) => {
const wrapper = shallowMount(MembershipStatusIndicator, {
propsData: { membershipStatus },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const statusToBootstrapClass: Record<MembershipStatus, string> = {
Missing: "label-danger",
"Waiver Expired": "label-warning",
"Missing Waiver": "label-warning",
"Missing Membership": "label-warning",
"Missing Dues": "label-warning",
"Expiring Soon": "label-info", // Special front-end only status
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ describe("current membership - 'Active' and 'Expiring Soon'", () => {
});
const paragraphs = wrapper.findAll("p");
expect(strippedText(paragraphs.at(0))).toEqual(
"Your membership is active, and expires on Jan 23, 1999."
"Your account is active! Dues are valid through Jan 23, 1999."
);
expect(paragraphs.at(1).text()).toEqual(
"Your waiver will expire on Jan 19, 1999."
Expand All @@ -76,13 +76,13 @@ describe("current membership - 'Active' and 'Expiring Soon'", () => {
});

expect(strippedText(wrapper)).toContain(
"Your membership is expiring soon! Renew today to keep your membership valid until Oct 2, 2026."
"Annual dues expire soon! Renew today to keep your account valid until Oct 2, 2026."
);
});
});

describe("bad membership - 'Expired,' 'Missing,' and 'Missing Membership'", () => {
it("tells the user if we've never had a membership before", () => {
describe("bad membership - 'Expired,' 'Missing,' and 'Missing Dues'", () => {
it("tells the user if we've never received a dues payment", () => {
const wrapper = shallowMount(MembershipStatusSummaryPersonal, {
propsData: {
data: {
Expand All @@ -93,16 +93,16 @@ describe("bad membership - 'Expired,' 'Missing,' and 'Missing Membership'", () =
},
});
expect(strippedText(wrapper)).toEqual(
"We have no membership information on file for any of your verified email addresses. " +
"You must become a member and sign a new waiver in order to participate on trips, rent gear, or use cabins."
"We have no information on file for any of your verified email addresses. " +
"You must pay annual dues and sign a waiver in order to participate on trips, rent gear, or use cabins."
);
expect(wrapper.html()).toContain('href="/profile/membership/"');
expect(wrapper.html()).toContain('href="/profile/waiver/"');
});
});

describe("bad waiver - 'Missing Waiver' and 'Waiver Expired'", () => {
it("prompts active members without a waiver to sign one", () => {
it("prompts members (even with current dues) without a waiver to sign one", () => {
const wrapper = shallowMount(MembershipStatusSummaryPersonal, {
propsData: {
data: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@
</p>
<div v-else-if="membershipStatus === 'Expiring Soon'">
<div class="alert alert-warning">
<h4>Your membership is expiring soon!</h4>
<h4>Annual dues expire soon!</h4>
<p>
<a href="/profile/membership/">Renew today</a>
to keep your membership valid until
to keep your account valid until
{{ renewalValidUntil | formatDate }}.
</p>
</div>
Expand All @@ -22,38 +22,39 @@
</div>
<div v-else-if="membershipStatus === 'Active'">
<p>
Your membership is active, and expires on
Your account is active! Dues are valid through
{{ data.membership.expires | formatDate }}.
</p>
<p>Your waiver will expire on {{ data.waiver.expires | formatDate }}.</p>
</div>
<div v-else-if="membershipStatus === 'Missing Membership'">
<p>We have a current waiver on file, but no active membership.</p>
<div v-else-if="membershipStatus === 'Missing Dues'">
<p>We have a current waiver on file, but no active dues.</p>
<p>
You can still participate in mini-trips, but you'll need
<a href="/profile/membership/">a full MITOC membership</a>
You can still participate in mini-trips, but you'll need to
<a href="/profile/membership/">pay annual dues</a>
in order to rent gear, use cabins, or join other trips.
</p>
</div>
<div v-else-if="membershipStatus === 'Missing'">
<p>
We have no membership information on file for any of your
We have no information on file for any of your
<a href="/accounts/email/">verified email addresses.</a>
</p>

<p>
You must <a href="/profile/membership/">become a member</a> and
<a href="/profile/waiver/">sign a new waiver</a>
You must <a href="/profile/membership/">pay annual dues</a> and
<a href="/profile/waiver/">sign a waiver</a>
in order to participate on trips, rent gear, or use cabins.
</p>
</div>
<div v-else-if="membershipStatus === 'Expired'">
<p>
Your membership expired on {{ data.membership.expires | formatDate }}.
Your last-paid dues expired on
{{ data.membership.expires | formatDate }}.
</p>
<p>
Please <a href="/profile/membership/">renew your membership</a> and
<a href="/profile/waiver/">sign a new waiver</a>.
Please <a href="/profile/membership/">pay annual dues</a> and
<a href="/profile/waiver/">sign a waiver</a>.
</p>
</div>
</div>
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/modules/membership/actions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ describe("getMemberStatus", () => {
email: "[email protected]",
},
waiver: { expires: "2020-02-02", active: true },
status: "Missing Membership",
status: "Missing Dues",
};
mockAxios.onGet("/users/37/membership.json").replyOnce((config) => {
return [200, rawResp];
Expand Down Expand Up @@ -51,7 +51,7 @@ describe("getMemberStatus", () => {
expires: waiverExpires,
},
// Renamed from the keyword `status`
membershipStatus: "Missing Membership",
membershipStatus: "Missing Dues",
};
expect(data).toEqual(expectedData);
});
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/modules/membership/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ interface Waiver {
// email: "[email protected]"
// },
// waiver: { expires: "2020-02-02", active: true },
// status: "Missing Membership"
// status: "Missing Dues"
// };
export interface MembershipResponse {
membership: RawMembership;
Expand Down
10 changes: 5 additions & 5 deletions frontend/src/modules/membership/status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const PADDED_RENEWAL_ALLOWED_WITH_DAYS_LEFT = Math.max(
RENEWAL_ALLOWED_WITH_DAYS_LEFT - 2
);

// When a membership is nearing the end of its time, prompt users to renew!
// When dues are going to expire soon, prompt users to renew!
const PROMPT_RENEWAL_WITH_DAYS_REMAINING = Math.min(
30,
PADDED_RENEWAL_ALLOWED_WITH_DAYS_LEFT
Expand All @@ -22,7 +22,7 @@ export type MembershipStatus =
| "Active"
| "Waiver Expired"
| "Missing Waiver"
| "Missing Membership"
| "Missing Dues"
| "Expiring Soon"
| "Expired"
| "Missing";
Expand Down Expand Up @@ -53,7 +53,7 @@ export function expiringSoon(expiresOn: Moment | null): boolean {
}

/*
* Return if we're positive that renewing membership today would extend membership by one year.
* Return if we're positive that renewing today would extend membership by one year.
*
* Towards the end of a one-year membership, we allow early renewal. This lets members ensure
* that they have uninterrupted membership, while also not double-paying for the overlap period
Expand All @@ -67,9 +67,9 @@ export function earlyRenewalAllowed(expiresOn: Moment | null): boolean {
}

/**
* Return the date that membership would be valid until if paying for a membership today.
* Return the date that membership would be valid until if paying dues today.
*
* In normal circumstances (first membership or renewal), that's just one full year from today.
* In normal circumstances (first payment or renewal), that's just one full year from today.
* If early renewal is permitted, we add remaining valid membership onto the next one.
*/
export function expirationIfRenewingToday(expiresOn: Moment | null): Moment {
Expand Down
28 changes: 14 additions & 14 deletions ws/api_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -492,13 +492,13 @@ def dispatch(self, request, *args, **kwargs):


class UserMembershipView(UserView):
"""Fetch the user's membership information."""
"""Fetch the user's dues and waiver information."""

def get(self, request, *args, **kwargs):
user = self.get_object()

# TODO: Hitting the gear database *every time* is probably not necessary.
# For most people, knowing that their membership & waiver are active is enough.
# For most people, knowing that their dues & waiver are active is enough.
# (We can rely on the membership cache to tell us if somebody's active)
# Similarly, if mitoc-gear is down, falling back on the cache would be nice.

Expand Down Expand Up @@ -563,15 +563,14 @@ def dispatch(self, request, *args, **kwargs):

class UpdateMembershipView(JWTView):
def post(self, request, *args, **kwargs):
"""Receive a message that the user's membership was updated."""
"""Receive a message that the user's dues and/or waiver were updated."""
participant = models.Participant.from_email(self.payload["email"])
if not participant: # Not in our system, nothing to do
return JsonResponse({})

keys = ("membership_expires", "waiver_expires")
update_fields = {
key: date.fromisoformat(self.payload[key])
for key in keys
for key in ("membership_expires", "waiver_expires")
if self.payload.get(key)
}
_membership, created = participant.update_membership(**update_fields)
Expand Down Expand Up @@ -611,22 +610,23 @@ class MembershipStatusesView(View):
"""

def post(self, request, *args, **kwargs):
"""Return a mapping of participant IDs to membership statuses."""
"""Return a mapping of participant IDs to dues/waiver statuses."""
postdata = json.loads(self.request.body)
par_pks = postdata.get("participant_ids")
if not isinstance(par_pks, list):
return JsonResponse({"message": "Bad request"}, status=400)

participants = models.Participant.objects.filter(pk__in=par_pks).select_related(
"membership"
return JsonResponse(
{
"memberships": {
participant.pk: membership_api.format_cached_membership(participant)
for participant in models.Participant.objects.filter(
pk__in=par_pks
).select_related("membership")
}
}
)

participant_memberships = {
participant.pk: membership_api.format_cached_membership(participant)
for participant in participants
}
return JsonResponse({"memberships": participant_memberships})

def dispatch(self, request, *args, **kwargs):
if not perm_utils.is_leader(request.user):
return JsonResponse({}, status=403)
Expand Down
6 changes: 3 additions & 3 deletions ws/cleanup.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ def lapsed_participants() -> QuerySet[models.Participant]:
lapsed_update = Q(profile_last_updated__lt=(now - multiple_update_periods))

today = now.date()
active_members = (
# Anybody with a current membership/waiver is active
active_participants = (
# Anybody with current dues/waiver is active
Q(membership__membership_expires__gte=today)
| Q(membership__waiver_expires__gte=today)
|
Expand All @@ -42,7 +42,7 @@ def lapsed_participants() -> QuerySet[models.Participant]:
Q(signup__trip__trip_date__gte=today)
)

return models.Participant.objects.filter(lapsed_update).exclude(active_members)
return models.Participant.objects.filter(lapsed_update).exclude(active_participants)


@transaction.atomic
Expand Down
2 changes: 1 addition & 1 deletion ws/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ def group_required(
allow_anonymous: bool = False,
allow_superusers: bool = True,
) -> Callable:
"""Requires user membership in at least one of the groups passed in.
"""Requires the user to belong to at least one of the Django groups.
If the user does not belong to any of these groups and `redir_url` is
specified, redirect them to that URL so that they may attempt again.
Expand Down
Loading

0 comments on commit d69bd7d

Please sign in to comment.