From 236278adca9a6ba81d571ee6090ac1f18c582136 Mon Sep 17 00:00:00 2001 From: Shreyas Telkar <142061608+shreyastelkar@users.noreply.github.com> Date: Thu, 11 Jul 2024 14:31:56 -0700 Subject: [PATCH] =?UTF-8?q?feat:=20=E2=9C=A8=20Adds=20a=20`BaseOrganizatio?= =?UTF-8?q?nJoinRequest`=20model=20to=20`Organizations`=20(#441)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Adds a join request model to handle requests made to join an organization. - Invitations and requests should cover the logic to join an organization in multiple ways. --- apps/organizations/admin.py | 22 ++++- apps/organizations/constants/defaults.py | 1 + apps/organizations/models.py | 100 ++++++++++++++++++++--- 3 files changed, 109 insertions(+), 14 deletions(-) diff --git a/apps/organizations/admin.py b/apps/organizations/admin.py index b203b15e..054d4d46 100644 --- a/apps/organizations/admin.py +++ b/apps/organizations/admin.py @@ -34,6 +34,14 @@ class HtkOrganizationInvitationInline(admin.TabularInline): can_delete = True +class HtkOrganizationJoinRequestInline(admin.TabularInline): + model = resolve_model_dynamically( + htk_setting('HTK_ORGANIZATION_JOIN_REQUEST_MODEL') + ) + extra = 0 + can_delete = True + + class HtkOrganizationTeamMemberInline(admin.TabularInline): model = resolve_model_dynamically( htk_setting('HTK_ORGANIZATION_TEAM_MEMBER_MODEL') @@ -68,7 +76,8 @@ class HtkOrganizationAdmin(admin.ModelAdmin): HtkOrganizationAttributeInline, # HtkOrganizationMemberInline, # HtkOrganizationTeamInline, - # HtkOrganizationInvitationInline, + HtkOrganizationInvitationInline, + HtkOrganizationJoinRequestInline, ) @@ -114,6 +123,17 @@ class HtkOrganizationInvitationAdmin(admin.ModelAdmin): ) +class HtkOrganizationJoinRequestAdmin(admin.ModelAdmin): + list_display = ( + 'id', + 'organization', + 'user', + 'accepted', + 'timestamp', + 'responded_at', + ) + + class HtkOrganizationTeamAdmin(admin.ModelAdmin): list_display = ( 'id', diff --git a/apps/organizations/constants/defaults.py b/apps/organizations/constants/defaults.py index 9c7154fd..e40bb27a 100644 --- a/apps/organizations/constants/defaults.py +++ b/apps/organizations/constants/defaults.py @@ -2,6 +2,7 @@ HTK_ORGANIZATION_ATTRIBUTE_MODEL = 'organizations.OrganizationAttribute' HTK_ORGANIZATION_MEMBER_MODEL = 'organizations.OrganizationMember' HTK_ORGANIZATION_INVITATION_MODEL = 'organizations.OrganizationInvitation' +HTK_ORGANIZATION_JOIN_REQUEST_MODEL = 'organizations.OrganizationJoinRequest' HTK_ORGANIZATION_TEAM_MODEL = 'organizations.OrganizationTeam' HTK_ORGANIZATION_TEAM_MEMBER_MODEL = 'organizations.OrganizationTeamMember' HTK_ORGANIZATION_TEAM_POSITION_MODEL = 'organizations.OrganizationTeamPosition' diff --git a/apps/organizations/models.py b/apps/organizations/models.py index 6619f518..650c2e12 100644 --- a/apps/organizations/models.py +++ b/apps/organizations/models.py @@ -1,5 +1,4 @@ # Python Standard Library Imports -import hashlib import uuid from typing import ( Any, @@ -10,8 +9,6 @@ from six.moves import collections_abc # Django Imports -from django.conf import settings -from django.contrib.auth import get_user_model from django.db import models # HTK Imports @@ -185,7 +182,13 @@ def add_member(self, user, role, allow_duplicates=False): def add_owner(self, user): OrganizationMember = get_model_organization_member() - new_owner = self.add_member(user, OrganizationMemberRoles.OWNER) + + # Owner should be an existing member + new_owner = ( + self.add_member(user, OrganizationMemberRoles.OWNER) + if OrganizationMember.objects.filter(user=user) + else None + ) return new_owner def modify_member_role(self, user, role): @@ -213,12 +216,10 @@ class Meta: ) def __str__(self): - value = ( - '{organization_name} Member - {member_name} (member_email)'.format( - organization_name=self.organization.name, - member_name=self.user.profile.get_full_name(), - member_email=self.user.email, - ) + value = '{organization_name} Member - {member_name} ({member_email})'.format( + organization_name=self.organization.name, + member_name=self.user.profile.get_full_name(), + member_email=self.user.email, ) return value @@ -246,10 +247,9 @@ def set_role(self, role, should_activate=False): class BaseAbstractOrganizationInvitation(HtkBaseModel): organization = fk_organization(related_name='invitations', required=True) - invited_by = models.ForeignKey( - settings.AUTH_USER_MODEL, - on_delete=models.CASCADE, + invited_by = fk_user( related_name='organization_invitations_sent', + required=True, ) user = fk_user( related_name='organization_invitations', @@ -335,6 +335,80 @@ def build_notification_message__declined(self): return msg +class BaseAbstractOrganizationJoinRequest(HtkBaseModel): + organization = fk_organization(related_name='join_requests', required=True) + user = fk_user( + related_name='organization_join_requests', + ) + accepted = models.BooleanField( + default=None, null=True + ) # True: accepted, False: declined, None: not responded yet + timestamp = models.DateTimeField(auto_now_add=True) + responded_at = models.DateTimeField(blank=True, null=True, default=None) + + class Meta: + abstract = True + verbose_name = 'Organization Join Request' + + def __str__(self): + value = '{organization_name} - {user} - {status}'.format( + organization_name=self.organization.name, + user=self.user, + status=self.status, + ) + return value + + def json_encode(self) -> Dict[str, Any]: + """Returns a dictionary that can be `json.dumps()`-ed as a JSON representation of this object""" + value = { + 'id': self.id, + 'organization': self.organization.name, + 'user': self.user.profile.get_full_name() if self.user else None, + 'accepted': self.accepted, + 'requested_at': self.timestamp, + 'responded_at': self.responded_at, + } + return value + + ## + # properties + + @property + def status(self) -> str: + status = ( + 'Requested' + if self.accepted is None + else 'Accepted' if self.accepted else 'Declined' + ) + + return status + + ## + # Notifications + + def _build_notification_message(self, subject, action): + msg = '{subject_name} ({subject_username}<{subject_email}>) request to join Organization <{organization_name}> has been {action}.'.format( # noqa: E501 + action=action, + subject_name=subject.profile.get_full_name(), + subject_username=subject.username, + subject_email=subject.email, + organization_name=self.organization.name, + ) + return msg + + def build_notification_message__created(self): + msg = self._build_notification_message(self.user, 'sent') + return msg + + def build_notification_message__accepted(self): + msg = self._build_notification_message(self.user, 'accepted') + return msg + + def build_notification_message__declined(self): + msg = self._build_notification_message(self.user, 'declined') + return msg + + class BaseAbstractOrganizationTeam(HtkBaseModel): name = models.CharField(max_length=128) organization = fk_organization(related_name='teams', required=True)