Skip to content
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
15 changes: 15 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,21 @@ the token in the ``Authorization`` header:
# send bearer token
http GET localhost:8000/api/v1/firmware/build/ "Authorization: Bearer $TOKEN"

Model helpers
-------------

The User model provides the following methods to check whether the user
is a member or an administrator of an organization:

.. code-block:: python

from openwisp_users.models import Organization, User

user = User.objects.first()
org = Organization.objects.first()
user.is_manager(org)
user.is_member(org)

Multitenancy mixins
-------------------

Expand Down
2 changes: 1 addition & 1 deletion openwisp_users/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ def get_formset(self, request, obj=None, **kwargs):
formset.form.base_fields[
'organization'
].queryset = Organization.objects.filter(
pk__in=request.user.organizations_pk
pk__in=request.user.organizations_dict.keys()
)
return formset

Expand Down
29 changes: 26 additions & 3 deletions openwisp_users/apps.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
from django.apps import AppConfig
from django.conf import settings
from django.core.cache import cache
from django.db.models.signals import post_delete, post_save
from django.utils.translation import ugettext_lazy as _
from openwisp_utils import settings as utils_settings
from swapper import get_model_name
from swapper import get_model_name, load_model

# from django.dispatch import receiver
from . import settings as app_settings


Expand All @@ -12,10 +15,10 @@ class OpenwispUsersConfig(AppConfig):
app_label = 'openwisp_users'
verbose_name = _('Users and Organizations')

def ready(self, *args, **kwargs):
super().ready(*args, **kwargs)
def ready(self):
self.add_default_menu_items()
self.set_default_settings()
self.connect_receivers()

def add_default_menu_items(self):
menu_setting = 'OPENWISP_DEFAULT_ADMIN_MENU_ITEMS'
Expand Down Expand Up @@ -44,3 +47,23 @@ def set_default_settings(self):
'Bearer': {'type': 'apiKey', 'in': 'header', 'name': 'Authorization'}
}
setattr(settings, 'SWAGGER_SETTINGS', SWAGGER_SETTINGS)

def connect_receivers(self):
OrganizationUser = load_model('openwisp_users', 'OrganizationUser')

post_save.connect(
self.update_organizations_dict,
sender=OrganizationUser,
dispatch_uid='post_save_update_organizations_dict',
)
post_delete.connect(
self.update_organizations_dict,
sender=OrganizationUser,
dispatch_uid='post_delete_update_organizations_dict',
)

def update_organizations_dict(cls, instance, **kwargs):
cache_key = 'user_{}_organizations'.format(instance.user.pk)
cache.delete(cache_key)
# forces caching
instance.user.organizations_dict
41 changes: 40 additions & 1 deletion openwisp_users/base/models.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
import logging
import uuid

from allauth.account.models import EmailAddress
from django.contrib.auth.models import AbstractUser as BaseUser
from django.contrib.auth.models import UserManager as BaseUserManager
from django.core.cache import cache
from django.core.exceptions import ValidationError
from django.db import models
from django.utils.functional import cached_property
from django.utils.translation import ugettext_lazy as _
from phonenumber_field.modelfields import PhoneNumberField
from swapper import load_model

logger = logging.getLogger(__name__)


class UserManager(BaseUserManager):
def _create_user(self, *args, **kwargs):
Expand Down Expand Up @@ -60,7 +64,10 @@ def organizations_pk(self):
"""
returns primary keys of organizations the user is associated to
"""

logger.warn(
"User.organizations_pk is deprecated in favor of User.organizations_dict"
" and will be removed in a future version"
)
manager = load_model('openwisp_users', 'OrganizationUser').objects
qs = (
manager.filter(user=self, organization__is_active=True)
Expand All @@ -70,6 +77,38 @@ def organizations_pk(self):
)
return qs

def is_member(self, organization):
org_pk = str(organization.pk)
return org_pk in self.organizations_dict

def is_manager(self, organization):
org_pk = str(organization.pk)
return (
org_pk in self.organizations_dict
and self.organizations_dict[org_pk]['is_admin'] is True
)

@property
def organizations_dict(self):
cache_key = 'user_{}_organizations'.format(self.pk)
organizations = cache.get(cache_key)
if organizations is not None:
return organizations

manager = load_model('openwisp_users', 'OrganizationUser').objects
org_users = manager.filter(
user=self, organization__is_active=True
).select_related('organization')

organizations = {}
for org_user in org_users:
org = org_user.organization
org_id = str(org.pk)
organizations[org_id] = {'name': org.name, 'is_admin': org_user.is_admin}

cache.set(cache_key, organizations, 86400 * 2) # Cache for two days
return organizations

