Skip to content
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

Open
wants to merge 36 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
42e953b
Make models, perms, serializers, urls, and views for ownershiprequest…
gabeweng Oct 13, 2024
9439212
add API view for admin to see all ownershiprequests older than a week
gabeweng Oct 15, 2024
32baced
merge conflicting migrations
gabeweng Oct 15, 2024
38224e3
Merge branch 'master' into feat/ownership-requests
gabeweng Oct 18, 2024
dc828b2
migrate ownershiprequests
gabeweng Oct 18, 2024
5fba7b8
edit descriptions of OwnershipRequestSerializer and OwnershipRequest
gabeweng Oct 18, 2024
6b511f0
rename, update ownershiprequest model fields
gabeweng Oct 18, 2024
f36d1f9
edit ownership_request.html template
gabeweng Oct 18, 2024
c5b6c4c
Combine club admin and superuser viewsets for ownership requests
gabeweng Oct 18, 2024
917c1fc
Specify YAML documentation for old_requests in OwnershipRequestManage…
gabeweng Oct 18, 2024
ec1597f
Fix permissions for OwnershipRequest management to not specify clubs.…
gabeweng Oct 20, 2024
d8ee660
Change url basename for user ownership requests
gabeweng Oct 30, 2024
f905122
Use update_or_create logic for Ownership Requests
gabeweng Oct 30, 2024
fa2b7e7
Write test for creating and viewing Ownership Requests
gabeweng Oct 30, 2024
5462032
Return 204 for Requester view for destroy ownershiprequest
gabeweng Nov 1, 2024
a16d758
Edit OwnershipRequestAdmin for admin dashboard
gabeweng Nov 1, 2024
ffed858
Create test for withdraw feature of OwnershipRequests
gabeweng Nov 1, 2024
60f0962
Create test for accept feature of OwnershipRequests
gabeweng Nov 1, 2024
d5e4b6c
Create test for destroy feature of OwnershipRequests
gabeweng Nov 1, 2024
d94a5f4
Merge branch 'master' into feat/ownership-requests
gabeweng Nov 8, 2024
4e31e86
Change comments and assert status codes for test_ownership_requests
gabeweng Nov 8, 2024
ac1f4de
Change basename for membership requests and add test for creating and…
gabeweng Nov 8, 2024
cf89829
Add test for withdrawing membership requests
gabeweng Nov 8, 2024
038a1bb
Add test for accepting membership requests
gabeweng Nov 8, 2024
826f54c
Add test for destroying membership requests
gabeweng Nov 8, 2024
ff1719e
Update Membership Requests model and logic to match Ownership Requests
gabeweng Nov 8, 2024
01dc669
Merge branch 'feat/ownership-requests' of https://github.com/pennlabs…
gabeweng Nov 8, 2024
eb6e86f
Make abstract base class Request for MembershipRequest and OwnershipR…
gabeweng Nov 15, 2024
c5d7941
Modify and rename request.html email template to membership_request.html
gabeweng Nov 15, 2024
f77b281
Shorten and rename tests for membership and ownership requests
gabeweng Nov 15, 2024
eb4fb0a
minor edits to ownership requests accept and destroy tests
gabeweng Nov 15, 2024
7b923a1
Fixing nits
gabeweng Nov 15, 2024
b244e89
Fix redundant UniqueTogether logic in Membership/OwnerhipRequestSeria…
gabeweng Nov 15, 2024
87af1ab
Merge branch 'master' into feat/ownership-requests
gabeweng Nov 17, 2024
6315065
handle invalid club code in request creation
gabeweng Nov 17, 2024
bf105d4
Change old-requests to all-requests
gabeweng Nov 22, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 34 additions & 7 deletions backend/clubs/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
MembershipRequest,
Note,
NoteTag,
OwnershipRequest,
Profile,
QuestionAnswer,
RecurringEvent,
Expand Down Expand Up @@ -263,25 +264,50 @@


