Allow for customization of workflow

This commit is contained in:
Johannes Schriewer 2020-12-22 21:35:05 +01:00
parent 444aeb37e6
commit 326de9444d
34 changed files with 315 additions and 129 deletions

View file

@ -4,4 +4,5 @@ from .layout import LayoutAdmin
from .item import ItemAdmin
from .manufacturer import ManufacturerAdmin
from .form_factor import FormFactorAdmin
from .tag import TagAdmin
from .tag import TagAdmin
from .settings import SettingsAdmin

View file

@ -0,0 +1,21 @@
from typing import Optional, TypeVar
from django.db.models import Model
from django.http.request import HttpRequest
from django.contrib import admin
from inventory.models import Settings
_ModelT = TypeVar("_ModelT", bound=Model)
class SettingsAdmin(admin.ModelAdmin):
def has_add_permission(self, request: HttpRequest) -> bool:
return False
def has_delete_permission(self, request: HttpRequest, obj: Optional[_ModelT] = None) -> bool:
return False
admin.site.register(Settings, SettingsAdmin)

View file

@ -0,0 +1,28 @@
# Generated by Django 3.1.4 on 2020-12-22 18:58
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('inventory', '0003_item_size'),
]
operations = [
migrations.AddField(
model_name='area',
name='show_sub_area',
field=models.BooleanField(default=True, help_text='Allow sub-areas to be defined in this area'),
),
migrations.AddField(
model_name='workshop',
name='show_boxes',
field=models.BooleanField(default=True, help_text='Allow boxes to be defined directly in this workshop'),
),
migrations.AlterField(
model_name='item',
name='size',
field=models.PositiveIntegerField(default=1, help_text='Number of sub-compartments this item takes up'),
),
]

View file

@ -0,0 +1,27 @@
# Generated by Django 3.1.4 on 2020-12-22 19:29
from django.db import migrations, models
import django.db.models.deletion
def create_settings(apps, schema_editor):
Settings = apps.get_model('inventory', 'Settings')
Settings.objects.create(default_container=None)
class Migration(migrations.Migration):
dependencies = [
('inventory', '0004_auto_20201222_1858'),
]
operations = [
migrations.CreateModel(
name='Settings',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('default_container', models.ForeignKey(blank=True, default=None, help_text='Default container to display when calling the index page', null=True, on_delete=django.db.models.deletion.SET_NULL, to='inventory.container')),
],
),
migrations.RunPython(create_settings)
]

View file

@ -9,3 +9,4 @@ from .manufacturer import Manufacturer
from .tag import Tag
from .workshop import Workshop
from .container import Container, CanBeContained
from .settings import Settings

View file

@ -6,6 +6,7 @@ from .container import Container, CanBeContained
class Area(CanBeContained, Container):
name = models.CharField(max_length=255, unique=True)
description = models.CharField(max_length=4096)
show_sub_area = models.BooleanField(default=True, help_text="Allow sub-areas to be defined in this area")
created_at = models.DateTimeField(auto_now_add=True)
changed_at = models.DateTimeField(auto_now=True)

View file

@ -1,4 +1,3 @@
from django.urls import reverse
from django.db import models
from django.apps import apps
@ -30,6 +29,9 @@ class Container(models.Model):
@property
def url(self):
_, obj = self.subclass
if obj is not None:
return obj.url
return None
@ -48,4 +50,4 @@ class CanBeContained(models.Model):
@property
def container_url(self):
_, obj = self.container.subclass
return obj.url
return obj.url

View file

@ -1,5 +1,4 @@
from django.db import models
from django.contrib.postgres.fields import JSONField, ArrayField
from .container import CanBeContained
@ -23,4 +22,4 @@ class Item(CanBeContained):
tags = models.ManyToManyField('inventory.Tag', blank=True)
def __str__(self):
return self.name
return self.name

View file

@ -11,4 +11,4 @@ class Manufacturer(models.Model):
icon = models.ImageField(null=True, blank=True)
def __str__(self):
return self.name
return self.name

View file

@ -0,0 +1,18 @@
from django.db import models
class Settings(models.Model):
default_container = models.ForeignKey(
'inventory.Container',
on_delete=models.SET_NULL,
default=None,
null=True,
blank=True,
help_text='Default container to display when calling the index page'
)
def __str__(self):
return 'Settings'
class Meta:
verbose_name_plural = 'Settings'

