Merge pull request #9 from jedie/develop

Develop
pull/10/head
Jens Diemer 2020-10-24 19:29:21 +02:00 zatwierdzone przez GitHub
commit e2bcc7ace6
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
15 zmienionych plików z 301 dodań i 7 usunięć

Wyświetl plik

@ -44,6 +44,8 @@ There exists two kind of installation/usage:
* local virtualenv (without docker)
* docker-compose
see below
=== prepare
{{{
@ -147,6 +149,28 @@ Notes:
----
== Multi user usage
PyInventory supports multiple users. The idea:
* Every normal user sees only his own created database entries
* All users used the Django admin
Note: All created Tags are shared for all existing users!
So setup a normal user:
* Set "Staff status"
* Unset "Superuser status"
* Add user to "normal_user" group
* Don't add any additional permissions
e.g.:
{{https://raw.githubusercontent.com/jedie/jedie.github.io/master/screenshots/PyInventory/PyInventory normal user example.png|normal user example}}
== Backwards-incompatible changes
Nothing, yet ;)

Wyświetl plik

@ -79,6 +79,8 @@ There exists two kind of installation/usage:
* docker-compose
see below
prepare
=======
@ -191,6 +193,34 @@ Screenshots
----
----------------
Multi user usage
----------------
PyInventory supports multiple users. The idea:
* Every normal user sees only his own created database entries
* All users used the Django admin
Note: All created Tags are shared for all existing users!
So setup a normal user:
* Set "Staff status"
* Unset "Superuser status"
* Add user to "normal_user" group
* Don't add any additional permissions
e.g.:
|normal user example|
.. |normal user example| image:: https://raw.githubusercontent.com/jedie/jedie.github.io/master/screenshots/PyInventory/PyInventory normal user example.png
------------------------------
Backwards-incompatible changes
------------------------------
@ -246,4 +276,4 @@ donation
------------
``Note: this file is generated from README.creole 2020-10-24 16:39:24 with "python-creole"``
``Note: this file is generated from README.creole 2020-10-24 17:15:43 with "python-creole"``

Wyświetl plik

@ -2,6 +2,10 @@ from reversion_compare.admin import CompareVersionAdmin
class BaseUserAdmin(CompareVersionAdmin):
def get_changelist(self, request, **kwargs):
self.user = request.user
return super().get_changelist(request, **kwargs)
def save_model(self, request, obj, form, change):
if obj.user_id is None:
obj.user = request.user

Wyświetl plik

@ -1,11 +1,14 @@
import tagulous
from adminsortable2.admin import SortableInlineAdminMixin
from django.contrib import admin
from django.contrib.admin.views.main import ChangeList
from django.template.loader import render_to_string
from django.utils.translation import ugettext_lazy as _
from import_export.admin import ImportExportMixin
from import_export.resources import ModelResource
from inventory.admin.base import BaseUserAdmin
from inventory.forms import ItemModelModelForm
from inventory.models import ItemLinkModel, ItemModel
@ -13,6 +16,15 @@ class ItemLinkModelInline(SortableInlineAdminMixin, admin.TabularInline):
model = ItemLinkModel
extra = 1
def get_queryset(self, request):
qs = super().get_queryset(request)
if not request.user.is_superuser:
# Display only own created entries
qs = qs.filter(user=request.user)
return qs
class ItemModelResource(ModelResource):
@ -20,17 +32,43 @@ class ItemModelResource(ModelResource):
model = ItemModel
class ItemModelChangeList(ChangeList):
def get_queryset(self, request):
"""
List always the base instances
"""
qs = super().get_queryset(request)
qs = qs.filter(parent__isnull=True)
return qs
@admin.register(ItemModel)
class ItemModelAdmin(ImportExportMixin, BaseUserAdmin):
form = ItemModelModelForm
def column_item(self, obj):
qs = ItemModel.objects.filter(user=self.user)
qs = qs.filter(parent=obj).sort()
context = {
'base_item': obj,
'sub_items': qs
}
return render_to_string(
template_name='admin/inventory/item/column_item.html',
context=context,
)
column_item.short_description = _('ItemModel.verbose_name_plural')
date_hierarchy = 'create_dt'
list_display = (
'kind', 'producer',
'name',
'parent', 'location',
'column_item',
'location',
'received_date', 'update_dt'
)
ordering = ('kind', 'producer', 'name')
list_display_links = ('name',)
list_display_links = None
list_filter = ('kind', 'location', 'producer', 'tags')
search_fields = ('name', 'description')
fieldsets = (
@ -76,5 +114,18 @@ class ItemModelAdmin(ImportExportMixin, BaseUserAdmin):
readonly_fields = ('id', 'create_dt', 'update_dt', 'user')
inlines = (ItemLinkModelInline,)
def get_changelist(self, request, **kwargs):
self.user = request.user
return ItemModelChangeList
def get_queryset(self, request):
qs = super().get_queryset(request)
if not request.user.is_superuser:
# Display only own created entries
qs = qs.filter(user=request.user)
return qs
tagulous.admin.enhance(ItemModel, ItemModelAdmin)

Wyświetl plik

@ -3,6 +3,7 @@ from import_export.admin import ImportExportMixin
from import_export.resources import ModelResource
from inventory.admin.base import BaseUserAdmin
from inventory.forms import LocationModelModelForm
from inventory.models import LocationModel
@ -14,4 +15,13 @@ class LocationModelResource(ModelResource):
@admin.register(LocationModel)
class LocationModelAdmin(ImportExportMixin, BaseUserAdmin):
pass
form = LocationModelModelForm
def get_queryset(self, request):
qs = super().get_queryset(request)
if not request.user.is_superuser:
# Display only own created entries
qs = qs.filter(user=request.user)
return qs

Wyświetl plik

@ -1,6 +1,8 @@
from pathlib import Path
from django.core.checks import Error, register
from django.core.checks import Error, Warning, register
from inventory.permissions import get_or_create_normal_user_group, setup_normal_user_permissions
@register()
@ -15,3 +17,25 @@ def inventory_checks(app_configs, **kwargs):
)
)
return errors
@register()
def inventory_user_groups(app_configs, **kwargs):
"""
Setup PyInventory user groups
"""
warnings = []
normal_user_group, created = get_or_create_normal_user_group()
if created:
warnings.append(
Warning(f'User group {normal_user_group} created')
)
updated = setup_normal_user_permissions(normal_user_group)
if updated:
warnings.append(
Warning(f'Update permissions for {normal_user_group}')
)
return warnings

35
inventory/forms.py 100644
Wyświetl plik

@ -0,0 +1,35 @@
from django import forms
from django.core.exceptions import FieldDoesNotExist
from inventory.request_dict import get_request_dict
class BaseUserOnlyModelForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Filter all related fields that has a "user" attribute for the current user
# e.g.:
# The user should only select his own "location" and "items"
user = get_request_dict()['user'] # get current user via threading.local()
for formfield in self.fields.values():
if not hasattr(formfield, 'queryset'):
continue
queryset = formfield.queryset
opts = queryset.model._meta
try:
opts.get_field('user')
except FieldDoesNotExist:
continue
formfield.queryset = queryset.filter(user=user)
class ItemModelModelForm(BaseUserOnlyModelForm):
pass
class LocationModelModelForm(BaseUserOnlyModelForm):
pass

Wyświetl plik

@ -0,0 +1,20 @@
from inventory.request_dict import clear_request_dict, get_request_dict
class RequestDictMiddleware:
"""
Make the "current user" information avaiable everywhere via threading.local()
Access e.g.:
user = get_request_dict()['user']
"""
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
get_request_dict().update(user=request.user)
response = self.get_response(request)
clear_request_dict()
return response

Wyświetl plik

@ -0,0 +1,19 @@
# Generated by Django 2.2.16 on 2020-10-24 16:30
import ckeditor_uploader.fields
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('inventory', '0002_auto_20201017_2211'),
]
operations = [
migrations.AlterField(
model_name='locationmodel',
name='description',
field=ckeditor_uploader.fields.RichTextUploadingField(blank=True, help_text='LocationModel.description.help_text', null=True, verbose_name='LocationModel.description.verbose_name'),
),
]

Wyświetl plik

@ -1,16 +1,24 @@
import tagulous.models
from ckeditor_uploader.fields import RichTextUploadingField
from django.db import models
from django.urls import reverse
from django.utils.translation import ugettext_lazy as _
from inventory.models.base import BaseModel
from inventory.models.links import BaseLink
class ItemQuerySet(models.QuerySet):
def sort(self):
return self.order_by('kind', 'producer', 'name')
class ItemModel(BaseModel):
"""
A Item that can be described and store somewhere ;)
"""
objects = ItemQuerySet.as_manager()
kind = tagulous.models.TagField(
case_sensitive=False,
force_lowercase=False,
@ -117,6 +125,14 @@ class ItemModel(BaseModel):
help_text=_('ItemModel.handed_over_price.help_text')
)
def local_admin_link(self):
url = reverse('admin:inventory_itemmodel_change', args=[self.id])
return url
def verbose_name(self):
parts = [str(part) for part in (self.kind, self.producer, self.name)]
return ' - '.join(part for part in parts if part)
def __str__(self):
if self.parent_id is None:
title = self.name

Wyświetl plik

@ -10,6 +10,7 @@ class LocationModel(BaseModel):
A Storage for items.
"""
description = RichTextUploadingField(
blank=True, null=True,
config_name='LocationModel.description',
verbose_name=_('LocationModel.description.verbose_name'),
help_text=_('LocationModel.description.help_text')

Wyświetl plik

@ -0,0 +1,34 @@
from django.contrib.auth.models import Group, Permission
from django.contrib.contenttypes.models import ContentType
from inventory.models import ItemLinkModel, ItemModel, LocationModel
NORMAL_USER_GROUP_NAME = 'normal user'
def get_permissions(*models):
content_types = []
for model in models:
content_types.append(ContentType.objects.get_for_model(model))
return Permission.objects.filter(content_type__in=content_types)
def get_or_create_normal_user_group():
return Group.objects.get_or_create(name=NORMAL_USER_GROUP_NAME)
def setup_normal_user_permissions(normal_user_group):
"""
Setup PyInventory "normal user" permissions
"""
assert normal_user_group.name == NORMAL_USER_GROUP_NAME
permissions = get_permissions(ItemModel, ItemLinkModel, LocationModel)
existing_permissions = normal_user_group.permissions.all()
if set(permissions) != set(existing_permissions):
normal_user_group.permissions.set(permissions)
return True
return False

Wyświetl plik

@ -0,0 +1,16 @@
import threading
__request_dict = threading.local()
def get_request_dict():
try:
return __request_dict.context
except AttributeError:
__request_dict.context = {}
return __request_dict.context
def clear_request_dict():
__request_dict.context = {}

Wyświetl plik

@ -0,0 +1,8 @@
<strong><a href="{{ base_item.local_admin_link }}">{{ base_item.name }}</a></strong>
{% if sub_items %}
<ul>
{% for item in sub_items %}
<li><a href="{{ item.local_admin_link }}">{{ item.verbose_name }}</a></li>
{% endfor %}
</ul>
{% endif %}

Wyświetl plik

@ -58,10 +58,12 @@ MIDDLEWARE = [
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'inventory.middlewares.RequestDictMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'django.middleware.security.SecurityMiddleware',
'django_tools.middlewares.ThreadLocal.ThreadLocalMiddleware',
]
TEMPLATES = [