Skip to content

feat(Organization): ✨ Add new API endpoint views to Organization app #400

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

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
88 changes: 86 additions & 2 deletions apps/organizations/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,17 @@
from functools import wraps

# Django Imports
from django.core.exceptions import ObjectDoesNotExist
from django.http import (
Http404,
HttpResponseForbidden,
)
from django.shortcuts import get_object_or_404

# HTK Imports
from htk.api.utils import json_response_error
from htk.api.utils import (
json_response_error,
json_response_not_found,
)
from htk.apps.organizations.enums import OrganizationMemberRoles
from htk.utils import htk_setting
from htk.utils.general import resolve_model_dynamically
Expand Down Expand Up @@ -122,3 +125,84 @@ def __call__(self, view_func):
return require_organization_permission(
OrganizationMemberRoles.MEMBER, content_type=self.content_type
)(view_func)


class require_organization_member_user(object):
Copy link
Member

Choose a reason for hiding this comment

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

Not sure if I like require_ as part of the decorator name prefix for all of these

def __init__(self, content_type='text/html'):
self.content_type = content_type

def __call__(self, view_func):
@wraps(view_func)
def wrapped_view(request, *args, **kwargs):
# NOTE: Actual view function might use organization or team_id. Do not pop it.
organization = kwargs.get('organization')
member_id = kwargs.get('member_id')

try:
member = organization.members.get(id=member_id)
except ObjectDoesNotExist:
if self.content_type == 'application/json':
response = json_response_not_found()
else:
raise Http404
else:
kwargs['member'] = member
response = view_func(request, *args, **kwargs)

return response

return wrapped_view


class require_organization_team(object):
def __init__(self, content_type='text/html'):
self.content_type = content_type

def __call__(self, view_func):
@wraps(view_func)
def wrapped_view(request, *args, **kwargs):
# NOTE: Actual view function might use organization or team_id. Do not pop it.
organization = kwargs.get('organization')
team_id = kwargs.get('team_id')

try:
team = organization.teams.get(id=team_id)
except ObjectDoesNotExist:
if self.content_type == 'application/json':
response = json_response_not_found()
else:
raise Http404
else:
kwargs['team'] = team
response = view_func(request, *args, **kwargs)

return response

return wrapped_view


class require_organization_invitation(object):
def __init__(self, content_type='text/html'):
self.content_type = content_type

def __call__(self, view_func):
@wraps(view_func)
def wrapped_view(request, *args, **kwargs):
# NOTE: Actual view function might use organization or team_id. Do not pop it.
organization = kwargs.get('organization')
invitation_id = kwargs.get('invitation_id')

try:
invitation = organization.invitations.get(id=invitation_id)
except ObjectDoesNotExist:
if self.content_type == 'application/json':
response = json_response_not_found()
else:
raise Http404
else:
kwargs['invitation'] = invitation
response = view_func(request, *args, **kwargs)

return response

return wrapped_view
98 changes: 95 additions & 3 deletions apps/organizations/forms.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,103 @@
# Django Imports
from django import forms
from htk.apps.accounts.utils.general import get_user_by_id

# HTK Imports
from htk.apps.accounts.utils import get_user_by_id, get_user_by_email
from htk.apps.organizations.utils import invite_organization_member

class HTKOrganizationTeamForm(forms.ModelForm):

class AbstractOrganizationInvitationForm(forms.ModelForm):
ERROR_MESSAGES = {
'already_accepted': 'This email address has already been accepted the invitation.',
}

class Meta:
fields = ['email']
widgets = {
'email': forms.EmailInput(attrs={'placeholder': 'Email'}),
}

def __init_subclass__(cls, model, **kwargs) -> None:
super().__init_subclass__(**kwargs)
cls.Meta.model = model

def __init__(self, organization, invited_by, *args, **kwargs):
super().__init__(*args, **kwargs)
self.instance.organization = organization
self.instance.invited_by = invited_by

def clean_email(self):
email = self.cleaned_data.get('email')

q = self.instance.organization.invitations.filter(email=email)
if q.exists():
self.instance = q.first()
else:
pass

if self.instance.accepted:
raise forms.ValidationError(self.ERROR_MESSAGES['already_accepted'])
else:
pass

return email

def save(self, request, *args, **kwargs):
invitation = super().save(commit=False)
invitation = invite_organization_member(request, invitation)
return invitation


class AbstractOrganizationMemberForm(forms.ModelForm):
ERROR_MESSAGES = {
'user_not_found': 'There is no user with this email address',
'already_member': 'That user is already a member of this organization.',
}

