Skip to content

Commit

Permalink
#26 - adding user management features.
Browse files Browse the repository at this point in the history
* Adding signals
* User listings via MultiTenantCompany queries
* Invite and accept invitations for users
* adding tests
---------

Co-authored-by: Sascha Dobbelaere <sascha@tweave.tech>
  • Loading branch information
sdobbelaere and Sascha Dobbelaere committed Nov 28, 2023
1 parent a7f9283 commit 5016320
Show file tree
Hide file tree
Showing 18 changed files with 462 additions and 162 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/codecov.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: CodeCov
name: Run Tests

on: [push, pull_request]

Expand Down Expand Up @@ -41,7 +41,7 @@ jobs:
POSTGRES_PORT: ${{ job.services.postgres.ports[5432] }}
REDIS_HOST: localhost
REDIS_PORT: ${{ job.services.redis.ports[6379] }}
run: cd OneSila && coverage run --source='.' manage.py test
run: cd OneSila && coverage run --source='.' manage.py test && coverage json
- name: Upload coverage reports to Codecov with GitHub Action
uses: codecov/codecov-action@v3
env:
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ htmlcov/
.cache
nosetests.xml
coverage.xml
coverage.json
*.cover
*.py,cover
.hypothesis/
Expand Down
74 changes: 66 additions & 8 deletions OneSila/core/factories/multi_tenant.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
from core.models.multi_tenant import MultiTenantUser
from django.core.exceptions import ValidationError

from core.signals import registered, invite_sent, invite_accepted, \
disabled, enabled


class RegisterUserFactory:
model = MultiTenantUser
Expand Down Expand Up @@ -34,19 +37,19 @@ def create_user(self):

self.user = user

def send_welcome(self):
pass
def send_signal(self):
registered.send(sender=self.user.__class__, instance=self.user)

@transaction.atomic
def run(self):
self.create_user()
self.send_welcome()
self.send_signal()


class InviteUserFactory:
model = MultiTenantUser
is_active = False
invitation_accepted = False
is_active = False

def __init__(self, *, multi_tenant_company, language, username, first_name="", last_name=""):
self.multi_tenant_company = multi_tenant_company
Expand Down Expand Up @@ -76,11 +79,66 @@ def create_user(self):
user.save()
self.user = user

def send_invite(self):
# FIXME: Send out (branded) email to the new user.
pass
def send_signal(self):
invite_sent.send(sender=self.user.__class__, instance=self.user)

@transaction.atomic
def run(self):
self.create_user()
self.send_invite()
self.send_signal()


class AcceptUserInviteFactory:
def __init__(self, user, password, language):
self.user = user

self.password = password
self.language = language

def update_user(self):
self.user.invitation_accepted = True
self.user.language = self.language
self.user.is_active = True

self.user.set_password(self.password)
self.user.save()

def send_signal(self):
invite_accepted.send(sender=self.user.__class__, instance=self.user)

@transaction.atomic
def run(self):
self.update_user()
self.send_signal()


class DisableUserFactory:
def __init__(self, user):
self.user = user

def disable_user(self):
self.user.set_inactive()

def send_signal(self):
disabled.send(sender=self.user.__class__, instance=self.user)

@transaction.atomic
def run(self):
self.disable_user()
self.send_signal()


class EnableUserFactory:
def __init__(self, user):
self.user = user

def enable_user(self):
self.user.set_active()

def send_signal(self):
enabled.send(sender=self.user.__class__, instance=self.user)

@transaction.atomic
def run(self):
self.enable_user()
self.send_signal()
12 changes: 12 additions & 0 deletions OneSila/core/models/multi_tenant.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,18 @@ def save(self, *args, **kwargs):

super().save(*args, **kwargs)

def set_active(self, save=True):
self.is_active = True

if save:
self.save()

def set_inactive(self, save=True):
self.is_active = False

if save:
self.save()

class Meta:
verbose_name = _("Multi tenant user")
verbose_name_plural = _("Multi tenant users")
Expand Down
1 change: 1 addition & 0 deletions OneSila/core/schema/multi_tenant/mutations/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .mutation_type import MultiTenantMutation
49 changes: 49 additions & 0 deletions OneSila/core/schema/multi_tenant/mutations/fields.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
from core.schema.multi_tenant.types.input import MultiTenantCompanyMyInput, \
MultiTenantCompanyPartialInput, MultiTenantUserPartialInput, \
MultiTenantUserInput, MultiTenantInviteUserInput, MultiTenantUserAcceptInviteInput, \
MultiTenantUserStatusInput
from core.schema.core.mutations import IsAuthenticated, default_extensions
from .mutation_classes import MyMultiTenantCompanyCreateMutation, \
MyMultiTentantCompanyUpdateMutation, UpdateMeMutation, \
RegisterUserMutation, InviteUserMutation, AcceptInvitationMutation, \
EnableUserMutation, DisableUserMutation


