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

Refactor "Site Manager" to "Forest Steward" to match business design #277

Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
Mulchlayertype,
Post,
Role,
RoleName,
Site,
Siteadmin,
Sitetreespecies,
Expand Down Expand Up @@ -456,9 +457,8 @@ def create_assets(self):
asset.asset.save(file_name, django_file, save=True)

def create_roles(self):
Role.objects.create(name="User")
Role.objects.create(name="SiteManager")
Role.objects.create(name="MegaAdmin")
for role in RoleName:
Role.objects.create(name=role)

def create_users(self):
User.objects.create_user(
Expand All @@ -467,37 +467,37 @@ def create_users(self):
password="Adminbeslogic!", # noqa: S106 # MOCK_PASSWORD
is_staff=True,
is_superuser=True,
role=Role.objects.get(name="MegaAdmin"),
role=Role.objects.get(name=RoleName.MegaAdmin),
)
User.objects.create_user(
username="TyrionLannister",
email="[email protected]",
password="tyrion123", # noqa: S106 # MOCK_PASSWORD
role=Role.objects.get(name="SiteManager"),
role=Role.objects.get(name=RoleName.ForestSteward),
)
User.objects.create_user(
username="DaenerysTargaryen",
email="[email protected]",
password="daenerys123", # noqa: S106 # MOCK_PASSWORD
role=Role.objects.get(name="SiteManager"),
role=Role.objects.get(name=RoleName.ForestSteward),
)
User.objects.create_user(
username="JonSnow",
email="[email protected]",
password="jon123", # noqa: S106 # MOCK_PASSWORD
role=Role.objects.get(name="SiteManager"),
role=Role.objects.get(name=RoleName.ForestSteward),
)
User.objects.create_user(
username="OberynMartell",
email="[email protected]",
password="oberyn123", # noqa: S106 # MOCK_PASSWORD
role=Role.objects.get(name="SiteManager"),
role=Role.objects.get(name=RoleName.ForestSteward),
)
User.objects.create_user(
username="NormalUser",
email="[email protected]",
password="normal123", # noqa: S106 # MOCK_PASSWORD
role=Role.objects.get(name="User"),
role=Role.objects.get(name=RoleName.User),
)

def create_sites(self):
Expand Down
Copy link
Contributor

Choose a reason for hiding this comment

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

Like the other project should we just consider redoing the db and handle migration later?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We've been squashing the migrations and nuking the DB out of lazyness/simplicity when it would require doing a manual migration script.

These should work just fine as is

Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Generated by Django 5.1 on 2024-10-29 21:39

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("canopeum_backend", "0001_initial"),
]

operations = [
migrations.AlterField(
model_name="role",
name="name",
field=models.CharField(
choices=[
("User", "User"),
("ForestSteward", "Foreststeward"),
("MegaAdmin", "Megaadmin"),
],
max_length=13,
),
),
]
34 changes: 19 additions & 15 deletions canopeum_backend/canopeum_backend/models.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import re
from datetime import UTC, datetime, timedelta
from typing import TYPE_CHECKING, Any, ClassVar, TypeVar, override
from enum import auto
from typing import TYPE_CHECKING, Any, ClassVar, Literal, TypeVar, cast, override

import googlemaps
from django.contrib.auth.models import AbstractUser
Expand All @@ -22,35 +23,38 @@


class RoleName(models.TextChoices):
USER = "User"
SITEMANAGER = "SiteManager"
MEGAADMIN = "MegaAdmin"
User = auto()
ForestSteward = auto()
MegaAdmin = auto()

@classmethod
def from_string(cls, value: str):
try:
return cls(value)
except ValueError:
return cls.USER
return cls.User


class Role(models.Model):
name = models.CharField(
max_length=11,
choices=RoleName.choices,
default=RoleName.USER,
# TODO: Request at https://github.com/typeddjango/django-stubs/issues
# for "choices" CharField to be understood as the enum type instead of a simple "str"
name = cast(
Literal["User", "ForestSteward", "MegaAdmin"],
models.CharField(max_length=max(len(val) for val in RoleName), choices=RoleName.choices),
)


class User(AbstractUser):
email = models.EmailField(
verbose_name="email address",
max_length=255,
unique=True,
)
email = models.EmailField(verbose_name="email address", max_length=255, unique=True)
USERNAME_FIELD = "email"
REQUIRED_FIELDS: ClassVar[list[str]] = []
role = models.ForeignKey[Role, Role](Role, models.RESTRICT, null=False, default=1)
role = models.ForeignKey[Role, Role](
Role,
models.RESTRICT,
null=False,
# Role.objects.get(name=RoleName.User).pk, but statically so we don't access DB during init
default=1,
)
if TYPE_CHECKING:
# Missing "id" in "Model" or some base "User" class?
id: int
Expand Down
29 changes: 12 additions & 17 deletions canopeum_backend/canopeum_backend/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,14 @@

from rest_framework import permissions

from .models import Comment, Request, Site, Siteadmin, User
from .models import Comment, Request, RoleName, Site, Siteadmin, User


class DeleteCommentPermission(permissions.BasePermission):
"""Deleting a comment is only allowed for admins or the comment's author."""

def has_object_permission(self, request: Request, view, obj: Comment):
current_user_role = request.user.role.name
if current_user_role == "MegaAdmin":
if request.user.role.name == RoleName.MegaAdmin:
return True
is_admin_for_this_post = obj.post.site.siteadmin_set.filter(
user__id__exact=request.user.id
Expand All @@ -25,10 +24,10 @@ class PublicSiteReadPermission(permissions.BasePermission):

def has_object_permission(self, request: Request, view, obj: Site) -> bool:
if obj.is_public or (
isinstance(request.user, User) and request.user.role.name == "MegaAdmin"
isinstance(request.user, User) and request.user.role.name == RoleName.MegaAdmin
):
return True
if not isinstance(request.user, User) or request.user.role.name != "SiteManager":
if not isinstance(request.user, User) or request.user.role.name != RoleName.ForestSteward:
return False

return (
Expand All @@ -40,32 +39,29 @@ class SiteAdminPermission(permissions.BasePermission):
"""Allows mega admins and a specific site's admin to perform site actions."""

def has_object_permission(self, request: Request, view, obj: Site) -> bool:
current_user_role = request.user.role.name
if current_user_role == "MegaAdmin":
if request.user.role.name == RoleName.MegaAdmin:
return True
return (
Siteadmin.objects.filter(user__id__exact=request.user.id).filter(site=obj.pk).exists()
)


class MegaAdminOrSiteManagerPermission(permissions.BasePermission):
"""Global permission for actions only allowed to MegaAdmin or SiteManager users."""
class MegaAdminOrForestStewardPermission(permissions.BasePermission):
"""Global permission for actions only allowed to MegaAdmin or ForestSteward users."""

# About the type ignore: Base permission return type is Literal True but should be bool
def has_permission(self, request, view):
current_user_role = request.user.role.name # pyright: ignore[reportAttributeAccessIssue]
return current_user_role in {"MegaAdmin", "SiteManager"}
def has_permission(self, request: Request, view):
return request.user.role.name in {RoleName.MegaAdmin, RoleName.ForestSteward}


class MegaAdminPermission(permissions.BasePermission):
"""Global permission for actions only allowed to MegaAdmin users."""

def has_permission(self, request: Request, view):
current_user_role = request.user.role.name
return current_user_role == "MegaAdmin"
return request.user.role.name == RoleName.MegaAdmin


READONLY_METHODS = ["GET", "HEAD", "OPTIONS"]
READONLY_METHODS = {"GET", "HEAD", "OPTIONS"}


class MegaAdminPermissionReadOnly(permissions.BasePermission):
Expand All @@ -77,8 +73,7 @@ class MegaAdminPermissionReadOnly(permissions.BasePermission):
def has_permission(self, request: Request, view):
if request.method in READONLY_METHODS:
return True
current_user_role = request.user.role.name
return current_user_role == "MegaAdmin"
return request.user.role.name == RoleName.MegaAdmin


class CurrentUserPermission(permissions.BasePermission):
Expand Down
10 changes: 4 additions & 6 deletions canopeum_backend/canopeum_backend/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,11 +111,11 @@ def create_user(self):
if user_invitation.is_expired():
raise serializers.ValidationError("INVITATION_EXPIRED") from None

role = Role.objects.get(name="SiteManager")
role = Role.objects.get(name=RoleName.ForestSteward)
except UserInvitation.DoesNotExist:
raise serializers.ValidationError("INVITATION_CODE_INVALID") from None
else:
role = Role.objects.get(name="User")
role = Role.objects.get(name=RoleName.User)

user = User.objects.create(
username=self.validated_data["username"],
Expand Down Expand Up @@ -145,15 +145,13 @@ class Meta:
exclude = ("password",)

def get_role(self, obj: User) -> RoleName:
role_name = obj.role.name
return RoleName.from_string(role_name) # type: ignore[no-any-return] # mypy false-positive
return RoleName.from_string(obj.role.name) # type: ignore[no-any-return] # mypy false-positive

def get_admin_site_ids(self, obj: User) -> list[int]:
return [siteadmin.site.pk for siteadmin in Siteadmin.objects.filter(user=obj)]

def get_followed_site_ids(self, obj: User) -> list[int]:
user_role = self.get_role(obj)
if user_role == RoleName.MEGAADMIN:
if obj.role.name == RoleName.MegaAdmin:
return [site.pk for site in Site.objects.all()]
return [site_follower.site.pk for site_follower in SiteFollower.objects.filter(user=obj)]

Expand Down
6 changes: 5 additions & 1 deletion canopeum_backend/canopeum_backend/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,11 @@
path("map/sites/", views.SiteMapListAPIView.as_view(), name="coordinate-list-sites"),
# User
path("users/", views.UserListAPIView.as_view(), name="user-list"),
path("users/site-managers", views.SiteManagersListAPIView.as_view(), name="site-managers-list"),
path(
"users/forest-stewards",
views.ForestStewardsListAPIView.as_view(),
name="forest-stewards-list",
),
path("users/<int:userId>/", views.UserDetailAPIView.as_view(), name="user-detail"),
path("users/current_user/", views.UserCurrentUserAPIView.as_view(), name="current-user"),
path(
Expand Down
30 changes: 15 additions & 15 deletions canopeum_backend/canopeum_backend/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
from canopeum_backend.permissions import (
CurrentUserPermission,
DeleteCommentPermission,
MegaAdminOrSiteManagerPermission,
MegaAdminOrForestStewardPermission,
MegaAdminPermission,
MegaAdminPermissionReadOnly,
PublicSiteReadPermission,
Expand Down Expand Up @@ -102,9 +102,9 @@


def get_public_sites_unless_admin(user: User | None):
if isinstance(user, User) and user.role.name == "MegaAdmin":
if isinstance(user, User) and user.role.name == RoleName.MegaAdmin:
sites = Site.objects.all()
elif isinstance(user, User) and user.role.name == "SiteManager":
elif isinstance(user, User) and user.role.name == RoleName.ForestSteward:
admin_site_ids = [siteadmin.site.pk for siteadmin in Siteadmin.objects.filter(user=user)]
sites = Site.objects.filter(Q(id__in=admin_site_ids) | Q(is_public=True))
else:
Expand All @@ -113,9 +113,9 @@ def get_public_sites_unless_admin(user: User | None):


def get_admin_sites(user: User):
if user.role.name == "MegaAdmin":
if user.role.name == RoleName.MegaAdmin:
return Site.objects.all()
if isinstance(user, User) and user.role.name == "SiteManager":
if isinstance(user, User) and user.role.name == RoleName.ForestSteward:
admin_site_ids = [siteadmin.site.pk for siteadmin in Siteadmin.objects.filter(user=user)]
return Site.objects.filter(Q(id__in=admin_site_ids))

Expand Down Expand Up @@ -182,7 +182,7 @@ def post(self, request: Request):


class TreeSpeciesAPIView(APIView):
permission_classes = (MegaAdminOrSiteManagerPermission,)
permission_classes = (MegaAdminOrForestStewardPermission,)

@extend_schema(responses=TreeTypeSerializer(many=True), operation_id="tree_species")
def get(self, request: Request):
Expand All @@ -200,7 +200,7 @@ def get(self, request: Request):


class FertilizerListAPIView(APIView):
permission_classes = (MegaAdminOrSiteManagerPermission,)
permission_classes = (MegaAdminOrForestStewardPermission,)

@extend_schema(
responses=FertilizerTypeSerializer(many=True), operation_id="fertilizer_allTypes"
Expand All @@ -212,7 +212,7 @@ def get(self, request):


class MulchLayerListAPIView(APIView):
permission_classes = (MegaAdminOrSiteManagerPermission,)
permission_classes = (MegaAdminOrForestStewardPermission,)

@extend_schema(
responses=MulchLayerTypeSerializer(many=True), operation_id="mulchLayer_allTypes"
Expand Down Expand Up @@ -408,7 +408,7 @@ def patch(self, request: Request, siteId):


class SiteSummaryListAPIView(APIView):
permission_classes = (MegaAdminOrSiteManagerPermission,)
permission_classes = (MegaAdminOrForestStewardPermission,)

@extend_schema(responses=SiteSummarySerializer(many=True), operation_id="site_summary_all")
def get(self, request: Request):
Expand Down Expand Up @@ -472,7 +472,7 @@ def patch(self, request: Request, siteId):
updated_admin_users_list = User.objects.filter(id__in=admin_ids)

for user in updated_admin_users_list:
if user not in existing_admin_users and user.role.name == RoleName.SITEMANAGER:
if user not in existing_admin_users and user.role.name == RoleName.ForestSteward:
Siteadmin.objects.create(
user=user,
site=site,
Expand Down Expand Up @@ -536,10 +536,10 @@ class AdminUserSitesAPIView(APIView):
operation_id="admin-user-sites_all",
)
def get(self, request: Request):
site_manager_users = User.objects.filter(role__name__iexact=RoleName.SITEMANAGER).order_by(
forest_stewards = User.objects.filter(role__name__iexact=RoleName.ForestSteward).order_by(
"username"
)
serializer = AdminUserSitesSerializer(site_manager_users, many=True)
serializer = AdminUserSitesSerializer(forest_stewards, many=True)
return Response(serializer.data)


Expand Down Expand Up @@ -1060,12 +1060,12 @@ def get(self, request: Request):
return Response(serializer.data)


class SiteManagersListAPIView(APIView):
class ForestStewardsListAPIView(APIView):
permission_classes = (MegaAdminPermission,)

@extend_schema(responses=UserSerializer(many=True), operation_id="user_allSiteManagers")
@extend_schema(responses=UserSerializer(many=True), operation_id="user_allForestStewards")
def get(self, request: Request):
users = User.objects.filter(role__name__iexact=RoleName.SITEMANAGER).order_by("username")
users = User.objects.filter(role__name__iexact=RoleName.ForestSteward).order_by("username")
serializer = UserSerializer(users, many=True)
return Response(serializer.data)

Expand Down
Loading
Loading