-
Notifications
You must be signed in to change notification settings - Fork 6
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add backend support for ownership requests #740
base: master
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Off to a great start! Left some comments; feel free to reply/reach out with any questions. Please also add some test cases when you get a chance 🙂
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the PR, this looks like a great start! Rohan was (very) thorough, not much to add. One last nit: consider updating the PR's title and description to be a little more descriptive. (Take a look at past PRs for examples.) It's a small detail, but when you're reviewing code years in the future, it's great to understand what the author was thinking.
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #740 +/- ##
==========================================
+ Coverage 71.88% 72.59% +0.70%
==========================================
Files 32 32
Lines 6933 7039 +106
==========================================
+ Hits 4984 5110 +126
+ Misses 1949 1929 -20 ☔ View full report in Codecov by Sentry. |
8808888
to
d5e4b6c
Compare
… viewing membership requests
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for resolving the comments from earlier! Left a few more, but mostly looks good to me!
@@ -0,0 +1,72 @@ | |||
# Generated by Django 5.0.4 on 2024-10-18 11:42 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would you be able to delete these migration files and make a new one? Typically, we try and stick to one migration per PR.
@@ -1082,44 +1082,100 @@ class MembershipRequest(models.Model): | |||
Used when users are not in the club but request membership from the owner |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, just occured to me that it might be beneficial to move the shared fields from OwnershipRequest
and MembershipRequest
into an abstract base class – lmk what you think!
"school", | ||
"username", | ||
) | ||
validators = [ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any reason why we're keeping this?
""" | ||
club = request.data.get("club", None) | ||
|
||
club_instance = Club.objects.get(code=club) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will raise an exception if the club does not exist, would recommend using Club.objects.filter(code=club).first()
and handling this case
@@ -2923,3 +2925,1035 @@ def test_club_approval_response_templates(self): | |||
content_type="application/json", | |||
) | |||
self.assertEqual(resp.status_code, 403) | |||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Appreciate the effort here but not sure if we need 1k lines of tests for this feature 😅 . From a quick glance, it looks like you're testing every action with every permission level – it's ok to omit testing everything except the most basic functionality.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1, unit tests are best when they're contained but comprehensive. (Here's a good read on it.) Just glancing through this, I see duplicate checks of the same functionality. (There are 11 repeated checks of the membership-requests-list
route alone.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As an example, I asked GPT to create a condensed version of the ownership requests tests. Might be a good starting point.
def test_request_creation_permissions(self):
"""Test who can create ownership requests"""
for user in [self.officer, self.member, self.non_member]:
self.client.force_login(user)
resp = self.client.post(
reverse("ownership-requests-list"),
{"club": self.club.code},
content_type="application/json"
)
self.assertEqual(resp.status_code, 201)
self.assertTrue(
OwnershipRequest.objects.filter(
club=self.club,
requester=user
).exists()
)
# Verify email was sent
self.assertEqual(len(mail.outbox), 1)
mail.outbox.clear()
def test_request_viewing_permissions(self):
"""Test who can view ownership requests"""
# Create a request
request = OwnershipRequest.objects.create(
club=self.club,
requester=self.officer
)
# Test requester can view own request
self.client.force_login(self.officer)
resp = self.client.get(reverse("ownership-requests-list"))
self.assertEqual(resp.status_code, 200)
self.assertEqual(len(resp.json()), 1)
# Test owner can view club requests
self.client.force_login(self.owner)
resp = self.client.get(
reverse("club-ownership-requests-list", args=(self.club.code,))
)
self.assertEqual(resp.status_code, 200)
self.assertEqual(len(resp.json()), 1)
# Test non-owners cannot view club requests
for user in [self.officer, self.member, self.non_member]:
self.client.force_login(user)
resp = self.client.get(
reverse("club-ownership-requests-list", args=(self.club.code,))
)
self.assertEqual(resp.status_code, 403)
def test_request_withdrawal(self):
"""Test request withdrawal functionality"""
request = OwnershipRequest.objects.create(
club=self.club,
requester=self.officer
)
self.client.force_login(self.officer)
resp = self.client.delete(
reverse("ownership-requests-detail", args=(self.club.code,))
)
self.assertIn(resp.status_code, [200, 204])
# Verify request is marked withdrawn
request.refresh_from_db()
self.assertTrue(request.withdrawn)
# Verify withdrawn request is not visible
resp = self.client.get(
reverse("ownership-requests-detail", args=(self.club.code,))
)
self.assertEqual(resp.status_code, 404)
def test_request_acceptance(self):
"""Test request acceptance functionality"""
request = OwnershipRequest.objects.create(
club=self.club,
requester=self.officer
)
# Test non-owners cannot accept
self.client.force_login(self.officer)
resp = self.client.post(
reverse("club-ownership-requests-accept",
kwargs={
"club_code": self.club.code,
"requester__username": self.officer.username
}
)
)
self.assertEqual(resp.status_code, 403)
# Test owner can accept
self.client.force_login(self.owner)
resp = self.client.post(
reverse("club-ownership-requests-accept",
kwargs={
"club_code": self.club.code,
"requester__username": self.officer.username
}
)
)
self.assertEqual(resp.status_code, 200)
# Verify membership was updated
self.assertTrue(
Membership.objects.filter(
club=self.club,
person=self.officer,
role=Membership.ROLE_OWNER
).exists()
)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great work Gabe! Rohan did a great job with his review, so not much to add outside of minor comments.
@@ -2923,3 +2925,1035 @@ def test_club_approval_response_templates(self): | |||
content_type="application/json", | |||
) | |||
self.assertEqual(resp.status_code, 403) | |||
|
|||
def test_ownershiprequests_create_and_view(self): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: can we make this test_ownership_requests_...
def send_request(self, request=None): | ||
domain = get_domain(request) | ||
|
||
edit_url = settings.EDIT_URL.format(domain=domain, club=self.club.code) | ||
|
||
club_name = self.club.name | ||
|
||
full_name = self.requester.get_full_name() | ||
|
||
context = { | ||
"club_name": club_name, | ||
"edit_url": f"{edit_url}/member", | ||
"full_name": full_name, | ||
} | ||
|
||
owner_emails = list( | ||
self.club.membership_set.filter( | ||
role=Membership.ROLE_OWNER, active=True | ||
).values_list("person__email", flat=True) | ||
) | ||
|
||
send_mail_helper( | ||
name="ownership_request", | ||
subject=f"Ownership Request from {full_name} for {club_name}", | ||
emails=owner_emails, | ||
context=context, | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: use whitespace to break up a large block of code into its smaller logical chunks. The attributes (domain
, edit_url
, etc.) should be grouped together. (Same goes for the rest of the PR.)
|
||
class UserOwnershipRequestSerializer(serializers.ModelSerializer): | ||
""" | ||
Used by the users to return the clubs that the user has sent OwnershipRequest to. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: should be "... that the user has sent an OwnershipRequest to."
queryset = OwnershipRequest.objects.filter( | ||
withdrawn=False, created_at__lte=timezone.now() - datetime.timedelta(days=7) | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@rm03 Do you know why we're limiting this to requests that are a week old? I feel like admins should be able to view all requests. We've had situations where someone needs to take over a club a day before the renewal deadline / Wharton applications / club fair.
<!-- TYPES: | ||
club_name: | ||
type: string | ||
edit_url: | ||
type: string | ||
full_name: | ||
type: string | ||
--> | ||
{% extends 'emails/base.html' %} | ||
|
||
{% block content %} | ||
<h2>Request for ownership for <b>{{ club_name }}</b> from <b>{{ full_name }}</b></h2> | ||
<p style="font-size: 1.2em"><b>{{ full_name }}</b> has submitted a request for ownership of <b>{{ club_name }}</b> through the Penn | ||
Clubs website. To approve this request, use the button below to navigate to the Penn Clubs website.</p> | ||
<a style="text-decoration: none; padding: 5px 20px; font-size: 1.5em; margin-top: 20px; color: white; background-color: #4954f4; border-radius: 3px; font-weight: bold" | ||
href="{{ edit_url }}">Approve Request</a> | ||
{% endblock %} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is great, do you mind updating the membership request email template to match this as well (including the file name format)?
{% extends 'emails/base.html' %} | ||
|
||
{% block content %} | ||
<h2>Request for ownership for <b>{{ club_name }}</b> from <b>{{ full_name }}</b></h2> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: might be cleaner to say "Request for Ownership of {club_name} from {full_name}"
Implement backend for new Ownership Requests feature and update Membership Request feature.
New:
Modified: