Group item: default "automatic" mode and can be disabled by filter action

Default admin change list group mode is "automatic":

* disable the grouping on every GET parameter (e.g.: search or use a filter)

User can also disable the grouping by click on "no" in change list filters ;)
pull/63/head
JensDiemer 2021-09-29 19:17:52 +02:00
rodzic c84bd9f32d
commit 10d31496f0
11 zmienionych plików z 489 dodań i 26 usunięć

Wyświetl plik

@ -157,6 +157,7 @@ Files are separated into: "/src/" and "/development/"
== history
* [[https://github.com/jedie/PyInventory/compare/v0.9.4...master|compare v0.9.4...master]] **dev**
** Group item: default "automatic" mode and can be disabled by filter action
** tbc
* [[https://github.com/jedie/PyInventory/compare/v0.9.3...v0.9.4|v0.9.4 - 15.09.2021]]
** Pin {{{psycopg < 2.9}}} because of https://github.com/psycopg/psycopg2/issues/1293

Wyświetl plik

@ -220,6 +220,8 @@ history
* `compare v0.9.4...master <https://github.com/jedie/PyInventory/compare/v0.9.4...master>`_ **dev**
* Group item: default "automatic" mode and can be disabled by filter action
* tbc
* `v0.9.4 - 15.09.2021 <https://github.com/jedie/PyInventory/compare/v0.9.3...v0.9.4>`_
@ -401,4 +403,4 @@ donation
------------
``Note: this file is generated from README.creole 2021-09-15 21:28:39 with "python-creole"``
``Note: this file is generated from README.creole 2021-09-29 19:16:20 with "python-creole"``

Wyświetl plik

@ -3,6 +3,7 @@ from reversion_compare.admin import CompareVersionAdmin
class BaseUserAdmin(CompareVersionAdmin):
def get_changelist(self, request, **kwargs):
self.request = request
self.user = request.user
return super().get_changelist(request, **kwargs)

Wyświetl plik

@ -1,7 +1,8 @@
import logging
import tagulous
from adminsortable2.admin import SortableInlineAdminMixin
from django.contrib import admin
from django.contrib.admin.views.main import ChangeList
from django.template.loader import render_to_string
from django.utils.html import format_html
from django.utils.translation import ugettext_lazy as _
@ -14,6 +15,9 @@ from inventory.models import ItemLinkModel, ItemModel
from inventory.models.item import ItemFileModel, ItemImageModel
logger = logging.getLogger(__name__)
class UserInlineMixin:
def get_queryset(self, request):
qs = super().get_queryset(request)
@ -62,14 +66,48 @@ class ItemModelResource(ModelResource):
model = ItemModel
class ItemModelChangeList(ChangeList):
def get_queryset(self, request):
"""
List always the base instances
"""
qs = super().get_queryset(request)
qs = qs.filter(parent__isnull=True)
return qs
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)
@ -88,13 +126,19 @@ class ItemModelAdmin(ImportExportMixin, BaseUserAdmin):
return qs
def column_item(self, obj):
# TODO: annotate "sub_items" !
qs = ItemModel.objects.filter(user=self.user)
qs = qs.filter(parent=obj).sort()
context = {
'base_item': obj,
'sub_items': qs
}
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,
@ -111,7 +155,7 @@ class ItemModelAdmin(ImportExportMixin, BaseUserAdmin):
)
ordering = ('kind', 'producer', 'name')
list_display_links = None
list_filter = ('kind', 'location', 'producer', 'tags')
list_filter = (GroupItemsListFilter, 'kind', 'location', 'producer', 'tags')
search_fields = ('name', 'description', 'kind__name', 'tags__name')
fieldsets = (
(_('Internals'), {
@ -156,9 +200,5 @@ class ItemModelAdmin(ImportExportMixin, BaseUserAdmin):
readonly_fields = ('id', 'create_dt', 'update_dt', 'user')
inlines = (ItemImageModelInline, ItemFileModelInline, ItemLinkModelInline)
def get_changelist(self, request, **kwargs):
self.user = request.user
return ItemModelChangeList
tagulous.admin.enhance(ItemModel, ItemModelAdmin)

Plik binarny nie jest wyświetlany.

Wyświetl plik

@ -7,8 +7,8 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-04-28 18:31+0200\n"
"PO-Revision-Date: 2021-04-28 18:29+0200\n"
"POT-Creation-Date: 2021-09-29 19:19+0200\n"
"PO-Revision-Date: 2021-09-29 19:19+0200\n"
"Last-Translator: Jens Diemer\n"
"Language-Team: \n"
"Language: de\n"
@ -18,6 +18,15 @@ msgstr ""
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: Poedit 2.3\n"
msgid "Group Items"
msgstr "Gegenstände Gruppieren"
msgid "Automatic"
msgstr "Automatisch"
msgid "No"
msgstr ""
msgid "ItemModel.verbose_name_plural"
msgstr "Gegenstände"
@ -188,7 +197,7 @@ msgid "ItemFileModel.file.verbose_name"
msgstr "Datei"
msgid "ItemFileModel.file.help_text"
msgstr ""
msgstr " "
msgid "ItemFileModel.verbose_name"
msgstr "Datei"

Wyświetl plik

@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-04-28 18:31+0200\n"
"POT-Creation-Date: 2021-09-29 19:19+0200\n"
"PO-Revision-Date: 2021-04-28 18:30+0200\n"
"Last-Translator: Jens Diemer\n"
"Language-Team: \n"
@ -18,6 +18,15 @@ msgstr ""
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: Poedit 2.3\n"
msgid "Group Items"
msgstr ""
msgid "Automatic"
msgstr ""
msgid "No"
msgstr ""
msgid "ItemModel.verbose_name_plural"
msgstr "Items"

Wyświetl plik

@ -1,4 +1,6 @@
from inventory_project.settings.base import * # noqa
# flake8: noqa: E405, F403
from inventory_project.settings.base import *
DATABASES = {
@ -11,3 +13,7 @@ DATABASES = {
SECRET_KEY = 'No individual secret for tests ;)'
DEBUG = True
LOGGING['formatters']['colored']['format'] = (
'%(log_color)s%(name)s %(levelname)8s %(cut_path)s:%(lineno)-3s %(message)s'
)

Wyświetl plik

@ -1,6 +1,13 @@
from bx_django_utils.test_utils.html_assertion import HtmlAssertionMixin
import datetime
import logging
from unittest import mock
from bx_django_utils.test_utils.datetime import MockDatetimeGenerator
from bx_django_utils.test_utils.html_assertion import HtmlAssertionMixin, assert_html_response_snapshot
from django.contrib.auth.models import User
from django.template.defaulttags import CsrfTokenNode
from django.test import TestCase
from django.utils import timezone
from django_tools.unittest_utils.mockup import ImageDummy
from model_bakery import baker
@ -22,7 +29,8 @@ class AdminTestCase(HtmlAssertionMixin, TestCase):
@classmethod
def setUpTestData(cls):
cls.normaluser = baker.make(
User, is_staff=True, is_active=True, is_superuser=False
User, username='NormalUser',
is_staff=True, is_active=True, is_superuser=False
)
assert cls.normaluser.user_permissions.count() == 0
group = get_or_create_normal_user_group()[0]
@ -136,3 +144,78 @@ class AdminTestCase(HtmlAssertionMixin, TestCase):
assert image.name == 'test.png'
assert image.item == item
assert image.user_id == self.normaluser.pk
def test_auto_group_items(self):
self.client.force_login(self.normaluser)
offset = datetime.timedelta(minutes=1)
with mock.patch.object(timezone, 'now', MockDatetimeGenerator(offset=offset)):
for main_item_no in range(1, 3):
main_item = ItemModel.objects.create(
id=f'00000000-000{main_item_no}-0000-0000-000000000000',
user=self.normaluser,
name=f'main item {main_item_no}'
)
main_item.full_clean()
for sub_item_no in range(1, 3):
sub_item = ItemModel.objects.create(
id=f'00000000-000{main_item_no}-000{sub_item_no}-0000-000000000000',
user=self.normaluser,
parent=main_item,
name=f'sub item {main_item_no}.{sub_item_no}'
)
sub_item.full_clean()
names = list(ItemModel.objects.order_by('id').values_list('name', flat=True))
assert names == [
'main item 1', 'sub item 1.1', 'sub item 1.2',
'main item 2', 'sub item 2.1', 'sub item 2.2',
]
# Default mode, without any GET parameter -> group "automatic":
with 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/',
)
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)
# Search should disable grouping:
with 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)'
]
assert_html_response_snapshot(response=response)

Wyświetl plik

@ -0,0 +1,160 @@
<!DOCTYPE html>
<html lang="en" >
<head>
<title>Select Item to change | PyInventory v0.9.4</title>
<link rel="stylesheet" type="text/css" href="/static/admin/css/base.css">
<link rel="stylesheet" type="text/css" href="/static/admin/css/changelists.css">
<script type="text/javascript" src="/admin/jsi18n/"></script>
<meta name="google" content="notranslate">
<meta name="robots" content="noindex,nofollow" />
<link rel="stylesheet" type="text/css" href="/static/inventory.css">
<script type="text/javascript" src="/static/admin/js/vendor/jquery/jquery.min.js"></script>
<script type="text/javascript" src="/static/admin/js/jquery.init.js"></script>
<script type="text/javascript" src="/static/admin/js/core.js"></script>
<script type="text/javascript" src="/static/admin/js/admin/RelatedObjectLookups.js"></script>
<script type="text/javascript" src="/static/admin/js/actions.min.js"></script>
<script type="text/javascript" src="/static/admin/js/urlify.js"></script>
<script type="text/javascript" src="/static/admin/js/prepopulate.min.js"></script>
<script type="text/javascript" src="/static/admin/js/vendor/xregexp/xregexp.min.js"></script>
<meta name="viewport" content="user-scalable=no, width=device-width, initial-scale=1.0, maximum-scale=1.0">
<link rel="stylesheet" type="text/css" href="/static/admin/css/responsive.css">
<meta name="robots" content="NONE,NOARCHIVE">
</head>
<body class=" app-inventory model-itemmodel change-list"
data-admin-utc-offset="7200">
<!-- Container -->
<div id="container">
<!-- Header -->
<div id="header">
<div id="branding">
<h1 id="site-name">
<a href="/admin/">PyInventory v0.9.4</a>
</h1>
</div>
<div id="user-tools">
Welcome,
<strong>NormalUser</strong>.
<a href="/">View site</a> /
<a href="/admin/password_change/">Change password</a> /
<a href="/admin/logout/">Log out</a>
</div>
</div>
<!-- END Header -->
<div class="breadcrumbs">
<a href="/admin/">Home</a>
&rsaquo; <a href="/admin/inventory/">Inventory</a>
&rsaquo; Items
</div>
<!-- Content -->
<div id="content" class="flex">
<h1>Select Item to change</h1>
<div id="content-main">
<ul class="object-tools">
<li><a href='/admin/inventory/itemmodel/import/' class="import_link">Import</a></li>
<li><a href="/admin/inventory/itemmodel/export/?" class="export_link">Export</a></li>
<li>
<a href="/admin/inventory/itemmodel/add/" class="addlink">
Add Item
</a>
</li>
</ul>
<div class="module filtered" id="changelist">
<div id="toolbar"><form id="changelist-search" method="get">
<div><!-- DIV needed for valid HTML -->
<label for="searchbar"><img src="/static/admin/img/search.svg" alt="Search"></label>
<input type="text" size="40" name="q" value="" id="searchbar" autofocus>
<input type="submit" value="Search">
<span class="small quiet">2 results (<a href="?">6 total</a>)</span>
</div>
</form></div>
<div class="xfull">
<ul class="toplinks">
<li class="date-back"><a href="?create_dt__year=2000">&lsaquo; 2000</a></li>
<li> <a href="?create_dt__day=1&amp;create_dt__month=1&amp;create_dt__year=2000">January 1</a></li>
</ul><br class="clear">
</div>
<div id="changelist-filter">
<h2>Filter</h2>
<h3> By Group Items </h3>
<ul>
<li class="selected">
<a href="?" title="Automatic">Automatic</a></li>
<li>
<a href="?grouping=no" title="No">No</a></li>
</ul>
</div>
<form id="changelist-form" method="post" novalidate>MockedCsrfTokenNode
<div class="actions">
<label>Action: <select name="action" required>
<option value="" selected>---------</option>
<option value="delete_selected">Delete selected Items</option>
</select></label><input type="hidden" name="select_across" value="0" class="select-across">
<button type="submit" class="button" title="Run the selected action" name="index" value="0">Go</button>
<span class="action-counter" data-actions-icnt="2">0 of 2 selected</span>
</div>
<div class="results">
<table id="result_list">
<thead>
<tr>
<th scope="col" class="action-checkbox-column">
<div class="text"><span><input type="checkbox" id="action-toggle"></span></div>
<div class="clear"></div>
</th>
<th scope="col" class="column-_tagulous_display_kind">
<div class="text"><span>Kind</span></div>
<div class="clear"></div>
</th>
<th scope="col" class="column-_tagulous_display_producer">
<div class="text"><span>Producer</span></div>
<div class="clear"></div>
</th>
<th scope="col" class="column-column_item">
<div class="text"><span>Items</span></div>
<div class="clear"></div>
</th>
<th scope="col" class="sortable column-location">
<div class="text"><a href="?o=4">Location</a></div>
<div class="clear"></div>
</th>
<th scope="col" class="sortable column-received_date">
<div class="text"><a href="?o=5">Received date</a></div>
<div class="clear"></div>
</th>
<th scope="col" class="sortable column-update_dt">
<div class="text"><a href="?o=6">Last update</a></div>
<div class="clear"></div>
</th>
</tr>
</thead>
<tbody>
<tr class="row1"><td class="action-checkbox"><input type="checkbox" name="_selected_action" value="00000000-0001-0000-0000-000000000000" class="action-select"></td><td class="field-_tagulous_display_kind">&nbsp;</td><td class="field-_tagulous_display_producer">&nbsp;</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><td class="field-received_date nowrap">-</td><td class="field-update_dt nowrap">Jan. 1, 2000, 1:01 a.m.</td></tr>
<tr class="row2"><td class="action-checkbox"><input type="checkbox" name="_selected_action" value="00000000-0002-0000-0000-000000000000" class="action-select"></td><td class="field-_tagulous_display_kind">&nbsp;</td><td class="field-_tagulous_display_producer">&nbsp;</td><td class="field-column_item"><strong><a href="/admin/inventory/itemmodel/00000000-0002-0000-0000-000000000000/change/">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>
</td><td class="field-location nowrap">-</td><td class="field-received_date nowrap">-</td><td class="field-update_dt nowrap">Jan. 1, 2000, 1:04 a.m.</td></tr>
</tbody>
</table>
</div>
<p class="paginator">
2 Items
</p>
</form>
</div>
</div>
<br class="clear">
</div>
<!-- END Content -->
<div id="footer">
<a href="https://github.com/jedie/PyInventory">https://github.com/jedie/PyInventory</a>
</div>
</div>
<!-- END Container -->
</body>
</html>

Wyświetl plik

@ -0,0 +1,152 @@
<!DOCTYPE html>
<html lang="en" >
<head>
<title>Select Item to change | PyInventory v0.9.4</title>
<link rel="stylesheet" type="text/css" href="/static/admin/css/base.css">
<link rel="stylesheet" type="text/css" href="/static/admin/css/changelists.css">
<script type="text/javascript" src="/admin/jsi18n/"></script>
<meta name="google" content="notranslate">
<meta name="robots" content="noindex,nofollow" />
<link rel="stylesheet" type="text/css" href="/static/inventory.css">
<script type="text/javascript" src="/static/admin/js/vendor/jquery/jquery.min.js"></script>
<script type="text/javascript" src="/static/admin/js/jquery.init.js"></script>
<script type="text/javascript" src="/static/admin/js/core.js"></script>
<script type="text/javascript" src="/static/admin/js/admin/RelatedObjectLookups.js"></script>
<script type="text/javascript" src="/static/admin/js/actions.min.js"></script>
<script type="text/javascript" src="/static/admin/js/urlify.js"></script>
<script type="text/javascript" src="/static/admin/js/prepopulate.min.js"></script>
<script type="text/javascript" src="/static/admin/js/vendor/xregexp/xregexp.min.js"></script>
<meta name="viewport" content="user-scalable=no, width=device-width, initial-scale=1.0, maximum-scale=1.0">
<link rel="stylesheet" type="text/css" href="/static/admin/css/responsive.css">
<meta name="robots" content="NONE,NOARCHIVE">
</head>
<body class=" app-inventory model-itemmodel change-list"
data-admin-utc-offset="7200">
<!-- Container -->
<div id="container">
<!-- Header -->
<div id="header">
<div id="branding">
<h1 id="site-name">
<a href="/admin/">PyInventory v0.9.4</a>
</h1>
</div>
<div id="user-tools">
Welcome,
<strong>NormalUser</strong>.
<a href="/">View site</a> /
<a href="/admin/password_change/">Change password</a> /
<a href="/admin/logout/">Log out</a>
</div>
</div>
<!-- END Header -->
<div class="breadcrumbs">
<a href="/admin/">Home</a>
&rsaquo; <a href="/admin/inventory/">Inventory</a>
&rsaquo; Items
</div>
<!-- Content -->
<div id="content" class="flex">
<h1>Select Item to change</h1>
<div id="content-main">
<ul class="object-tools">
<li><a href='/admin/inventory/itemmodel/import/' class="import_link">Import</a></li>
<li><a href="/admin/inventory/itemmodel/export/?q=sub+item+2." class="export_link">Export</a></li>
<li>
<a href="/admin/inventory/itemmodel/add/?_changelist_filters=q%3Dsub%2Bitem%2B2." class="addlink">
Add Item
</a>
</li>
</ul>
<div class="module filtered" id="changelist">
<div id="toolbar"><form id="changelist-search" method="get">
<div><!-- DIV needed for valid HTML -->
<label for="searchbar"><img src="/static/admin/img/search.svg" alt="Search"></label>
<input type="text" size="40" name="q" value="sub item 2." id="searchbar" autofocus>
<input type="submit" value="Search">
<span class="small quiet">2 results (<a href="?">6 total</a>)</span>
</div>
</form></div>
<div class="xfull">
<ul class="toplinks">
<li class="date-back"><a href="?create_dt__year=2000&amp;q=sub+item+2.">&lsaquo; 2000</a></li>
<li> <a href="?create_dt__day=1&amp;create_dt__month=1&amp;create_dt__year=2000&amp;q=sub+item+2.">January 1</a></li>
</ul><br class="clear">
</div>
<div id="changelist-filter">
<h2>Filter</h2>
<h3> By Group Items </h3>
<ul>
<li class="selected">
<a href="?q=sub+item+2." title="Automatic">Automatic</a></li>
<li>
<a href="?grouping=no&amp;q=sub+item+2." title="No">No</a></li>
</ul>
</div>
<form id="changelist-form" method="post" novalidate>MockedCsrfTokenNode
<div class="actions">
<label>Action: <select name="action" required>
<option value="" selected>---------</option>
<option value="delete_selected">Delete selected Items</option>
</select></label><input type="hidden" name="select_across" value="0" class="select-across">
<button type="submit" class="button" title="Run the selected action" name="index" value="0">Go</button>
<span class="action-counter" data-actions-icnt="2">0 of 2 selected</span>
</div>
<div class="results">
<table id="result_list">
<thead>
<tr>
<th scope="col" class="action-checkbox-column">
<div class="text"><span><input type="checkbox" id="action-toggle"></span></div>
<div class="clear"></div>
</th>
<th scope="col" class="column-_tagulous_display_kind">
<div class="text"><span>Kind</span></div>
<div class="clear"></div>
</th>
<th scope="col" class="column-_tagulous_display_producer">
<div class="text"><span>Producer</span></div>
<div class="clear"></div>
</th>
<th scope="col" class="column-column_item">
<div class="text"><span>Items</span></div>
<div class="clear"></div>
</th>
<th scope="col" class="sortable column-location">
<div class="text"><a href="?o=4&amp;q=sub+item+2.">Location</a></div>
<div class="clear"></div>
</th>
<th scope="col" class="sortable column-received_date">
<div class="text"><a href="?o=5&amp;q=sub+item+2.">Received date</a></div>
<div class="clear"></div>
</th>
<th scope="col" class="sortable column-update_dt">
<div class="text"><a href="?o=6&amp;q=sub+item+2.">Last update</a></div>
<div class="clear"></div>
</th>
</tr>
</thead>
<tbody>
<tr class="row1"><td class="action-checkbox"><input type="checkbox" name="_selected_action" value="00000000-0002-0001-0000-000000000000" class="action-select"></td><td class="field-_tagulous_display_kind">&nbsp;</td><td class="field-_tagulous_display_producer">&nbsp;</td><td class="field-column_item"><strong><a href="/admin/inventory/itemmodel/00000000-0002-0001-0000-000000000000/change/">sub item 2.1</a></strong>
</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 class="row2"><td class="action-checkbox"><input type="checkbox" name="_selected_action" value="00000000-0002-0002-0000-000000000000" class="action-select"></td><td class="field-_tagulous_display_kind">&nbsp;</td><td class="field-_tagulous_display_producer">&nbsp;</td><td class="field-column_item"><strong><a href="/admin/inventory/itemmodel/00000000-0002-0002-0000-000000000000/change/">sub item 2.2</a></strong>
</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
</p>
</form>
</div>
</div>
<br class="clear">
</div>
<!-- END Content -->
<div id="footer">
<a href="https://github.com/jedie/PyInventory">https://github.com/jedie/PyInventory</a>
</div>
</div>
<!-- END Container -->
</body>
</html>