class MembershipRequestAdmin(admin.ModelAdmin):
search_fields = ("person__username", "person__email", "club__name", "club__pk")
list_display = ("person", "club", "email", "withdrew", "is_member")
list_filter = ("withdrew",)
search_fields = (
"requester__username",
"requester__email",
"club__name",
"club__pk",
)
list_display = ("requester", "club", "email", "withdrawn", "is_member")
list_filter = ("withdrawn",)

def person(self, obj):
return obj.person.username
def requester(self, obj):
return obj.requester.username

Check warning on line 277 in backend/clubs/admin.py

View check run for this annotation

Codecov / codecov/patch

backend/clubs/admin.py#L277

Added line #L277 was not covered by tests

def club(self, obj):
return obj.club.name

def email(self, obj):
return obj.person.email
return obj.requester.email

Check warning on line 283 in backend/clubs/admin.py

View check run for this annotation

Codecov / codecov/patch

backend/clubs/admin.py#L283

Added line #L283 was not covered by tests

def is_member(self, obj):
return obj.club.membership_set.filter(person__pk=obj.person.pk).exists()
return obj.club.membership_set.filter(person__pk=obj.requester.pk).exists()

Check warning on line 286 in backend/clubs/admin.py

View check run for this annotation

Codecov / codecov/patch

backend/clubs/admin.py#L286

Added line #L286 was not covered by tests

is_member.boolean = True


class OwnershipRequestAdmin(admin.ModelAdmin):
search_fields = (
"requester__username",
"requester__email",
"club__name",
"created_at",
)
list_display = ("requester", "club", "email", "withdrawn", "created_at")
list_filter = ("withdrawn",)

def requester(self, obj):
return obj.requester.username

Check warning on line 302 in backend/clubs/admin.py

View check run for this annotation

Codecov / codecov/patch

backend/clubs/admin.py#L302

Added line #L302 was not covered by tests

def club(self, obj):
return obj.club.name

Check warning on line 305 in backend/clubs/admin.py

View check run for this annotation

Codecov / codecov/patch

backend/clubs/admin.py#L305

Added line #L305 was not covered by tests

def email(self, obj):
return obj.requester.email

Check warning on line 308 in backend/clubs/admin.py

View check run for this annotation

Codecov / codecov/patch

backend/clubs/admin.py#L308

Added line #L308 was not covered by tests