def register_my_multi_tenant_company():
extensions = [IsAuthenticated()]
return MyMultiTenantCompanyCreateMutation(MultiTenantCompanyMyInput, extensions=extensions)


def update_my_multi_tenant_company():
extensions = default_extensions
return MyMultiTentantCompanyUpdateMutation(MultiTenantCompanyPartialInput, extensions=extensions)


def update_me():
extensions = default_extensions
return UpdateMeMutation(MultiTenantUserPartialInput, extensions=extensions)


def register_user():
extensions = []
return RegisterUserMutation(MultiTenantUserInput, extensions=extensions)


def invite_user():
extensions = default_extensions
return InviteUserMutation(MultiTenantInviteUserInput, extensions=extensions)


def accept_user_invitation():
extensions = []
return AcceptInvitationMutation(MultiTenantUserAcceptInviteInput, extensions=extensions)


def disable_user():
extensions = default_extensions
return DisableUserMutation(MultiTenantUserStatusInput, extensions=extensions)


def enable_user():
extensions = default_extensions
return EnableUserMutation(MultiTenantUserStatusInput, extensions=extensions)
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from strawberry import UNSET
from strawberry_django import auth as strawberry_auth

from strawberry.relay.utils import from_base64
from strawberry_django.resolvers import django_resolver
from strawberry_django.mutations import resolvers
from strawberry_django.auth.utils import get_current_user
Expand All @@ -8,28 +9,19 @@
from strawberry_django.auth.exceptions import IncorrectUsernamePasswordError

from django.db import transaction
from django.core.exceptions import ValidationError
from django.contrib import auth
from django.contrib.auth.password_validation import validate_password
from django.db.utils import IntegrityError
from django.conf import settings

from typing import cast, Type
from asgiref.sync import async_to_sync, sync_to_async
from asgiref.sync import async_to_sync

from channels import auth as channels_auth
from channels.db import database_sync_to_async

from core.schema.core.mutations import create, type, DjangoUpdateMutation, \
DjangoCreateMutation, GetMultiTenantCompanyMixin, default_extensions, \
update, Info, models, Iterable, Any, IsAuthenticated
from core.schema.core.mixins import GetQuerysetMultiTenantMixin
from core.factories.multi_tenant import InviteUserFactory, RegisterUserFactory

from .types.types import MultiTenantUserType, MultiTenantCompanyType
from .types.input import MultiTenantUserInput, MultiTenantUserPartialInput, \
MultiTenantCompanyPartialInput, MultiTenantCompanyInput, \
MultiTenantInviteUserInput, MultiTenantCompanyMyInput
from core.factories.multi_tenant import InviteUserFactory, RegisterUserFactory, \
AcceptUserInviteFactory, EnableUserFactory, DisableUserFactory


class CleanupDataMixin:
Expand Down Expand Up @@ -96,7 +88,16 @@ def create(self, data: dict[str, Any], *, info: Info):


class AcceptInvitationMutation(DjangoUpdateMutation):
pass
def update(self, info: Info, instance: models.Model, data: dict[str, Any]):
# Do not optimize anything while retrieving the object to update
with DjangoOptimizerExtension.disabled():
fac = AcceptUserInviteFactory(
user=instance,
password=data['password'],
language=data['language'])
fac.run()

return fac.user