class Meta:
fields = ['email']
widgets = {
'email': forms.EmailInput(attrs={'placeholder': 'Email'}),
}

def __init_subclass__(cls, model, **kwargs) -> None:
super().__init_subclass__(**kwargs)
cls.Meta.model = model

def __init__(self, organization, *args, **kwargs):
super().__init__(*args, **kwargs)
self.instance.organization = organization

def clean_email(self):
email = self.cleaned_data.get('email')
user = get_user_by_email(email)

if user is None:
raise forms.ValidationError(self.ERROR_MESSAGES['user_not_found'])
else:
pass

q = self.instance.organization.members.filter(user__id=user.id)
if q.exists():
raise forms.ValidationError(self.ERROR_MESSAGES['already_member'])
else:
pass

return email


class AbstractOrganizationTeamForm(forms.ModelForm):
ERROR_MESSAGES = {
'already_member': 'A team with this name already exists',
}

class Meta:
fields = ['name']

def __init_subclass__(cls, model, **kwargs) -> None:
super().__init_subclass__(**kwargs)
cls.Meta.model = model

def __init__(self, organization, *args, **kwargs):
super().__init__(*args, **kwargs)
self.instance.organization = organization
Expand All @@ -27,7 +115,7 @@ def clean_name(self):
return team_name


class HTKOrganizationTeamMemberForm(forms.ModelForm):
class AbstractOrganizationTeamMemberForm(forms.ModelForm):
ERROR_MESSAGES = {
'user_not_found': 'There is no user with this email address',
'already_member': 'That user is already a member of this team.',
Expand All @@ -38,6 +126,10 @@ class HTKOrganizationTeamMemberForm(forms.ModelForm):
class Meta:
fields = ['user_id']

def __init_subclass__(cls, model, **kwargs) -> None:
super().__init_subclass__(**kwargs)
cls.Meta.model = model

def __init__(self, team, *args, **kwargs):
super().__init__(*args, **kwargs)
self.instance.team = team
Expand Down
28 changes: 18 additions & 10 deletions apps/organizations/models.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
# Python Standard Library Imports
import collections
import hashlib
import uuid
from typing import (
Any,
Expand All @@ -10,7 +9,6 @@
# Django Imports
from django.conf import settings
from django.db import models
from django.contrib.auth import get_user_model

# HTK Imports
from htk.apps.organizations.enums import (
Expand Down Expand Up @@ -180,7 +178,6 @@ def add_member(self, user, role, allow_duplicates=False):
return member

def add_owner(self, user):
OrganizationMember = get_model_organization_member()
new_owner = self.add_member(user, OrganizationMemberRoles.OWNER)
return new_owner

Expand Down Expand Up @@ -217,12 +214,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

Expand Down Expand Up @@ -322,7 +317,7 @@ def status(self) -> str:
# Notifications

def _build_notification_message(self, subject, verb):
msg = '{subject_name} ({subject_username}<{email}>) has {verb} an invitation for Organization <{organization_name}>'.format( # noqa: E501
msg = '{subject_name} ({subject_username}<{email}>) has {verb} an invitation for Organization <{organization_name}>'.format( # noqa: E501
verb=verb,
subject_name=subject.profile.get_full_name(),
subject_username=subject.username,
Expand Down Expand Up @@ -410,6 +405,19 @@ def __str__(self):
)
return value

def json_encode(self):
value = super().json_encode()
profile = self.user.profile
value.update(
{
'first_name': self.user.first_name,
'last_name': self.user.last_name,
'display_name': profile.display_name,
'username': self.user.username,
}
)
return value


class BaseAbstractOrganizationTeamPosition(HtkBaseModel):
name = models.CharField(max_length=128)
Expand Down
7 changes: 6 additions & 1 deletion apps/organizations/utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Django Imports
from django.apps import apps
from django.contrib.sites.shortcuts import get_current_site
from django.utils import timezone

# HTK Imports
from htk.apps.organizations.emailers import send_invitation_email
Expand Down Expand Up @@ -77,7 +78,7 @@ def invite_organization_member(request, invitation):
first_name=invitation.first_name,
last_name=invitation.last_name,
site=get_current_site(request),
enable_early_access=True
enable_early_access=True,
)
early_access_code = prelaunch_signup.early_access_code
else:
Expand All @@ -90,8 +91,12 @@ def invite_organization_member(request, invitation):
pass

# Save invitation so that updatedAt field will be updated
invitation.invited_at = timezone.now()
invitation.invited_by = request.user
invitation.save()

send_invitation_email(
request, invitation, early_access_code=early_access_code
)

return invitation
Loading