Skip to content
This repository has been archived by the owner on Mar 11, 2020. It is now read-only.

Workspaces #32

Draft
wants to merge 9 commits into
base: develop
Choose a base branch
from
Draft
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
3 changes: 1 addition & 2 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "*"
Expand All @@ -22,6 +20,7 @@ djangorestframework-camel-case = "*"
django-oauth-toolkit = "*"
django-cors-middleware = "*"
pyuploadcare = "*"
dry-rest-permissions = "*"

[requires]
python_version = "3.6"
75 changes: 41 additions & 34 deletions Pipfile.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

45 changes: 45 additions & 0 deletions postpost/api/middlewares.py
Original file line number Diff line number Diff line change
@@ -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.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
code for extracting workspace by request data.
code for extracting workspace from request data.

"""

def __init__(self, get_response):
"""
Standard interface of django middleware.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
Standard interface of django middleware.
A 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.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
One of the django middleware hook.
One of the django middleware hooks.


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
46 changes: 46 additions & 0 deletions postpost/api/migrations/0003_workspaces.py
Original file line number Diff line number Diff line change
@@ -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')},
),
]
6 changes: 4 additions & 2 deletions postpost/api/models/__init__.py
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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):
Expand All @@ -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)
Expand All @@ -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)
23 changes: 23 additions & 0 deletions postpost/api/models/workspace.py
Original file line number Diff line number Diff line change
@@ -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)
46 changes: 46 additions & 0 deletions postpost/api/models/workspace_member.py
Original file line number Diff line number Diff line change
@@ -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')
43 changes: 43 additions & 0 deletions postpost/api/permissions.py
Original file line number Diff line number Diff line change
@@ -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
Loading