WIP: Fix #102 by store tree information for Item and Location
pull/109/head
Jens Diemer 2022-07-24 20:09:05 +02:00 zatwierdzone przez GitHub
commit e2f66b7d3a
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
25 zmienionych plików z 891 dodań i 279 usunięć

Wyświetl plik

@ -176,6 +176,11 @@ Files are separated into: "/src/" and "/development/"
* [[https://github.com/jedie/PyInventory/compare/v0.13.1...main|compare v0.13.1...main]] **dev**
** tbc
* [[https://github.com/jedie/PyInventory/compare/v0.13.1...v0.14.0rc3|v0.14.0rc3 - 22.07.2022]]
** [[https://github.com/jedie/PyInventory/issues/102|Fix #102]] by remove limitation of item parents.
** Remove "Group Items" functionality
** Replace "Group Items" change list filter by "Limit tree depth" for Item and Location.
** Display Item and Location as a tree.
* [[https://github.com/jedie/PyInventory/compare/v0.13.0...v0.13.1|v0.13.1 - 21.07.2022]]
** Rename git "master" branch into "main"
** Update CI/Test setup:

Wyświetl plik

@ -241,11 +241,21 @@ Files are separated into: "/src/" and "/development/"
history
-------
* `compare v0.13.1...main <https://github.com/jedie/PyInventory/compare/v0.13.1...main>`_ **dev**
* `compare v0.13.1...main <https://github.com/jedie/PyInventory/compare/v0.13.1...main>`_ **dev**
* tbc
* `v0.13.1 - 21.07.2022 <https://github.com/jedie/PyInventory/compare/v0.13.0...v0.13.1>`_
* `v0.14.0rc3 - 22.07.2022 <https://github.com/jedie/PyInventory/compare/v0.13.1...v0.14.0rc3>`_
* `Fix #102 <https://github.com/jedie/PyInventory/issues/102>`_ by remove limitation of item parents.
* Remove "Group Items" functionality
* Replace "Group Items" change list filter by "Limit tree depth" for Item and Location.
* Display Item and Location as a tree.
* `v0.13.1 - 21.07.2022 <https://github.com/jedie/PyInventory/compare/v0.13.0...v0.13.1>`_
* Rename git "master" branch into "main"
@ -257,37 +267,37 @@ history
* Replace Selenium tests with Playwright
* `v0.13.0 - 01.01.2022 <https://github.com/jedie/PyInventory/compare/v0.12.0...v0.13.0>`_
* `v0.13.0 - 01.01.2022 <https://github.com/jedie/PyInventory/compare/v0.12.0...v0.13.0>`_
* `Update requirements, e.g.: Django v3.2 <https://github.com/jedie/PyInventory/pull/83>`_
* `v0.12.0 - 22.11.2021 <https://github.com/jedie/PyInventory/compare/v0.11.0...v0.12.0>`_
* `v0.12.0 - 22.11.2021 <https://github.com/jedie/PyInventory/compare/v0.11.0...v0.12.0>`_
* NEW: `Protect user to overwrite newer Item/Memo/Location with a older one (e.g.: in other browser TAB) <https://github.com/jedie/PyInventory/pull/78>`_
* update requirements
* `v0.11.0 - 09.10.2021 <https://github.com/jedie/PyInventory/compare/v0.10.1...v0.11.0>`_
* `v0.11.0 - 09.10.2021 <https://github.com/jedie/PyInventory/compare/v0.10.1...v0.11.0>`_
* NEW: Memo model/admin: Store Information (incl. images/files/links) independent of items/locations
* Bugfix CKEditor sizes and fix toolbar (e.g.: remove useless pdf generator button and add sourcecode function)
* `v0.10.1 - 09.10.2021 <https://github.com/jedie/PyInventory/compare/v0.10.0...v0.10.1>`_
* `v0.10.1 - 09.10.2021 <https://github.com/jedie/PyInventory/compare/v0.10.0...v0.10.1>`_
* Update to Django 3.1.x
* Don't make requests to the a name for a Link, if we already have one or if last request was not long ago.
* `v0.10.0 - 29.09.2021 <https://github.com/jedie/PyInventory/compare/v0.9.4...v0.10.0>`_
* `v0.10.0 - 29.09.2021 <https://github.com/jedie/PyInventory/compare/v0.9.4...v0.10.0>`_
* Group item: default "automatic" mode and can be disabled by filter action
* `v0.9.4 - 15.09.2021 <https://github.com/jedie/PyInventory/compare/v0.9.3...v0.9.4>`_
* `v0.9.4 - 15.09.2021 <https://github.com/jedie/PyInventory/compare/v0.9.3...v0.9.4>`_
* Pin ``psycopg < 2.9`` because of `https://github.com/psycopg/psycopg2/issues/1293 <https://github.com/psycopg/psycopg2/issues/1293>`_
* `v0.9.3 - 15.09.2021 <https://github.com/jedie/PyInventory/compare/v0.9.2...v0.9.3>`_
* `v0.9.3 - 15.09.2021 <https://github.com/jedie/PyInventory/compare/v0.9.2...v0.9.3>`_
* Optimize "items" changelist queries
@ -295,7 +305,7 @@ history
* Expand ``run_testserver`` command and recognize address and port argument
* `v0.9.2 - 11.05.2021 <https://github.com/jedie/PyInventory/compare/v0.9.1...v0.9.2>`_
* `v0.9.2 - 11.05.2021 <https://github.com/jedie/PyInventory/compare/v0.9.1...v0.9.2>`_
* Update requirements
@ -307,17 +317,17 @@ history
* Add a auto login if Django dev. server is used.
* `v0.9.0 - 11.04.2021 <https://github.com/jedie/PyInventory/compare/v0.8.4...v0.9.0>`_
* `v0.9.0 - 11.04.2021 <https://github.com/jedie/PyInventory/compare/v0.8.4...v0.9.0>`_
* Use `https://github.com/jedie/dev-shell <https://github.com/jedie/dev-shell>`_ for development
* `v0.8.4 - 19.01.2021 <https://github.com/jedie/PyInventory/compare/v0.8.3...v0.8.4>`_
* `v0.8.4 - 19.01.2021 <https://github.com/jedie/PyInventory/compare/v0.8.3...v0.8.4>`_
* Search items in change list by "kind" and "tags", too
* update requirements
* `v0.8.3 - 29.12.2020 <https://github.com/jedie/PyInventory/compare/v0.8.2...v0.8.3>`_
* `v0.8.3 - 29.12.2020 <https://github.com/jedie/PyInventory/compare/v0.8.2...v0.8.3>`_
* update requirements
@ -325,11 +335,11 @@ history
* Small project setup changes
* `v0.8.2 - 20.12.2020 <https://github.com/jedie/PyInventory/compare/v0.8.1...v0.8.2>`_
* `v0.8.2 - 20.12.2020 <https://github.com/jedie/PyInventory/compare/v0.8.1...v0.8.2>`_
* Bugfix `#33 <https://github.com/jedie/PyInventory/issues/33>`_: Upload images to new created Items
* `v0.8.1 - 09.12.2020 <https://github.com/jedie/PyInventory/compare/v0.8.0...v0.8.1>`_
* `v0.8.1 - 09.12.2020 <https://github.com/jedie/PyInventory/compare/v0.8.0...v0.8.1>`_
* Fix migration: Don't create "/media/migrate.log" if there is nothing to migrate
@ -339,11 +349,11 @@ history
* update requirements
* `v0.8.0 - 06.12.2020 <https://github.com/jedie/PyInventory/compare/v0.7.0...v0.8.0>`_
* `v0.8.0 - 06.12.2020 <https://github.com/jedie/PyInventory/compare/v0.7.0...v0.8.0>`_
* Outsource the "MEDIA file serve" part into `django.tools.serve_media_app <https://github.com/jedie/django-tools/tree/master/django_tools/serve_media_app#readme>`_
* `v0.7.0 - 23.11.2020 <https://github.com/jedie/PyInventory/compare/v0.6.0...v0.7.0>`_
* `v0.7.0 - 23.11.2020 <https://github.com/jedie/PyInventory/compare/v0.6.0...v0.7.0>`_
* Change deployment setup:
@ -357,15 +367,15 @@ history
* pull all docker images before build
* `v0.6.0 - 15.11.2020 <https://github.com/jedie/PyInventory/compare/v0.5.0...v0.6.0>`_
* `v0.6.0 - 15.11.2020 <https://github.com/jedie/PyInventory/compare/v0.5.0...v0.6.0>`_
* User can store images to every item: The image can only be accessed by the same user.
* `v0.5.0 - 14.11.2020 <https://github.com/jedie/PyInventory/compare/v0.4.2...v0.5.0>`_
* `v0.5.0 - 14.11.2020 <https://github.com/jedie/PyInventory/compare/v0.4.2...v0.5.0>`_
* Merge separate git branches into one: "/src/" and "/development/" `#19 <https://github.com/jedie/PyInventory/issues/19>`_
* `v0.4.2 - 13.11.2020 <https://github.com/jedie/PyInventory/compare/v0.4.1...v0.4.2>`_
* `v0.4.2 - 13.11.2020 <https://github.com/jedie/PyInventory/compare/v0.4.1...v0.4.2>`_
* Serve static files by Caddy
@ -373,11 +383,11 @@ history
* reduce CKEditor plugins
* `v0.4.1 - 2.11.2020 <https://github.com/jedie/PyInventory/compare/v0.4.0...v0.4.1>`_
* `v0.4.1 - 2.11.2020 <https://github.com/jedie/PyInventory/compare/v0.4.0...v0.4.1>`_
* Small bugfixes
* `v0.4.0 - 1.11.2020 <https://github.com/jedie/PyInventory/compare/v0.3.2...v0.4.0>`_
* `v0.4.0 - 1.11.2020 <https://github.com/jedie/PyInventory/compare/v0.3.2...v0.4.0>`_
* Move docker stuff and production use information into separate git branch
@ -385,11 +395,11 @@ history
* Add django-processinfo: collect information about the running server processes
* `v0.3.2 - 26.10.2020 <https://github.com/jedie/PyInventory/compare/v0.3.0...v0.3.2>`_
* `v0.3.2 - 26.10.2020 <https://github.com/jedie/PyInventory/compare/v0.3.0...v0.3.2>`_
* Bugfix missing translations
* `v0.3.0 - 26.10.2020 <https://github.com/jedie/PyInventory/compare/v0.2.0...v0.3.0>`_
* `v0.3.0 - 26.10.2020 <https://github.com/jedie/PyInventory/compare/v0.2.0...v0.3.0>`_
* setup production usage:
@ -407,7 +417,7 @@ history
* Bugfix for using manage commands ``dumpdata`` and ``loaddata``
* `v0.2.0 - 24.10.2020 <https://github.com/jedie/PyInventory/compare/v0.1.0...v0.2.0>`_
* `v0.2.0 - 24.10.2020 <https://github.com/jedie/PyInventory/compare/v0.1.0...v0.2.0>`_
* Simplify item change list by nested item
@ -419,7 +429,7 @@ history
* Add docker-compose usage
* `v0.1.0 - 17.10.2020 <https://github.com/jedie/PyInventory/compare/v0.0.1...v0.1.0>`_
* `v0.1.0 - 17.10.2020 <https://github.com/jedie/PyInventory/compare/v0.0.1...v0.1.0>`_
* Enhance models, admin and finish project setup
@ -462,4 +472,4 @@ donation
------------
``Note: this file is generated from README.creole 2022-07-21 16:53:31 with "python-creole"``
``Note: this file is generated from README.creole 2022-07-22 19:32:28 with "python-creole"``

Wyświetl plik

@ -1,3 +1,3 @@
export PROJECT_NAME=pyinventory
export PROJECT_PACKAGE_NAME=pyinventory
export PROJECT_VERSION=0.13.1
export PROJECT_VERSION=0.14.0rc3

Wyświetl plik

@ -1,6 +1,6 @@
[tool.poetry]
name = "PyInventory"
version = "0.13.1"
version = "0.14.0rc3"
description = "Web based management to catalog things including state and location etc. using Python/Django."
authors = ["JensDiemer <git@jensdiemer.de>"]
homepage = "https://github.com/jedie/PyInventory"

Wyświetl plik

@ -4,4 +4,4 @@
:license: GNU GPL v3 or above, see LICENSE for more details.
"""
__version__ = "0.13.1"
__version__ = "0.14.0rc3"

Wyświetl plik

@ -1,6 +1,7 @@
from adminsortable2.admin import SortableInlineAdminMixin
from django.contrib import admin
from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _
from reversion_compare.admin import CompareVersionAdmin
from inventory.forms import OnlyUserRelationsModelForm
@ -66,3 +67,21 @@ class BaseFileModelInline(UserInlineMixin, SortableInlineAdminMixin, admin.Tabul
fields = (
'position', 'file', 'name', 'tags'
)
class LimitTreeDepthListFilter(admin.SimpleListFilter):
title = _('Limit tree depth')
parameter_name = 'level'
def lookups(self, request, model_admin):
return (
('1', _('Only root')),
('2', _('Root + first sub')),
('3', _('Root + first + second sub')),
)
def queryset(self, request, queryset):
level = self.value()
if level:
level = int(level)
return queryset.filter(level__lte=level)

Wyświetl plik

@ -2,8 +2,10 @@ import logging
import tagulous
from adminsortable2.admin import SortableInlineAdminMixin
from django.conf import settings
from django.contrib import admin
from django.template.loader import render_to_string
from django.urls import reverse
from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _
from import_export.admin import ImportExportMixin
from import_export.resources import ModelResource
@ -12,10 +14,12 @@ from inventory.admin.base import (
BaseFileModelInline,
BaseImageModelInline,
BaseUserAdmin,
LimitTreeDepthListFilter,
UserInlineMixin,
)
from inventory.models import ItemLinkModel, ItemModel
from inventory.models.item import ItemFileModel, ItemImageModel
from inventory.string_utils import ltruncatechars
logger = logging.getLogger(__name__)
@ -39,52 +43,25 @@ class ItemModelResource(ModelResource):
model = ItemModel
class GroupItemsListFilter(admin.SimpleListFilter):
title = _('Group Items')
parameter_name = 'grouping'
GET_KEY_AUTO = 'auto'
GET_KEY_NO = 'no'
def lookups(self, request, model_admin):
return (
(self.GET_KEY_AUTO, _('Automatic')),
(self.GET_KEY_NO, _('No')),
)
def value(self):
return super().value() or self.GET_KEY_AUTO
def queryset(self, request, queryset):
auto_mode = self.value() == self.GET_KEY_AUTO
if auto_mode:
request.group_items = not request.GET.keys()
else:
request.group_items = self.value() != self.GET_KEY_NO
logger.info('Group items: %r (auto mode: %r)', request.group_items, auto_mode)
if request.group_items:
queryset = queryset.filter(parent__isnull=True)
return queryset
def choices(self, changelist):
for lookup, title in self.lookup_choices:
if lookup == self.GET_KEY_AUTO:
query_string = changelist.get_query_string(remove=[self.parameter_name])
else:
query_string = changelist.get_query_string({self.parameter_name: lookup})
yield {
'selected': self.value() == lookup,
'query_string': query_string,
'display': title,
}
@admin.register(ItemModel)
class ItemModelAdmin(ImportExportMixin, BaseUserAdmin):
@admin.display(ordering='path_str', description=_('ItemModel.verbose_name'))
def item(self, obj):
path = obj.path
if len(path) > 1:
prefixes = ' '.join(path[:-1] + [''])
prefixes = ltruncatechars(prefixes, max_length=settings.TREE_PATH_STR_MAX_LENGTH)
else:
prefixes = ''
item = path[-1]
url = reverse('admin:inventory_itemmodel_change', args=[obj.pk])
return format_html(
'<a href="{}">{}<strong>{}</strong></a>',
url,
prefixes,
item,
)
def get_queryset(self, request):
qs = super().get_queryset(request)
qs = qs.prefetch_related(
@ -93,37 +70,11 @@ class ItemModelAdmin(ImportExportMixin, BaseUserAdmin):
)
return qs
def column_item(self, obj):
context = {
'base_item': obj,
}
if self.request.group_items: # Attribute added in GroupItemsListFilter.queryset()
logger.debug('Display sub items inline')
# TODO: annotate "sub_items" !
qs = ItemModel.objects.filter(
user=self.user # user added in BaseUserAdmin.get_changelist()
)
qs = qs.filter(parent=obj).sort()
context['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',
'column_item',
'location',
'received_date', 'update_dt'
)
ordering = ('kind', 'producer', 'name')
list_display = ('item', 'kind', 'producer', 'location', 'received_date', 'update_dt')
ordering = ('path_str',)
list_display_links = None
list_filter = (GroupItemsListFilter, 'kind', 'location', 'producer', 'tags')
list_filter = (LimitTreeDepthListFilter, 'kind', 'location', 'producer', 'tags')
search_fields = ('name', 'description', 'kind__name', 'tags__name')
fieldsets = (
(_('Internals'), {

Wyświetl plik

@ -1,9 +1,12 @@
from django.conf import settings
from django.contrib import admin
from django.utils.translation import gettext_lazy as _
from import_export.admin import ImportExportMixin
from import_export.resources import ModelResource
from inventory.admin.base import BaseUserAdmin
from inventory.admin.base import BaseUserAdmin, LimitTreeDepthListFilter
from inventory.models import LocationModel
from inventory.string_utils import ltruncatechars
class LocationModelResource(ModelResource):
@ -14,4 +17,12 @@ class LocationModelResource(ModelResource):
@admin.register(LocationModel)
class LocationModelAdmin(ImportExportMixin, BaseUserAdmin):
pass
@admin.display(ordering='path_str', description=_('LocationModel.verbose_name'))
def location(self, obj):
text = ' '.join(obj.path)
text = ltruncatechars(text, max_length=settings.TREE_PATH_STR_MAX_LENGTH)
return text
list_display = ('location', 'create_dt', 'update_dt')
list_filter = (LimitTreeDepthListFilter,)
ordering = ('path_str',)

Wyświetl plik

@ -0,0 +1,62 @@
import time
from argparse import OPTIONAL
from django.apps import apps
from django.core.management.base import BaseCommand
class PrintDuration:
def __init__(self, stdout):
self.stdout = stdout
def __enter__(self):
self.start_time = time.monotonic()
def __exit__(self, exc_type, exc_val, exc_tb):
duration = (time.monotonic() - self.start_time) * 1000
self.stdout.write(f'(Done in: {duration:.1f}ms)')
class Command(BaseCommand):
help = 'Repair tree information'
def add_arguments(self, parser):
parser.add_argument(
'model_name',
metavar='model_name',
nargs=OPTIONAL,
default='itemmodel',
choices=['itemmodel', 'locationmodel'],
help='Model Name (default: "%(default)s")',
)
def handle(self, *args, **options):
self.stdout.write()
self.stdout.write('=' * 100)
self.stdout.write(self.help)
self.stdout.write('-' * 100)
model_name = options['model_name']
ModelClass = apps.get_model(app_label='inventory', model_name=model_name)
self.print_info(ModelClass, text='Old information about model:')
self.stdout.write('_' * 100)
self.stdout.write(f'Clean tree information on model: {ModelClass._meta.verbose_name!r}')
with PrintDuration(self.stdout):
ModelClass.objects.update(path=None, path_str=None, level=None)
self.stdout.write('_' * 100)
self.stdout.write(f'Repair tree model: {ModelClass._meta.verbose_name!r}')
with PrintDuration(self.stdout):
ModelClass.tree_objects.update_tree_info()
self.print_info(ModelClass, text='New information about model:')
def print_info(self, ModelClass, text):
self.stdout.write('_' * 100)
self.stdout.write(f'{text} {ModelClass._meta.verbose_name!r}')
with PrintDuration(self.stdout):
data = ModelClass.objects.values('level', 'path_str', 'path', 'name')
for entry in data:
self.stdout.write(repr(entry))

Wyświetl plik

@ -0,0 +1,104 @@
# Generated by Django 3.2.14 on 2022-07-21 18:36
import django.db.models.deletion
import tagulous.models.models
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('inventory', '0010_version_protect_models'),
]
operations = [
migrations.AddField(
model_name='itemmodel',
name='level',
field=models.PositiveSmallIntegerField(blank=True, editable=False, null=True),
),
migrations.AddField(
model_name='itemmodel',
name='path',
field=models.JSONField(blank=True, editable=False, null=True),
),
migrations.AddField(
model_name='itemmodel',
name='path_str',
field=models.TextField(blank=True, editable=False, null=True),
),
migrations.AddField(
model_name='locationmodel',
name='level',
field=models.PositiveSmallIntegerField(blank=True, editable=False, null=True),
),
migrations.AddField(
model_name='locationmodel',
name='path',
field=models.JSONField(blank=True, editable=False, null=True),
),
migrations.AddField(
model_name='locationmodel',
name='path_str',
field=models.TextField(blank=True, editable=False, null=True),
),
migrations.AlterField(
model_name='itemmodel',
name='parent',
field=models.ForeignKey(
blank=True,
help_text='LocationModel.parent.help_text',
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to='inventory.itemmodel',
verbose_name='LocationModel.parent.verbose_name',
),
),
migrations.CreateModel(
name='Tagulous_BaseParentTreeModel_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.AlterModelOptions(
name='itemmodel',
options={
'ordering': ('path_str',),
'verbose_name': 'ItemModel.verbose_name',
'verbose_name_plural': 'ItemModel.verbose_name_plural',
},
),
migrations.AlterModelOptions(
name='locationmodel',
options={
'ordering': ('path_str',),
'verbose_name': 'LocationModel.verbose_name',
'verbose_name_plural': 'LocationModel.verbose_name_plural',
},
),
]

Wyświetl plik

@ -0,0 +1,21 @@
# Generated by Django 3.2.14 on 2022-07-21 18:36
from django.core import management
from django.db import migrations
from inventory.management.commands import tree
def forward_code(apps, schema_editor):
management.call_command(tree.Command(), model_name='itemmodel')
management.call_command(tree.Command(), model_name='locationmodel')
class Migration(migrations.Migration):
dependencies = [
('inventory', '0011_parent_tree1'),
]
operations = [
migrations.RunPython(forward_code, reverse_code=migrations.RunPython.noop),
]

Wyświetl plik

@ -1,3 +1,7 @@
import logging
import re
import time
import unicodedata
import uuid
import tagulous.models
@ -6,6 +10,12 @@ from django.conf import settings
from django.db import models
from django.utils.translation import gettext_lazy as _
from inventory.parent_tree import ValuesListTree
from inventory.string_utils import ltruncatechars
logger = logging.getLogger(__name__)
class BaseModel(TimetrackingBaseModel):
id = models.UUIDField(
@ -45,6 +55,115 @@ class BaseModel(TimetrackingBaseModel):
abstract = True
def nomalize_text(text):
"""
>>> nomalize_text('Foo Bar 1 §$% äö-üß +')
'foobar1aou'
"""
text = unicodedata.normalize('NFKD', text).encode('ascii', 'ignore').decode('ascii')
text = text.lower()
text = re.sub(r'[^\w]', '', text)
return text
def generate_path_str(path):
"""
>>> generate_path_str(['Foo', 'B a r', '1 §$% äö-üß +'])
'foo 0 bar 0 1aou'
"""
# The choice of the separator is very important for the correct sorting by the database!
# Use 0, because this character is used for sorting and is the first character in the charset.
# The spaces are only visual separators ;)
return ' 0 '.join(nomalize_text(part) for part in path)
class ParentTreeModelManager(models.Manager):
def update_tree_info(self):
start_time = time.monotonic()
values = self.all().values('pk', 'name', 'parent__pk', 'path')
tree = ValuesListTree(values=values)
tree_path = tree.get_tree_path()
logger.debug('Tree path: %r', tree_path)
update_path_info = tree.get_update_path_info()
duration = (time.monotonic() - start_time) * 1000
logger.info('Get update_path_info: %r in %ims', update_path_info, duration)
if not update_path_info:
logger.info('No tree path changed, ok')
else:
start_time = time.monotonic()
entries = self.filter(pk__in=update_path_info.keys())
for entry in entries:
path = update_path_info[entry.pk]
entry.path = path
entry.path_str = generate_path_str(path)
entry.level = len(path)
self.bulk_update(entries, ['path', 'path_str', 'level'])
duration = (time.monotonic() - start_time) * 1000
logger.info('Update %i entries in %ims', len(entries), duration)
class BaseParentTreeModel(BaseModel):
path = models.JSONField(
blank=True,
null=True,
editable=False,
)
path_str = models.TextField(
blank=True,
null=True,
editable=False,
)
level = models.PositiveSmallIntegerField(
blank=True,
null=True,
editable=False,
)
parent = models.ForeignKey(
'self',
on_delete=models.SET_NULL,
blank=True,
null=True,
verbose_name=_('LocationModel.parent.verbose_name'),
help_text=_('LocationModel.parent.help_text'),
)
objects = models.Manager()
tree_objects = ParentTreeModelManager()
def save(self, **kwargs):
if not self.path:
if self.parent:
path = self.parent.path
if path:
self.path = [*path, self.name]
else:
self.path = [self.name]
self.path_str = generate_path_str(self.path)
self.level = len(self.path)
logger.info('Init path with: %r', self.path)
self.full_clean()
super().save(**kwargs)
self.__class__.tree_objects.update_tree_info()
def __str__(self):
if self.path:
text = ' '.join(self.path)
text = ltruncatechars(text, max_length=settings.TREE_PATH_STR_MAX_LENGTH)
return text
return self.name
class Meta:
abstract = True
class BaseAttachmentModel(BaseModel):
"""
Base model to store files or images to Items

Wyświetl plik

@ -10,7 +10,7 @@ from django.utils.translation import gettext_lazy as _
from django_tools.model_version_protect.models import VersionProtectBaseModel
from django_tools.serve_media_app.models import user_directory_path
from inventory.models.base import BaseItemAttachmentModel, BaseModel
from inventory.models.base import BaseItemAttachmentModel, BaseParentTreeModel
from inventory.models.links import BaseLink
@ -22,7 +22,7 @@ class ItemQuerySet(models.QuerySet):
return self.order_by('kind', 'producer', 'name')
class ItemModel(BaseModel, VersionProtectBaseModel):
class ItemModel(BaseParentTreeModel, VersionProtectBaseModel):
"""
A Item that can be described and store somewhere ;)
"""
@ -63,14 +63,6 @@ class ItemModel(BaseModel, VersionProtectBaseModel):
verbose_name=_('ItemModel.location.verbose_name'),
help_text=_('ItemModel.location.help_text')
)
parent = models.ForeignKey(
'self',
limit_choices_to={'parent_id': None},
on_delete=models.SET_NULL,
blank=True, null=True,
verbose_name=_('ItemModel.parent.verbose_name'),
help_text=_('ItemModel.parent.help_text')
)
# ________________________________________________________________________
# lent
@ -142,21 +134,8 @@ class ItemModel(BaseModel, VersionProtectBaseModel):
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
else:
title = f'{self.name} {self.parent}'
if self.producer:
title = f'{self.producer} - {title}'
if self.location_id is not None:
title = f'{title} ({self.location})'
return title
class Meta:
ordering = ('path_str',)
verbose_name = _('ItemModel.verbose_name')
verbose_name_plural = _('ItemModel.verbose_name_plural')

Wyświetl plik

@ -1,12 +1,11 @@
from ckeditor_uploader.fields import RichTextUploadingField
from django.db import models
from django.utils.translation import gettext_lazy as _
from django_tools.model_version_protect.models import VersionProtectBaseModel
from inventory.models.base import BaseModel
from inventory.models.base import BaseParentTreeModel
class LocationModel(BaseModel, VersionProtectBaseModel):
class LocationModel(BaseParentTreeModel, VersionProtectBaseModel):
"""
A Storage for items.
"""
@ -16,20 +15,8 @@ class LocationModel(BaseModel, VersionProtectBaseModel):
verbose_name=_('LocationModel.description.verbose_name'),
help_text=_('LocationModel.description.help_text')
)
parent = models.ForeignKey(
'self',
on_delete=models.SET_NULL,
blank=True, null=True,
verbose_name=_('LocationModel.parent.verbose_name'),
help_text=_('LocationModel.parent.help_text')
)
def __str__(self):
if self.parent_id is None:
return self.name
else:
return f'{self.name} {self.parent}'
class Meta:
ordering = ('path_str',)
verbose_name = _('LocationModel.verbose_name')
verbose_name_plural = _('LocationModel.verbose_name_plural')

Wyświetl plik

@ -0,0 +1,71 @@
class TreeNode:
def __init__(self, pk, name, current_path):
self.pk = pk
self.name = name
self.current_path = current_path
self.parent_node = None
self.path = None
def _set_parent(self, parent_node):
self.parent_node = parent_node
def _get_path(self):
if self.parent_node:
parent_path = self.parent_node._get_path()
return [*parent_path, self.name]
else:
return [self.name]
def _set_tree_info(self):
self.path = self._get_path()
@property
def path_string(self):
return ' / '.join(self.path)
def __str__(self):
return f'pk:{self.pk} name:"{self.name}" path:"{self.path_string}"'
def __repr__(self):
return f'<TreeNode {self.__str__()}>'
class ValuesListTree:
def __init__(self, values):
nodes = {}
# init all nodes:
for entry in values:
pk = entry['pk']
name = entry['name']
current_path = entry['path']
nodes[pk] = TreeNode(pk=pk, name=name, current_path=current_path)
# Set parents:
for entry in values:
parent_pk = entry['parent__pk']
if parent_pk:
pk = entry['pk']
node = nodes[pk]
parent_node = nodes[parent_pk]
node._set_parent(parent_node=parent_node)
# Set tree info:
nodes = list(nodes.values())
for node in nodes:
node._set_tree_info()
# Oder by hierarchy:
nodes.sort(key=lambda x: x.path)
self.nodes = nodes
def get_tree_path(self):
return [node.path_string for node in self.nodes]
def get_update_path_info(self):
update_info = {}
for node in self.nodes:
if node.current_path != node.path:
update_info[node.pk] = node.path
return update_info

Wyświetl plik

@ -0,0 +1,15 @@
def ltruncatechars(text, max_length, truncate=''):
"""
>>> ltruncatechars('1234567890', max_length=10)
'1234567890'
>>> ltruncatechars('1234567890', max_length=5)
'…7890'
>>> ltruncatechars('1234567890', max_length=6)
'…67890'
>>> ltruncatechars('1234567890', max_length=6, truncate='...')
'...890'
"""
if len(text) > max_length:
length = max_length - len(truncate)
text = truncate + text[-length:]
return text

Wyświetl plik

@ -1,8 +0,0 @@
<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

@ -0,0 +1,29 @@
import io
from django.core import management
from django.test import TestCase
from model_bakery import baker
from inventory.management.commands import tree
from inventory.models import ItemModel
class ManagementCommandTestCase(TestCase):
def test_tree_command(self):
baker.make(ItemModel, name='Foo Bar')
ItemModel.objects.update(path_str='OLD', path=['OLD'])
output = io.StringIO()
management.call_command(tree.Command(), stdout=output)
output = output.getvalue()
assert 'Repair tree information' in output
assert "Old information about model: 'Item'" in output
assert "{'level': 1, 'path_str': 'OLD', 'path': ['OLD'], 'name': 'Foo Bar'}" in output
assert "New information about model: 'Item'" in output
assert (
"{'level': 1, 'path_str': 'foobar', 'path': ['Foo Bar'], 'name': 'Foo Bar'}" in output
)

Wyświetl plik

@ -0,0 +1,43 @@
import random
from inventory.parent_tree import ValuesListTree
def test_values_list_tree():
values_list = [
(1, '1.', None),
(2, '1.1.', 1),
(3, '1.1.1', 2),
(4, '1.1.2', 2),
(5, '1.2.', 1),
(6, '2.', None),
]
random.shuffle(values_list)
values = [
{'pk': entry[0], 'name': entry[1], 'parent__pk': entry[2], 'path': ''}
for entry in values_list
]
tree = ValuesListTree(values)
tree_path = tree.get_tree_path()
assert tree_path == [
'1.',
'1. / 1.1.',
'1. / 1.1. / 1.1.1',
'1. / 1.1. / 1.1.2',
'1. / 1.2.',
'2.',
]
update_path_info = tree.get_update_path_info()
assert update_path_info == {
1: ['1.'],
2: ['1.', '1.1.'],
3: ['1.', '1.1.', '1.1.1'],
4: ['1.', '1.1.', '1.1.2'],
5: ['1.', '1.2.'],
6: ['2.'],
}
node_three = tree.nodes[2]
assert str(node_three) == 'pk:3 name:"1.1.1" path:"1. / 1.1. / 1.1.1"'
assert repr(node_three) == '<TreeNode pk:3 name:"1.1.1" path:"1. / 1.1. / 1.1.1">'

Wyświetl plik

@ -0,0 +1,127 @@
from bx_django_utils.test_utils.assert_queries import AssertQueries
from django.test import TestCase
from inventory.admin import ItemModelAdmin, LocationModelAdmin
from inventory.models import ItemModel, LocationModel
from inventory_project.tests.fixtures import get_normal_user
class TreeModelTests(TestCase):
@classmethod
def setUpTestData(cls):
cls.normaluser = get_normal_user()
def test_parent_tree_model(self):
for main_item_no in range(1, 3):
main_item = ItemModel.objects.create(
user=self.normaluser,
name=f'{main_item_no}.',
)
main_item.full_clean()
for sub_item_no in range(1, 3):
sub_item = ItemModel.objects.create(
parent=main_item,
user=self.normaluser,
name=f'{main_item_no}.{sub_item_no}.',
)
sub_item.full_clean()
for sub_sub_item_no in range(1, 3):
sub_sub_item = ItemModel.objects.create(
parent=sub_item,
user=self.normaluser,
name=f'{main_item_no}.{sub_item_no}.{sub_sub_item_no}.',
)
sub_sub_item.full_clean()
data = list(ItemModel.objects.values_list('level', 'path_str', 'name'))
assert data == [
(1, '1', '1.'),
(2, '1 0 11', '1.1.'),
(3, '1 0 11 0 111', '1.1.1.'),
(3, '1 0 11 0 112', '1.1.2.'),
(2, '1 0 12', '1.2.'),
(3, '1 0 12 0 121', '1.2.1.'),
(3, '1 0 12 0 122', '1.2.2.'),
(1, '2', '2.'),
(2, '2 0 21', '2.1.'),
(3, '2 0 21 0 211', '2.1.1.'),
(3, '2 0 21 0 212', '2.1.2.'),
(2, '2 0 22', '2.2.'),
(3, '2 0 22 0 221', '2.2.1.'),
(3, '2 0 22 0 222', '2.2.2.'),
]
item_2_1 = ItemModel.objects.get(name='2.1.')
item_2_1.name = 'NEW 2.1. Name'
with AssertQueries() as queries:
item_2_1.save()
data = list(ItemModel.objects.values_list('level', 'path_str', 'name'))
assert data == [
(1, '1', '1.'),
(2, '1 0 11', '1.1.'),
(3, '1 0 11 0 111', '1.1.1.'),
(3, '1 0 11 0 112', '1.1.2.'),
(2, '1 0 12', '1.2.'),
(3, '1 0 12 0 121', '1.2.1.'),
(3, '1 0 12 0 122', '1.2.2.'),
(1, '2', '2.'),
(2, '2 0 22', '2.2.'),
(3, '2 0 22 0 221', '2.2.1.'),
(3, '2 0 22 0 222', '2.2.2.'),
(2, '2 0 new21name', 'NEW 2.1. Name'),
(3, '2 0 new21name 0 211', '2.1.1.'),
(3, '2 0 new21name 0 212', '2.1.2.'),
]
itemmodel_count = 1 # full_clean(): Check if parent exists
itemmodel_count += 1 # VersionProtectBaseModel: Check version
itemmodel_count += 1 # VersionProtectBaseModel: Save new version
itemmodel_count += 1 # Get info for tree update
itemmodel_count += 1 # Fetch the items to update
itemmodel_count += 1 # Bulk update save
queries.assert_queries(
table_counts={
'inventory_itemmodel': itemmodel_count,
'auth_user': 1, # full_clean(): Check if user exists
},
double_tables=False,
duplicated=True,
similar=True,
)
def test_parent_tree_model_ordering(self):
assert LocationModel._meta.ordering == ('path_str',)
assert LocationModelAdmin.ordering == ('path_str',)
assert ItemModel._meta.ordering == ('path_str',)
assert ItemModelAdmin.ordering == ('path_str',)
def create(name, parent=None):
instance = ItemModel.objects.create(user=self.normaluser, name=name, parent=parent)
instance.full_clean()
return instance
# Create a "Special" case for the correct ordering:
# 1. all "PC-1" entries
# 2. all "PC1640" entries
#
# The correct order depends on the seperator, here: " 0 "
pc1 = create(name='PC-1')
pc1640 = create(name='PC1640 SD')
create(name='FZ-502 Rev A 5.25″ Floppy', parent=pc1)
create(name='1,44MB / 3.5" Floppy FD-235HF- 3800-U', parent=pc1)
create(name='PC 1640ECD', parent=pc1640)
data = list(ItemModel.objects.values_list('level', 'path_str', 'name'))
assert data == [
(1, 'pc1', 'PC-1'),
(2, 'pc1 0 144mb35floppyfd235hf3800u', '1,44MB / 3.5" Floppy FD-235HF- 3800-U'),
(2, 'pc1 0 fz502reva525floppy', 'FZ-502 Rev A 5.25″ Floppy'),
(1, 'pc1640sd', 'PC1640 SD'),
(2, 'pc1640sd 0 pc1640ecd', 'PC 1640ECD'),
]

Wyświetl plik

@ -35,6 +35,11 @@ print(f'BASE_PATH:{BASE_PATH}')
###############################################################################
# Max length of Item/Location "path name" in change list:
TREE_PATH_STR_MAX_LENGTH = 70
###############################################################################
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = False

Wyświetl plik

@ -1,5 +1,4 @@
import datetime
import logging
from unittest import mock
from bx_django_utils.test_utils.datetime import MockDatetimeGenerator
@ -8,16 +7,13 @@ from bx_django_utils.test_utils.html_assertion import (
assert_html_response_snapshot,
)
from bx_py_utils.test_utils.snapshot import assert_html_snapshot
from django.contrib.auth.models import User
from django.template.defaulttags import CsrfTokenNode, NowNode
from django.test import TestCase, override_settings
from django.utils import timezone
from django_tools.unittest_utils.mockup import ImageDummy
from model_bakery import baker
from inventory import __version__
from inventory.models import ItemImageModel, ItemModel
from inventory.permissions import get_or_create_normal_user_group
from inventory_project.tests.fixtures import get_normal_user
@ -120,10 +116,13 @@ class AdminTestCase(HtmlAssertionMixin, TestCase):
item = ItemModel.objects.first()
self.assert_messages(response, expected_messages=[
f'The Item “<a href="/admin/inventory/itemmodel/{item.pk}/change/"> - name</a>”'
' was added successfully.'
])
self.assert_messages(
response,
expected_messages=[
f'The Item “<a href="/admin/inventory/itemmodel/{item.pk}/change/">name</a>”'
' was added successfully.'
],
)
assert item.user_id == self.normaluser.pk
@ -171,10 +170,13 @@ class AdminTestCase(HtmlAssertionMixin, TestCase):
item = ItemModel.objects.first()
self.assert_messages(response, expected_messages=[
f'The Item “<a href="/admin/inventory/itemmodel/{item.pk}/change/"> - name</a>”'
' was added successfully.'
])
self.assert_messages(
response,
expected_messages=[
f'The Item “<a href="/admin/inventory/itemmodel/{item.pk}/change/">name</a>”'
' was added successfully.'
],
)
assert item.user_id == self.normaluser.pk
@ -211,52 +213,21 @@ class AdminTestCase(HtmlAssertionMixin, TestCase):
'main item 2', 'sub item 2.1', 'sub item 2.2',
]
# Default mode, without any GET parameter -> group "automatic":
with mock.patch.object(NowNode, 'render', return_value='MockedNowNode'), \
mock.patch.object(CsrfTokenNode, 'render', return_value='MockedCsrfTokenNode'), \
self.assertLogs(logger='inventory', level=logging.DEBUG) as logs:
with mock.patch.object(NowNode, 'render', return_value='MockedNowNode'), mock.patch.object(
CsrfTokenNode, 'render', return_value='MockedCsrfTokenNode'
):
response = self.client.get(
path='/admin/inventory/itemmodel/',
)
assert response.status_code == 200
self.assert_html_parts(response, parts=(
f'<title>Select Item to change | PyInventory v{__version__}</title>',
'<a href="/admin/inventory/itemmodel/00000000-0001-0000-0000-000000000000/change/">'
'main item 1</a>',
'<li><a href="/admin/inventory/itemmodel/00000000-0001-0001-0000-000000000000/change/">'
'sub item 1.1</a></li>',
))
assert logs.output == [
'INFO:inventory.admin.item:Group items: True (auto mode: True)',
'DEBUG:inventory.admin.item:Display sub items inline',
'DEBUG:inventory.admin.item:Display sub items inline'
]
assert_html_response_snapshot(response=response, validate=False)
# Search should disable grouping:
with mock.patch.object(NowNode, 'render', return_value='MockedNowNode'), \
mock.patch.object(CsrfTokenNode, 'render', return_value='MockedCsrfTokenNode'), \
self.assertLogs(logger='inventory', level=logging.DEBUG) as logs:
response = self.client.get(
path='/admin/inventory/itemmodel/?q=sub+item+2.',
)
assert response.status_code == 200
self.assert_html_parts(response, parts=(
'<input type="text" size="40" name="q" value="sub item 2." id="searchbar" autofocus>',
'2 results (<a href="?">6 total</a>)',
'<a href="/admin/inventory/itemmodel/00000000-0002-0001-0000-000000000000/change/">'
'sub item 2.1</a>',
'<a href="/admin/inventory/itemmodel/00000000-0002-0002-0000-000000000000/change/">'
'sub item 2.2</a>',
))
assert logs.output == [
# grouping disabled?
'INFO:inventory.admin.item:Group items: False (auto mode: True)'
]
self.assert_html_parts(
response,
parts=(
f'<title>Select Item to change | PyInventory v{__version__}</title>',
'<a href="/admin/inventory/itemmodel/00000000-0001-0000-0000-000000000000/change/">'
'<strong>main item 1</strong></a>',
'<a href="/admin/inventory/itemmodel/00000000-0001-0001-0000-000000000000/change/">'
'main item 1 <strong>sub item 1.1</strong></a>',
),
)
assert_html_response_snapshot(response=response, validate=False)

Wyświetl plik

@ -31,13 +31,6 @@
</label>
<input autofocus="" id="searchbar" name="q" size="40" type="text" value=""/>
<input type="submit" value="Search"/>
<span class="small quiet">
2 results (
<a href="?">
6 total
</a>
)
</span>
</div>
</form>
</div>
@ -74,8 +67,8 @@
<button class="button" name="index" title="Run the selected action" type="submit" value="0">
Go
</button>
<span class="action-counter" data-actions-icnt="2">
0 of 2 selected
<span class="action-counter" data-actions-icnt="6">
0 of 6 selected
</span>
</div>
<div class="results">
@ -91,6 +84,21 @@
<div class="clear">
</div>
</th>
<th class="sortable column-item sorted ascending" scope="col">
<div class="sortoptions">
<a class="sortremove" href="?o=" title="Remove from sorting">
</a>
<a class="toggle ascending" href="?o=-1" title="Toggle sorting">
</a>
</div>
<div class="text">
<a href="?o=-1">
Item
</a>
</div>
<div class="clear">
</div>
</th>
<th class="column-_tagulous_display_kind" scope="col">
<div class="text">
<span>
@ -109,18 +117,9 @@
<div class="clear">
</div>
</th>
<th class="column-column_item" scope="col">
<div class="text">
<span>
Items
</span>
</div>
<div class="clear">
</div>
</th>
<th class="sortable column-location" scope="col">
<div class="text">
<a href="?o=4">
<a href="?o=4.1">
Location
</a>
</div>
@ -129,7 +128,7 @@
</th>
<th class="sortable column-received_date" scope="col">
<div class="text">
<a href="?o=5">
<a href="?o=5.1">
Received date
</a>
</div>
@ -138,7 +137,7 @@
</th>
<th class="sortable column-update_dt" scope="col">
<div class="text">
<a href="?o=6">
<a href="?o=6.1">
Last update
</a>
</div>
@ -152,29 +151,17 @@
<td class="action-checkbox">
<input class="action-select" name="_selected_action" type="checkbox" value="00000000-0001-0000-0000-000000000000"/>
</td>
<td class="field-item">
<a href="/admin/inventory/itemmodel/00000000-0001-0000-0000-000000000000/change/">
<strong>
main item 1
</strong>
</a>
</td>
<td class="field-_tagulous_display_kind">
</td>
<td class="field-_tagulous_display_producer">
</td>
<td class="field-column_item">
<strong>
<a href="/admin/inventory/itemmodel/00000000-0001-0000-0000-000000000000/change/">
main item 1
</a>
</strong>
<ul>
<li>
<a href="/admin/inventory/itemmodel/00000000-0001-0001-0000-000000000000/change/">
sub item 1.1
</a>
</li>
<li>
<a href="/admin/inventory/itemmodel/00000000-0001-0002-0000-000000000000/change/">
sub item 1.2
</a>
</li>
</ul>
</td>
<td class="field-location nowrap">
-
</td>
@ -187,30 +174,70 @@
</tr>
<tr>
<td class="action-checkbox">
<input class="action-select" name="_selected_action" type="checkbox" value="00000000-0002-0000-0000-000000000000"/>
<input class="action-select" name="_selected_action" type="checkbox" value="00000000-0001-0001-0000-000000000000"/>
</td>
<td class="field-item">
<a href="/admin/inventory/itemmodel/00000000-0001-0001-0000-000000000000/change/">
main item 1
<strong>
sub item 1.1
</strong>
</a>
</td>
<td class="field-_tagulous_display_kind">
</td>
<td class="field-_tagulous_display_producer">
</td>
<td class="field-column_item">
<strong>
<a href="/admin/inventory/itemmodel/00000000-0002-0000-0000-000000000000/change/">
<td class="field-location nowrap">
-
</td>
<td class="field-received_date nowrap">
-
</td>
<td class="field-update_dt nowrap">
Jan. 1, 2000, 1:02 a.m.
</td>
</tr>
<tr>
<td class="action-checkbox">
<input class="action-select" name="_selected_action" type="checkbox" value="00000000-0001-0002-0000-000000000000"/>
</td>
<td class="field-item">
<a href="/admin/inventory/itemmodel/00000000-0001-0002-0000-000000000000/change/">
main item 1
<strong>
sub item 1.2
</strong>
</a>
</td>
<td class="field-_tagulous_display_kind">
</td>
<td class="field-_tagulous_display_producer">
</td>
<td class="field-location nowrap">
-
</td>
<td class="field-received_date nowrap">
-
</td>
<td class="field-update_dt nowrap">
Jan. 1, 2000, 1:03 a.m.
</td>
</tr>
<tr>
<td class="action-checkbox">
<input class="action-select" name="_selected_action" type="checkbox" value="00000000-0002-0000-0000-000000000000"/>
</td>
<td class="field-item">
<a href="/admin/inventory/itemmodel/00000000-0002-0000-0000-000000000000/change/">
<strong>
main item 2
</a>
</strong>
<ul>
<li>
<a href="/admin/inventory/itemmodel/00000000-0002-0001-0000-000000000000/change/">
sub item 2.1
</a>
</li>
<li>
<a href="/admin/inventory/itemmodel/00000000-0002-0002-0000-000000000000/change/">
sub item 2.2
</a>
</li>
</ul>
</strong>
</a>
</td>
<td class="field-_tagulous_display_kind">
</td>
<td class="field-_tagulous_display_producer">
</td>
<td class="field-location nowrap">
-
@ -222,11 +249,63 @@
Jan. 1, 2000, 1:04 a.m.
</td>
</tr>
<tr>
<td class="action-checkbox">
<input class="action-select" name="_selected_action" type="checkbox" value="00000000-0002-0001-0000-000000000000"/>
</td>
<td class="field-item">
<a href="/admin/inventory/itemmodel/00000000-0002-0001-0000-000000000000/change/">
main item 2
<strong>
sub item 2.1
</strong>
</a>
</td>
<td class="field-_tagulous_display_kind">
</td>
<td class="field-_tagulous_display_producer">
</td>
<td class="field-location nowrap">
-
</td>
<td class="field-received_date nowrap">
-
</td>
<td class="field-update_dt nowrap">
Jan. 1, 2000, 1:05 a.m.
</td>
</tr>
<tr>
<td class="action-checkbox">
<input class="action-select" name="_selected_action" type="checkbox" value="00000000-0002-0002-0000-000000000000"/>
</td>
<td class="field-item">
<a href="/admin/inventory/itemmodel/00000000-0002-0002-0000-000000000000/change/">
main item 2
<strong>
sub item 2.2
</strong>
</a>
</td>
<td class="field-_tagulous_display_kind">
</td>
<td class="field-_tagulous_display_producer">
</td>
<td class="field-location nowrap">
-
</td>
<td class="field-received_date nowrap">
-
</td>
<td class="field-update_dt nowrap">
Jan. 1, 2000, 1:06 a.m.
</td>
</tr>
</tbody>
</table>
</div>
<p class="paginator">
2 Items
6 Items
</p>
</form>
</div>
@ -235,17 +314,27 @@
Filter
</h2>
<h3>
By Group Items
By Limit tree depth
</h3>
<ul>
<li class="selected">
<a href="?" title="Automatic">
Automatic
<a href="?" title="All">
All
</a>
</li>
<li>
<a href="?grouping=no" title="No">
No
<a href="?level=1" title="Only root">
Only root
</a>
</li>
<li>
<a href="?level=2" title="Root + first sub">
Root + first sub
</a>
</li>
<li>
<a href="?level=3" title="Root + first + second sub">
Root + first + second sub
</a>
</li>
</ul>

Wyświetl plik

@ -171,6 +171,7 @@
</a>
</div>
<div class="help">
Locations can be nested. Example: The box 12 in cupboard 3
</div>
</div>
</div>

Wyświetl plik

@ -2,7 +2,7 @@
<html dir="ltr" lang="en">
<head>
<title>
Change Item | PyInventory v0.13.1
Change Item | PyInventory v0.14.0rc3
</title>
<link href="/static/admin/css/base.css" rel="stylesheet" type="text/css"/>
<link href="/static/admin/css/nav_sidebar.css" rel="stylesheet" type="text/css"/>
@ -80,7 +80,7 @@
<div id="branding">
<h1 id="site-name">
<a href="/admin/">
PyInventory v0.13.1
PyInventory v0.14.0rc3
</a>
</h1>
</div>
@ -116,7 +116,7 @@
<a href="/admin/inventory/itemmodel/">
Items
</a>
- A new Name!
name
</div>
<div class="main shifted" id="main">
<button aria-label="Toggle navigation" class="sticky toggle-nav-sidebar" id="toggle-nav-sidebar">
@ -175,7 +175,7 @@
Change Item
</h1>
<h2>
- A new Name!
name
</h2>
<div id="content-main">
<ul class="object-tools">
@ -367,7 +367,7 @@
---------
</option>
<option value="<removed-UUID>">
- name
name
</option>
</select>
<a class="related-widget-wrapper-link change-related" data-href-template="/admin/inventory/itemmodel/__fk__/change/?_to_field=id&_popup=1" id="change_id_parent" title="Change selected Item">
@ -381,6 +381,7 @@
</a>
</div>
<div class="help">
Locations can be nested. Example: The box 12 in cupboard 3
</div>
</div>
</div>