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

Feat/ownership requests #740

Draft
wants to merge 10 commits into
base: master
Choose a base branch
from
22 changes: 22 additions & 0 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 @@ -282,6 +283,26 @@
is_member.boolean = True


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

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

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

View check run for this annotation

Codecov / codecov/patch

backend/clubs/admin.py#L297

Added line #L297 was not covered by tests

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

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

View check run for this annotation

Codecov / codecov/patch

backend/clubs/admin.py#L300

Added line #L300 was not covered by tests

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

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

View check run for this annotation

Codecov / codecov/patch

backend/clubs/admin.py#L303

Added line #L303 was not covered by tests


class MembershipAdmin(admin.ModelAdmin):
search_fields = (
"person__username",
Expand Down Expand Up @@ -443,6 +464,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,44 @@
# Generated by Django 5.0.4 on 2024-10-18 11:42

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
),
),
]
52 changes: 52 additions & 0 deletions backend/clubs/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1122,6 +1122,58 @@
unique_together = (("person", "club"),)


class OwnershipRequest(models.Model):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Realized that you probably used MembershipRequest as a reference, feel free to make the suggested changes there as well

"""
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 1143 in backend/clubs/models.py

View check run for this annotation

Codecov / codecov/patch

backend/clubs/models.py#L1143

Added line #L1143 was not covered by tests

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

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

View check run for this annotation

Codecov / codecov/patch

backend/clubs/models.py#L1146

Added line #L1146 was not covered by tests

edit_url = settings.EDIT_URL.format(domain=domain, club=self.club.code)

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

View check run for this annotation

Codecov / codecov/patch

backend/clubs/models.py#L1148

Added line #L1148 was not covered by tests

club_name = self.club.name

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

View check run for this annotation

Codecov / codecov/patch

backend/clubs/models.py#L1150

Added line #L1150 was not covered by tests

full_name = self.requester.get_full_name()

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

View check run for this annotation

Codecov / codecov/patch

backend/clubs/models.py#L1152

Added line #L1152 was not covered by tests

context = {

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

View check run for this annotation

Codecov / codecov/patch

backend/clubs/models.py#L1154

Added line #L1154 was not covered by tests
"club_name": club_name,
"edit_url": f"{edit_url}/member",
"full_name": full_name,
}

owner_emails = list(

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

View check run for this annotation

Codecov / codecov/patch

backend/clubs/models.py#L1160

Added line #L1160 was not covered by tests
self.club.membership_set.filter(
role=Membership.ROLE_OWNER, active=True
).values_list("person__email", flat=True)
)

send_mail_helper(

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

View check run for this annotation

Codecov / codecov/patch

backend/clubs/models.py#L1166

Added line #L1166 was not covered by tests
name="ownership_request",
subject=f"Ownership Request from {full_name} for {club_name}",
emails=owner_emails,
context=context,
)

class Meta:
unique_together = (("requester", "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 @@
return membership is not None and membership.role <= Membership.ROLE_OFFICER


class OwnershipRequestPermission(permissions.BasePermission):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work!

"""
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#L449-L450

Added lines #L449 - L450 were not covered by tests

if request.user.has_perm("clubs.manage_club"):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I'm not mistaken, this will allow officers to handle ownership requests. See here

return True

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

View check run for this annotation

Codecov / codecov/patch

backend/clubs/permissions.py#L452-L453

Added lines #L452 - L453 were 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

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

View check run for this annotation

Codecov / codecov/patch

backend/clubs/permissions.py#L455-L457

Added lines #L455 - L457 were not covered by tests


class InvitePermission(permissions.BasePermission):
"""
Officers and higher can list/delete invitations.
Expand Down
67 changes: 67 additions & 0 deletions backend/clubs/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
MembershipRequest,
Note,
NoteTag,
OwnershipRequest,
Profile,
QuestionAnswer,
Report,
Expand Down Expand Up @@ -1999,6 +2000,72 @@
fields = ("club", "club_name", "person")


class OwnershipRequestSerializer(serializers.ModelSerializer):
"""
Used by club owners to see who has requested to be owner of the club.
"""

requester = 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="requester.username", read_only=True)
email = serializers.EmailField(source="requester.email", read_only=True)

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

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

Check warning on line 2023 in backend/clubs/serializers.py

View check run for this annotation

Codecov / codecov/patch

backend/clubs/serializers.py#L2023

Added line #L2023 was not covered by tests

class Meta:
model = OwnershipRequest
fields = (
"club",
"created_at",
"email",
"graduation_year",
"major",
"name",
"requester",
"school",
"username",
)
validators = [
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is redundant as we're already enforcing it at the database level

validators.UniqueTogetherValidator(
queryset=OwnershipRequest.objects.all(), fields=["club", "requester"]
)
]


class UserOwnershipRequestSerializer(serializers.ModelSerializer):
"""
Used by the users to return the clubs that the user has sent OwnershipRequest to.
"""

requester = 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)

Check warning on line 2058 in backend/clubs/serializers.py

View check run for this annotation

Codecov / codecov/patch

backend/clubs/serializers.py#L2058

Added line #L2058 was not covered by tests

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

Check warning on line 2060 in backend/clubs/serializers.py

View check run for this annotation

Codecov / codecov/patch

backend/clubs/serializers.py#L2060

Added line #L2060 was not covered by tests

return obj

Check warning on line 2062 in backend/clubs/serializers.py

View check run for this annotation

Codecov / codecov/patch

backend/clubs/serializers.py#L2062

Added line #L2062 was not covered by tests

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


class MinimalUserProfileSerializer(serializers.ModelSerializer):
"""
A profile serializer used for the list view of all users.
Expand Down
12 changes: 11 additions & 1 deletion backend/clubs/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@
MemberViewSet,
NoteViewSet,
OptionListView,
OwnershipRequestManagementViewSet,
OwnershipRequestViewSet,
QuestionAnswerViewSet,
ReportViewSet,
SchoolViewSet,
Expand Down Expand Up @@ -70,7 +72,10 @@
router.register(r"clubvisits", ClubVisitViewSet, basename="clubvisits")
router.register(r"searches", SearchQueryViewSet, basename="searches")
router.register(r"memberships", MembershipViewSet, basename="members")
router.register(r"requests", MembershipRequestViewSet, basename="requests")
router.register(r"requests/membership", MembershipRequestViewSet, basename="requests")
router.register(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that we have requests for both ownership and membership, might be better to have the routes as /requests/ownership and /requests/membership

r"requests/ownership", OwnershipRequestViewSet, basename="ownershiprequests"
)
router.register(r"tickets", TicketViewSet, basename="tickets")

router.register(r"schools", SchoolViewSet, basename="schools")
Expand Down Expand Up @@ -108,6 +113,11 @@
MembershipRequestOwnerViewSet,
basename="club-membership-requests",
)
clubs_router.register(
r"ownershiprequests",
OwnershipRequestManagementViewSet,
basename="club-ownership-requests",
)
clubs_router.register(r"advisors", AdvisorViewSet, basename="club-advisors")
clubs_router.register(
r"applications", ClubApplicationViewSet, basename="club-applications"
Expand Down
Loading
Loading