kopia lustrzana https://github.com/jedie/PyInventory
commit
e2bcc7ace6
|
@ -44,6 +44,8 @@ There exists two kind of installation/usage:
|
||||||
* local virtualenv (without docker)
|
* local virtualenv (without docker)
|
||||||
* docker-compose
|
* docker-compose
|
||||||
|
|
||||||
|
see below
|
||||||
|
|
||||||
=== prepare
|
=== 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
|
== Backwards-incompatible changes
|
||||||
|
|
||||||
Nothing, yet ;)
|
Nothing, yet ;)
|
||||||
|
|
32
README.rst
32
README.rst
|
@ -79,6 +79,8 @@ There exists two kind of installation/usage:
|
||||||
|
|
||||||
* docker-compose
|
* docker-compose
|
||||||
|
|
||||||
|
see below
|
||||||
|
|
||||||
prepare
|
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
|
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"``
|
|
@ -2,6 +2,10 @@ from reversion_compare.admin import CompareVersionAdmin
|
||||||
|
|
||||||
|
|
||||||
class BaseUserAdmin(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):
|
def save_model(self, request, obj, form, change):
|
||||||
if obj.user_id is None:
|
if obj.user_id is None:
|
||||||
obj.user = request.user
|
obj.user = request.user
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
import tagulous
|
import tagulous
|
||||||
from adminsortable2.admin import SortableInlineAdminMixin
|
from adminsortable2.admin import SortableInlineAdminMixin
|
||||||
from django.contrib import admin
|
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 django.utils.translation import ugettext_lazy as _
|
||||||
from import_export.admin import ImportExportMixin
|
from import_export.admin import ImportExportMixin
|
||||||
from import_export.resources import ModelResource
|
from import_export.resources import ModelResource
|
||||||
|
|
||||||
from inventory.admin.base import BaseUserAdmin
|
from inventory.admin.base import BaseUserAdmin
|
||||||
|
from inventory.forms import ItemModelModelForm
|
||||||
from inventory.models import ItemLinkModel, ItemModel
|
from inventory.models import ItemLinkModel, ItemModel
|
||||||
|
|
||||||
|
|
||||||
|
@ -13,6 +16,15 @@ class ItemLinkModelInline(SortableInlineAdminMixin, admin.TabularInline):
|
||||||
model = ItemLinkModel
|
model = ItemLinkModel
|
||||||
extra = 1
|
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):
|
class ItemModelResource(ModelResource):
|
||||||
|
|
||||||
|
@ -20,17 +32,43 @@ class ItemModelResource(ModelResource):
|
||||||
model = ItemModel
|
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)
|
@admin.register(ItemModel)
|
||||||
class ItemModelAdmin(ImportExportMixin, BaseUserAdmin):
|
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'
|
date_hierarchy = 'create_dt'
|
||||||
list_display = (
|
list_display = (
|
||||||
'kind', 'producer',
|
'kind', 'producer',
|
||||||
'name',
|
'column_item',
|
||||||
'parent', 'location',
|
'location',
|
||||||
'received_date', 'update_dt'
|
'received_date', 'update_dt'
|
||||||
)
|
)
|
||||||
ordering = ('kind', 'producer', 'name')
|
ordering = ('kind', 'producer', 'name')
|
||||||
list_display_links = ('name',)
|
list_display_links = None
|
||||||
list_filter = ('kind', 'location', 'producer', 'tags')
|
list_filter = ('kind', 'location', 'producer', 'tags')
|
||||||
search_fields = ('name', 'description')
|
search_fields = ('name', 'description')
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
|
@ -76,5 +114,18 @@ class ItemModelAdmin(ImportExportMixin, BaseUserAdmin):
|
||||||
readonly_fields = ('id', 'create_dt', 'update_dt', 'user')
|
readonly_fields = ('id', 'create_dt', 'update_dt', 'user')
|
||||||
inlines = (ItemLinkModelInline,)
|
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)
|
tagulous.admin.enhance(ItemModel, ItemModelAdmin)
|
||||||
|
|
|
@ -3,6 +3,7 @@ from import_export.admin import ImportExportMixin
|
||||||
from import_export.resources import ModelResource
|
from import_export.resources import ModelResource
|
||||||
|
|
||||||
from inventory.admin.base import BaseUserAdmin
|
from inventory.admin.base import BaseUserAdmin
|
||||||
|
from inventory.forms import LocationModelModelForm
|
||||||
from inventory.models import LocationModel
|
from inventory.models import LocationModel
|
||||||
|
|
||||||
|
|
||||||
|
@ -14,4 +15,13 @@ class LocationModelResource(ModelResource):
|
||||||
|
|
||||||
@admin.register(LocationModel)
|
@admin.register(LocationModel)
|
||||||
class LocationModelAdmin(ImportExportMixin, BaseUserAdmin):
|
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
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
from pathlib import Path
|
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()
|
@register()
|
||||||
|
@ -15,3 +17,25 @@ def inventory_checks(app_configs, **kwargs):
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return errors
|
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
|
||||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -1,16 +1,24 @@
|
||||||
import tagulous.models
|
import tagulous.models
|
||||||
from ckeditor_uploader.fields import RichTextUploadingField
|
from ckeditor_uploader.fields import RichTextUploadingField
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.urls import reverse
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from inventory.models.base import BaseModel
|
from inventory.models.base import BaseModel
|
||||||
from inventory.models.links import BaseLink
|
from inventory.models.links import BaseLink
|
||||||
|
|
||||||
|
|
||||||
|
class ItemQuerySet(models.QuerySet):
|
||||||
|
def sort(self):
|
||||||
|
return self.order_by('kind', 'producer', 'name')
|
||||||
|
|
||||||
|
|
||||||
class ItemModel(BaseModel):
|
class ItemModel(BaseModel):
|
||||||
"""
|
"""
|
||||||
A Item that can be described and store somewhere ;)
|
A Item that can be described and store somewhere ;)
|
||||||
"""
|
"""
|
||||||
|
objects = ItemQuerySet.as_manager()
|
||||||
|
|
||||||
kind = tagulous.models.TagField(
|
kind = tagulous.models.TagField(
|
||||||
case_sensitive=False,
|
case_sensitive=False,
|
||||||
force_lowercase=False,
|
force_lowercase=False,
|
||||||
|
@ -117,6 +125,14 @@ class ItemModel(BaseModel):
|
||||||
help_text=_('ItemModel.handed_over_price.help_text')
|
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):
|
def __str__(self):
|
||||||
if self.parent_id is None:
|
if self.parent_id is None:
|
||||||
title = self.name
|
title = self.name
|
||||||
|
|
|
@ -10,6 +10,7 @@ class LocationModel(BaseModel):
|
||||||
A Storage for items.
|
A Storage for items.
|
||||||
"""
|
"""
|
||||||
description = RichTextUploadingField(
|
description = RichTextUploadingField(
|
||||||
|
blank=True, null=True,
|
||||||
config_name='LocationModel.description',
|
config_name='LocationModel.description',
|
||||||
verbose_name=_('LocationModel.description.verbose_name'),
|
verbose_name=_('LocationModel.description.verbose_name'),
|
||||||
help_text=_('LocationModel.description.help_text')
|
help_text=_('LocationModel.description.help_text')
|
||||||
|
|
|
@ -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
|
|
@ -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 = {}
|
|
@ -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 %}
|
|
@ -58,10 +58,12 @@ MIDDLEWARE = [
|
||||||
'django.middleware.common.CommonMiddleware',
|
'django.middleware.common.CommonMiddleware',
|
||||||
'django.middleware.csrf.CsrfViewMiddleware',
|
'django.middleware.csrf.CsrfViewMiddleware',
|
||||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||||
|
|
||||||
|
'inventory.middlewares.RequestDictMiddleware',
|
||||||
|
|
||||||
'django.contrib.messages.middleware.MessageMiddleware',
|
'django.contrib.messages.middleware.MessageMiddleware',
|
||||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||||
'django.middleware.security.SecurityMiddleware',
|
'django.middleware.security.SecurityMiddleware',
|
||||||
'django_tools.middlewares.ThreadLocal.ThreadLocalMiddleware',
|
|
||||||
]
|
]
|
||||||
|
|
||||||
TEMPLATES = [
|
TEMPLATES = [
|
||||||
|
|
Ładowanie…
Reference in New Issue