View file

@ -6,6 +6,7 @@ from .container import Container
class Workshop(Container):
name = models.CharField(max_length=255, unique=True)
description = models.CharField(max_length=4096)
show_boxes = models.BooleanField(default=True, help_text="Allow boxes to be defined directly in this workshop")
created_at = models.DateTimeField(auto_now_add=True)
changed_at = models.DateTimeField(auto_now=True)

View file

@ -10,12 +10,15 @@ tr {
right: 0;
top: 0;
bottom: 0;
min-width: 100px;
min-height: 75px;
}
.cell {
flex-grow: 1;
display: inline-block;
position: relative;
}
.cell:nth-child(2n) {

View file

@ -54,11 +54,11 @@ table.box th {
nav {
display: flex;
align-items: baseline;
justify-content: space-between;
background-color: #292981;
color: #ffffff;
padding: 20px;
margin-bottom: 20px;
}
nav form {
@ -73,6 +73,12 @@ nav button {
padding: 8px 10px 8px 10px;
}
nav .icon-only-button {
border: none;
background-color: transparent;
}
h1 {
margin-top: 20px;
}
@ -103,29 +109,29 @@ nav .icon {
filter: invert();
}
main {
margin: auto;
nav ul {
margin: 0;
padding: 0;
display: inline-block;
}
@media (min-width: 1025px) {
h1, h2 {
width: 80%;
}
nav ul li {
display: inline-block;
text-indent: 0;
margin: 0;
padding: 0 10px 0 0;
}
main {
width: 80%;
}
nav .spacer {
flex-grow: 2;
}
main {
margin: auto;
padding: 20px;
}
@media (max-width: 1024px) {
h1, h2 {
width: 95%;
}
main {
width: 95%;
}
body {
font-size: 12px;
}
@ -144,4 +150,24 @@ h2 .icon {
width: 25px;
height: 25px;
top: 3px;
}
}
ul.nav-list {
display: flex;
flex-direction: column;
flex-wrap: wrap;
height: 100%;
width: 30%;
margin: 0;
padding: 0;
}
ul.nav-list li {
display: flex;
justify-content: space-between;
border: 1px solid black;
margin-bottom: -1px;
padding: 10px;
text-indent: 0;
list-style-type: none;
}

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><!-- Font Awesome Free 5.15.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><path d="M259.3 17.8L194 150.2 47.9 171.5c-26.2 3.8-36.7 36.1-17.7 54.6l105.7 103-25 145.5c-4.5 26.3 23.2 46 46.4 33.7L288 439.6l130.7 68.7c23.2 12.2 50.9-7.4 46.4-33.7l-25-145.5 105.7-103c19-18.5 8.5-50.8-17.7-54.6L382 150.2 316.7 17.8c-11.7-23.6-45.6-23.9-57.4 0z"/></svg>

After

Width:  |  Height:  |  Size: 516 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><!-- Font Awesome Free 5.15.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><path d="M528.1 171.5L382 150.2 316.7 17.8c-11.7-23.6-45.6-23.9-57.4 0L194 150.2 47.9 171.5c-26.2 3.8-36.7 36.1-17.7 54.6l105.7 103-25 145.5c-4.5 26.3 23.2 46 46.4 33.7L288 439.6l130.7 68.7c23.2 12.2 50.9-7.4 46.4-33.7l-25-145.5 105.7-103c19-18.5 8.5-50.8-17.7-54.6zM388.6 312.3l23.7 138.4L288 385.4l-124.3 65.3 23.7-138.4-100.6-98 139-20.2 62.2-126 62.2 126 139 20.2-100.6 98z"/></svg>

After

Width:  |  Height:  |  Size: 628 B

View file

@ -13,6 +13,15 @@
{% block header_bar %}
{% endblock %}
</h2>
<div class="spacer">
</div>
<ul>
{% block header_icons %}
{% endblock %}
</ul>
{% if user.is_authenticated %}
<form method="POST" action="{% url "logout" %}">
{% csrf_token %}

View file

@ -9,28 +9,34 @@
<span class="small">{{ area.description}}</span>
{% endblock %}
{% block header_icons %}
{% include 'inventory/set_index.html' with item=area is_index=is_index %}
{% endblock %}
{% block content %}
<h3>Areas</h3>
<ul>
{% for a in area.area_related.all %}
<li>
<a href="{% url 'area-detail' a.id %}" title="{{ a.description }}">{{ a.name }}</a>
{% if user.is_staff %}
<a class="edit" href="{% url "admin:inventory_area_change" object_id=area.pk %}"><img class="icon" src="{% static "inventory/img/edit.svg" %}"></a>
{% endif %}
</li>
{% empty %}
<li>No areas defined</li>
{% endfor %}
</ul>
{% if area.show_sub_area %}
<h3>Areas</h3>
<ul class="nav-list">
{% for a in area.area_related.all %}
<li>
<a href="{% url 'area-detail' a.id %}" title="{{ a.description }}">{{ a.name }}</a>
{% if user.is_staff %}
<a class="edit" href="{% url "admin:inventory_area_change" object_id=area.pk %}"><img class="icon" src="{% static "inventory/img/edit.svg" %}"></a>
{% endif %}
</li>
{% empty %}
<li>No areas defined</li>
{% endfor %}
</ul>
{% if user.is_staff %}
<p><a href="{% url "admin:inventory_area_add" %}?container={{ area.id }}&index=0">Create new area...</a></p>
{% if user.is_staff %}
<p><a href="{% url "admin:inventory_area_add" %}?container={{ area.id }}&index=0">Create new area...</a></p>
{% endif %}
{% endif %}
<h3>Boxes</h3>
<ul>
<h3>Containers</h3>
<ul class="nav-list">
{% for box in area.box_related.all %}
<li>
<a href="{% url 'box-detail' box.id %}" title="{{ box.description }}">{{ box.name }} ({{ box.layout.name }})</a>
@ -39,7 +45,7 @@
{% endif %}
</li>
{% empty %}
<li>No boxes defined</li>
<li>No containers defined</li>
{% endfor %}
</ul>

View file

@ -0,0 +1,18 @@
{% extends "base.html" %}
{% load static %}
{% block head %}
<link rel="stylesheet" type="text/css" href="{% static "inventory/css/cell.css" %}">
{% endblock %}
{% block title %}{{ object.name }}{% endblock %}
{% block header_bar %}
<a href="{{ object.container_url }}"><img class="icon" src="{% static "inventory/img/back.svg" %}"></a>
{{ object.name }}
<span class="small">{{ object.description}}</span></h2>
{% endblock %}
{% block header_icons %}
{% include 'inventory/set_index.html' with item=object is_index=is_index %}
{% endblock %}

View file

@ -1,12 +1,4 @@
{% extends "base.html" %}
{% block title %}{{ box.container.display_name }} - {{ box.name }}{% endblock %}
{% block header_bar %}
<a href="{{ box.container_url }}"><img class="icon" src="{% static "inventory/img/back.svg" %}"></a>
{{ box.container.display_name }} - {{ box.name }}
<span class="small">{{ box.description}}</span>
{% endblock %}
{% extends "inventory/box-detail.html" %}
{% block content %}

View file

@ -1,19 +1,7 @@
{% extends "base.html" %}
{% extends "inventory/box-detail.html" %}
{% load static %}
{% load admin_urls %}
{% block head %}
<link rel="stylesheet" type="text/css" href="{% static "inventory/css/cell.css" %}">
{% endblock %}
{% block title %}{{ object.name }}{% endblock %}
{% block header_bar %}
<a href="{{ object.container_url }}"><img class="icon" src="{% static "inventory/img/back.svg" %}"></a>
{{ object.name }}
<span class="small">{{ object.description}}</span>{% endblock %}
{% endblock %}
{% block content %}
<table class="box">
<tbody>

View file

@ -1,19 +1,7 @@
{% extends "base.html" %}
{% extends "inventory/box-detail.html" %}
{% load static %}
{% load admin_urls %}
{% block head %}
<link rel="stylesheet" type="text/css" href="{% static "inventory/css/cell.css" %}">
{% endblock %}
{% block title %}{{ object.name }}{% endblock %}
{% block header_bar %}
<a href="{{ object.container_url }}"><img class="icon" src="{% static "inventory/img/back.svg" %}"></a>
{{ object.name }}
<span class="small">{{ object.description}}</span></h2>
{% endblock %}
{% block content %}
<table class="box">
<tbody>

View file

@ -1,19 +1,7 @@
{% extends "base.html" %}
{% extends "inventory/box-detail.html" %}
{% load static %}
{% load admin_urls %}
{% block head %}
<link rel="stylesheet" type="text/css" href="{% static "inventory/css/cell.css" %}">
{% endblock %}
{% block title %}{{ object.name }}{% endblock %}
{% block header_bar %}
<a href="{{ object.container_url }}"><img class="icon" src="{% static "inventory/img/back.svg" %}"></a>
{{ object.name }}
<span class="small">{{ object.description}}</span></h2>
{% endblock %}
{% block content %}
<table class="box">
<tbody>

View file

@ -1,19 +1,7 @@
{% extends "base.html" %}
{% extends "inventory/box-detail.html" %}
{% load static %}
{% load admin_urls %}
{% block head %}
<link rel="stylesheet" type="text/css" href="{% static "inventory/css/cell.css" %}">
{% endblock %}
{% block title %}{{ object.name }}{% endblock %}
{% block header_bar %}
<a href="{{ object.container_url }}"><img class="icon" src="{% static "inventory/img/back.svg" %}"></a>
{{ object.name }}
<span class="small">{{ object.description}}</span></h2>
{% endblock %}
{% block content %}
{% for part in layouted %}
<table class="box">

View file

@ -0,0 +1,15 @@
{% load static %}
<li>
<form method="POST" action="{% url "index" %}">
{% csrf_token %}
<input type="hidden" name="index_id" value='{{ item.id }}'>
<button class="icon-only-button">
{% if is_index %}
<img class="icon" src="{% static "inventory/img/star-filled.svg" %}">
{% else %}
<img class="icon" src="{% static "inventory/img/star-outline.svg" %}">
{% endif %}
</button>
</form>
</li>

View file

@ -9,9 +9,13 @@
<span class="small">{{ workshop.description}}</span>
{% endblock %}
{% block header_icons %}
{% include 'inventory/set_index.html' with item=workshop is_index=is_index %}
{% endblock %}
{% block content %}
<h3>Areas</h3>
<ul>
<ul class="nav-list">
{% for area in workshop.area_related.all %}
<li>
<a href="{% url 'area-detail' area.id %}" title="{{ area.description }}">{{ area.name }}</a>
@ -29,22 +33,24 @@
<p><a href="{% url "admin:inventory_area_add" %}?container={{ workshop.id }}&index=0">Create new area...</a></p>
{% endif %}
<h3>Boxes</h3>
<ul>
{% for box in workshop.box_related.all %}
<li>
<a href="{% url 'box-detail' box.id %}" title="{{ box.description }}">{{ box.name }} ({{ box.layout.name }})</a>
({{ box.description }})
{% if user.is_staff %}
<a class="edit" href="{% url "admin:inventory_box_change" object_id=box.pk %}"><img class="icon" src="{% static "inventory/img/edit.svg" %}"></a>
{% endif %}
</li>
{% empty %}
<li>No boxes defined</li>
{% endfor %}
</ul>
{% if workshop.show_boxes %}
<h3>Containers</h3>
<ul class="nav-list">
{% for box in workshop.box_related.all %}
<li>
<a href="{% url 'box-detail' box.id %}" title="{{ box.description }}">{{ box.name }} ({{ box.layout.name }})</a>
({{ box.description }})
{% if user.is_staff %}
<a class="edit" href="{% url "admin:inventory_box_change" object_id=box.pk %}"><img class="icon" src="{% static "inventory/img/edit.svg" %}"></a>
{% endif %}
</li>
{% empty %}
<li>No containers defined</li>
{% endfor %}
</ul>
{% if user.is_staff %}
<p><a href="{% url "admin:inventory_box_add" %}?container={{ workshop.id }}&index=0">Create new box...</a></p>
{% if user.is_staff %}
<p><a href="{% url "admin:inventory_box_add" %}?container={{ workshop.id }}&index=0">Create new container...</a></p>
{% endif %}
{% endif %}
{% endblock %}

View file

@ -8,7 +8,7 @@
{% endblock %}
{% block content %}
<ul>
<ul class="nav-list">
{% for workshop in object_list %}
<li>
<a href="{% url 'workshop-detail' workshop.id %}" title="{{ workshop.description }}">{{ workshop.name }}</a>
@ -23,4 +23,5 @@
<p><a href="{% url "admin:inventory_workshop_add" %}?layout=1">Create new workshop...</a></p>
{% endif %}
{% endblock %}

View file

@ -17,7 +17,7 @@ Including another URLconf
from django.urls import path
from .views import WorkshopListView, AreaListView, BoxListView, ItemListView, ManufacturerListView, DistributorListView
from .views import WorkshopView, AreaView, BoxView, ItemView, DistributorView, ManufacturerView
from .views import WorkshopView, AreaView, BoxView, ItemView, DistributorView, ManufacturerView, IndexView
urlpatterns = [
path('workshops', WorkshopListView.as_view(), name='workshop-list'),
@ -33,4 +33,5 @@ urlpatterns = [
path('manufacturer/<int:pk>', ManufacturerView.as_view(), name='manufacturer-detail'),
path('distributors', DistributorListView.as_view(), name='distributor-list'),
path('distributor/<int:pk>', DistributorView.as_view(), name='distributor-detail'),
path('', IndexView.as_view(), name='index')
]

View file

@ -4,3 +4,14 @@ from .distributor import DistributorView, DistributorListView
from .item import ItemView, ItemListView
from .manufacturer import ManufacturerView, ManufacturerListView
from .workshop import WorkshopView, WorkshopListView
from .index import IndexView
__all__ = [
AreaView, AreaListView,
BoxView, BoxListView,
DistributorView, DistributorListView,
ItemView, ItemListView,
ManufacturerView, ManufacturerListView,
WorkshopView, WorkshopListView,
IndexView
]

View file

@ -4,9 +4,11 @@ from django.views.generic import ListView, DetailView
from inventory.models import Area
from .utils import CanBeIndexMixin
@method_decorator(login_required, name='dispatch')
class AreaView(DetailView):
class AreaView(CanBeIndexMixin, DetailView):
context_object_name = 'area'
queryset = Area.objects.all().select_related(
'container',

View file

@ -6,9 +6,11 @@ from django.db.models import QuerySet
from inventory.models import Box
from .utils import CanBeIndexMixin
@method_decorator(login_required, name='dispatch')
class BoxView(DetailView):
class BoxView(CanBeIndexMixin, DetailView):
context_object_name = 'box'
template_name_field = 'template_name'
queryset = Box.objects.all().select_related(

30
inventory/views/index.py Normal file
View file

@ -0,0 +1,30 @@
from django.urls import reverse
from django.shortcuts import redirect
from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator
from django.views.generic import View
from inventory.models import Settings
@method_decorator(login_required, name='dispatch')
class IndexView(View):
def get(self, request):
settings = Settings.objects.first()
if settings.default_container is not None:
print(settings.default_container.url)
return redirect(settings.default_container.url)
else:
return redirect(reverse('workshop-list'))
def post(self, request):
if 'index_id' in request.POST:
settings = Settings.objects.first()
new_container_id = int(request.POST['index_id'])
if settings.default_container_id == new_container_id:
settings.default_container = None
else:
settings.default_container_id = new_container_id
settings.save()
return redirect(request.META['HTTP_REFERER'])

11
inventory/views/utils.py Normal file
View file

@ -0,0 +1,11 @@
from typing import Any, Dict
from inventory.models import Settings
class CanBeIndexMixin:
def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
context = super().get_context_data(**kwargs)
context['is_index'] = Settings.objects.first().default_container_id == self.object.id
print(context)
return context

View file

@ -1,12 +1,14 @@
from typing import Any, Dict
from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator
from django.views.generic import ListView, DetailView
from inventory.models import Workshop
from inventory.models import Workshop, Settings
from .utils import CanBeIndexMixin
@method_decorator(login_required, name='dispatch')
class WorkshopView(DetailView):
class WorkshopView(CanBeIndexMixin, DetailView):
context_object_name = 'workshop'
queryset = Workshop.objects.all().prefetch_related('area_related', 'box_related')
@ -14,4 +16,3 @@ class WorkshopView(DetailView):
@method_decorator(login_required, name='dispatch')
class WorkshopListView(ListView):
model = Workshop

View file

@ -21,7 +21,6 @@ from django.views.generic import RedirectView
urlpatterns = [
path('', include('inventory.urls')),
path('', RedirectView.as_view(pattern_name='workshop-list', permanent=False)),
path('accounts/', include('django.contrib.auth.urls')),
path('admin/', admin.site.urls),
]