-
-
Notifications
You must be signed in to change notification settings - Fork 75
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #56 from OltarzewskiK/issues/45
Move MultitenantAdminMixin from openwisp-utils to openwisp-users
- Loading branch information
Showing
10 changed files
with
442 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
from django.contrib import admin | ||
from django.db.models import Q | ||
from django.utils.translation import ugettext_lazy as _ | ||
|
||
|
||
class MultitenantAdminMixin(object): | ||
""" | ||
Mixin that makes a ModelAdmin class multitenant: | ||
users will see only the objects related to the organizations | ||
they are associated with. | ||
""" | ||
multitenant_shared_relations = [] | ||
multitenant_parent = None | ||
|
||
def __init__(self, *args, **kwargs): | ||
super(MultitenantAdminMixin, self).__init__(*args, **kwargs) | ||
parent = self.multitenant_parent | ||
shared_relations = self.multitenant_shared_relations | ||
if parent and parent not in shared_relations: | ||
self.multitenant_shared_relations.append(parent) | ||
|
||
def get_repr(self, obj): | ||
return str(obj) | ||
|
||
get_repr.short_description = _('name') | ||
|
||
def get_queryset(self, request): | ||
""" | ||
If current user is not superuser, show only the | ||
objects associated to organizations he/she is associated with | ||
""" | ||
qs = super(MultitenantAdminMixin, self).get_queryset(request) | ||
user = request.user | ||
if user.is_superuser: | ||
return qs | ||
if hasattr(self.model, 'organization'): | ||
return qs.filter(organization__in=user.organizations_pk) | ||
elif not self.multitenant_parent: | ||
return qs | ||
else: | ||
qsarg = '{0}__organization__in'.format(self.multitenant_parent) | ||
return qs.filter(**{qsarg: user.organizations_pk}) | ||
|
||
def _edit_form(self, request, form): | ||
""" | ||
Modifies the form querysets as follows; | ||
if current user is not superuser: | ||
* show only relevant organizations | ||
* show only relations associated to relevant organizations | ||
or shared relations | ||
else show everything | ||
""" | ||
fields = form.base_fields | ||
if not request.user.is_superuser: | ||
orgs_pk = request.user.organizations_pk | ||
# organizations relation; | ||
# may be readonly and not present in field list | ||
if 'organization' in fields: | ||
org_field = fields['organization'] | ||
org_field.queryset = org_field.queryset.filter(pk__in=orgs_pk) | ||
# other relations | ||
q = Q(organization__in=orgs_pk) | Q(organization=None) | ||
for field_name in self.multitenant_shared_relations: | ||
# each relation may be readonly | ||
# and not present in field list | ||
if field_name not in fields: | ||
continue | ||
field = fields[field_name] | ||
field.queryset = field.queryset.filter(q) | ||
|
||
def get_form(self, request, obj=None, **kwargs): | ||
form = super(MultitenantAdminMixin, self).get_form(request, obj, **kwargs) | ||
self._edit_form(request, form) | ||
return form | ||
|
||
def get_formset(self, request, obj=None, **kwargs): | ||
formset = super(MultitenantAdminMixin, self).get_formset(request, obj=None, **kwargs) | ||
self._edit_form(request, formset.form) | ||
return formset | ||
|
||
|
||
class MultitenantOrgFilter(admin.RelatedFieldListFilter): | ||
""" | ||
Admin filter that shows only organizations the current | ||
user is associated with in its available choices | ||
""" | ||
multitenant_lookup = 'pk__in' | ||
|
||
def field_choices(self, field, request, model_admin): | ||
if request.user.is_superuser: | ||
return super(MultitenantOrgFilter, self).field_choices(field, request, model_admin) | ||
organizations = request.user.organizations_pk | ||
return field.get_choices(include_blank=False, | ||
limit_choices_to={self.multitenant_lookup: organizations}) | ||
|
||
|
||
class MultitenantRelatedOrgFilter(MultitenantOrgFilter): | ||
""" | ||
Admin filter that shows only objects which have a relation with | ||
one of the organizations the current user is associated with | ||
""" | ||
multitenant_lookup = 'organization__in' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
class CreateMixin(object): | ||
def _create_book(self, **kwargs): | ||
options = dict(name='test-book', | ||
author='test-author') | ||
options.update(kwargs) | ||
b = self.book_model(**options) | ||
b.full_clean() | ||
b.save() | ||
return b | ||
|
||
def _create_shelf(self, **kwargs): | ||
options = dict(name='test-shelf') | ||
options.update(kwargs) | ||
s = self.shelf_model(**options) | ||
s.full_clean() | ||
s.save() | ||
return s |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
from django.contrib import admin | ||
from openwisp_users.multitenancy import (MultitenantAdminMixin, | ||
MultitenantOrgFilter, | ||
MultitenantRelatedOrgFilter) | ||
|
||
from .models import Book, Shelf | ||
|
||
|
||
class BaseAdmin(MultitenantAdminMixin, admin.ModelAdmin): | ||
pass | ||
|
||
|
||
class ShelfAdmin(BaseAdmin): | ||
list_display = ['name', 'organization'] | ||
list_filter = [('organization', MultitenantOrgFilter)] | ||
fields = ['name', 'organization', 'created', 'modified'] | ||
|
||
|
||
class BookAdmin(BaseAdmin): | ||
list_display = ['name', 'author', 'organization', 'shelf'] | ||
list_filter = [('organization', MultitenantOrgFilter), | ||
('shelf', MultitenantRelatedOrgFilter)] | ||
fields = ['name', 'author', 'organization', 'shelf', 'created', 'modified'] | ||
multitenant_shared_relations = ['shelf'] | ||
|
||
def change_view(self, request, object_id, form_url='', extra_context=None): | ||
extra_context = extra_context or {} | ||
extra_context.update({ | ||
'additional_buttons': [ | ||
{ | ||
'type': 'button', | ||
'url': 'DUMMY', | ||
'class': 'previewbook', | ||
'value': 'Preview book', | ||
}, | ||
{ | ||
'type': 'button', | ||
'url': 'DUMMY', | ||
'class': 'downloadbook', | ||
'value': 'Download book', | ||
} | ||
] | ||
}) | ||
return super(BookAdmin, self).change_view(request, object_id, form_url, extra_context) | ||
|
||
|
||
admin.site.register(Shelf, ShelfAdmin) | ||
admin.site.register(Book, BookAdmin) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
from django.apps import AppConfig | ||
|
||
|
||
class TestAppConfig(AppConfig): | ||
name = 'testapp' | ||
label = 'testapp' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
# Generated by Django 2.1.3 on 2018-11-29 21:44 | ||
|
||
from django.db import migrations, models | ||
import django.db.models.deletion | ||
import django.utils.timezone | ||
import model_utils.fields | ||
import openwisp_users.mixins | ||
import uuid | ||
|
||
|
||
class Migration(migrations.Migration): | ||
|
||
dependencies = [ | ||
('openwisp_users', '0004_default_groups'), | ||
('testapp', '0002_config_template'), | ||
] | ||
|
||
operations = [ | ||
migrations.CreateModel( | ||
name='Book', | ||
fields=[ | ||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), | ||
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), | ||
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), | ||
('name', models.CharField(max_length=64, verbose_name='name')), | ||
('author', models.CharField(max_length=64, verbose_name='author')), | ||
('organization', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='openwisp_users.Organization', verbose_name='organization')), | ||
], | ||
options={ | ||
'abstract': False, | ||
}, | ||
bases=(openwisp_users.mixins.ValidateOrgMixin, models.Model), | ||
), | ||
migrations.CreateModel( | ||
name='Shelf', | ||
fields=[ | ||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), | ||
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), | ||
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), | ||
('name', models.CharField(max_length=64, verbose_name='name')), | ||
('organization', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='openwisp_users.Organization', verbose_name='organization')), | ||
], | ||
options={ | ||
'abstract': False, | ||
}, | ||
bases=(openwisp_users.mixins.ValidateOrgMixin, models.Model), | ||
), | ||
migrations.AddField( | ||
model_name='book', | ||
name='shelf', | ||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='testapp.Shelf'), | ||
), | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.