def clean(self):
if self.email == '':
self.email = None
Expand Down
8 changes: 4 additions & 4 deletions openwisp_users/multitenancy.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,12 @@ def get_queryset(self, request):
if user.is_superuser:
return qs
if hasattr(self.model, 'organization'):
return qs.filter(organization__in=user.organizations_pk)
return qs.filter(organization__in=user.organizations_dict.keys())
elif not self.multitenant_parent:
return qs
else:
qsarg = '{0}__organization__in'.format(self.multitenant_parent)
return qs.filter(**{qsarg: user.organizations_pk})
return qs.filter(**{qsarg: user.organizations_dict.keys()})

def _edit_form(self, request, form):
"""
Expand All @@ -60,7 +60,7 @@ def _edit_form(self, request, form):
"""
fields = form.base_fields
if not request.user.is_superuser:
orgs_pk = request.user.organizations_pk
orgs_pk = request.user.organizations_dict.keys()
# organizations relation;
# may be readonly and not present in field list
if 'organization' in fields:
Expand Down Expand Up @@ -120,7 +120,7 @@ class MultitenantOrgFilter(admin.RelatedFieldListFilter):
def field_choices(self, field, request, model_admin):
if request.user.is_superuser:
return super().field_choices(field, request, model_admin)
organizations = request.user.organizations_pk
organizations = request.user.organizations_dict.keys()
return field.get_choices(
include_blank=False,
limit_choices_to={self.multitenant_lookup: organizations},
Expand Down
61 changes: 61 additions & 0 deletions openwisp_users/tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,67 @@ def test_organizations_pk_empty(self):
user = self._create_user(username='organizations_pk')
self.assertEqual(len(user.organizations_pk), 0)

def test_organizations_dict(self):
user = self._create_user(username='organizations_pk')
self.assertEqual(user.organizations_dict, {})
org1 = self._create_org(name='org1')
org2 = self._create_org(name='org2')
self._create_org(name='org3')
ou1 = OrganizationUser.objects.create(
user=user, organization=org1, is_admin=True
)
ou2 = OrganizationUser.objects.create(user=user, organization=org2)

organizations_dict = {
str(org1.pk): {'name': org1.name, 'is_admin': ou1.is_admin},
str(org2.pk): {'name': org2.name, 'is_admin': ou2.is_admin},
}
self.assertEqual(user.organizations_dict, organizations_dict)
self.assertEqual(len(user.organizations_dict), 2)

ou2.delete()
self.assertEqual(len(user.organizations_dict), 1)

def test_organizations_dict_cache(self):
user = self._create_user(username='organizations_pk')
org1 = self._create_org(name='org1')

with self.assertNumQueries(1):
list(user.organizations_dict)

with self.assertNumQueries(0):
list(user.organizations_dict)

OrganizationUser.objects.create(user=user, organization=org1)

# cache is automatically updated
with self.assertNumQueries(0):
list(user.organizations_dict)

def test_is_manager(self):
user = self._create_user(username='organizations_pk')
org1 = self._create_org(name='org1')
org2 = self._create_org(name='org2')
self.assertFalse(user.is_manager(org1))
self.assertFalse(user.is_manager(org2))
ou = OrganizationUser.objects.create(user=user, organization=org1)
self.assertFalse(user.is_manager(org1))
self.assertFalse(user.is_manager(org2))
ou.is_admin = True
ou.save()
self.assertTrue(user.is_manager(org1))
self.assertFalse(user.is_manager(org2))

def test_is_member(self):
user = self._create_user(username='organizations_pk')
org1 = self._create_org(name='org1')
org2 = self._create_org(name='org2')
self.assertFalse(user.is_member(org1))
self.assertFalse(user.is_member(org2))
OrganizationUser.objects.create(user=user, organization=org1)
self.assertTrue(user.is_member(org1))
self.assertFalse(user.is_member(org2))

def test_organization_repr(self):
org = self._create_org(name='org1', is_active=False)
self.assertIn('disabled', str(org))
Expand Down
5 changes: 4 additions & 1 deletion openwisp_users/tests/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,10 @@ def _additional_params_add(self):

class TestMultitenantAdminMixin(object):
def setUp(self):
User.objects.create_superuser(
user = User.objects.create_superuser(
username='admin', password='tester', email='admin@admin.com'
)
user.organizations_dict # force caching

def _login(self, username='admin', password='tester'):
self.client.login(username=username, password=password)
Expand Down Expand Up @@ -59,6 +60,7 @@ def _create_operator(self, organizations=[], **kwargs):
operator.user_permissions.add(*self.get_operator_permissions())
for organization in organizations:
OrganizationUser.objects.create(user=operator, organization=organization)
operator.organizations_dict # force caching
return operator

def _test_multitenant_admin(self, url, visible, hidden, select_widget=False):
Expand Down Expand Up @@ -150,6 +152,7 @@ def _create_operator(self):
)
user_permissions = Permission.objects.filter(codename__endswith='user')
operator.user_permissions.add(*user_permissions)
operator.organizations_dict # force caching
return operator

def _get_org(self, org_name='test org'):
Expand Down