diff --git a/Pipfile b/Pipfile index bba84e5..078f9b9 100644 --- a/Pipfile +++ b/Pipfile @@ -7,10 +7,8 @@ verify_ssl = true [packages] django = "==2.1.5" wemake-python-styleguide = "==0.6.3" - # see https://github.com/wemake-services/wemake-python-styleguide/pull/472#issuecomment-460057878 flake8 = '==3.6.0' - django-rest-framework = "*" pillow = "*" drf-writable-nested = "*" @@ -22,6 +20,7 @@ djangorestframework-camel-case = "*" django-oauth-toolkit = "*" django-cors-middleware = "*" pyuploadcare = "*" +dry-rest-permissions = "*" [requires] python_version = "3.6" diff --git a/Pipfile.lock b/Pipfile.lock index b352d2c..91d85bc 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "ccc0ffae47eb3b647f9e734c807a34e7376445b34f6c7160a64a5d1affadd3ac" + "sha256": "7e1e14b7525ed646e9fe9508e038eabcf0b2849fce4243dac4783eba29fa0a8e" }, "pipfile-spec": 6, "requires": { @@ -17,10 +17,10 @@ "default": { "amqp": { "hashes": [ - "sha256:9f181e4aef6562e6f9f45660578fc1556150ca06e836ecb9e733e6ea10b48464", - "sha256:c3d7126bfbc640d076a01f1f4f6e609c0e4348508150c1f61336b0d83c738d2b" + "sha256:16056c952e8029ce8db097edf0d7c2fe2ba9de15d30ba08aee2c5221273d8e23", + "sha256:6816eed27521293ee03aa9ace300a07215b11fee4e845588a9b863a7ba30addb" ], - "version": "==2.4.0" + "version": "==2.4.1" }, "astor": { "hashes": [ @@ -136,6 +136,13 @@ ], "version": "==1.13.0" }, + "dry-rest-permissions": { + "hashes": [ + "sha256:1f40461184063390e5b24e9c5602eb8cc8c3c2433c796f39a5332065bfbddd2b", + "sha256:f3fe685760004ce182801602819b43ebfa922e587036f1f5a5c10ffcfa646039" + ], + "version": "==0.1.10" + }, "eradicate": { "hashes": [ "sha256:f9af01c544ccd8f71bc2f7f3fa39dc363d842cfcb9c730a83676a59026ab5f24" @@ -328,10 +335,10 @@ }, "kombu": { "hashes": [ - "sha256:1ef049243aa05f29e988ab33444ec7f514375540eaa8e0b2e1f5255e81c5e56d", - "sha256:3c9dca2338c5d893f30c151f5d29bfb81196748ab426d33c362ab51f1e8dbf78" + "sha256:529df9e0ecc0bad9fc2b376c3ce4796c41b482cf697b78b71aea6ebe7ca353c8", + "sha256:7a2cbed551103db9a4e2efafe9b63222e012a61a18a881160ad797b9d4e1d0a1" ], - "version": "==4.2.2.post1" + "version": "==4.3.0" }, "markupsafe": { "hashes": [ @@ -459,10 +466,10 @@ }, "python-dateutil": { "hashes": [ - "sha256:063df5763652e21de43de7d9e00ccf239f953a832941e37be541614732cdfc93", - "sha256:88f9287c0174266bb0d8cedd395cfba9c58e87e5ad86b2ce58859bc11be3cf02" + "sha256:7e6584c74aeed623791615e26efd690f29817a27c73085b78e4bad02493df2fb", + "sha256:c89805f6f4d64db21ed966fda138f8a5ed7a4fdbc1a8ee329ce1b74e3c74da9e" ], - "version": "==2.7.5" + "version": "==2.8.0" }, "pytz": { "hashes": [ @@ -509,30 +516,30 @@ }, "ruamel.yaml": { "hashes": [ - "sha256:18078354bfcf00d51bcc17984aded80840379aed36036f078479e191b59bc059", - "sha256:211e6ef2530f44fc3197c713892678e7fbfbc40a1db6741179d6981514be1674", - "sha256:2e8f7cee12a2372cec4480fe81086b1fdab163f4b56e58b5592a105c52973b78", - "sha256:48cc8e948a7ec4917bf94adff2cc1255e98f1eef5e1961889886acc4ff3a7194", - "sha256:4a0c7f970aa0e30bc541f690fbd14aca19de1cab70787180de5083b902ec40b5", - "sha256:5dd0ea7c5c703e8675f3caf2898a50b4dadaa52838f8e104637a452a05e03030", - "sha256:612fb4833f1978ceb7fd7a24d86a5ebd103bcc408394f3af621293194658cf1b", - "sha256:61c421a7a2b8e2886a94fbe29866df6b99451998abaa1584b9fdc9c10c33e40b", - "sha256:6483416847980aa7090b697d177a8754c4f340683cc84abd38da7b850826687d", - "sha256:6622f3b0cae7ed6fe5d3d6a6d8d8cb9413a05b408d69a789a57b77a616bb6562", - "sha256:80b2acde0d1b9d25e5c041960a9149480c15c6d9f4c24b8ddb381b14e9e70ea4", - "sha256:8f9ed94be17f306485df8fd0274a30f130a73f127798657d4dc65b1f89ec7a36", - "sha256:9a6b94cc9b6e738036426498ac9fe8ca05afea4249fb9dec1be32ce4823d5756", - "sha256:a4b11dfe421a9836c723107a4ccc9cab9674de611ba60b8212e85526ea8bf254", - "sha256:a55e55c6ecb5725ba472f9b811940e8d258a32fb36f5793dbc38582d6f377f3f", - "sha256:a736ab1d8c2d5566254a1a2ee38e7c5460520bcccd4a8f0feb25a4463735e5a7", - "sha256:c29d0a3cffa5a25f5259bfeac06ffdc5e7d1fd38a0a26a6664d160192730434f", - "sha256:c33458217a8c352b59c86065c4f05f3f1ac28b01c3e1a422845c306237446bf3", - "sha256:cc9bd3c3fa8a928f7b6e19fe8de13a61deb91f257eccbe0d16114ce8c54cdc81", - "sha256:d63b7c828a7358ce5b03a3e2c2a3e5a7058a954f8919334cb09b3d8541d1fff6", - "sha256:fbd301680a3563e84d667042dac1c5d50ef402ecf1f4b1763507a6877b8181ad", - "sha256:fc67e79e2f5083be6fd1000c4646e13a891585772a503f56f51f845b547fe621" - ], - "version": "==0.15.87" + "sha256:0289a685479d059b94683cd6cb47ffb790c05c20a6c4da395361025d52493d0a", + "sha256:0adf1d9b8e88dc6b151a3199b1dd7be0c8ee10d6c2ebd2a9e2a13224f4481cdf", + "sha256:13657c26780bba5824764cddb0f2933217fd59cfcca0e2ee1b2f759e7e58ef8e", + "sha256:3815f688de7316fcd3ba5ceda642e902044c5c1a8fb5e4dc245d99db3eb3121b", + "sha256:4e61c0b96805d1e2ec53cb1698ca6086a47aa1e1d09857144eb60216e7894ce3", + "sha256:4f0d57ead5414456cb899c3746a8d30f566c22bb90c97da76f76e79147cb2d61", + "sha256:51916929902ff054e189d29bc418788a5dc3a4e89a89065beed694f537383ca3", + "sha256:5b7ea0ee24680157666f730f3a8c173f386c66e8c103458af20d97276e7e54d3", + "sha256:6b1b1ee0a028b9cdc1bc3ec1f75480fc0d3fbcc9e0212a716b129b6f26e34587", + "sha256:6db27f789c7efdbc59b8650c37a09dde0db019560bc19a07c905b65158a18bba", + "sha256:7a8c8f825fd52f3586d583f621cdf3a03b9dc8833933ae401554b246b48026d5", + "sha256:7ab0c27094ef27a21e0094dc671c456bd4a62811c14e27407ae8bc3aa8cc8111", + "sha256:8e06bcf212b45dffe6c2415693c32b4c7d4ff55c03268a3033217f7a673d07ba", + "sha256:9826e3c85549b3fc87786466a7a96dcadec59802a9ed077b905349ef1cac7b14", + "sha256:ac56193c47a31c9efa151064a9e921865cdad0f7a991d229e7197e12fe8e0cd7", + "sha256:aef88ec2927b0454709026a761918c02b69e5df9c061b49634d7993c0848580d", + "sha256:d8591fdfd076d8121a456aaff0bbea6d5753023896f4559b710d4e56d1ac6418", + "sha256:e033423fd6b4ddfd47f0f5ebe81e896129d85fd5219c5e66effb4de06a1fea7a", + "sha256:e05017af8c1164fee33aa2677df7eaeb6d2fa76e22baf7960f9e8f1b04657151", + "sha256:e4cd2ccd4d455206826a7c59fda13a9008ae994de66a7b0df2c0bb81121fab01", + "sha256:e4f525efdecc075e6b0d96df0ae4bd2ad17c7280ebe66035f468c5c3da53fe0d", + "sha256:fd5f09c399cdc92586b54ee28f68f23f1d5649177d7ceb22ec975b5e69e1b722" + ], + "version": "==0.15.88" }, "six": { "hashes": [ diff --git a/postpost/api/middlewares.py b/postpost/api/middlewares.py new file mode 100644 index 0000000..66c7972 --- /dev/null +++ b/postpost/api/middlewares.py @@ -0,0 +1,45 @@ +from api.models import Workspace + + +class GlobalWorkspaceMiddleware(object): + """ + Add request-relevant workspace object to request object. + + Because most of requests to our API tied to specific workspace (e.g. + work with publications), the middleware saves us from a lot of repeated + code for extracting workspace by request data. + """ + + def __init__(self, get_response): + """ + Standard interface of django middleware. + + See more: + https://docs.djangoproject.com/en/2.1/topics/http/middleware/#init-get-response + """ + self.get_response = get_response + + def __call__(self, request): + """ + Executed before view and other middlewares are called. + + And this method does nothing. + """ + return self.get_response(request) + + def process_view(self, request, view_func, view_args, view_kwargs): + """ + One of the django middleware hook. + + Uses here for inject relevant workspace to request objects. + Search Workspace instance by url params that a router usually + generates. + + See more about this middleware hook: + https://docs.djangoproject.com/en/2.1/topics/http/middleware/#process-view + """ + workspace_name = view_kwargs.get('workspace_pk') or view_kwargs.get('workspace_name') + if workspace_name: + workspace = Workspace.objects.filter(name=workspace_name).first() + if workspace: + request.workspace = workspace diff --git a/postpost/api/migrations/0003_workspaces.py b/postpost/api/migrations/0003_workspaces.py new file mode 100644 index 0000000..52e4c56 --- /dev/null +++ b/postpost/api/migrations/0003_workspaces.py @@ -0,0 +1,46 @@ +# Generated by Django 2.1.5 on 2019-02-06 19:09 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('api', '0002_auto_20190128_2219'), + ] + + operations = [ + migrations.CreateModel( + name='Workspace', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.SlugField(unique=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ], + ), + migrations.CreateModel( + name='WorkspaceMember', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('role', models.CharField(choices=[('publisher', 'Publisher: only create and edit publications'), ('admin', 'Admin: also can edit platforms and members')], default='publisher', max_length=64)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('member', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.Workspace')), + ], + ), + migrations.AddField( + model_name='publication', + name='workspace', + field=models.ForeignKey(default=None, on_delete=django.db.models.deletion.CASCADE, to='api.Workspace'), + preserve_default=False, + ), + migrations.AlterUniqueTogether( + name='workspacemember', + unique_together={('member', 'workspace')}, + ), + ] diff --git a/postpost/api/models/__init__.py b/postpost/api/models/__init__.py index ffaf91c..02f69ac 100644 --- a/postpost/api/models/__init__.py +++ b/postpost/api/models/__init__.py @@ -1,2 +1,4 @@ -from api.models.platform_settings import PlatformPost # noqa: F401 -from api.models.publications import Publication # noqa: F401 +from api.models.platform_post import PlatformPost # noqa: F401 +from api.models.publication import Publication # noqa: F401 +from api.models.workspace import Workspace # noqa: F401 +from api.models.workspace_member import WorkspaceMember # noqa: F401 diff --git a/postpost/api/models/platform_settings.py b/postpost/api/models/platform_post.py similarity index 100% rename from postpost/api/models/platform_settings.py rename to postpost/api/models/platform_post.py diff --git a/postpost/api/models/publications.py b/postpost/api/models/publication.py similarity index 72% rename from postpost/api/models/publications.py rename to postpost/api/models/publication.py index 157f4c3..852595b 100644 --- a/postpost/api/models/publications.py +++ b/postpost/api/models/publication.py @@ -1,7 +1,10 @@ from django.db import models from pyuploadcare.dj import models as uploadcare_models +from rest_framework.request import Request +from api import permissions from api.models import PlatformPost +from api.models.workspace import Workspace class Publication(models.Model): @@ -12,6 +15,7 @@ class Publication(models.Model): text = models.TextField() picture = uploadcare_models.ImageField(blank=True, null=True) + workspace = models.ForeignKey(Workspace, on_delete=models.CASCADE, null=False) scheduled_at = models.DateTimeField(blank=True, null=True) created_at = models.DateTimeField(auto_now_add=True, editable=False) @@ -34,3 +38,11 @@ def current_status(self) -> str: return PlatformPost.SENDING_STATUS else: return PlatformPost.SUCCESS_STATUS + + @staticmethod + def has_read_permission(request: Request) -> bool: + return permissions.check_workspace_member(request) + + @staticmethod + def has_write_permission(request: Request) -> bool: + return permissions.check_workspace_member(request) diff --git a/postpost/api/models/workspace.py b/postpost/api/models/workspace.py new file mode 100644 index 0000000..7653f78 --- /dev/null +++ b/postpost/api/models/workspace.py @@ -0,0 +1,23 @@ +from django.db import models +from rest_framework.request import Request + +from api import permissions + + +class Workspace(models.Model): + """ + Workspace — space with members, publications and tuned platforms. Has a unique name. + """ + + name = models.SlugField(unique=True) + + created_at = models.DateTimeField(auto_now_add=True, editable=False) + updated_at = models.DateTimeField(auto_now=True, editable=False) + + @staticmethod + def has_list_permission(request: Request) -> bool: + return permissions.check_authenticated(request) + + @staticmethod + def has_create_permission(request: Request) -> bool: + return permissions.check_authenticated(request) \ No newline at end of file diff --git a/postpost/api/models/workspace_member.py b/postpost/api/models/workspace_member.py new file mode 100644 index 0000000..718dfc5 --- /dev/null +++ b/postpost/api/models/workspace_member.py @@ -0,0 +1,46 @@ +from django.conf import settings +from django.db import models +from rest_framework.request import Request + +from api import permissions +from api.models.workspace import Workspace + +PUBLISHER_ROLE = 'publisher' +ADMIN_ROLE = 'admin' +WORKSPACE_ROLES = [ + (PUBLISHER_ROLE, 'Publisher: only create and edit publications'), + (ADMIN_ROLE, 'Admin: also can edit platforms and members'), +] + + +class WorkspaceMember(models.Model): + """ + Many-to-many junction table user <-> workspace with role. + """ + + member = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + null=False, + ) + workspace = models.ForeignKey( + Workspace, + on_delete=models.CASCADE, + null=False, + ) + role = models.CharField( + choices=WORKSPACE_ROLES, + default=PUBLISHER_ROLE, + null=False, + max_length=64, # noqa: Z432 + ) + + created_at = models.DateTimeField(auto_now_add=True, editable=False) + updated_at = models.DateTimeField(auto_now=True, editable=False) + + @staticmethod + def has_list_permission(request: Request) -> bool: + return permissions.check_workspace_member(request) + + class Meta(object): + unique_together = ('member', 'workspace') diff --git a/postpost/api/permissions.py b/postpost/api/permissions.py new file mode 100644 index 0000000..e7e983a --- /dev/null +++ b/postpost/api/permissions.py @@ -0,0 +1,43 @@ +from rest_framework import permissions +from rest_framework.request import Request + +from api.models import WorkspaceMember +from api.models.workspace_member import ADMIN_ROLE + + +def check_authenticated(request: Request): + return request.user.is_authenticated + + +def check_workspace_admin(request: Request): + """ + Checks that the user role is admin role in current workspace. + """ + is_workspace_admin = WorkspaceMember.objects.filter( + workspace=request.workspace, + member=request.user, + role=ADMIN_ROLE, + ).exists() + return is_workspace_admin + + +def check_workspace_member(request: Request): + """ + Just check user membership in current workspace. + """ + is_workspace_member = WorkspaceMember.objects.filter( + workspace=request.workspace, + member=request.user, + ).exists() + return is_workspace_member + + +def check_superuser(request: Request): + """ + Check standard django is_superuser flag :shrug:. + + More info: + https://docs.djangoproject.com/en/2.1/ref/contrib/auth/#django.contrib.auth.models.User.is_superuser + """ + is_superuser = request.user.is_authenticated() and request.user.is_superuser + return is_superuser diff --git a/postpost/api/serializers.py b/postpost/api/serializers.py index a4f0197..4df285f 100644 --- a/postpost/api/serializers.py +++ b/postpost/api/serializers.py @@ -11,6 +11,7 @@ from rest_framework.validators import UniqueValidator from api import models +from api.models.workspace_member import ADMIN_ROLE class VKGroupSettingsSerializer(serializers.ModelSerializer): @@ -105,6 +106,14 @@ def validate_platform_posts(self, platform_posts: Sequence[models.PlatformPost]) if len(platform_posts) == 0: raise serializers.ValidationError('Must be set one or more platform settings') + def create(self, validated_data): + """ + Add current workspace from context to data for creating object. + """ + workspace = self.context['request'].workspace + validated_data['workspace'] = workspace + return super().create(validated_data) + class UserRegistrationSerializer(serializers.ModelSerializer): """ @@ -199,3 +208,28 @@ class Meta(object): 'access_token', 'refresh_token', ] + + +class WorkspaceSerializer(serializers.ModelSerializer): + """ + TODO: A. + """ + + def create(self, validated_data): + """ + TODO: A. + """ + workspace = super().create(validated_data) + models.WorkspaceMember.objects.create( + workspace=workspace, + member=self.context['request'].user, + role=ADMIN_ROLE, + ) + return workspace + + class Meta(object): + model = models.Workspace + fields = [ + 'id', + 'name', + ] diff --git a/postpost/api/tasks.py b/postpost/api/tasks.py index 5738904..66c237a 100644 --- a/postpost/api/tasks.py +++ b/postpost/api/tasks.py @@ -1,11 +1,11 @@ import logging import os -from datetime import datetime import requests from celery import shared_task from celery.schedules import crontab from celery.task import periodic_task +from django.utils import timezone from api import vkontakte from api.models import PlatformPost @@ -71,7 +71,7 @@ def send_scheduled_posts(): """ unsent_posts = PlatformPost.objects.filter( current_status=PlatformPost.SCHEDULED_STATUS, - publication__scheduled_at__lte=datetime.now(), + publication__scheduled_at__lte=timezone.now(), ) logger.info('%s unsent platform-specific post found', unsent_posts.count()) scheduled_posts = list(unsent_posts) # get sql select before updating diff --git a/postpost/api/urls.py b/postpost/api/urls.py index 6839615..64311cd 100644 --- a/postpost/api/urls.py +++ b/postpost/api/urls.py @@ -1,9 +1,32 @@ -from django.urls import path +from rest_framework_nested import routers from api import views -urlpatterns = [ - path('users/', views.UserRegistration.as_view(), name='users_registration'), - path('publications/', views.PublicationList.as_view(), name='publications_list'), - path('publications/', views.Publication.as_view(), name='publications_details'), -] +router = routers.SimpleRouter() +router.register('users', views.UserRegistration) +router.register('workspaces', views.WorkspaceListCreate) +workspaces_router = routers.NestedSimpleRouter(router, 'workspaces', lookup='workspace') + +# workspaces_router.register( +# 'publications', +# views.WorkspacePublication, +# base_name='workspace-publication', +# ) +workspaces_router.register( + 'members', + views.WorkspaceMemberCreate, + base_name='workspace-memberrr', +) +workspaces_router.register( + 'members', + views.WorkspaceMemberList, + base_name='workspace-member', +) +# workspaces_router.register( +# 'members', +# views.WorkspaceMemberUpdateDestroy, +# base_name='workspace-member', +# ) + + +urlpatterns = router.urls + workspaces_router.urls diff --git a/postpost/api/views.py b/postpost/api/views.py index df5c69e..9215e02 100644 --- a/postpost/api/views.py +++ b/postpost/api/views.py @@ -1,33 +1,108 @@ -from rest_framework.generics import CreateAPIView, ListCreateAPIView, RetrieveUpdateDestroyAPIView +from django.contrib.auth import models as contrib_models +from dry_rest_permissions.generics import DRYPermissions +from rest_framework import mixins, viewsets, generics +from rest_framework.generics import ListCreateAPIView from rest_framework.permissions import AllowAny, IsAuthenticated from api import models, serializers -class PublicationList(ListCreateAPIView): +class WorkspacePublication(viewsets.ModelViewSet): """ - Very basic view for Publications objects. + View for get, delete and change publication entity. """ - permission_classes = [IsAuthenticated] - queryset = models.Publication.objects.all() + permission_classes = [DRYPermissions] serializer_class = serializers.PublicationSerializer + def get_queryset(self): + """ + TODO: move this filter to `backend_filter`. + """ + return models.Publication.objects.filter( + workspace=self.request.workspace, + ) + -class Publication(RetrieveUpdateDestroyAPIView): +class UserRegistration(mixins.CreateModelMixin, viewsets.GenericViewSet): """ - View for get, delete and change publication entity. + Register user and generate access/refresh token immediately. + """ + + permission_classes = [AllowAny] + serializer_class = serializers.UserRegistrationSerializer + queryset = contrib_models.User.objects.all() + + +class WorkspaceListCreate(generics.ListCreateAPIView): + """ + Workspace views. + """ + + permission_classes = [DRYPermissions] + serializer_class = serializers.WorkspaceSerializer + + def get_queryset(self): + workspace_ids = models.WorkspaceMember.objects.select_related( + 'workspace', + ).filter( + member=self.request.user, + ).values_list( + 'workspace', + flat=True, + ) + return models.Workspace.objects.filter(id__in=workspace_ids) + + +class WorkspaceMemberList(mixins.ListModelMixin, viewsets.GenericViewSet): + """ + List of members available for all of workspace members. """ - permission_classes = [IsAuthenticated] - queryset = models.Publication.objects.all() serializer_class = serializers.PublicationSerializer + permission_classes = [DRYPermissions] + def get_queryset(self): + """ + Filter members by current workspace. + """ + return models.WorkspaceMember.objects.filter( + workspace=self.request.workspace, + ).select_related( + 'user', + ) -class UserRegistration(CreateAPIView): + +class WorkspaceMemberCreate(mixins.CreateModelMixin, viewsets.GenericViewSet): """ - Register user and generate access/refresh token immediately. + Superuser view for creating member. + + For example, can used for emergency restoring workspace. """ - permission_classes = [AllowAny] - serializer_class = serializers.UserRegistrationSerializer + permission_classes = [DRYPermissions] + serializer_class = serializers.PublicationSerializer # FIXME + + +class WorkspaceMemberUpdateDestroy( + mixins.UpdateModelMixin, + mixins.DestroyModelMixin, + viewsets.GenericViewSet, +): + """ + Allow removal membership and role changing for other members in workspace. + """ + + permission_classes = [DRYPermissions] + serializer_class = serializers.PublicationSerializer # FIXME + + def get_queryset(self): + """ + Filter members by current workspace. + """ + return models.WorkspaceMember.objects.filter( + workspace=self.request.workspace, + ).select_related( + 'user', + ) + diff --git a/postpost/main/settings.py b/postpost/main/settings.py index ed7a1c8..d12f0d4 100644 --- a/postpost/main/settings.py +++ b/postpost/main/settings.py @@ -38,6 +38,7 @@ 'django.contrib.messages', 'django.contrib.staticfiles', 'rest_framework', + 'dry_rest_permissions', 'drf_yasg', 'oauth2_provider', 'corsheaders', @@ -46,6 +47,7 @@ ] MIDDLEWARE = [ + 'api.middlewares.GlobalWorkspaceMiddleware', 'corsheaders.middleware.CorsMiddleware', 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware',