Added owner model to admin page and added test cases

This commit is contained in:
eeintech 2021-01-13 11:38:37 -05:00
parent 0a0a47a5e4
commit 28fb1b5fab
11 changed files with 1125 additions and 929 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -20,6 +20,6 @@ class Migration(migrations.Migration):
migrations.AddField( migrations.AddField(
model_name='stocklocation', model_name='stocklocation',
name='owner', name='owner',
field=models.ForeignKey(blank=True, help_text='Select Owner', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='stock_locations', to='users.Owner'), field=models.ForeignKey(blank=True, help_text='Select Owner', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='stock_locations', to='users.Owner'),
), ),
] ]

View File

@ -49,7 +49,7 @@ class StockLocation(InvenTreeTree):
Stock locations can be heirarchical as required Stock locations can be heirarchical as required
""" """
owner = models.ForeignKey(Owner, on_delete=models.CASCADE, blank=True, null=True, owner = models.ForeignKey(Owner, on_delete=models.SET_NULL, blank=True, null=True,
help_text='Select Owner', help_text='Select Owner',
related_name='stock_locations') related_name='stock_locations')

View File

@ -21,7 +21,7 @@ InvenTree | {% trans "Stock Item" %} - {{ item }}
{% if not user in owners and not user.is_superuser %} {% if not user in owners and not user.is_superuser %}
<div class='alert alert-block alert-info'> <div class='alert alert-block alert-info'>
{% trans "You are not the owner of this item. This stock item cannot be edited." %}<br> {% trans "You are not in the list of owners of this item. This stock item cannot be edited." %}<br>
</div> </div>
{% endif %} {% endif %}
{% endif %} {% endif %}

View File

@ -11,7 +11,8 @@ from django.views.generic import DetailView, ListView, UpdateView
from django.forms.models import model_to_dict from django.forms.models import model_to_dict
from django.forms import HiddenInput from django.forms import HiddenInput
from django.urls import reverse from django.urls import reverse
from django.contrib.auth.models import Group, User from django.contrib.auth import get_user_model
from django.contrib.auth.models import Group
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
@ -145,28 +146,53 @@ class StockLocationEdit(AjaxUpdateView):
# Is ownership control enabled? # Is ownership control enabled?
stock_ownership_control = InvenTreeSetting.get_setting('STOCK_OWNERSHIP_CONTROL') stock_ownership_control = InvenTreeSetting.get_setting('STOCK_OWNERSHIP_CONTROL')
owner = location.owner
if not stock_ownership_control: if not stock_ownership_control:
# Hide owner field
form.fields['owner'].widget = HiddenInput() form.fields['owner'].widget = HiddenInput()
else: else:
form.fields['owner'].initial = owner # Get location's owner
if location.parent: location_owner = location.owner
form.fields['owner'].initial = location.parent.owner
if not self.request.user.is_superuser: if location_owner:
form.fields['owner'].disabled = True if location.parent:
try:
# If location has parent and owner: automatically select parent's owner
parent_owner = location.parent.owner
form.fields['owner'].initial = parent_owner
except AttributeError:
pass
else:
# If current owner exists: automatically select it
form.fields['owner'].initial = location_owner
# Update queryset or disable field (only if not admin)
if not self.request.user.is_superuser:
if type(location_owner.owner) is Group:
user_as_owner = Owner.get_owner(self.request.user)
queryset = location_owner.get_related_owners(include_group=True)
if user_as_owner not in queryset:
# Only owners or admin can change current owner
form.fields['owner'].disabled = True
else:
form.fields['owner'].queryset = queryset
return form return form
def save(self, object, form, **kwargs): def save(self, object, form, **kwargs):
""" If location has children and ownership control is enabled: """ If location has children and ownership control is enabled:
- update all children's owners with location's owner - update owner of all children location of this location
- update owner for all stock items at this location
""" """
self.object = form.save() self.object = form.save()
# Is ownership control enabled?
stock_ownership_control = InvenTreeSetting.get_setting('STOCK_OWNERSHIP_CONTROL') stock_ownership_control = InvenTreeSetting.get_setting('STOCK_OWNERSHIP_CONTROL')
if stock_ownership_control: if stock_ownership_control:
# Get authorized users
authorized_owners = self.object.owner.get_related_owners() authorized_owners = self.object.owner.get_related_owners()
# Update children locations # Update children locations
@ -1414,6 +1440,7 @@ class StockItemEdit(AjaxUpdateView):
# Is ownership control enabled? # Is ownership control enabled?
stock_ownership_control = InvenTreeSetting.get_setting('STOCK_OWNERSHIP_CONTROL') stock_ownership_control = InvenTreeSetting.get_setting('STOCK_OWNERSHIP_CONTROL')
if not stock_ownership_control: if not stock_ownership_control:
form.fields['owner'].widget = HiddenInput() form.fields['owner'].widget = HiddenInput()
else: else:
@ -1426,14 +1453,18 @@ class StockItemEdit(AjaxUpdateView):
if location_owner: if location_owner:
form.fields['owner'].initial = location_owner form.fields['owner'].initial = location_owner
# Check location owner type and filter # Check location's owner type and filter potential owners
if type(location_owner.owner) is Group: if type(location_owner.owner) is Group:
user_as_owner = Owner.get_owner(self.request.user) user_as_owner = Owner.get_owner(self.request.user)
queryset = location_owner.get_related_owners(include_group=True) queryset = location_owner.get_related_owners(include_group=True)
if user_as_owner in queryset: if user_as_owner in queryset:
form.fields['owner'].initial = user_as_owner form.fields['owner'].initial = user_as_owner
form.fields['owner'].queryset = queryset form.fields['owner'].queryset = queryset
elif type(location_owner.owner) is User:
elif type(location_owner.owner) is get_user_model():
# If location's owner is a user: automatically set owner field and disable it
form.fields['owner'].disabled = True form.fields['owner'].disabled = True
form.fields['owner'].initial = location_owner form.fields['owner'].initial = location_owner
@ -1446,14 +1477,18 @@ class StockItemEdit(AjaxUpdateView):
if item_owner: if item_owner:
form.fields['owner'].initial = item_owner form.fields['owner'].initial = item_owner
# Check location owner type and filter # Check item's owner type and filter potential owners
if type(item_owner.owner) is Group: if type(item_owner.owner) is Group:
user_as_owner = Owner.get_owner(self.request.user) user_as_owner = Owner.get_owner(self.request.user)
queryset = item_owner.get_related_owners(include_group=True) queryset = item_owner.get_related_owners(include_group=True)
if user_as_owner in queryset: if user_as_owner in queryset:
form.fields['owner'].initial = user_as_owner form.fields['owner'].initial = user_as_owner
form.fields['owner'].queryset = queryset form.fields['owner'].queryset = queryset
elif type(item_owner.owner) is User:
elif type(item_owner.owner) is get_user_model():
# If item's owner is a user: automatically set owner field and disable it
form.fields['owner'].disabled = True form.fields['owner'].disabled = True
form.fields['owner'].initial = item_owner form.fields['owner'].initial = item_owner
@ -1533,10 +1568,12 @@ class StockLocationCreate(AjaxCreateView):
# Is ownership control enabled? # Is ownership control enabled?
stock_ownership_control = InvenTreeSetting.get_setting('STOCK_OWNERSHIP_CONTROL') stock_ownership_control = InvenTreeSetting.get_setting('STOCK_OWNERSHIP_CONTROL')
if not stock_ownership_control: if not stock_ownership_control:
# Hide owner field
form.fields['owner'].widget = HiddenInput() form.fields['owner'].widget = HiddenInput()
else: else:
# If user did not selected owner, automatically match to parent's owner # If user did not selected owner: automatically match to parent's owner
if not form['owner'].data: if not form['owner'].data:
try: try:
parent_id = form['parent'].value() parent_id = form['parent'].value()
@ -1806,15 +1843,18 @@ class StockItemCreate(AjaxCreateView):
location_owner = None location_owner = None
if location_owner: if location_owner:
# Check location owner type and filter # Check location's owner type and filter potential owners
if type(location_owner.owner) is Group: if type(location_owner.owner) is Group:
user_as_owner = Owner.get_owner(self.request.user) user_as_owner = Owner.get_owner(self.request.user)
queryset = location_owner.get_related_owners() queryset = location_owner.get_related_owners()
if user_as_owner in queryset: if user_as_owner in queryset:
form.fields['owner'].initial = user_as_owner form.fields['owner'].initial = user_as_owner
form.fields['owner'].queryset = queryset form.fields['owner'].queryset = queryset
elif type(location_owner.owner) is User:
elif type(location_owner.owner) is get_user_model():
# If location's owner is a user: automatically set owner field and disable it
form.fields['owner'].disabled = True form.fields['owner'].disabled = True
form.fields['owner'].initial = location_owner form.fields['owner'].initial = location_owner

View File

@ -11,7 +11,7 @@ from django.contrib.auth.models import Group
from django.contrib.auth.admin import UserAdmin from django.contrib.auth.admin import UserAdmin
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from users.models import RuleSet from users.models import RuleSet, Owner
User = get_user_model() User = get_user_model()
@ -206,8 +206,17 @@ class InvenTreeUserAdmin(UserAdmin):
) )
class OwnerAdmin(admin.ModelAdmin):
"""
Custom admin interface for the Owner model
"""
pass
admin.site.unregister(Group) admin.site.unregister(Group)
admin.site.register(Group, RoleGroupAdmin) admin.site.register(Group, RoleGroupAdmin)
admin.site.unregister(User) admin.site.unregister(User)
admin.site.register(User, InvenTreeUserAdmin) admin.site.register(User, InvenTreeUserAdmin)
admin.site.register(Owner, OwnerAdmin)

View File

@ -39,6 +39,14 @@ class UsersConfig(AppConfig):
def update_owners(self): def update_owners(self):
from users.models import create_owner from django.contrib.auth import get_user_model
from django.contrib.auth.models import Group
from users.models import Owner
create_owner(full_update=True) # Create group owners
for group in Group.objects.all():
Owner.create(group)
# Create user owners
for user in get_user_model().objects.all():
Owner.create(user)

View File

@ -1,6 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from django.contrib.auth.models import User, Group, Permission from django.contrib.auth import get_user_model
from django.contrib.auth.models import Group, Permission
from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.db.models import UniqueConstraint, Q from django.db.models import UniqueConstraint, Q
@ -9,7 +10,7 @@ from django.db import models
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.dispatch import receiver from django.dispatch import receiver
from django.db.models.signals import post_save from django.db.models.signals import post_save, post_delete
class RuleSet(models.Model): class RuleSet(models.Model):
@ -393,32 +394,42 @@ def check_user_role(user, role, permission):
class Owner(models.Model): class Owner(models.Model):
""" """
An owner is either a group or user. The Owner class is a proxy for a Group or User instance.
Owner can be associated to any InvenTree model (part, stock, etc.) Owner can be associated to any InvenTree model (part, stock, build, etc.)
owner_type: Model type (Group or User)
owner_id: Group or User instance primary key
owner: Returns the Group or User instance combining the owner_type and owner_id fields
""" """
class Meta: class Meta:
# Ensure all owners are unique
constraints = [ constraints = [
UniqueConstraint(fields=['owner_type', 'owner_id'], UniqueConstraint(fields=['owner_type', 'owner_id'],
name='unique_owner') name='unique_owner')
] ]
owner_type = models.ForeignKey(ContentType, on_delete=models.CASCADE, null=True, blank=True) owner_type = models.ForeignKey(ContentType, on_delete=models.CASCADE, null=True, blank=True)
owner_id = models.PositiveIntegerField(null=True, blank=True) owner_id = models.PositiveIntegerField(null=True, blank=True)
owner = GenericForeignKey('owner_type', 'owner_id') owner = GenericForeignKey('owner_type', 'owner_id')
def __str__(self): def __str__(self):
""" Defines the owner string representation """
return f'{self.owner} ({self.owner_type.name})' return f'{self.owner} ({self.owner_type.name})'
@classmethod @classmethod
def create(cls, owner): def create(cls, obj):
""" Check if owner exist then create new owner entry """
existing_owner = cls.get_owner(owner) # Check for existing owner
existing_owner = cls.get_owner(obj)
if not existing_owner: if not existing_owner:
# Create new owner # Create new owner
try: try:
return cls.objects.create(owner=owner) return cls.objects.create(owner=obj)
except IntegrityError: except IntegrityError:
return None return None
@ -426,16 +437,18 @@ class Owner(models.Model):
@classmethod @classmethod
def get_owner(cls, user_or_group): def get_owner(cls, user_or_group):
""" Get owner instance for a group or user """
user_model = get_user_model()
owner = None owner = None
content_type_id = 0 content_type_id = 0
content_type_id_list = [ContentType.objects.get_for_model(Group).id, content_type_id_list = [ContentType.objects.get_for_model(Group).id,
ContentType.objects.get_for_model(User).id] ContentType.objects.get_for_model(user_model).id]
# If instance type is obvious: set content type # If instance type is obvious: set content type
if type(user_or_group) is Group: if type(user_or_group) is Group:
content_type_id = content_type_id_list[0] content_type_id = content_type_id_list[0]
elif type(user_or_group) is User: elif type(user_or_group) is get_user_model():
content_type_id = content_type_id_list[1] content_type_id = content_type_id_list[1]
if content_type_id: if content_type_id:
@ -462,8 +475,8 @@ class Owner(models.Model):
# Check whether user_or_group is a User instance # Check whether user_or_group is a User instance
try: try:
user = User.objects.get(pk=user_or_group.id) user = user_model.objects.get(pk=user_or_group.id)
except User.DoesNotExist: except user_model.DoesNotExist:
user = None user = None
if user: if user:
@ -478,45 +491,50 @@ class Owner(models.Model):
return owner return owner
def get_related_owners(self, include_group=False): def get_related_owners(self, include_group=False):
"""
Get all owners "related" to an owner.
This method is useful to retrieve all "user-type" owners linked to a "group-type" owner
"""
owner_users = None user_model = get_user_model()
related_owners = None
if type(self.owner) is Group: if type(self.owner) is Group:
users = User.objects.filter(groups__name=self.owner.name) users = user_model.objects.filter(groups__name=self.owner.name)
if include_group: if include_group:
query = Q(owner_id__in=users, owner_type=ContentType.objects.get_for_model(User).id) | \ # Include "group-type" owner in the query
query = Q(owner_id__in=users, owner_type=ContentType.objects.get_for_model(user_model).id) | \
Q(owner_id=self.owner.id, owner_type=ContentType.objects.get_for_model(Group).id) Q(owner_id=self.owner.id, owner_type=ContentType.objects.get_for_model(Group).id)
else: else:
query = Q(owner_id__in=users, owner_type=ContentType.objects.get_for_model(User).id) query = Q(owner_id__in=users, owner_type=ContentType.objects.get_for_model(user_model).id)
owner_users = Owner.objects.filter(query) related_owners = Owner.objects.filter(query)
elif type(self.owner) is User: elif type(self.owner) is user_model:
owner_users = [self] related_owners = [self]
return owner_users return related_owners
def create_owner(full_update=False, owner=None): @receiver(post_save, sender=Group, dispatch_uid='create_owner')
""" Create all owners """ @receiver(post_save, sender=get_user_model(), dispatch_uid='create_owner')
def create_owner(sender, instance, **kwargs):
if full_update: """
# Create group owners Callback function to create a new owner instance
for group in Group.objects.all(): after either a new group or user instance is saved.
Owner.create(owner=group) """
# Create user owners Owner.create(obj=instance)
for user in User.objects.all():
Owner.create(owner=user)
else:
if owner:
Owner.create(owner=owner)
@receiver(post_save, sender=Group, dispatch_uid='create_missing_owner') @receiver(post_delete, sender=Group, dispatch_uid='delete_owner')
@receiver(post_save, sender=User, dispatch_uid='create_missing_owner') @receiver(post_delete, sender=get_user_model(), dispatch_uid='delete_owner')
def create_missing_owner(sender, instance, created, **kwargs): def delete_owner(sender, instance, **kwargs):
""" Create owner instance after either user or group object is saved. """ """
Callback function to delete an owner instance
after either a new group or user instance is deleted.
"""
create_owner(owner=instance) owner = Owner.get_owner(instance)
owner.delete()

View File

@ -3,9 +3,10 @@ from __future__ import unicode_literals
from django.test import TestCase from django.test import TestCase
from django.apps import apps from django.apps import apps
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Group from django.contrib.auth.models import Group
from users.models import RuleSet from users.models import RuleSet, Owner
class RuleSetModelTest(TestCase): class RuleSetModelTest(TestCase):
@ -157,3 +158,48 @@ class RuleSetModelTest(TestCase):
# There should now not be any permissions assigned to this group # There should now not be any permissions assigned to this group
self.assertEqual(group.permissions.count(), 0) self.assertEqual(group.permissions.count(), 0)
class OwnerModelTest(TestCase):
"""
Some simplistic tests to ensure the Owner model is setup correctly.
"""
def setUp(self):
""" Add users and groups """
# Create a new user
self.user = get_user_model().objects.create_user(
username='john',
email='john@email.com',
password='custom123',
)
# Put the user into a new group
self.group = Group.objects.create(name='new_group')
self.user.groups.add(self.group)
def test_owner(self):
# Check that owner was created for user
user_as_owner = Owner.get_owner(self.user)
self.assertEqual(type(user_as_owner), Owner)
# Check that owner was created for group
group_as_owner = Owner.get_owner(self.group)
self.assertEqual(type(group_as_owner), Owner)
# Get related owners (user + group)
related_owners = group_as_owner.get_related_owners(include_group=True)
self.assertTrue(user_as_owner in related_owners)
self.assertTrue(group_as_owner in related_owners)
# Delete user and verify owner was deleted too
self.user.delete()
user_as_owner = Owner.get_owner(self.user)
self.assertEqual(user_as_owner, None)
# Delete group and verify owner was deleted too
self.group.delete()
group_as_owner = Owner.get_owner(self.group)
self.assertEqual(group_as_owner, None)