Skip to content

Commit

Permalink
OrganizationInvite basic functionality, aggregate todo things for das…
Browse files Browse the repository at this point in the history
…hboard-y things
  • Loading branch information
Allan Almazan committed Dec 28, 2023
1 parent c62cbca commit a201b15
Show file tree
Hide file tree
Showing 8 changed files with 84 additions and 10 deletions.
1 change: 1 addition & 0 deletions todo_everything/accounts/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@


class AccountAdmin(admin.ModelAdmin):
fields = ("email", "password", "is_active", "is_staff", "is_superuser")
inlines = [
OrganizationAccountsInline,
]
Expand Down
19 changes: 19 additions & 0 deletions todo_everything/organizations/api.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from django.db.models import Q
from rest_framework import permissions, viewsets

from . import models
Expand All @@ -19,3 +20,21 @@ class OrganizationViewSet(viewsets.ModelViewSet):

def get_queryset(self):
return self.request.user.organizations.all()


class OrganizationInviteViewSet(viewsets.ModelViewSet):
queryset = models.OrganizationInvite.objects.none()
serializer_class = serializers.OrganizationInviteSerializer
permission_classes = [
permissions.IsAuthenticated,
permissions.IsAdminUser | org_permissions.IsOrganizationAdmin,
]

def get_queryset(self):
is_inviter = Q(account_inviter=self.request.user)
# Only get instances where the user was already attached.
# Checking invites via current user email would be dangerous since a
# user can change their email at any time and query against current
# invites for that email.
is_invited = Q(invited_account=self.request.user)
return self.queryset.filter(is_inviter | is_invited)
6 changes: 4 additions & 2 deletions todo_everything/organizations/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,10 @@ class OrganizationInvite(TimeStampedModel, models.Model):
related_name="accounts_invited",
)
invited_email = models.EmailField(_("Email address of the invitation"))
# `invited_account` will be populated once a user creates an account from an invitation.
# Also note that a single user can be invited to many orgs, but should only have one invitation per org.
# `invited_account` will be populated if the user already had an account
# or once the user accepts an invitation.
# Also note that a single user can be invited to many orgs, but should
# only have one invitation per org.
invited_account = models.ForeignKey(
get_user_model(),
on_delete=models.CASCADE,
Expand Down
13 changes: 13 additions & 0 deletions todo_everything/organizations/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,18 @@ def has_object_permission(self, request, view, obj: models.Organization):


class IsOrganizationAdmin(permissions.BasePermission):
role = models.ORGANIZATION_ADMIN

def has_permission(self, request, view):
# TODO: Enforce this structure when using this permissions class.
# i.e. `request.data.organization` must exist, but should probably be configurable.
organization_id = request.data.get("organization", None)
if not organization_id:
raise ValueError("organization must be provided in request data")
return request.user and request.user.organizations.filter(
organizationaccounts__organization=organization_id,
organizationaccounts__role=self.role,
)

def has_object_permission(self, request, view, obj):
return request.user and request.user.organizations.filter(pk=obj.pk).exists()
17 changes: 17 additions & 0 deletions todo_everything/organizations/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,20 @@ class OrganizationPublicSerializer(serializers.ModelSerializer):
class Meta:
model = models.Organization
fields = ("name",)


class OrganizationInviteSerializer(serializers.ModelSerializer):
# TODO: Can potentially use ModelSerializer for this
# organization = serializers.IntegerField(required=True)
account_inviter = serializers.HiddenField(default=serializers.CurrentUserDefault())
# invited_email = serializers.EmailField(required=True)

class Meta:
model = models.OrganizationInvite
fields = (
"organization",
"account_inviter",
"invited_email",
"invited_account",
"role",
)
2 changes: 2 additions & 0 deletions todo_everything/todo_everything/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
"rest_framework",
"rest_framework_simplejwt.token_blacklist",
"celery",
"django_filters",
# Maybe don't include on prod-like things
"django_extensions",
)
Expand Down Expand Up @@ -195,6 +196,7 @@
"DEFAULT_AUTHENTICATION_CLASSES": [
"rest_framework_simplejwt.authentication.JWTAuthentication",
],
"DEFAULT_FILTER_BACKENDS": ["django_filters.rest_framework.DjangoFilterBackend"],
}

SIMPLE_JWT = {
Expand Down
4 changes: 3 additions & 1 deletion todo_everything/todo_everything/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@

router = routers.DefaultRouter()
router.register(r"account", accounts_api.AccountViewSet)
router.register(r"organizations", organizations_api.OrganizationViewSet)
router.register(r"organization", organizations_api.OrganizationViewSet)
router.register(r"organization-invite", organizations_api.OrganizationInviteViewSet)
router.register(r"profile", accounts_api.AccountProfileViewSet)
router.register(r"todo", todos_api.TodoViewSet)

Expand All @@ -42,5 +43,6 @@
name="account_register",
),
path("api/", include(router.urls)),
path("api/todo-overview/", todos_api.TodoOverviewView.as_view()),
path("admin/", admin.site.urls),
]
32 changes: 25 additions & 7 deletions todo_everything/todos/api.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import logging

from django.db.models import Q
from rest_framework import permissions, status, viewsets
from django.db.models import Count, Q
from rest_framework import permissions, status, views, viewsets
from rest_framework.response import Response

from . import models
Expand All @@ -16,6 +16,9 @@ class TodoViewSet(viewsets.ModelViewSet):
permission_classes = [permissions.IsAuthenticated, todo_permissions.IsOwner]
read_serializer_class = serializers.TodoSerializer
write_serializer_class = serializers.TodoWriteSerializer
filterset_fields = [
"organization",
]

def get_serializer_class(self):
if self.action == "create":
Expand All @@ -32,13 +35,13 @@ def get_queryset(self):
return self.queryset

created_by_self = Q(created_by=self.request.user)
owned_by_org = self.request.GET.get("org", None)
owned_by_org = self.request.query_params.get("org", None)
if owned_by_org and isinstance(owned_by_org, int):
the_filter = Q(created_by_self | Q(organization_id=owned_by_org))
the_filter = Q(organization_id=owned_by_org)
else:
the_filter = Q(created_by_self)
the_filter = created_by_self

return models.Todo.available_objects.filter(the_filter).order_by("-created")
return models.Todo.available_objects.filter(the_filter)

def create(self, request, *args, **kwargs):
"""
Expand All @@ -53,9 +56,24 @@ def create(self, request, *args, **kwargs):
self.perform_create(serializer)

# Used write_serializer's updated data on the read_serializer for creating a response.
# This potentially causes a hit from another(?) serialization.
# This potentially causes a hit from another(?) seria lization.
serializer = self.read_serializer_class(serializer.instance)
headers = self.get_success_headers(serializer.data)
return Response(
serializer.data, status=status.HTTP_201_CREATED, headers=headers
)


class TodoOverviewView(views.APIView):
permission_classes = [permissions.IsAuthenticated]

def get(self, request, format=None):
# TODO: Probably cache this as it will eventually be an expensive-ish operation.
created_by_self = Q(created_by=self.request.user)
owned_by_orgs = Q(organization__in=self.request.user.organizations.all())
queryset = (
models.Todo.available_objects.filter(created_by_self | owned_by_orgs)
.values("organization")
.annotate(num_todos=Count("id"))
)
return Response(data=queryset, status=status.HTTP_200_OK)

0 comments on commit a201b15

Please sign in to comment.