from collections import OrderedDict
from uuid import uuid4
from django.contrib.auth.models import Group as DjangoGroup
from django.db import models
from django.db.models.signals import post_save, post_delete
from django.conf import settings as django_settings
from django.contrib.auth.models import User as DefaultUser
DjangoUser = getattr(django_settings, 'AUTH_USER_MODEL', DefaultUser)
from jsonfield import JSONField
from mptt.models import MPTTModel, TreeForeignKey
from slugify import slugify
import exceptions
from .perms import assign_object_to_member
[docs]class Member(models.Model):
"""Member represents a person that can be related to one or more groups.
:Parameters:
- `first_name`: member's first name (required)
- `last_name`: member's last name (required)
- `username`: member's username, used as base for django auth's integration
- `email`: member's email
- `django_user`: django auth related User (related name: `groups_manager_member`)
- `django_auth_sync`: synchronize or not the member (if setting DJANGO_AUTH_SYNC is True)
(default: True)
"""
first_name = models.CharField(max_length=255)
last_name = models.CharField(max_length=255)
username = models.CharField(max_length=255, default='', blank=True)
email = models.EmailField(max_length=255, default='', blank=True)
django_user = models.ForeignKey(DjangoUser, null=True, blank=True, on_delete=models.SET_NULL,
related_name='groups_manager_member')
django_auth_sync = models.BooleanField(default=True, blank=True)
class Meta:
ordering = ('last_name', 'first_name')
def __unicode__(self):
return self.full_name
def save(self, *args, **kwargs):
if not self.username:
self.username = slugify(self.full_name, to_lower=True, separator="_")
super(Member, self).save(*args, **kwargs)
@property
def full_name(self):
return '%s %s' % (self.first_name, self.last_name)
[docs] def has_perm(self, perm, obj=None):
"""Bind of django user's ``has_perm`` method (use it as a shortcut). """
try:
return self.django_user.has_perm(perm, obj)
except AttributeError:
raise exceptions.MemberDjangoUserSyncError(
"Can't check for perm %s since member %s has no django_user" % (perm, self))
[docs] def has_perms(self, perm_list, obj=None):
"""Bind of django user's ``has_perms`` method (use it as a shortcut). """
try:
return self.django_user.has_perms(perm_list, obj)
except AttributeError:
raise exceptions.MemberDjangoUserSyncError(
"Can't check for perms %s since member %s has no django_user" % (perm_list, self))
[docs] def assign_object(self, group, obj, **kwargs):
"""Assign an object to the member honours the group relation.
:Parameters:
- `group`: the member group (required)
- `obj`: the object to assign (required)
:Kwargs:
- `custom_permissions`: a full or partial redefinition of PERMISSIONS setting.
.. note::
This method needs django-guardian.
"""
group_member = GroupMember.objects.get(group=group, member=self)
return assign_object_to_member(group_member, obj, **kwargs)
def member_save(sender, instance, created, *args, **kwargs):
'''
Add User to Django Users
'''
from settings import GROUPS_MANAGER
if GROUPS_MANAGER['AUTH_MODELS_SYNC'] and instance.django_auth_sync:
prefix = GROUPS_MANAGER['USER_USERNAME_PREFIX']
suffix = GROUPS_MANAGER['USER_USERNAME_SUFFIX']
if suffix == '_$$random':
suffix = '_%s' % str(uuid4())[:8]
username = '%s%s%s' % (prefix, instance.username, suffix)
if not instance.django_user:
UserModel = instance._meta.get_field('django_user').rel.to
django_user = UserModel(
first_name=instance.first_name,
last_name=instance.last_name,
username=username
)
if instance.email:
django_user.email = instance.email
django_user.save()
instance.django_user = django_user
instance.save()
else:
if (instance.django_user.username != username and
suffix != '_$$random') \
or (instance.django_user.username[:-len(suffix)] != username[:-len(suffix)] and
GROUPS_MANAGER['USER_USERNAME_SUFFIX'] == '_$$random'):
instance.django_user.username = username
instance.django_user.first_name = instance.first_name
instance.django_user.last_name = instance.last_name
instance.django_user.email = instance.email
instance.django_user.save()
def member_delete(sender, instance, *args, **kwargs):
'''
Remove the related Django Group
'''
from settings import GROUPS_MANAGER
if GROUPS_MANAGER['AUTH_MODELS_SYNC'] and instance.django_auth_sync:
if instance.django_user:
django_user = instance.django_user
django_user.delete()
post_save.connect(member_save, sender=Member)
post_delete.connect(member_delete, sender=Member)
[docs]class GroupType(models.Model):
"""This model represents the kind of the group. One group could have only one type.
This objects could describe the group's nature (i.e. Organization, Division, ecc).
:Parameters:
- `label`: (required)
- `codename`: unique codename; if not set, it's autogenerated by slugifying the label
(lower case)
"""
label = models.CharField(max_length=255)
codename = models.SlugField(unique=True, blank=True, max_length=255)
class Meta:
ordering = ('label', )
def __unicode__(self):
return self.label
def save(self, *args, **kwargs):
if not self.codename:
self.codename = slugify(self.label, to_lower=True)
super(GroupType, self).save(*args, **kwargs)
[docs]class GroupEntity(models.Model):
"""This model represents the entities of a group. One group could have more than one entity.
This objects could describe the group's properties (i.e. Administrators, Users, ecc).
:Parameters:
- `label`: (required)
- `codename`: unique codename; if not set, it's autogenerated by slugifying the label
(lower case)
"""
label = models.CharField(max_length=255)
codename = models.SlugField(unique=True, blank=True, max_length=255)
class Meta:
ordering = ('label', )
def __unicode__(self):
return self.label
def save(self, *args, **kwargs):
if not self.codename:
self.codename = slugify(self.label, to_lower=True)
super(GroupEntity, self).save(*args, **kwargs)
[docs]class Group(MPTTModel):
"""This model represents the group. Each group could have a parent group (via the `parent`
attribute).
:Parameters:
- `name`: (required)
- `codename`: NON unique codename; if not set, it's autogenerated by slugifying the label
(lower case)
- `description`: text field
- `comment`: text field
- `full_name`: auto generated full name starting from tree root
- `properties`: jsonfield properties
- `group_members`: m2m to Member, through GroupMember model (related name: `groups`)
- `group_type`: foreign key to GroupType (related name: `groups`)
- `group_entities`: m2m to GroupEntity (related name: `groups`)
- `django_group`: django auth related group
- `django_auth_sync`: synchronize or not the group (if setting DJANGO_AUTH_SYNC is True)
(default: True)
- `level`: ``django-mptt`` level attribute
(default: True)
.. note::
If you want to add a custom manager for a sublcass of Group, use django-mppt
mptt.models.TreeManager.
"""
name = models.CharField(max_length=255, unique=True)
codename = models.SlugField(blank=True, max_length=255)
description = models.TextField(default='', blank=True)
comment = models.TextField(default='', blank=True)
parent = TreeForeignKey('self', null=True, blank=True, related_name='subgroups')
full_name = models.CharField(max_length=255, default='', blank=True)
properties = JSONField(default={}, blank=True,
load_kwargs={'object_pairs_hook': OrderedDict})
group_members = models.ManyToManyField(Member, through='GroupMember', related_name='groups')
group_type = models.ForeignKey(GroupType, null=True, blank=True, on_delete=models.SET_NULL,
related_name='groups')
group_entities = models.ManyToManyField(GroupEntity, null=True, blank=True,
related_name='groups')
django_group = models.ForeignKey(DjangoGroup, null=True, blank=True, on_delete=models.SET_NULL)
django_auth_sync = models.BooleanField(default=True, blank=True)
class Meta:
ordering = ('name', )
class MPTTMeta:
level_attr = 'level'
order_insertion_by = ['name', ]
def __unicode__(self):
return '%s' % self.name
def save(self, *args, **kwargs):
self.full_name = self._get_full_name()[:255]
if not self.codename:
self.codename = slugify(self.name, to_lower=True)
super(Group, self).save(*args, **kwargs)
def _get_full_name(self):
if self.parent:
return '%s - %s' % (self.parent._get_full_name(), self.name)
return self.name
[docs] def get_members(self, subgroups=False):
"""Return group members.
:Parameters:
- `subgroups`: return also descendants members (default: `False`)
"""
members = list(self.group_members.all())
if subgroups:
for subgroup in self.subgroups.all():
members += subgroup.members
members = list(set(members))
return members
@property
[docs] def members(self):
"""Return group members. """
return self.get_members(True)
@property
[docs] def users(self):
"""Return group django users. """
users = []
for member in self.members:
if member.django_user:
users.append(member.django_user)
return users
[docs] def get_entities(self, subgroups=False):
"""Return group entities.
:Parameters:
- `subgroups`: return also descendants entities (default: `False`)
"""
entities = list(self.group_entities.all())
if subgroups:
for subgroup in self.subgroups.all():
entities += subgroup.entities
entities = list(set(entities))
return entities
@property
[docs] def entities(self):
"""Return group entities."""
return self.get_entities(True)
[docs] def add_member(self, member, roles=[]):
"""Add a member to the group.
:Parameters:
- `member`: member (required)
- `roles`: list of roles. Each role could be a role id, a role label or codename,
a role instance (optional, default: ``[]``)
"""
if not self.id:
raise exceptions.GroupNotSavedError(
"You must save the group before to create a relation with members")
if not member.id:
raise exceptions.MemberNotSavedError(
"You must save the member before to create a relation with groups")
group_member = GroupMember(member=member, group=self)
group_member.save()
if roles:
for role in roles:
if isinstance(role, GroupMemberRole):
group_member.roles.add(role)
elif isinstance(role, int):
role_obj = GroupMemberRole.objects.get(id=role)
group_member.roles.add(role_obj)
else:
try:
role_obj = GroupMemberRole.objects.get(models.Q(label=role) |
models.Q(codename=role))
group_member.roles.add(role_obj)
except Exception as e:
raise exceptions.GetRoleError(e)
return group_member
[docs] def assign_object(self, obj, **kwargs):
"""Assign an object to the group.
:Parameters:
- `obj`: the object to assign (required)
:Kwargs:
- `custom_permissions`: a full or partial redefinition of PERMISSIONS setting.
.. note::
This method needs django-guardian.
.. todo::
Create a method that allows to assign_objects directly to group.
"""
raise NotImplementedError("This method has not yet implemented")
def group_save(sender, instance, created, *args, **kwargs):
'''
Add Group to Django Groups
'''
from settings import GROUPS_MANAGER
if GROUPS_MANAGER['AUTH_MODELS_SYNC'] and instance.django_auth_sync:
# create a name compatible with django group name limit of 80 chars
prefix = GROUPS_MANAGER['GROUP_NAME_PREFIX']
suffix = GROUPS_MANAGER['GROUP_NAME_SUFFIX']
if suffix == '_$$random':
suffix = '_%s' % str(uuid4())[:8]
parent_name = ''
if instance.parent:
parent_name = '%s-' % instance.parent.name
name = '%s%s%s%s' % (prefix, parent_name, instance.name, suffix)
if not instance.django_group:
django_group = DjangoGroup(name=name)
django_group.save()
instance.django_group = django_group
instance.save()
elif (instance.django_group.name != name and
GROUPS_MANAGER['GROUP_NAME_SUFFIX'] != '_$$random') \
or (instance.django_group.name[:-len(suffix)] != name[:-len(suffix)] and
GROUPS_MANAGER['GROUP_NAME_SUFFIX'] == '_$$random'):
instance.django_group.name = name
instance.django_group.save()
def group_delete(sender, instance, *args, **kwargs):
'''
Remove the related Django Group
'''
from settings import GROUPS_MANAGER
if GROUPS_MANAGER['AUTH_MODELS_SYNC'] and instance.django_auth_sync:
if instance.django_group:
django_group = instance.django_group
django_group.delete()
post_save.connect(group_save, sender=Group)
post_delete.connect(group_delete, sender=Group)
[docs]class GroupMemberRole(models.Model):
"""This model represents the role of a user in a relation with a group
(i.e. Administrator, User, ecc).
:Parameters:
- `label`: (required)
- `codename`: unique codename; if not set, it's autogenerated by slugifying the label
(lower case)
"""
label = models.CharField(max_length=255)
codename = models.SlugField(unique=True, blank=True, max_length=255)
class Meta:
ordering = ('label', )
def __unicode__(self):
return self.label
def save(self, *args, **kwargs):
if not self.codename:
self.codename = slugify(self.label, to_lower=True)
super(GroupMemberRole, self).save(*args, **kwargs)
[docs]class GroupMember(models.Model):
"""This model represents the intermediate model of the relation between a Member and a Group.
This middleware can have one or more GroupMemberRole associated.
A member could be in a group only once (group - member pair is unique).
:Parameters:
- `group`: Group (required)
- `member`: Member (required)
- `roles`: m2m to GroupMemberRole
"""
group = models.ForeignKey(Group, related_name='group_membership')
member = models.ForeignKey(Member, related_name='group_membership')
roles = models.ManyToManyField(GroupMemberRole, null=True, blank=True)
class Meta:
ordering = ('group', 'member')
unique_together = (('group', 'member'), )
def __unicode__(self):
return '%s - %s' % (self.group.name, self.member.full_name)
[docs] def assign_object(self, obj, **kwargs):
"""Assign an object to the member.
:Parameters:
- `obj`: the object to assign (required)
:Kwargs:
- `custom_permissions`: a full or partial redefinition of PERMISSIONS setting.
.. note::
This method needs django-guardian.
"""
return assign_object_to_member(self, obj, **kwargs)
def group_member_save(sender, instance, created, *args, **kwargs):
'''
Add Django User to Django Groups
'''
from settings import GROUPS_MANAGER
if GROUPS_MANAGER['AUTH_MODELS_SYNC']:
django_user = instance.member.django_user
django_group = instance.group.django_group
if django_user and django_group:
if django_group not in django_user.groups.all():
django_user.groups.add(django_group)
def group_member_delete(sender, instance, *args, **kwargs):
'''
Remove Django User from Django Groups
'''
from settings import GROUPS_MANAGER
if GROUPS_MANAGER['AUTH_MODELS_SYNC']:
django_user = instance.member.django_user
django_group = instance.group.django_group
if django_user and django_group:
if django_group in django_user.groups.all():
django_user.groups.remove(django_group)
post_save.connect(group_member_save, sender=GroupMember)
post_delete.connect(group_member_delete, sender=GroupMember)