class MyMultiTenantCompanyCreateMutation(GetMultiTenantCompanyMixin, DjangoCreateMutation):
Expand Down Expand Up @@ -152,47 +153,21 @@ def resolver(self, source: Any, info: Info, args: list[Any], kwargs: dict[str, A
return self.update(info, instance, resolvers.parse_input(info, vdata))


def register_my_multi_tenant_company():
extensions = [IsAuthenticated()]
return MyMultiTenantCompanyCreateMutation(MultiTenantCompanyMyInput, extensions=extensions)


def update_my_multi_tenant_company():
extensions = default_extensions
return MyMultiTentantCompanyUpdateMutation(MultiTenantCompanyPartialInput, extensions=extensions)


def update_me():
extensions = default_extensions
return UpdateMeMutation(MultiTenantUserPartialInput, extensions=extensions)


def register_user():
extensions = []
return RegisterUserMutation(MultiTenantUserInput, extensions=extensions)


def invite_user():
extensions = default_extensions
return InviteUserMutation(MultiTenantInviteUserInput, extensions=extensions)


@type(name="Mutation")
class MultiTenantMutation:
login: MultiTenantUserType = strawberry_auth.login()
logout = strawberry_auth.logout()
class DisableUserMutation(DjangoUpdateMutation):
def update(self, info: Info, instance: models.Model, data: dict[str, Any]):
# Do not optimize anything while retrieving the object to update
with DjangoOptimizerExtension.disabled():
fac = DisableUserFactory(user=instance)
fac.run()
return fac.user

register_user: MultiTenantUserType = register_user()
register_my_multi_tenant_company: MultiTenantCompanyType = register_my_multi_tenant_company()

update_me: MultiTenantUserType = update_me()
update_my_multi_tenant_company: MultiTenantCompanyType = update_my_multi_tenant_company()
class EnableUserMutation(DjangoUpdateMutation):
def update(self, info: Info, instance: models.Model, data: dict[str, Any]):
# Do not optimize anything while retrieving the object to update
with DjangoOptimizerExtension.disabled():
fac = EnableUserFactory(user=instance)
fac.run()

invite_user: MultiTenantUserType = invite_user()
# TODO: Invite user mutation.
# this mutation will:
# create "un-activated" user
# assign to multi-tenant-company
# send out email to invite and resturn invitation link
# + query/mutation flow for the user to actually subscribe.
# + mutation to re-invite the user
instance.refresh_from_db()
return instance
26 changes: 26 additions & 0 deletions OneSila/core/schema/multi_tenant/mutations/mutation_type.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from core.schema.core.mutations import type
from strawberry_django import auth as strawberry_auth
from .fields import register_user, register_my_multi_tenant_company, \
update_me, update_my_multi_tenant_company, invite_user, \
accept_user_invitation, disable_user, enable_user

from core.schema.multi_tenant.types.types import MultiTenantUserType, \
MultiTenantCompanyType


@type(name="Mutation")
class MultiTenantMutation:
login: MultiTenantUserType = strawberry_auth.login()
logout = strawberry_auth.logout()

register_user: MultiTenantUserType = register_user()
register_my_multi_tenant_company: MultiTenantCompanyType = register_my_multi_tenant_company()

update_me: MultiTenantUserType = update_me()
update_my_multi_tenant_company: MultiTenantCompanyType = update_my_multi_tenant_company()

invite_user: MultiTenantUserType = invite_user()
accept_user_invitation: MultiTenantUserType = accept_user_invitation()

disable_user: MultiTenantUserType = disable_user()
enable_user: MultiTenantUserType = enable_user()
11 changes: 3 additions & 8 deletions OneSila/core/schema/multi_tenant/queries.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,12 @@
from strawberry_django import auth, field

from core.models.multi_tenant import MultiTenantCompany
from core.schema.core.helpers import get_multi_tenant_company
from core.schema.multi_tenant.types.types import MultiTenantUserType, MultiTenantCompanyType
from core.schema.core.queries import node, connection, ListConnectionWithTotalCount, \
type, field, default_extensions, Info
from core.schema.core.helpers import get_multi_tenant_company
from .types.types import MultiTenantUserType, MultiTenantCompanyType


from strawberry_django.fields.field import StrawberryDjangoField
from strawberry import relay


def my_multi_tenant_company_resolver(info: Info) -> MultiTenantCompany:
def my_multi_tenant_company_resolver(info: Info) -> MultiTenantCompanyType:
multi_tenant_company = get_multi_tenant_company(info)
return multi_tenant_company

Expand Down
16 changes: 14 additions & 2 deletions OneSila/core/schema/multi_tenant/types/filters.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,14 @@
from core.schema.types.types import auto
from core.schema.types.filters import filter
from core.schema.core.types.types import auto
from core.schema.core.types.filters import filter

from core.models.multi_tenant import MultiTenantCompany, MultiTenantUser


@filter(MultiTenantUser)
class MultiTenantUserFilter:
first_name: auto
last_name: auto
username: auto
is_active: auto
invitation_accepted: auto
is_multi_tenant_company_owner: auto
Loading

0 comments on commit 5016320

Please sign in to comment.