kopia lustrzana https://github.com/jedie/PyInventory
commit
e2bcc7ace6
|
@ -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 ;)
|
||||
|
|
32
README.rst
32
README.rst
|
@ -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"``
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
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
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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.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 = [
|
||||
|
|
Ładowanie…
Reference in New Issue