From 964177d0bba3a1e56ebd5a274b6fbbeedc79a431 Mon Sep 17 00:00:00 2001 From: JensDiemer Date: Sun, 15 Nov 2020 12:53:05 +0100 Subject: [PATCH 1/6] Store Images to Items --- src/inventory/admin/item.py | 34 +++++++++-- .../migrations/0004_item_user_images.py | 55 +++++++++++++++++ src/inventory/models/__init__.py | 2 +- src/inventory/models/item.py | 60 +++++++++++++++++++ src/inventory/permissions.py | 12 +++- src/inventory/views/__init__.py | 0 src/inventory/views/media_files.py | 47 +++++++++++++++ src/inventory_project/urls.py | 4 +- 8 files changed, 203 insertions(+), 11 deletions(-) create mode 100644 src/inventory/migrations/0004_item_user_images.py create mode 100644 src/inventory/views/__init__.py create mode 100644 src/inventory/views/media_files.py diff --git a/src/inventory/admin/item.py b/src/inventory/admin/item.py index e8b469c..788e640 100644 --- a/src/inventory/admin/item.py +++ b/src/inventory/admin/item.py @@ -3,6 +3,7 @@ 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.html import format_html from django.utils.translation import ugettext_lazy as _ from import_export.admin import ImportExportMixin from import_export.resources import ModelResource @@ -10,12 +11,10 @@ from import_export.resources import ModelResource from inventory.admin.base import BaseUserAdmin from inventory.forms import ItemModelModelForm from inventory.models import ItemLinkModel, ItemModel +from inventory.models.item import ItemImageModel -class ItemLinkModelInline(SortableInlineAdminMixin, admin.TabularInline): - model = ItemLinkModel - extra = 1 - +class UserInlineMixin: def get_queryset(self, request): qs = super().get_queryset(request) @@ -26,8 +25,31 @@ class ItemLinkModelInline(SortableInlineAdminMixin, admin.TabularInline): 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( + ( + '' + '' + ), + 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: model = ItemModel @@ -112,7 +134,7 @@ class ItemModelAdmin(ImportExportMixin, BaseUserAdmin): )}), ) readonly_fields = ('id', 'create_dt', 'update_dt', 'user') - inlines = (ItemLinkModelInline,) + inlines = (ItemImageModelInline, ItemLinkModelInline) def get_changelist(self, request, **kwargs): self.user = request.user diff --git a/src/inventory/migrations/0004_item_user_images.py b/src/inventory/migrations/0004_item_user_images.py new file mode 100644 index 0000000..5279632 --- /dev/null +++ b/src/inventory/migrations/0004_item_user_images.py @@ -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',), + }, + ), + ] diff --git a/src/inventory/models/__init__.py b/src/inventory/models/__init__.py index 683e3ba..984fdc1 100644 --- a/src/inventory/models/__init__.py +++ b/src/inventory/models/__init__.py @@ -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 diff --git a/src/inventory/models/item.py b/src/inventory/models/item.py index ea30908..04b6808 100644 --- a/src/inventory/models/item.py +++ b/src/inventory/models/item.py @@ -1,13 +1,21 @@ +import logging +from pathlib import Path + import tagulous.models +from bx_py_utils.filename import clean_filename from ckeditor_uploader.fields import RichTextUploadingField from django.db import models from django.urls import reverse +from django.utils.crypto import get_random_string from django.utils.translation import ugettext_lazy as _ from inventory.models.base import BaseModel from inventory.models.links import BaseLink +logger = logging.getLogger(__name__) + + class ItemQuerySet(models.QuerySet): def sort(self): return self.order_by('kind', 'producer', 'name') @@ -167,3 +175,55 @@ class ItemLinkModel(BaseLink): verbose_name = _('ItemLinkModel.verbose_name') verbose_name_plural = _('ItemLinkModel.verbose_name_plural') 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 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',) diff --git a/src/inventory/permissions.py b/src/inventory/permissions.py index 95f3abe..0a32348 100644 --- a/src/inventory/permissions.py +++ b/src/inventory/permissions.py @@ -1,7 +1,7 @@ from django.contrib.auth.models import Group, Permission 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' @@ -16,15 +16,21 @@ def get_permissions(*models): 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) 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 - permissions = get_permissions(ItemModel, ItemLinkModel, LocationModel) + permissions = get_permissions(ItemImageModel, ItemLinkModel, ItemModel, LocationModel) existing_permissions = normal_user_group.permissions.all() if set(permissions) != set(existing_permissions): diff --git a/src/inventory/views/__init__.py b/src/inventory/views/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/inventory/views/media_files.py b/src/inventory/views/media_files.py new file mode 100644 index 0000000..875e4bf --- /dev/null +++ b/src/inventory/views/media_files.py @@ -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 + ) diff --git a/src/inventory_project/urls.py b/src/inventory_project/urls.py index fe6e563..5bf2685 100644 --- a/src/inventory_project/urls.py +++ b/src/inventory_project/urls.py @@ -4,6 +4,8 @@ from django.contrib import admin from django.urls import path from django.views.generic import RedirectView +from inventory.views.media_files import UserMediaView + admin.autodiscover() @@ -13,12 +15,12 @@ urlpatterns = [ # Don't use i18n_patterns() here url(r'^$', RedirectView.as_view(url='/admin/')), path('ckeditor/', include('ckeditor_uploader.urls')), # TODO: check permissions? + path('media/user_//', UserMediaView.as_view()) ] if settings.SERVE_FILES: 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: From be5364cd7ded889bdea260b97f6d10a62e6bf4fa Mon Sep 17 00:00:00 2001 From: JensDiemer Date: Sun, 15 Nov 2020 13:16:13 +0100 Subject: [PATCH 2/6] update translations --- Makefile | 4 +-- src/inventory/locale/de/LC_MESSAGES/django.mo | Bin 4549 -> 4915 bytes src/inventory/locale/de/LC_MESSAGES/django.po | 28 ++++++++++++++++-- src/inventory/locale/en/LC_MESSAGES/django.mo | Bin 3693 -> 4000 bytes src/inventory/locale/en/LC_MESSAGES/django.po | 26 ++++++++++++++-- 5 files changed, 52 insertions(+), 6 deletions(-) diff --git a/Makefile b/Makefile index d17d8c6..72b49ff 100644 --- a/Makefile +++ b/Makefile @@ -86,8 +86,8 @@ createsuperuser: ## Create super user ./manage.sh createsuperuser messages: ## Make and compile locales message files - ./manage.sh makemessages --all --no-location --no-obsolete - ./manage.sh compilemessages + ./manage.sh makemessages --all --no-location --no-obsolete --ignore=htmlcov --ignore=.tox --ignore=volumes + ./manage.sh compilemessages -v 0 ############################################################################## diff --git a/src/inventory/locale/de/LC_MESSAGES/django.mo b/src/inventory/locale/de/LC_MESSAGES/django.mo index 98d352d26940fc2dfdbbc1276d0d441566173d01..ad94665a1b387f59a8d15fcf70df1b20aee8a7de 100644 GIT binary patch delta 1702 zcmZwHOKeP09LMp0(bl6yz3X}VP^AwY)u7a)q|!9?mZ}9E=7y1Z(3x6PWMU%{3kktW z;!z8+D4B>?Btk4KiAIQsM68INM?}K+cjt5=aguXC_uO;lod5lwbLUw_wkq>}(x~%B zX{FAl){Zdi##N)ZP%h`2O~)%Z6Yt|Re1)a>8E0UAf!Qpai)uH!_IB4!y7nm?NB;nh zG0WI3ZpwJ@0O#O4^zavI!dZo8<+uzLz;4v|KAeXaUH@Iz{}wgRH>||TqsU@HSTA5GsSeQGu0?%>}#y70?b; ze=n;443@FJ4RWJMU+4jTckLPENddHA5IHLKCs7k#LhaH+)K)x3E$|kVsUN6ulZvR= zfKzcjD)4U1DDvan=#2(Y0X#)b@X58U*o>rDIo9M+5v*%`E`V-Sdk=DatjD$YyMEub z54m;>yBK$5Jo(qe&w0Rh+e_ygH}C^)=J_YrZkmu=uo*Rejcd0d=g>M(fow%$x9zCR z^q|J~;}krBB*!jHApgq1RrlaIE~Gt#Y@_`_rF_gpvytfG60Af`)P-6whFUOzjd%=6 zk`1D^?h)!mKjIerg6eO{l;kGpK(^gFQ335jMW}uSbQ-n59n=C(P;dMl^}FA$zmSdQ z;M)|e#o4HN+HoE3bnT0%z%$pl(VJa&4{o3qx``X{9_p*G$+STyDr7u3wfeImyH=46<Ec@aPEZ3w6Rr~*rP2mF+u?oTf?O=7{!y7;}>*Tr8){srwq BzAOL$ delta 1378 zcmXxkKWI}?6vy!sn>O}OOw;0& zvQ072jgMd-UceoA19##hH($kG;#J&*?E$j^Okxa+7{?Oox<&W>GAhAes03Trdh;=i zvA#_)(8NXDj@K}X_pu*eqbl__PIn?|W)J7IjiN1324;Oc~c?qO3 z#`;!dppuqQH=IX5R!}8=idvwGTKFgGcWuMI{*S^b+btEij3?|1s2# zucEHI?&cTD3^`6{OZ;g(=>D3yt%vSZ0~ RA~{^S6rA-{4un>l{sYXIW~cxF diff --git a/src/inventory/locale/de/LC_MESSAGES/django.po b/src/inventory/locale/de/LC_MESSAGES/django.po index 826350e..53a63ec 100644 --- a/src/inventory/locale/de/LC_MESSAGES/django.po +++ b/src/inventory/locale/de/LC_MESSAGES/django.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-10-26 19:24+0100\n" -"PO-Revision-Date: 2020-10-17 18:05+0200\n" +"POT-Creation-Date: 2020-11-15 13:09+0100\n" +"PO-Revision-Date: 2020-11-15 13:14+0100\n" "Last-Translator: Jens Diemer\n" "Language-Team: \n" "Language: de\n" @@ -166,6 +166,26 @@ msgstr "Link" msgid "ItemLinkModel.verbose_name_plural" 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" msgstr "Name" @@ -208,6 +228,10 @@ msgstr "Standort" msgid "LocationModel.verbose_name_plural" msgstr "Standorte" +#, python-format +msgid "Image \"%(path)s\" does not exist" +msgstr "" + msgid "German" msgstr "" diff --git a/src/inventory/locale/en/LC_MESSAGES/django.mo b/src/inventory/locale/en/LC_MESSAGES/django.mo index deb855e723ef21f580b9d725907ecf19adc1e741..7dafe3ad2e29cf95d7120ccb4c0da1be2066dfae 100644 GIT binary patch literal 4000 zcma);O>7)B6vy2X3f&gy2c_jJxF57h)7kEBC`~tw5N%QjNgzr$1q5g{nQ@XKvz}$f z+ol(UI20jN1ZoLLTo4B;iiCu?AdVnzfGTl72m}am0ErVqs*n)ye;!Y=U$d#To}Zte z{rqe{+xz2|t~VIcQRKUj|5?M>N$`j3@ImUip0Vv&@=c6&fv~1vjW)NG+wwwBHQ@Wqly| zdl;nlQ%3(JxC!!8M)twYkfo7d0P$ll;zQ%UZt9mz{aZ%91R^BZ`$qldM*bYc z*0V2+{542^Tru*uM*p3We*rOd_N$TqH1c0YUJK(WvM!MP+6vP8>;SRFtQ*wx2lf1o zd>Evoc*MvZQeATrEoe@Nbkyq5|{dyo$y)0}7?lnx-1FG-^K>`MfY=1F%6LQbcCeD|AjFL(sG z2iZdA$dq!(6bF>(yiub4->OUToXu6mZbh9wD_|b^C^DT1I#&mgv&fWoB2&!J{Y7Vl z60KQtqV=U1xt|or>`07zav>}2ipb6jzgkveQ8m?{7U4_~i83|Ps)@iAe%5oFdMsC` zbZ9e3+(j!=Iw^ZDYCDcrq;#r9B5d19D;heDDp47&*mJ^oPsO^fZPlF&w9ci;M5oU6 zx^mU8g|^SAJMB^IUww-Xs`}1xM^{8n=v9>$$foJF>RSeho6I^+xna}EwFXs6$9Y@2 z!Yv0-mt9*muWxLkrO`DQtA?Jly0y5~HJDRD+oh@Z3{6rDEqKyR)A>TG@@yDX(x>0l zBxMlYD#gu2*QskwD$ifViMlpvrUJ)K4J1ENrgW~_Ay%X9LK{_D$1refPQ$@o3BYQi zOz9j7M|h_ZgsJh@*kUy^w3%(^uxv)RSj|j&0599=7`-X>>#LKiK;QFK+-_y%DPJ^( zs#ZqIR<)??;C8Q@T8paQIMdo%=r?zus>X&UELv}ym1k!>wM_?8E%DT7rCm%lw&-BH zUX`rlWEeav9A%BV)+rH2xKE2bGbO4)sH}--&U39}wYkVD1*o2U%$gFXJ!)hPCy>nV5J=mJ1atK zL}KSXIalNZGoH#!>gUHg5rmbf$YuN*MMH%_9v6ptB;Ps2^Sy(aC#;8r6k+W8WFTBm z@j|vggFifv(tn20VfyK!{%}^ptIjwt%?nOXGCwmPa4+Hta&QKwcrXhAHJC|wmgD{4 zjxBk`mRJZ5^F3!g-{&)eV?x}nsi1-z$+3O^Y%iOJMGUQ9b>jfN>TyM+Lsoy_0UKU< z7TL7)o~sEiYn2&zK<8IGbmI7NhRiVq&K}lJ8KYo} z(^XeMZ&do`P0S`i3S}I_C?NLq6^P=LxzLj4%QE?0ua8}#n^ON@h}a}8p}qsq?4&;P zoX$L?>8Wvi>(?alBGT)R^_C)1!p-uLMY~e<5n;54Gr=Oy7q}<6Q>)GdcIfhcj6)Ya KODrrGBKAL1@)~>q delta 1249 zcmX|>KWI}?6o*eVO>LW08*OYgHvOAM)7B)$hF08M6iT6jVh5MdQ4tY^ij8y;K}0JB zK|}>{R7(V*i?}FGRdg;LtPWME4kGB__scu=z{z)i=f0PF?z!i_hpF$$%5pqBZ&(RW z;-5lhN8ydl{9w<+W;@{m+y&pk1pEx!;UCxrTkFhr!yzbr%yrpy#r0jN|9Jts%qq6T zAVuH@?1n9o;DI9CgC2*y@C@vSbFdFC!VFx2dat2Ahz~#|G65Ao>+#zje+fH?e}p}( zZ$BBN2_&`z6*vr)&?Ho*(@=@b`+U{s-$Qm_>re^C8vgLjB(| zOtQZHWuS%H8-s}=jH4%DE1ZHo@G?}Q51@AN1}cG7sQ2re0^JR>9ODrP>|E50Q?m1L z6TIY{h80a*@dsw1UbyD^Iy{NK>H3Rv4RTtx4)y#m$XfOrDv@x^j8m}`)O;FlgBjN& zumyc6M*a1`7=dOu4wcYpf3WQFDc2Vur)3vi&%mwdtFCWBPQ&J0--AlzzU#+6|HSpP z81-jt3m$j{rC+;Vf=cWIRAMVoJ70xV-PT~O00@+eHuLx Ks&+KbhW-KVl3!>5 diff --git a/src/inventory/locale/en/LC_MESSAGES/django.po b/src/inventory/locale/en/LC_MESSAGES/django.po index 7abaccc..79387d7 100644 --- a/src/inventory/locale/en/LC_MESSAGES/django.po +++ b/src/inventory/locale/en/LC_MESSAGES/django.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-10-26 19:24+0100\n" -"PO-Revision-Date: 2020-10-17 19:12+0200\n" +"POT-Creation-Date: 2020-11-15 13:09+0100\n" +"PO-Revision-Date: 2020-11-15 13:15+0100\n" "Last-Translator: Jens Diemer\n" "Language-Team: \n" "Language: en\n" @@ -164,6 +164,24 @@ msgstr "Link" msgid "ItemLinkModel.verbose_name_plural" 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" msgstr "Name" @@ -206,6 +224,10 @@ msgstr "Location" msgid "LocationModel.verbose_name_plural" msgstr "Locations" +#, python-format +msgid "Image \"%(path)s\" does not exist" +msgstr "" + msgid "German" msgstr "" From 68d82b220341d4c56b8ef7f74f323f178c92b279 Mon Sep 17 00:00:00 2001 From: JensDiemer Date: Sun, 15 Nov 2020 14:31:02 +0100 Subject: [PATCH 3/6] bugfix image __str__() im name is None --- src/inventory/models/item.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/inventory/models/item.py b/src/inventory/models/item.py index 04b6808..fca0df7 100644 --- a/src/inventory/models/item.py +++ b/src/inventory/models/item.py @@ -212,6 +212,9 @@ class ItemImageModel(BaseModel): 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 From 5ac950c884bb05d39024e8a21fe910d381457649 Mon Sep 17 00:00:00 2001 From: JensDiemer Date: Sun, 15 Nov 2020 14:31:20 +0100 Subject: [PATCH 4/6] remove appended slash to /media/ urls --- src/inventory_project/urls.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/inventory_project/urls.py b/src/inventory_project/urls.py index 5bf2685..064b2fb 100644 --- a/src/inventory_project/urls.py +++ b/src/inventory_project/urls.py @@ -15,7 +15,7 @@ urlpatterns = [ # Don't use i18n_patterns() here url(r'^$', RedirectView.as_view(url='/admin/')), path('ckeditor/', include('ckeditor_uploader.urls')), # TODO: check permissions? - path('media/user_//', UserMediaView.as_view()) + path('media/user_/', UserMediaView.as_view()) ] From a238d5cb84de4598413b30e45616a9e1396fbc0d Mon Sep 17 00:00:00 2001 From: JensDiemer Date: Sun, 15 Nov 2020 14:31:37 +0100 Subject: [PATCH 5/6] Add tests to uploaded user images --- src/inventory/tests/__init__.py | 0 src/inventory/tests/fixtures/__init__.py | 0 src/inventory/tests/fixtures/users.py | 15 ++++++ src/inventory/tests/test_item_images.py | 63 ++++++++++++++++++++++++ 4 files changed, 78 insertions(+) create mode 100644 src/inventory/tests/__init__.py create mode 100644 src/inventory/tests/fixtures/__init__.py create mode 100644 src/inventory/tests/fixtures/users.py create mode 100644 src/inventory/tests/test_item_images.py diff --git a/src/inventory/tests/__init__.py b/src/inventory/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/inventory/tests/fixtures/__init__.py b/src/inventory/tests/fixtures/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/inventory/tests/fixtures/users.py b/src/inventory/tests/fixtures/users.py new file mode 100644 index 0000000..0b0a95c --- /dev/null +++ b/src/inventory/tests/fixtures/users.py @@ -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 diff --git a/src/inventory/tests/test_item_images.py b/src/inventory/tests/test_item_images.py new file mode 100644 index 0000000..7fd386b --- /dev/null +++ b/src/inventory/tests/test_item_images.py @@ -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() From 7cafe18e1102686e2ff439d564ce1d8f084ae6d0 Mon Sep 17 00:00:00 2001 From: JensDiemer Date: Sun, 15 Nov 2020 14:38:04 +0100 Subject: [PATCH 6/6] Bugfix file excluding for lint/format tools --- .flake8 | 2 +- Makefile | 8 ++++---- pyproject.toml | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.flake8 b/.flake8 index 3cc9471..ca4524e 100644 --- a/.flake8 +++ b/.flake8 @@ -2,6 +2,6 @@ # Move to pyproject.toml after: https://gitlab.com/pycqa/flake8/-/issues/428 # [flake8] -exclude = .tox, .pytest_cache, *.egg-info, */migrations/*, volumes +exclude = .pytest_cache, .tox, dist, htmlcov, */migrations/*, volumes #ignore = E402 max-line-length = 119 diff --git a/Makefile b/Makefile index 72b49ff..5920d79 100644 --- a/Makefile +++ b/Makefile @@ -43,15 +43,15 @@ update: check-poetry ## update the sources and installation poetry update 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 flake8 . fix-code-style: ## Fix code formatting - poetry run flynt -e "volumes" --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 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 "./htmlcov/*" ! -path "*/volumes/*" 2>/dev/null` 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 poetry run tox --listenvs diff --git a/pyproject.toml b/pyproject.toml index 4532a4a..5ad7049 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -94,7 +94,7 @@ build-backend = "poetry.masonry.api" [tool.autopep8] # https://github.com/hhatto/autopep8#pyprojecttoml max_line_length = 120 -exclude = "*/migrations/*" +exclude="*/htmlcov/*,*/migrations/*,*/volumes/*" [tool.isort] @@ -102,7 +102,7 @@ exclude = "*/migrations/*" atomic=true line_length=120 case_sensitive=false -skip_glob=["*/migrations/*","*/volumes/*"] +skip_glob=["*/htmlcov/*","*/migrations/*","*/volumes/*"] multi_line_output=3 include_trailing_comma=true 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 minversion = "6.0" DJANGO_SETTINGS_MODULE="inventory_project.settings.tests" -norecursedirs = ".* .git __pycache__ coverage* dist volumes" +norecursedirs = ".* .git __pycache__ coverage* dist htmlcov volumes" # sometimes helpfull "addopts" arguments: # -vv # --verbose