class MembershipAdmin(admin.ModelAdmin):
search_fields = (
"person__username",
Expand Down Expand Up @@ -443,6 +469,7 @@
admin.site.register(Major, MajorAdmin)
admin.site.register(Membership, MembershipAdmin)
admin.site.register(MembershipInvite, MembershipInviteAdmin)
admin.site.register(OwnershipRequest, OwnershipRequestAdmin)
admin.site.register(Profile, ProfileAdmin)
admin.site.register(QuestionAnswer, QuestionAnswerAdmin)
admin.site.register(RecurringEvent)
Expand Down
38 changes: 38 additions & 0 deletions backend/clubs/migrations/0118_ownershiprequest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Generated by Django 5.0.4 on 2024-10-18 05:04

import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("clubs", "0117_clubapprovalresponsetemplate"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]

operations = [
migrations.CreateModel(
name="OwnershipRequest",
fields=[
("id", models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID")),
("withdrew", models.BooleanField(default=False)),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
("club", models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="clubs.club")),
("person", models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL)),
],
options={
"unique_together": {("person", "club")},
},
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Generated by Django 5.0.4 on 2024-10-18 11:42
gabeweng marked this conversation as resolved.
Show resolved Hide resolved

import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("clubs", "0118_ownershiprequest"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]

operations = [
migrations.RenameField(
model_name="ownershiprequest",
old_name="withdrew",
new_name="withdrawn",
),
migrations.RenameField(
model_name="ownershiprequest",
old_name="person",
new_name="requester",
),
migrations.AlterField(
model_name="ownershiprequest",
name="club",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="ownership_requests",
to="clubs.club"
),
),
migrations.AlterField(
model_name="ownershiprequest",
name="requester",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="ownership_requests",
to=settings.AUTH_USER_MODEL
),
),
migrations.RenameField(
model_name="membershiprequest",
old_name="withdrew",
new_name="withdrawn",
),
migrations.RenameField(
model_name="membershiprequest",
old_name="person",
new_name="requester",
),
migrations.AlterField(
model_name="membershiprequest",
name="club",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="membership_requests",
to="clubs.club"
),
),
migrations.AlterField(
model_name="membershiprequest",
name="requester",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="membership_requests",
to=settings.AUTH_USER_MODEL
),
),
]
86 changes: 71 additions & 15 deletions backend/clubs/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1082,44 +1082,100 @@
Used when users are not in the club but request membership from the owner
gabeweng marked this conversation as resolved.
Show resolved Hide resolved
"""

person = models.ForeignKey(get_user_model(), on_delete=models.CASCADE)
club = models.ForeignKey(Club, on_delete=models.CASCADE)
requester = models.ForeignKey(
get_user_model(), on_delete=models.CASCADE, related_name="membership_requests"
)
club = models.ForeignKey(
Club, on_delete=models.CASCADE, related_name="membership_requests"
)

withdrew = models.BooleanField(default=False)
withdrawn = models.BooleanField(default=False)

created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)

def __str__(self):
return "<MembershipRequest: {} for {}, with email {}>".format(
self.person.username, self.club.code, self.person.email
)
return f"<MembershipRequest: {self.requester.username} for {self.club.code}>"

Check warning on line 1098 in backend/clubs/models.py

View check run for this annotation

Codecov / codecov/patch

backend/clubs/models.py#L1098

Added line #L1098 was not covered by tests

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": self.club.name,
"edit_url": "{}/member".format(
settings.EDIT_URL.format(domain=domain, club=self.club.code)
),
"full_name": self.person.get_full_name(),
"club_name": club_name,
"edit_url": f"{edit_url}/member",
"full_name": full_name,
}

emails = self.club.get_officer_emails()

if emails:
send_mail_helper(
name="request",
subject="Membership Request from {} for {}".format(
self.person.get_full_name(), self.club.name
),
subject=f"Membership Request from {full_name} for {club_name}",
emails=emails,
context=context,
)

class Meta:
unique_together = (("person", "club"),)
unique_together = (("requester", "club"),)


class OwnershipRequest(models.Model):
gabeweng marked this conversation as resolved.
Show resolved Hide resolved
"""
Represents a user's request to take ownership of a club
"""

requester = models.ForeignKey(
get_user_model(), on_delete=models.CASCADE, related_name="ownership_requests"
)
club = models.ForeignKey(
Club, on_delete=models.CASCADE, related_name="ownership_requests"
)

withdrawn = models.BooleanField(default=False)

created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)

def __str__(self):
return f"<OwnershipRequest: {self.requester.username} for {self.club.code}>"

Check warning on line 1147 in backend/clubs/models.py

View check run for this annotation

Codecov / codecov/patch

backend/clubs/models.py#L1147

Added line #L1147 was not covered by tests

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,
)
gabeweng marked this conversation as resolved.
Show resolved Hide resolved

class Meta:
unique_together = (("requester", "club"),)


class Advisor(models.Model):
Expand Down
17 changes: 17 additions & 0 deletions backend/clubs/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,23 @@
return membership is not None and membership.role <= Membership.ROLE_OFFICER


class OwnershipRequestPermission(permissions.BasePermission):
gabeweng marked this conversation as resolved.
Show resolved Hide resolved
"""
Only owners can view and modify ownership requests.
"""

def has_permission(self, request, view):
if not request.user.is_authenticated:
return False

if "club_code" not in view.kwargs:
return False

Check warning on line 450 in backend/clubs/permissions.py

View check run for this annotation

Codecov / codecov/patch

backend/clubs/permissions.py#L450

Added line #L450 was not covered by tests

obj = Club.objects.get(code=view.kwargs["club_code"])
membership = find_membership_helper(request.user, obj)
return membership is not None and membership.role == Membership.ROLE_OWNER


class InvitePermission(permissions.BasePermission):
"""
Officers and higher can list/delete invitations.
Expand Down
Loading
Loading