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

♻️(backend) refactor post hackathon to a first working version #4

Merged
merged 1 commit into from
Feb 23, 2024
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
1 change: 0 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,6 @@ test-back-parallel: ## run all back-end tests in parallel
bin/pytest -n auto $${args:-${1}}
.PHONY: test-back-parallel


makemigrations: ## run django makemigrations for the publish project.
@echo "$(BOLD)Running makemigrations$(RESET)"
@$(COMPOSE) up -d postgresql
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Publish

publish is an application to handle users and teams.
publish is an application to handle users and templates.

publish is built on top of [Django Rest
Framework](https://www.django-rest-framework.org/).
Expand Down
2 changes: 1 addition & 1 deletion src/backend/.pylintrc
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ ignore-on-opaque-inference=yes
# for classes with dynamically set attributes). This supports the use of
# qualified names.
ignored-classes=optparse.Values,thread._local,_thread._local,responses,
Team,Contact
Template,Contact

# List of module names for which member attributes should not be checked
# (useful for modules/projects where namespaces are manipulated during runtime
Expand Down
76 changes: 62 additions & 14 deletions src/backend/core/admin.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,11 @@
"""Admin classes and registrations for Magnify's core app."""
"""Admin classes and registrations for core app."""
from django.contrib import admin
from django.contrib.auth import admin as auth_admin
from django.utils.translation import gettext_lazy as _

from . import models


class IdentityInline(admin.TabularInline):
"""Inline admin class for user identities."""
sampaccoud marked this conversation as resolved.
Show resolved Hide resolved

model = models.Identity
extra = 0


@admin.register(models.User)
class UserAdmin(admin.ModelAdmin):
"""User admin interface declaration."""

inlines = (IdentityInline,)


class TemplateAccessInline(admin.TabularInline):
"""Inline admin class for template accesses."""
Expand All @@ -25,6 +14,65 @@ class TemplateAccessInline(admin.TabularInline):
extra = 0


@admin.register(models.User)
class UserAdmin(auth_admin.UserAdmin):
"""Admin class for the User model"""

fieldsets = (
(
None,
{
"fields": (
"id",
"admin_email",
"password",
)
},
),
(_("Personal info"), {"fields": ("sub", "email", "language", "timezone")}),
(
_("Permissions"),
{
"fields": (
"is_active",
"is_device",
"is_staff",
"is_superuser",
"groups",
"user_permissions",
),
},
),
(_("Important dates"), {"fields": ("created_at", "updated_at")}),
)
add_fieldsets = (
(
None,
{
"classes": ("wide",),
"fields": ("email", "password1", "password2"),
},
),
)
inlines = (TemplateAccessInline,)
list_display = (
"id",
"sub",
"admin_email",
"email",
"is_active",
"is_staff",
"is_superuser",
"is_device",
"created_at",
"updated_at",
)
list_filter = ("is_staff", "is_superuser", "is_device", "is_active")
ordering = ("is_active", "-is_superuser", "-is_staff", "-is_device", "-updated_at")
readonly_fields = ("id", "sub", "email", "created_at", "updated_at")
search_fields = ("id", "sub", "admin_email", "email")


@admin.register(models.Template)
class TemplateAdmin(admin.ModelAdmin):
"""Template admin interface declaration."""
Expand Down
15 changes: 12 additions & 3 deletions src/backend/core/api/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,16 @@ class IsAuthenticated(permissions.BasePermission):
"""

def has_permission(self, request, view):
return bool(request.auth) if request.auth else request.user.is_authenticated
return bool(request.auth) or request.user.is_authenticated


class IsAuthenticatedOrSafe(IsAuthenticated):
"""Allows access to authenticated users (or anonymous users but only on safe methods)."""

def has_permission(self, request, view):
if request.method in permissions.SAFE_METHODS:
return True
return super().has_permission(request, view)


class IsSelf(IsAuthenticated):
Expand Down Expand Up @@ -45,10 +54,10 @@ def has_object_permission(self, request, view, obj):
return False


class AccessPermission(IsAuthenticated):
class AccessPermission(permissions.BasePermission):
"""Permission class for access objects."""

def has_object_permission(self, request, view, obj):
"""Check permission for a given object."""
abilities = obj.get_abilities(request.user)
return abilities.get(request.method.lower(), False)
return abilities.get(view.action, False)
sampaccoud marked this conversation as resolved.
Show resolved Hide resolved
71 changes: 30 additions & 41 deletions src/backend/core/api/serializers.py
Original file line number Diff line number Diff line change
@@ -1,31 +1,12 @@
"""Client serializers for the publish core app."""
from django.utils.translation import gettext_lazy as _

from rest_framework import exceptions, serializers
from timezone_field.rest_framework import TimeZoneSerializerField

from core import models


class ContactSerializer(serializers.ModelSerializer):
"""Serialize contacts."""

class Meta:
model = models.Contact
fields = [
"id",
"base",
"data",
"full_name",
"owner",
"short_name",
]
read_only_fields = ["id", "owner"]

def update(self, instance, validated_data):
"""Make "base" field readonly but only for update/patch."""
validated_data.pop("base", None)
return super().update(instance, validated_data)


class UserSerializer(serializers.ModelSerializer):
"""Serialize users."""

Expand All @@ -42,13 +23,14 @@ class Meta:
]
read_only_fields = ["id", "is_device", "is_staff"]

class TeamAccessSerializer(serializers.ModelSerializer):
"""Serialize team accesses."""

class TemplateAccessSerializer(serializers.ModelSerializer):
"""Serialize template accesses."""
sampaccoud marked this conversation as resolved.
Show resolved Hide resolved

abilities = serializers.SerializerMethodField(read_only=True)

class Meta:
model = models.TeamAccess
model = models.TemplateAccess
fields = ["id", "user", "role", "abilities"]
read_only_fields = ["id", "abilities"]

Expand Down Expand Up @@ -80,58 +62,65 @@ def validate(self, attrs):
message = (
f"You are only allowed to set role to {', '.join(can_set_role_to)}"
if can_set_role_to
else "You are not allowed to set this role for this team."
else "You are not allowed to set this role for this template."
)
raise exceptions.PermissionDenied(message)

# Create
else:
try:
team_id = self.context["team_id"]
template_id = self.context["template_id"]
except KeyError as exc:
raise exceptions.ValidationError(
"You must set a team ID in kwargs to create a new team access."
"You must set a template ID in kwargs to create a new template access."
) from exc

if not models.TeamAccess.objects.filter(
team=team_id,
if not models.TemplateAccess.objects.filter(
template=template_id,
user=user,
role__in=[models.RoleChoices.OWNER, models.RoleChoices.ADMIN],
).exists():
raise exceptions.PermissionDenied(
"You are not allowed to manage accesses for this team."
"You are not allowed to manage accesses for this template."
)

if (
role == models.RoleChoices.OWNER
and not models.TeamAccess.objects.filter(
team=team_id,
and not models.TemplateAccess.objects.filter(
template=template_id,
user=user,
role=models.RoleChoices.OWNER,
).exists()
):
raise exceptions.PermissionDenied(
"Only owners of a team can assign other users as owners."
"Only owners of a template can assign other users as owners."
)

attrs["team_id"] = self.context["team_id"]
attrs["template_id"] = self.context["template_id"]
return attrs


class TeamSerializer(serializers.ModelSerializer):
"""Serialize teams."""
class TemplateSerializer(serializers.ModelSerializer):
"""Serialize templates."""

abilities = serializers.SerializerMethodField(read_only=True)
accesses = TeamAccessSerializer(many=True, read_only=True)
accesses = TemplateAccessSerializer(many=True, read_only=True)

class Meta:
model = models.Team
fields = ["id", "name", "accesses", "abilities"]
model = models.Template
fields = ["id", "title", "accesses", "abilities"]
read_only_fields = ["id", "accesses", "abilities"]

def get_abilities(self, team) -> dict:
def get_abilities(self, template) -> dict:
"""Return abilities of the logged-in user on the instance."""
request = self.context.get("request")
if request:
return team.get_abilities(request.user)
return template.get_abilities(request.user)
return {}


# pylint: disable=abstract-method
class DocumentGenerationSerializer(serializers.Serializer):
"""Serializer to receive a request to generate a document on a template."""

body = serializers.CharField(label=_("Markdown Body"))
Loading
Loading