Skip to content

Commit

Permalink
Add backend support for ownership requests (#740)
Browse files Browse the repository at this point in the history
  • Loading branch information
gabeweng authored Feb 6, 2025
1 parent 3c7ec78 commit c8c9849
Show file tree
Hide file tree
Showing 14 changed files with 1,075 additions and 79 deletions.
46 changes: 39 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,55 @@ def email(self, obj):


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

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

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

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()

is_member.boolean = True


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

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

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

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

def is_owner(self, obj):
return obj.club.membership_set.filter(
person__pk=obj.requester.pk, role=Membership.ROLE_OWNER
).exists()


class MembershipAdmin(admin.ModelAdmin):
search_fields = (
"person__username",
Expand Down Expand Up @@ -443,6 +474,7 @@ class ClubApprovalResponseTemplateAdmin(admin.ModelAdmin):
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
94 changes: 94 additions & 0 deletions backend/clubs/migrations/0118_ownershiprequest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# 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")},
},
),
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="%(class)ss",
to="clubs.club"
),
),
migrations.AlterField(
model_name="ownershiprequest",
name="requester",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="%(class)ss",
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="%(class)ss",
to="clubs.club"
),
),
migrations.AlterField(
model_name="membershiprequest",
name="requester",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="%(class)ss",
to=settings.AUTH_USER_MODEL
),
),
]
78 changes: 59 additions & 19 deletions backend/clubs/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1077,49 +1077,89 @@ def __str__(self):
return "<SearchQuery: {} at {}>".format(self.query, self.created_at)


class MembershipRequest(models.Model):
class JoinRequest(models.Model):
"""
Used when users are not in the club but request membership from the owner
Abstract base class for Membership Request and Ownership Request
"""

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="%(class)ss"
)
club = models.ForeignKey(Club, on_delete=models.CASCADE, related_name="%(class)ss")

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)

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


class MembershipRequest(JoinRequest):
"""
Used when users are not in the club but request membership from the owner
"""

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}>"

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
),
name="membership_request",
subject=f"Membership Request from {full_name} for {club_name}",
emails=emails,
context=context,
)

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

class OwnershipRequest(JoinRequest):
"""
Represents a user's request to take ownership of a club
"""

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

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,
)


class Advisor(models.Model):
Expand Down
20 changes: 20 additions & 0 deletions backend/clubs/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,26 @@ def has_permission(self, request, view):
return membership is not None and membership.role <= Membership.ROLE_OFFICER


class OwnershipRequestPermission(permissions.BasePermission):
"""
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

if request.user.has_perm("clubs.manage_club"):
return True

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

0 comments on commit c8c9849

Please sign in to comment.