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 3 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
23 changes: 23 additions & 0 deletions backend/clubs/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
MembershipRequest,
Note,
NoteTag,
OwnershipRequest,
Profile,
QuestionAnswer,
RecurringEvent,
Expand Down Expand Up @@ -281,6 +282,27 @@ def is_member(self, obj):
is_member.boolean = True


class OwnershipRequestAdmin(admin.ModelAdmin):
search_fields = (
"person__username",
"person__email",
"club__name",
"club__pk",
gabeweng marked this conversation as resolved.
Show resolved Hide resolved
"created_at",
)
list_display = ("person", "club", "email", "withdrew", "created_at")
list_filter = ("withdrew",)

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

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

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


class MembershipAdmin(admin.ModelAdmin):
search_fields = (
"person__username",
Expand Down Expand Up @@ -438,6 +460,7 @@ class ApplicationSubmissionAdmin(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
41 changes: 41 additions & 0 deletions backend/clubs/migrations/0114_ownershiprequest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Generated by Django 5.0.4 on 2024-10-06 05:12
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", "0113_badge_message"),
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")},
},
),
]
14 changes: 14 additions & 0 deletions backend/clubs/migrations/0117_merge_20241015_1652.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Generated by Django 5.0.4 on 2024-10-15 20:52

from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
("clubs", "0114_ownershiprequest"),
("clubs", "0116_alter_club_approved_on_and_more"),
]

operations = [
]
48 changes: 48 additions & 0 deletions backend/clubs/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1122,6 +1122,54 @@ class Meta:
unique_together = (("person", "club"),)


class OwnershipRequest(models.Model):
gabeweng marked this conversation as resolved.
Show resolved Hide resolved
"""
Used when users request ownership 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)
gabeweng marked this conversation as resolved.
Show resolved Hide resolved

withdrew = models.BooleanField(default=False)
gabeweng marked this conversation as resolved.
Show resolved Hide resolved

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

def __str__(self):
return "<OwnershipRequest: {} for {}, with email {}>".format(
self.person.username, self.club.code, self.person.email
gabeweng marked this conversation as resolved.
Show resolved Hide resolved
)

def send_request(self, request=None):
domain = get_domain(request)

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

owner_emails = list(
self.club.membership_set.filter(
role=Membership.ROLE_OWNER, active=True
).values_list("person__email", flat=True)
)

send_mail_helper(
name="ownershiprequest",
subject="Ownership Request from {} for {}".format(
self.person.get_full_name(), self.club.name
),
emails=owner_emails,
context=context,
)

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


class Advisor(models.Model):
"""
Represents one faculty advisor or point of contact for a club.
Expand Down
20 changes: 20 additions & 0 deletions backend/clubs/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,26 @@ def has_permission(self, request, view):
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

if request.user.has_perm("clubs.manage_club"):
gabeweng marked this conversation as resolved.
Show resolved Hide resolved
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
65 changes: 65 additions & 0 deletions backend/clubs/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
MembershipRequest,
Note,
NoteTag,
OwnershipRequest,
Profile,
QuestionAnswer,
Report,
Expand Down Expand Up @@ -1998,6 +1999,70 @@ class Meta:
fields = ("club", "club_name", "person")


class OwnershipRequestSerializer(serializers.ModelSerializer):
"""
Used by club owners/officers to see who has requested to be owner of the club.
gabeweng marked this conversation as resolved.
Show resolved Hide resolved
"""

person = serializers.HiddenField(default=serializers.CurrentUserDefault())
club = serializers.SlugRelatedField(queryset=Club.objects.all(), slug_field="code")
name = serializers.SerializerMethodField("get_full_name")
username = serializers.CharField(source="person.username", read_only=True)
email = serializers.EmailField(source="person.email", read_only=True)

school = SchoolSerializer(many=True, source="person.profile.school", read_only=True)
major = MajorSerializer(many=True, source="person.profile.major", read_only=True)
graduation_year = serializers.IntegerField(
source="person.profile.graduation_year", read_only=True
)

def get_full_name(self, obj):
return obj.person.get_full_name()

class Meta:
model = OwnershipRequest
fields = (
"club",
"created_at",
"email",
"graduation_year",
"major",
"name",
"person",
"school",
"username",
)
validators = [
gabeweng marked this conversation as resolved.
Show resolved Hide resolved
validators.UniqueTogetherValidator(
queryset=OwnershipRequest.objects.all(), fields=["club", "person"]
)
]


class UserOwnershipRequestSerializer(serializers.ModelSerializer):
"""
Used by the users to return the clubs that the user has sent OwnershipRequest to.
gabeweng marked this conversation as resolved.
Show resolved Hide resolved
"""

person = serializers.HiddenField(default=serializers.CurrentUserDefault())
club = serializers.SlugRelatedField(queryset=Club.objects.all(), slug_field="code")
club_name = serializers.CharField(source="club.name", read_only=True)

def create(self, validated_data):
"""
Send an email when a ownership request is created.
"""
obj = super().create(validated_data)

obj.send_request(self.context["request"])

return obj

class Meta:
model = OwnershipRequest
fields = ("club", "club_name", "person")


class MinimalUserProfileSerializer(serializers.ModelSerializer):
"""
A profile serializer used for the list view of all users.
Expand Down
16 changes: 16 additions & 0 deletions backend/clubs/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@
MemberViewSet,
NoteViewSet,
OptionListView,
OwnershipRequestOwnerViewSet,
OwnershipRequestSuperuserAPIView,
OwnershipRequestViewSet,
QuestionAnswerViewSet,
ReportViewSet,
SchoolViewSet,
Expand Down Expand Up @@ -70,6 +73,9 @@
router.register(r"searches", SearchQueryViewSet, basename="searches")
router.register(r"memberships", MembershipViewSet, basename="members")
router.register(r"requests", MembershipRequestViewSet, basename="requests")
router.register(
gabeweng marked this conversation as resolved.
Show resolved Hide resolved
r"ownershiprequests", OwnershipRequestViewSet, basename="ownershiprequests"
)
router.register(r"tickets", TicketViewSet, basename="tickets")

router.register(r"schools", SchoolViewSet, basename="schools")
Expand Down Expand Up @@ -106,6 +112,11 @@
MembershipRequestOwnerViewSet,
basename="club-membership-requests",
)
clubs_router.register(
r"ownershiprequests",
OwnershipRequestOwnerViewSet,
basename="club-ownership-requests",
)
clubs_router.register(r"advisors", AdvisorViewSet, basename="club-advisors")
clubs_router.register(
r"applications", ClubApplicationViewSet, basename="club-applications"
Expand Down Expand Up @@ -163,6 +174,11 @@
path(r"emailpreview/", email_preview, name="email-preview"),
path(r"scripts/", ScriptExecutionView.as_view(), name="scripts"),
path(r"options/", OptionListView.as_view(), name="options"),
path(
r"ownershiprequestsadmin/",
OwnershipRequestSuperuserAPIView.as_view(),
name="ownershiprequestsadmin",
),
path(r"social/", include("social_django.urls", namespace="social")),
path(
r"webhook/meeting/",
Expand Down
Loading
Loading