kopia lustrzana https://github.com/jedie/PyInventory
commit
4b81a41b84
2
.flake8
2
.flake8
|
@ -2,6 +2,6 @@
|
||||||
# Move to pyproject.toml after: https://gitlab.com/pycqa/flake8/-/issues/428
|
# Move to pyproject.toml after: https://gitlab.com/pycqa/flake8/-/issues/428
|
||||||
#
|
#
|
||||||
[flake8]
|
[flake8]
|
||||||
exclude = .tox, .pytest_cache, *.egg-info, */migrations/*, volumes
|
exclude = .pytest_cache, .tox, dist, htmlcov, */migrations/*, volumes
|
||||||
#ignore = E402
|
#ignore = E402
|
||||||
max-line-length = 119
|
max-line-length = 119
|
||||||
|
|
12
Makefile
12
Makefile
|
@ -43,15 +43,15 @@ update: check-poetry ## update the sources and installation
|
||||||
poetry update
|
poetry update
|
||||||
|
|
||||||
lint: ## Run code formatters and linter
|
lint: ## Run code formatters and linter
|
||||||
poetry run flynt -e "volumes" --fail-on-change --line_length=${MAX_LINE_LENGTH} .
|
poetry run flynt -e "volumes" -e "htmlcov" --fail-on-change --line_length=${MAX_LINE_LENGTH} .
|
||||||
poetry run isort --check-only .
|
poetry run isort --check-only .
|
||||||
poetry run flake8 .
|
poetry run flake8 .
|
||||||
|
|
||||||
fix-code-style: ## Fix code formatting
|
fix-code-style: ## Fix code formatting
|
||||||
poetry run flynt -e "volumes" --line_length=${MAX_LINE_LENGTH} .
|
poetry run flynt -e "volumes" -e "htmlcov" --line_length=${MAX_LINE_LENGTH} .
|
||||||
poetry run pyupgrade --exit-zero-even-if-changed --py3-plus --py36-plus --py37-plus `find . -name "*.py" -type f ! -path "./.tox/*" ! -path "./volumes/*" 2>/dev/null`
|
poetry run pyupgrade --exit-zero-even-if-changed --py3-plus --py36-plus --py37-plus `find . -name "*.py" -type f ! -path "./.tox/*" ! -path "./htmlcov/*" ! -path "*/volumes/*" 2>/dev/null`
|
||||||
poetry run isort .
|
poetry run isort .
|
||||||
poetry run autopep8 --exclude="volumes,migrations" --aggressive --aggressive --in-place --recursive .
|
poetry run autopep8 --aggressive --aggressive --in-place --recursive .
|
||||||
|
|
||||||
tox-listenvs: check-poetry ## List all tox test environments
|
tox-listenvs: check-poetry ## List all tox test environments
|
||||||
poetry run tox --listenvs
|
poetry run tox --listenvs
|
||||||
|
@ -86,8 +86,8 @@ createsuperuser: ## Create super user
|
||||||
./manage.sh createsuperuser
|
./manage.sh createsuperuser
|
||||||
|
|
||||||
messages: ## Make and compile locales message files
|
messages: ## Make and compile locales message files
|
||||||
./manage.sh makemessages --all --no-location --no-obsolete
|
./manage.sh makemessages --all --no-location --no-obsolete --ignore=htmlcov --ignore=.tox --ignore=volumes
|
||||||
./manage.sh compilemessages
|
./manage.sh compilemessages -v 0
|
||||||
|
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
|
|
|
@ -94,7 +94,7 @@ build-backend = "poetry.masonry.api"
|
||||||
[tool.autopep8]
|
[tool.autopep8]
|
||||||
# https://github.com/hhatto/autopep8#pyprojecttoml
|
# https://github.com/hhatto/autopep8#pyprojecttoml
|
||||||
max_line_length = 120
|
max_line_length = 120
|
||||||
exclude = "*/migrations/*"
|
exclude="*/htmlcov/*,*/migrations/*,*/volumes/*"
|
||||||
|
|
||||||
|
|
||||||
[tool.isort]
|
[tool.isort]
|
||||||
|
@ -102,7 +102,7 @@ exclude = "*/migrations/*"
|
||||||
atomic=true
|
atomic=true
|
||||||
line_length=120
|
line_length=120
|
||||||
case_sensitive=false
|
case_sensitive=false
|
||||||
skip_glob=["*/migrations/*","*/volumes/*"]
|
skip_glob=["*/htmlcov/*","*/migrations/*","*/volumes/*"]
|
||||||
multi_line_output=3
|
multi_line_output=3
|
||||||
include_trailing_comma=true
|
include_trailing_comma=true
|
||||||
known_first_party=["inventory","inventory_project","inventory_tests"]
|
known_first_party=["inventory","inventory_project","inventory_tests"]
|
||||||
|
@ -116,7 +116,7 @@ lines_after_imports=2
|
||||||
# https://docs.pytest.org/en/latest/customize.html#pyproject-toml
|
# https://docs.pytest.org/en/latest/customize.html#pyproject-toml
|
||||||
minversion = "6.0"
|
minversion = "6.0"
|
||||||
DJANGO_SETTINGS_MODULE="inventory_project.settings.tests"
|
DJANGO_SETTINGS_MODULE="inventory_project.settings.tests"
|
||||||
norecursedirs = ".* .git __pycache__ coverage* dist volumes"
|
norecursedirs = ".* .git __pycache__ coverage* dist htmlcov volumes"
|
||||||
# sometimes helpfull "addopts" arguments:
|
# sometimes helpfull "addopts" arguments:
|
||||||
# -vv
|
# -vv
|
||||||
# --verbose
|
# --verbose
|
||||||
|
|
|
@ -3,6 +3,7 @@ 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.contrib.admin.views.main import ChangeList
|
||||||
from django.template.loader import render_to_string
|
from django.template.loader import render_to_string
|
||||||
|
from django.utils.html import format_html
|
||||||
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
|
||||||
|
@ -10,12 +11,10 @@ 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.forms import ItemModelModelForm
|
||||||
from inventory.models import ItemLinkModel, ItemModel
|
from inventory.models import ItemLinkModel, ItemModel
|
||||||
|
from inventory.models.item import ItemImageModel
|
||||||
|
|
||||||
|
|
||||||
class ItemLinkModelInline(SortableInlineAdminMixin, admin.TabularInline):
|
class UserInlineMixin:
|
||||||
model = ItemLinkModel
|
|
||||||
extra = 1
|
|
||||||
|
|
||||||
def get_queryset(self, request):
|
def get_queryset(self, request):
|
||||||
qs = super().get_queryset(request)
|
qs = super().get_queryset(request)
|
||||||
|
|
||||||
|
@ -26,8 +25,31 @@ class ItemLinkModelInline(SortableInlineAdminMixin, admin.TabularInline):
|
||||||
return qs
|
return qs
|
||||||
|
|
||||||
|
|
||||||
class ItemModelResource(ModelResource):
|
class ItemLinkModelInline(UserInlineMixin, SortableInlineAdminMixin, admin.TabularInline):
|
||||||
|
model = ItemLinkModel
|
||||||
|
extra = 0
|
||||||
|
|
||||||
|
|
||||||
|
class ItemImageModelInline(UserInlineMixin, SortableInlineAdminMixin, admin.TabularInline):
|
||||||
|
def preview(self, instance):
|
||||||
|
return format_html(
|
||||||
|
(
|
||||||
|
'<a href="{url}" title="{name}"'
|
||||||
|
' target="_blank" class="image_file_input_preview">'
|
||||||
|
'<img style="width:9em;" src="{url}"></a>'
|
||||||
|
),
|
||||||
|
url=instance.image.url,
|
||||||
|
name=instance.name,
|
||||||
|
)
|
||||||
|
model = ItemImageModel
|
||||||
|
extra = 0
|
||||||
|
fields = (
|
||||||
|
'position', 'preview', 'image', 'name', 'tags'
|
||||||
|
)
|
||||||
|
readonly_fields = ('preview',)
|
||||||
|
|
||||||
|
|
||||||
|
class ItemModelResource(ModelResource):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ItemModel
|
model = ItemModel
|
||||||
|
|
||||||
|
@ -112,7 +134,7 @@ class ItemModelAdmin(ImportExportMixin, BaseUserAdmin):
|
||||||
)}),
|
)}),
|
||||||
)
|
)
|
||||||
readonly_fields = ('id', 'create_dt', 'update_dt', 'user')
|
readonly_fields = ('id', 'create_dt', 'update_dt', 'user')
|
||||||
inlines = (ItemLinkModelInline,)
|
inlines = (ItemImageModelInline, ItemLinkModelInline)
|
||||||
|
|
||||||
def get_changelist(self, request, **kwargs):
|
def get_changelist(self, request, **kwargs):
|
||||||
self.user = request.user
|
self.user = request.user
|
||||||
|
|
Plik binarny nie jest wyświetlany.
|
@ -7,8 +7,8 @@ msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: \n"
|
"Project-Id-Version: \n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2020-10-26 19:24+0100\n"
|
"POT-Creation-Date: 2020-11-15 13:09+0100\n"
|
||||||
"PO-Revision-Date: 2020-10-17 18:05+0200\n"
|
"PO-Revision-Date: 2020-11-15 13:14+0100\n"
|
||||||
"Last-Translator: Jens Diemer\n"
|
"Last-Translator: Jens Diemer\n"
|
||||||
"Language-Team: \n"
|
"Language-Team: \n"
|
||||||
"Language: de\n"
|
"Language: de\n"
|
||||||
|
@ -166,6 +166,26 @@ msgstr "Link"
|
||||||
msgid "ItemLinkModel.verbose_name_plural"
|
msgid "ItemLinkModel.verbose_name_plural"
|
||||||
msgstr "Links"
|
msgstr "Links"
|
||||||
|
|
||||||
|
msgid "ItemImageModel.image.verbose_name"
|
||||||
|
msgstr "Bild"
|
||||||
|
|
||||||
|
msgid "ItemImageModel.image.help_text"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "ItemImageModel.name.verbose_name"
|
||||||
|
msgstr "Name"
|
||||||
|
|
||||||
|
msgid "ItemImageModel.name.help_text"
|
||||||
|
msgstr ""
|
||||||
|
"Optionalen Namen passend zum Bild (Wird automatisch aus dem Dateinamen "
|
||||||
|
"gesetzt)"
|
||||||
|
|
||||||
|
msgid "ItemImageModel.verbose_name"
|
||||||
|
msgstr "Bild"
|
||||||
|
|
||||||
|
msgid "ItemImageModel.verbose_name_plural"
|
||||||
|
msgstr "Bilder"
|
||||||
|
|
||||||
msgid "BaseLink.name.verbose_name"
|
msgid "BaseLink.name.verbose_name"
|
||||||
msgstr "Name"
|
msgstr "Name"
|
||||||
|
|
||||||
|
@ -208,6 +228,10 @@ msgstr "Standort"
|
||||||
msgid "LocationModel.verbose_name_plural"
|
msgid "LocationModel.verbose_name_plural"
|
||||||
msgstr "Standorte"
|
msgstr "Standorte"
|
||||||
|
|
||||||
|
#, python-format
|
||||||
|
msgid "Image \"%(path)s\" does not exist"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "German"
|
msgid "German"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|
Plik binarny nie jest wyświetlany.
|
@ -7,8 +7,8 @@ msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: \n"
|
"Project-Id-Version: \n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2020-10-26 19:24+0100\n"
|
"POT-Creation-Date: 2020-11-15 13:09+0100\n"
|
||||||
"PO-Revision-Date: 2020-10-17 19:12+0200\n"
|
"PO-Revision-Date: 2020-11-15 13:15+0100\n"
|
||||||
"Last-Translator: Jens Diemer\n"
|
"Last-Translator: Jens Diemer\n"
|
||||||
"Language-Team: \n"
|
"Language-Team: \n"
|
||||||
"Language: en\n"
|
"Language: en\n"
|
||||||
|
@ -164,6 +164,24 @@ msgstr "Link"
|
||||||
msgid "ItemLinkModel.verbose_name_plural"
|
msgid "ItemLinkModel.verbose_name_plural"
|
||||||
msgstr "Links"
|
msgstr "Links"
|
||||||
|
|
||||||
|
msgid "ItemImageModel.image.verbose_name"
|
||||||
|
msgstr "Image"
|
||||||
|
|
||||||
|
msgid "ItemImageModel.image.help_text"
|
||||||
|
msgstr " "
|
||||||
|
|
||||||
|
msgid "ItemImageModel.name.verbose_name"
|
||||||
|
msgstr "Name"
|
||||||
|
|
||||||
|
msgid "ItemImageModel.name.help_text"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "ItemImageModel.verbose_name"
|
||||||
|
msgstr "Image"
|
||||||
|
|
||||||
|
msgid "ItemImageModel.verbose_name_plural"
|
||||||
|
msgstr "Images"
|
||||||
|
|
||||||
msgid "BaseLink.name.verbose_name"
|
msgid "BaseLink.name.verbose_name"
|
||||||
msgstr "Name"
|
msgstr "Name"
|
||||||
|
|
||||||
|
@ -206,6 +224,10 @@ msgstr "Location"
|
||||||
msgid "LocationModel.verbose_name_plural"
|
msgid "LocationModel.verbose_name_plural"
|
||||||
msgstr "Locations"
|
msgstr "Locations"
|
||||||
|
|
||||||
|
#, python-format
|
||||||
|
msgid "Image \"%(path)s\" does not exist"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "German"
|
msgid "German"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,55 @@
|
||||||
|
# Generated by Django 2.2.17 on 2020-11-15 11:09
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import inventory.models.item
|
||||||
|
import tagulous.models.fields
|
||||||
|
import tagulous.models.models
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
('inventory', '0003_auto_20201024_1830'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Tagulous_ItemImageModel_tags',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=255, unique=True)),
|
||||||
|
('slug', models.SlugField()),
|
||||||
|
('count', models.IntegerField(default=0, help_text='Internal counter of how many times this tag is in use')),
|
||||||
|
('protected', models.BooleanField(default=False, help_text='Will not be deleted when the count reaches 0')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'ordering': ('name',),
|
||||||
|
'abstract': False,
|
||||||
|
'unique_together': {('slug',)},
|
||||||
|
},
|
||||||
|
bases=(tagulous.models.models.BaseTagModel, models.Model),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ItemImageModel',
|
||||||
|
fields=[
|
||||||
|
('create_dt', models.DateTimeField(blank=True, editable=False, help_text='ModelTimetrackingMixin.create_dt.help_text', null=True, verbose_name='ModelTimetrackingMixin.create_dt.verbose_name')),
|
||||||
|
('update_dt', models.DateTimeField(blank=True, editable=False, help_text='ModelTimetrackingMixin.update_dt.help_text', null=True, verbose_name='ModelTimetrackingMixin.update_dt.verbose_name')),
|
||||||
|
('id', models.UUIDField(default=uuid.uuid4, editable=False, help_text='BaseModel.id.help_text', primary_key=True, serialize=False, verbose_name='BaseModel.id.verbose_name')),
|
||||||
|
('image', models.ImageField(help_text='ItemImageModel.image.help_text', upload_to=inventory.models.item.user_directory_path, verbose_name='ItemImageModel.image.verbose_name')),
|
||||||
|
('name', models.CharField(blank=True, help_text='ItemImageModel.name.help_text', max_length=255, null=True, verbose_name='ItemImageModel.name.verbose_name')),
|
||||||
|
('position', models.PositiveSmallIntegerField(default=0)),
|
||||||
|
('item', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='inventory.ItemModel')),
|
||||||
|
('tags', tagulous.models.fields.TagField(_set_tag_meta=True, blank=True, case_sensitive=False, force_lowercase=False, help_text='BaseModel.tags.help_text', max_count=10, space_delimiter=False, to='inventory.Tagulous_ItemImageModel_tags', verbose_name='BaseModel.tags.verbose_name')),
|
||||||
|
('user', models.ForeignKey(editable=False, help_text='BaseModel.user.help_text', on_delete=django.db.models.deletion.CASCADE, related_name='+', to=settings.AUTH_USER_MODEL, verbose_name='BaseModel.user.verbose_name')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'ItemImageModel.verbose_name',
|
||||||
|
'verbose_name_plural': 'ItemImageModel.verbose_name_plural',
|
||||||
|
'ordering': ('position',),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
|
@ -1,2 +1,2 @@
|
||||||
from inventory.models.item import ItemLinkModel, ItemModel # noqa
|
from inventory.models.item import ItemImageModel, ItemLinkModel, ItemModel # noqa
|
||||||
from inventory.models.location import LocationModel # noqa
|
from inventory.models.location import LocationModel # noqa
|
||||||
|
|
|
@ -1,13 +1,21 @@
|
||||||
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
import tagulous.models
|
import tagulous.models
|
||||||
|
from bx_py_utils.filename import clean_filename
|
||||||
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.urls import reverse
|
||||||
|
from django.utils.crypto import get_random_string
|
||||||
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
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class ItemQuerySet(models.QuerySet):
|
class ItemQuerySet(models.QuerySet):
|
||||||
def sort(self):
|
def sort(self):
|
||||||
return self.order_by('kind', 'producer', 'name')
|
return self.order_by('kind', 'producer', 'name')
|
||||||
|
@ -167,3 +175,58 @@ class ItemLinkModel(BaseLink):
|
||||||
verbose_name = _('ItemLinkModel.verbose_name')
|
verbose_name = _('ItemLinkModel.verbose_name')
|
||||||
verbose_name_plural = _('ItemLinkModel.verbose_name_plural')
|
verbose_name_plural = _('ItemLinkModel.verbose_name_plural')
|
||||||
ordering = ('position',)
|
ordering = ('position',)
|
||||||
|
|
||||||
|
|
||||||
|
def user_directory_path(instance, filename):
|
||||||
|
"""
|
||||||
|
Upload to /MEDIA_ROOT/...
|
||||||
|
"""
|
||||||
|
random_string = get_random_string()
|
||||||
|
filename = clean_filename(filename)
|
||||||
|
filename = f'user_{instance.user.id}/{random_string}/{filename}'
|
||||||
|
logger.info(f'Upload filename: {filename!r}')
|
||||||
|
return filename
|
||||||
|
|
||||||
|
|
||||||
|
class ItemImageModel(BaseModel):
|
||||||
|
"""
|
||||||
|
Store Images to Items
|
||||||
|
"""
|
||||||
|
image = models.ImageField(
|
||||||
|
upload_to=user_directory_path,
|
||||||
|
verbose_name=_('ItemImageModel.image.verbose_name'),
|
||||||
|
help_text=_('ItemImageModel.image.help_text')
|
||||||
|
)
|
||||||
|
name = models.CharField(
|
||||||
|
null=True, blank=True,
|
||||||
|
max_length=255,
|
||||||
|
verbose_name=_('ItemImageModel.name.verbose_name'),
|
||||||
|
help_text=_('ItemImageModel.name.help_text')
|
||||||
|
)
|
||||||
|
item = models.ForeignKey(
|
||||||
|
ItemModel, on_delete=models.CASCADE
|
||||||
|
)
|
||||||
|
position = models.PositiveSmallIntegerField(
|
||||||
|
# Note: Will be set in admin via adminsortable2
|
||||||
|
# The JavaScript which performs the sorting is 1-indexed !
|
||||||
|
default=0, blank=False, null=False
|
||||||
|
)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name or self.image.name
|
||||||
|
|
||||||
|
def full_clean(self, **kwargs):
|
||||||
|
if not self.name:
|
||||||
|
filename = Path(self.image.name).name
|
||||||
|
self.name = clean_filename(filename)
|
||||||
|
|
||||||
|
if self.user_id is None:
|
||||||
|
# inherit owner of this link from item instance
|
||||||
|
self.user_id = self.item.user_id
|
||||||
|
|
||||||
|
return super().full_clean(**kwargs)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _('ItemImageModel.verbose_name')
|
||||||
|
verbose_name_plural = _('ItemImageModel.verbose_name_plural')
|
||||||
|
ordering = ('position',)
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
from django.contrib.auth.models import Group, Permission
|
from django.contrib.auth.models import Group, Permission
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
|
||||||
from inventory.models import ItemLinkModel, ItemModel, LocationModel
|
from inventory.models import ItemImageModel, ItemLinkModel, ItemModel, LocationModel
|
||||||
|
|
||||||
|
|
||||||
NORMAL_USER_GROUP_NAME = 'normal user'
|
NORMAL_USER_GROUP_NAME = 'normal user'
|
||||||
|
@ -16,15 +16,21 @@ def get_permissions(*models):
|
||||||
|
|
||||||
|
|
||||||
def get_or_create_normal_user_group():
|
def get_or_create_normal_user_group():
|
||||||
|
"""
|
||||||
|
Will be called by:
|
||||||
|
inventory.signals.post_migrate_callback()
|
||||||
|
"""
|
||||||
return Group.objects.get_or_create(name=NORMAL_USER_GROUP_NAME)
|
return Group.objects.get_or_create(name=NORMAL_USER_GROUP_NAME)
|
||||||
|
|
||||||
|
|
||||||
def setup_normal_user_permissions(normal_user_group):
|
def setup_normal_user_permissions(normal_user_group):
|
||||||
"""
|
"""
|
||||||
Setup PyInventory "normal user" permissions
|
Setup PyInventory "normal user" permissions.
|
||||||
|
Will be called by:
|
||||||
|
inventory.signals.post_migrate_callback()
|
||||||
"""
|
"""
|
||||||
assert normal_user_group.name == NORMAL_USER_GROUP_NAME
|
assert normal_user_group.name == NORMAL_USER_GROUP_NAME
|
||||||
permissions = get_permissions(ItemModel, ItemLinkModel, LocationModel)
|
permissions = get_permissions(ItemImageModel, ItemLinkModel, ItemModel, LocationModel)
|
||||||
existing_permissions = normal_user_group.permissions.all()
|
existing_permissions = normal_user_group.permissions.all()
|
||||||
|
|
||||||
if set(permissions) != set(existing_permissions):
|
if set(permissions) != set(existing_permissions):
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from model_bakery import baker
|
||||||
|
|
||||||
|
from inventory.permissions import get_or_create_normal_user_group
|
||||||
|
|
||||||
|
|
||||||
|
def get_normal_pyinventory_user(**baker_kwargs):
|
||||||
|
pyinventory_user_group = get_or_create_normal_user_group()[0]
|
||||||
|
pyinventory_user = baker.make(
|
||||||
|
User,
|
||||||
|
is_staff=True, is_active=True, is_superuser=False,
|
||||||
|
**baker_kwargs
|
||||||
|
)
|
||||||
|
pyinventory_user.groups.set([pyinventory_user_group])
|
||||||
|
return pyinventory_user
|
|
@ -0,0 +1,63 @@
|
||||||
|
import tempfile
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
from django.http import FileResponse
|
||||||
|
from django.test import TestCase, override_settings
|
||||||
|
from model_bakery import baker
|
||||||
|
|
||||||
|
from inventory.models import ItemImageModel
|
||||||
|
from inventory.tests.fixtures.users import get_normal_pyinventory_user
|
||||||
|
|
||||||
|
|
||||||
|
class ItemImagesTestCase(TestCase):
|
||||||
|
def test_basics(self):
|
||||||
|
pyinventory_user1 = get_normal_pyinventory_user(id=1)
|
||||||
|
pyinventory_user2 = get_normal_pyinventory_user(id=2)
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory() as tmpdir, override_settings(MEDIA_ROOT=tmpdir):
|
||||||
|
print(tmpdir)
|
||||||
|
|
||||||
|
with self.assertLogs('inventory') as logs:
|
||||||
|
with mock.patch('inventory.models.item.get_random_string', return_value='DrgCCsMrdIBJ'):
|
||||||
|
image_instance = baker.make(
|
||||||
|
ItemImageModel,
|
||||||
|
user=pyinventory_user1,
|
||||||
|
_create_files=True
|
||||||
|
)
|
||||||
|
assert image_instance.image is not None
|
||||||
|
url = image_instance.image.url
|
||||||
|
# url = f'/media/{image_instance.image}'
|
||||||
|
assert url == '/media/user_1/DrgCCsMrdIBJ/mock_img.jpeg'
|
||||||
|
assert logs.output == [
|
||||||
|
"INFO:inventory.models.item:"
|
||||||
|
"Upload filename: 'user_1/DrgCCsMrdIBJ/mock_img.jpeg'"
|
||||||
|
]
|
||||||
|
|
||||||
|
# Anonymous user can't access:
|
||||||
|
|
||||||
|
with self.assertLogs('inventory') as logs, self.assertLogs('django'):
|
||||||
|
response = self.client.get(url)
|
||||||
|
assert response.status_code == 403
|
||||||
|
assert logs.output == [
|
||||||
|
'ERROR:inventory.views.media_files:Anonymous try to access files from: 1'
|
||||||
|
]
|
||||||
|
|
||||||
|
# Wrong user should not access:
|
||||||
|
|
||||||
|
self.client.force_login(user=pyinventory_user2)
|
||||||
|
|
||||||
|
with self.assertLogs('inventory') as logs, self.assertLogs('django'):
|
||||||
|
response = self.client.get(url)
|
||||||
|
assert response.status_code == 403
|
||||||
|
assert logs.output == [
|
||||||
|
'ERROR:inventory.views.media_files:Wrong user ID: 2 is not 1'
|
||||||
|
]
|
||||||
|
|
||||||
|
# The right user should access:
|
||||||
|
|
||||||
|
self.client.force_login(user=pyinventory_user1)
|
||||||
|
|
||||||
|
response = self.client.get(url)
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert isinstance(response, FileResponse)
|
||||||
|
assert response.getvalue() == image_instance.image.read()
|
|
@ -0,0 +1,47 @@
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.exceptions import PermissionDenied
|
||||||
|
from django.http import Http404
|
||||||
|
from django.utils.translation import gettext as _
|
||||||
|
from django.views.generic.base import View
|
||||||
|
from django.views.static import serve
|
||||||
|
|
||||||
|
from inventory.models import ItemImageModel
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class UserMediaView(View):
|
||||||
|
"""
|
||||||
|
Serve MEDIA_URL files, but check the current user:
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get(self, request, user_id, path):
|
||||||
|
media_path = f'user_{user_id}/{path}'
|
||||||
|
|
||||||
|
if not request.user.is_superuser:
|
||||||
|
if request.user.id != user_id:
|
||||||
|
# A user tries to access a file from a other use?
|
||||||
|
if request.user.id is None:
|
||||||
|
logger.error(f'Anonymous try to access files from: {user_id!r}')
|
||||||
|
else:
|
||||||
|
logger.error(f'Wrong user ID: {request.user.id!r} is not {user_id!r}')
|
||||||
|
raise PermissionDenied
|
||||||
|
|
||||||
|
# Check if the image really exists:
|
||||||
|
qs = ItemImageModel.objects.filter(
|
||||||
|
user_id=request.user.id,
|
||||||
|
image=media_path
|
||||||
|
)
|
||||||
|
if not qs.exists():
|
||||||
|
raise Http404(_('Image "%(path)s" does not exist') % {'path': media_path})
|
||||||
|
|
||||||
|
# Send the file to the user:
|
||||||
|
return serve(
|
||||||
|
request,
|
||||||
|
path=media_path,
|
||||||
|
document_root=settings.MEDIA_ROOT,
|
||||||
|
show_indexes=False
|
||||||
|
)
|
|
@ -4,6 +4,8 @@ from django.contrib import admin
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
from django.views.generic import RedirectView
|
from django.views.generic import RedirectView
|
||||||
|
|
||||||
|
from inventory.views.media_files import UserMediaView
|
||||||
|
|
||||||
|
|
||||||
admin.autodiscover()
|
admin.autodiscover()
|
||||||
|
|
||||||
|
@ -13,12 +15,12 @@ urlpatterns = [ # Don't use i18n_patterns() here
|
||||||
url(r'^$', RedirectView.as_view(url='/admin/')),
|
url(r'^$', RedirectView.as_view(url='/admin/')),
|
||||||
|
|
||||||
path('ckeditor/', include('ckeditor_uploader.urls')), # TODO: check permissions?
|
path('ckeditor/', include('ckeditor_uploader.urls')), # TODO: check permissions?
|
||||||
|
path('media/user_<int:user_id>/<path:path>', UserMediaView.as_view())
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
if settings.SERVE_FILES:
|
if settings.SERVE_FILES:
|
||||||
urlpatterns += static.static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
|
urlpatterns += static.static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
|
||||||
urlpatterns += static.static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
|
||||||
|
|
||||||
|
|
||||||
if settings.DEBUG:
|
if settings.DEBUG:
|
||||||
|
|
Ładowanie…
Reference in New Issue