Improve user experience in admin and UI (#45)

This commit is contained in:
2025-01-18 15:37:56 +01:00
committed by GitHub
parent c467fb24ca
commit ddcf06994d
17 changed files with 364 additions and 106 deletions

View File

@@ -2,7 +2,7 @@ import html
from django.conf import settings from django.conf import settings
from django.contrib import admin from django.contrib import admin
from django.utils.html import strip_tags from django.utils.html import format_html, strip_tags
from adminsortable2.admin import SortableAdminBase, SortableInlineAdminMixin from adminsortable2.admin import SortableAdminBase, SortableInlineAdminMixin
from ram.admin import publish, unpublish from ram.admin import publish, unpublish
@@ -93,12 +93,7 @@ class BookAdmin(SortableAdminBase, admin.ModelAdmin):
), ),
( (
"Notes", "Notes",
{ {"classes": ("collapse",), "fields": ("notes",)},
"classes": ("collapse",),
"fields": (
"notes",
)
},
), ),
( (
"Audit", "Audit",
@@ -150,7 +145,8 @@ class BookAdmin(SortableAdminBase, admin.ModelAdmin):
"{}:{}".format(property.property.name, property.value) "{}:{}".format(property.property.name, property.value)
for property in obj.property.all() for property in obj.property.all()
) )
data.append([ data.append(
[
obj.title, obj.title,
obj.authors_list.replace(",", settings.CSV_SEPARATOR_ALT), obj.authors_list.replace(",", settings.CSV_SEPARATOR_ALT),
obj.publisher.name, obj.publisher.name,
@@ -166,7 +162,8 @@ class BookAdmin(SortableAdminBase, admin.ModelAdmin):
obj.price, obj.price,
html.unescape(strip_tags(obj.notes)), html.unescape(strip_tags(obj.notes)),
properties, properties,
]) ]
)
return generate_csv(header, data, "bookshelf_books.csv") return generate_csv(header, data, "bookshelf_books.csv")
@@ -185,9 +182,15 @@ class AuthorAdmin(admin.ModelAdmin):
@admin.register(Publisher) @admin.register(Publisher)
class PublisherAdmin(admin.ModelAdmin): class PublisherAdmin(admin.ModelAdmin):
list_display = ("name", "country") list_display = ("name", "country_flag")
search_fields = ("name",) search_fields = ("name",)
@admin.display(description="Country")
def country_flag(self, obj):
return format_html(
'<img src="{}" /> {}'.format(obj.country.flag, obj.country.name)
)
@admin.register(Catalog) @admin.register(Catalog)
class CatalogAdmin(SortableAdminBase, admin.ModelAdmin): class CatalogAdmin(SortableAdminBase, admin.ModelAdmin):
@@ -237,12 +240,7 @@ class CatalogAdmin(SortableAdminBase, admin.ModelAdmin):
), ),
( (
"Notes", "Notes",
{ {"classes": ("collapse",), "fields": ("notes",)},
"classes": ("collapse",),
"fields": (
"notes",
)
},
), ),
( (
"Audit", "Audit",
@@ -287,7 +285,8 @@ class CatalogAdmin(SortableAdminBase, admin.ModelAdmin):
"{}:{}".format(property.property.name, property.value) "{}:{}".format(property.property.name, property.value)
for property in obj.property.all() for property in obj.property.all()
) )
data.append([ data.append(
[
obj.__str__(), obj.__str__(),
obj.manufacturer.name, obj.manufacturer.name,
obj.years, obj.years,
@@ -304,7 +303,8 @@ class CatalogAdmin(SortableAdminBase, admin.ModelAdmin):
obj.price, obj.price,
html.unescape(strip_tags(obj.notes)), html.unescape(strip_tags(obj.notes)),
properties, properties,
]) ]
)
return generate_csv(header, data, "bookshelf_catalogs.csv") return generate_csv(header, data, "bookshelf_catalogs.csv")

View File

@@ -0,0 +1,34 @@
# Generated by Django 5.1.4 on 2025-01-18 11:20
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("bookshelf", "0020_alter_basebookdocument_unique_together_and_more"),
]
operations = [
migrations.AddField(
model_name="basebookdocument",
name="creation_time",
field=models.DateTimeField(
auto_now_add=True, default=django.utils.timezone.now
),
preserve_default=False,
),
migrations.AddField(
model_name="basebookdocument",
name="updated_time",
field=models.DateTimeField(auto_now=True),
),
migrations.AlterField(
model_name="basebookdocument",
name="private",
field=models.BooleanField(
default=False, help_text="Document will be visible only to logged users"
),
),
]

View File

@@ -1,4 +1,5 @@
from django.contrib import admin from django.contrib import admin
from django.utils.html import format_html
from adminsortable2.admin import SortableAdminBase, SortableInlineAdminMixin from adminsortable2.admin import SortableAdminBase, SortableInlineAdminMixin
from ram.admin import publish, unpublish from ram.admin import publish, unpublish
@@ -28,10 +29,16 @@ class ConsistAdmin(SortableAdminBase, admin.ModelAdmin):
"updated_time", "updated_time",
) )
list_filter = ("company", "era", "published") list_filter = ("company", "era", "published")
list_display = ("__str__",) + list_filter list_display = ("__str__",) + list_filter + ("country_flag",)
search_fields = ("identifier",) + list_filter search_fields = ("identifier",) + list_filter
save_as = True save_as = True
@admin.display(description="Country")
def country_flag(self, obj):
return format_html(
'<img src="{}" /> {}'.format(obj.country.flag, obj.country)
)
fieldsets = ( fieldsets = (
( (
None, None,

View File

@@ -39,6 +39,10 @@ class Consist(BaseModel):
def get_absolute_url(self): def get_absolute_url(self):
return reverse("consist", kwargs={"uuid": self.uuid}) return reverse("consist", kwargs={"uuid": self.uuid})
@property
def country(self):
return self.company.country
def clean(self): def clean(self):
if self.consist_item.filter(rolling_stock__published=False).exists(): if self.consist_item.filter(rolling_stock__published=False).exists():
raise ValidationError( raise ValidationError(

View File

@@ -1,4 +1,5 @@
from django.contrib import admin from django.contrib import admin
from django.utils.html import format_html
from adminsortable2.admin import SortableAdminMixin from adminsortable2.admin import SortableAdminMixin
from ram.admin import publish, unpublish from ram.admin import publish, unpublish
@@ -47,18 +48,30 @@ class ScaleAdmin(admin.ModelAdmin):
@admin.register(Company) @admin.register(Company)
class CompanyAdmin(admin.ModelAdmin): class CompanyAdmin(admin.ModelAdmin):
readonly_fields = ("logo_thumbnail",) readonly_fields = ("logo_thumbnail",)
list_display = ("name", "country") list_display = ("name", "country_flag")
list_filter = list_display list_filter = ("name", "country")
search_fields = ("name",) search_fields = ("name",)
@admin.display(description="Country")
def country_flag(self, obj):
return format_html(
'<img src="{}" /> {}'.format(obj.country.flag, obj.country.name)
)
@admin.register(Manufacturer) @admin.register(Manufacturer)
class ManufacturerAdmin(admin.ModelAdmin): class ManufacturerAdmin(admin.ModelAdmin):
readonly_fields = ("logo_thumbnail",) readonly_fields = ("logo_thumbnail",)
list_display = ("name", "category") list_display = ("name", "category", "country_flag")
list_filter = ("category",) list_filter = ("category",)
search_fields = ("name",) search_fields = ("name",)
@admin.display(description="Country")
def country_flag(self, obj):
return format_html(
'<img src="{}" /> {}'.format(obj.country.flag, obj.country.name)
)
@admin.register(Tag) @admin.register(Tag)
class TagAdmin(admin.ModelAdmin): class TagAdmin(admin.ModelAdmin):
@@ -76,7 +89,7 @@ class RollingStockTypeAdmin(SortableAdminMixin, admin.ModelAdmin):
@admin.register(GenericDocument) @admin.register(GenericDocument)
class GenericDocumentAdmin(admin.ModelAdmin): class GenericDocumentAdmin(admin.ModelAdmin):
readonly_fields = ("size",) readonly_fields = ("size", "creation_time", "updated_time")
list_display = ( list_display = (
"__str__", "__str__",
"description", "description",
@@ -88,4 +101,32 @@ class GenericDocumentAdmin(admin.ModelAdmin):
"description", "description",
"file", "file",
) )
fieldsets = (
(
None,
{
"fields": (
"private",
"description",
"file",
"size",
"tags",
)
},
),
(
"Notes",
{"classes": ("collapse",), "fields": ("notes",)},
),
(
"Audit",
{
"classes": ("collapse",),
"fields": (
"creation_time",
"updated_time",
),
},
),
)
actions = [publish, unpublish] actions = [publish, unpublish]

View File

@@ -0,0 +1,66 @@
# Generated by Django 5.1.4 on 2025-01-18 11:20
import django.utils.timezone
import django_countries.fields
import tinymce.models
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("metadata", "0021_genericdocument"),
]
operations = [
migrations.AddField(
model_name="decoderdocument",
name="creation_time",
field=models.DateTimeField(
auto_now_add=True, default=django.utils.timezone.now
),
preserve_default=False,
),
migrations.AddField(
model_name="decoderdocument",
name="updated_time",
field=models.DateTimeField(auto_now=True),
),
migrations.AddField(
model_name="genericdocument",
name="creation_time",
field=models.DateTimeField(
auto_now_add=True, default=django.utils.timezone.now
),
preserve_default=False,
),
migrations.AddField(
model_name="genericdocument",
name="notes",
field=tinymce.models.HTMLField(blank=True),
),
migrations.AddField(
model_name="genericdocument",
name="updated_time",
field=models.DateTimeField(auto_now=True),
),
migrations.AddField(
model_name="manufacturer",
name="country",
field=django_countries.fields.CountryField(blank=True, max_length=2),
),
migrations.AlterField(
model_name="decoderdocument",
name="private",
field=models.BooleanField(
default=False, help_text="Document will be visible only to logged users"
),
),
migrations.AlterField(
model_name="genericdocument",
name="private",
field=models.BooleanField(
default=False, help_text="Document will be visible only to logged users"
),
),
]

View File

@@ -6,6 +6,8 @@ from django.dispatch.dispatcher import receiver
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django_countries.fields import CountryField from django_countries.fields import CountryField
from tinymce import models as tinymce
from ram.models import Document from ram.models import Document
from ram.utils import DeduplicatedStorage, get_image_preview, slugify from ram.utils import DeduplicatedStorage, get_image_preview, slugify
from ram.managers import PublicManager from ram.managers import PublicManager
@@ -34,6 +36,7 @@ class Manufacturer(models.Model):
category = models.CharField( category = models.CharField(
max_length=64, choices=settings.MANUFACTURER_TYPES max_length=64, choices=settings.MANUFACTURER_TYPES
) )
country = CountryField(blank=True)
website = models.URLField(blank=True) website = models.URLField(blank=True)
logo = models.ImageField( logo = models.ImageField(
upload_to=os.path.join("images", "manufacturers"), upload_to=os.path.join("images", "manufacturers"),
@@ -237,6 +240,7 @@ class Tag(models.Model):
class GenericDocument(Document): class GenericDocument(Document):
notes = tinymce.HTMLField(blank=True)
tags = models.ManyToManyField(Tag, blank=True) tags = models.ManyToManyField(Tag, blank=True)
class Meta: class Meta:

View File

@@ -7,7 +7,10 @@
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
<tr> <tr>
<th colspan="2" scope="row">Company</th> <th scope="row">Company</th>
{% if d.item.freelance %}
<th class="text-end" scope="row"><span class="badge text-bg-secondary">Freelance</span></th>
{% endif %}
</tr> </tr>
</thead> </thead>
<tbody class="table-group-divider"> <tbody class="table-group-divider">
@@ -27,7 +30,7 @@
</tr> </tr>
<tr> <tr>
<th class="w-33" scope="row">Country</th> <th class="w-33" scope="row">Country</th>
<td>{{ d.item.country.name }} <img src="{{ d.item.country.flag }}" alt="{{ d.item.country }}"> <td><img src="{{ d.item.country.flag }}" alt="{{ d.item.country }}"> {{ d.item.country.name }}</td>
</tr> </tr>
{% if d.item.freelance %} {% if d.item.freelance %}
<tr> <tr>

View File

@@ -24,7 +24,10 @@
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
<tr> <tr>
<th colspan="2" scope="row">Consist</th> <th scope="row">Consist</th>
{% if d.item.company.freelance %}
<th class="text-end" scope="row"><span class="badge text-bg-secondary">Freelance</span></th>
{% endif %}
</tr> </tr>
</thead> </thead>
<tbody class="table-group-divider"> <tbody class="table-group-divider">

View File

@@ -22,7 +22,10 @@
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
<tr> <tr>
<th colspan="2" scope="row">Rolling stock</th> <th scope="row">Rolling stock</th>
{% if d.item.rolling_class.company.freelance %}
<th class="text-end" scope="row"><span class="badge text-bg-secondary">Freelance</span></th>
{% endif %}
</tr> </tr>
</thead> </thead>
<tbody class="table-group-divider"> <tbody class="table-group-divider">

View File

@@ -79,7 +79,10 @@
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
<tr> <tr>
<th colspan="2" scope="row">Data</th> <th scope="row">Consist</th>
{% if consist.company.freelance %}
<th class="text-end" scope="row"><span class="badge text-bg-secondary">Freelance</span></th>
{% endif %}
</tr> </tr>
</thead> </thead>
<tbody class="table-group-divider"> <tbody class="table-group-divider">

View File

@@ -73,7 +73,10 @@
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
<tr> <tr>
<th colspan="2" scope="row">Rolling stock</th> <th scope="row">Rolling stock</th>
{% if company.freelance %}
<th class="text-end" scope="row"><span class="badge text-bg-secondary">Freelance</span></th>
{% endif %}
</tr> </tr>
</thead> </thead>
<tbody class="table-group-divider"> <tbody class="table-group-divider">
@@ -87,6 +90,12 @@
<a href="{% url 'filtered' _filter="company" search=company.slug %}">{{ company }}</a> {{ company.extended_name_pp }} <a href="{% url 'filtered' _filter="company" search=company.slug %}">{{ company }}</a> {{ company.extended_name_pp }}
</td> </td>
</tr> </tr>
<tr>
<th scope="row">Country</th>
<td>
<img src="{{ company.country.flag }}" alt="{{ company.country }}"> {{ company.country.name }}
</td>
</tr>
<tr> <tr>
<th scope="row">Class</th> <th scope="row">Class</th>
<td>{{ class.identifier }}</td> <td>{{ class.identifier }}</td>
@@ -281,7 +290,10 @@
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
<tr> <tr>
<th colspan="2" scope="row">Company data</th> <th scope="row">Company data</th>
{% if company.freelance %}
<th class="text-end" scope="row"><span class="badge text-bg-secondary">Freelance</th>
{% endif %}
</tr> </tr>
</thead> </thead>
<tbody class="table-group-divider"> <tbody class="table-group-divider">
@@ -293,18 +305,16 @@
{% endif %} {% endif %}
<tr> <tr>
<th class="w-33" scope="row">Name</th> <th class="w-33" scope="row">Name</th>
<td><a href="{% url 'filtered' _filter="company" search=company.slug %}">{{ company.name }}</a> {{ company.extended_name_pp }}</td> <td>
<a href="{% url 'filtered' _filter="company" search=company.slug %}">{{ company.name }}</a> {{ company.extended_name_pp }}
</td>
</tr> </tr>
<tr> <tr>
<th class="w-33" scope="row">Country</th> <th class="w-33" scope="row">Country</th>
<td>{{ company.country.name }} <img src="{{ company.country.flag }}" alt="{{ company.country }}"> <td>
<img src="{{ company.country.flag }}" alt="{{ company.country }}"> {{ company.country.name }}
</td>
</tr> </tr>
{% if company.freelance %}
<tr>
<th class="w-33" scope="row">Notes</th>
<td>A <em>freelance</em> company</td>
</tr>
{% endif %}
</tbody> </tbody>
</table> </table>
</div> </div>

View File

@@ -1,4 +1,4 @@
from ram.utils import git_suffix from ram.utils import git_suffix
__version__ = "0.16.0" __version__ = "0.16.1"
__version__ += git_suffix(__file__) __version__ += git_suffix(__file__)

View File

@@ -28,7 +28,12 @@ class Document(models.Model):
upload_to="files/", upload_to="files/",
storage=DeduplicatedStorage(), storage=DeduplicatedStorage(),
) )
private = models.BooleanField(default=False) private = models.BooleanField(
default=False,
help_text="Document will be visible only to logged users",
)
creation_time = models.DateTimeField(auto_now_add=True)
updated_time = models.DateTimeField(auto_now=True)
class Meta: class Meta:
abstract = True abstract = True

View File

@@ -2,7 +2,7 @@ import html
from django.conf import settings from django.conf import settings
from django.contrib import admin from django.contrib import admin
from django.utils.html import strip_tags from django.utils.html import format_html, strip_tags
from adminsortable2.admin import SortableAdminBase, SortableInlineAdminMixin from adminsortable2.admin import SortableAdminBase, SortableInlineAdminMixin
@@ -31,7 +31,7 @@ class RollingClassPropertyInline(admin.TabularInline):
class RollingClass(admin.ModelAdmin): class RollingClass(admin.ModelAdmin):
inlines = (RollingClassPropertyInline,) inlines = (RollingClassPropertyInline,)
autocomplete_fields = ("manufacturer",) autocomplete_fields = ("manufacturer",)
list_display = ("__str__", "type", "company") list_display = ("__str__", "type", "company", "country_flag")
list_filter = ("company", "type__category", "type") list_filter = ("company", "type__category", "type")
search_fields = ( search_fields = (
"identifier", "identifier",
@@ -40,6 +40,12 @@ class RollingClass(admin.ModelAdmin):
) )
save_as = True save_as = True
@admin.display(description="Country")
def country_flag(self, obj):
return format_html(
'<img src="{}" /> {}'.format(obj.country.flag, obj.country)
)
class RollingStockDocInline(admin.TabularInline): class RollingStockDocInline(admin.TabularInline):
model = RollingStockDocument model = RollingStockDocument
@@ -87,6 +93,21 @@ class RollingStockDocumentAdmin(admin.ModelAdmin):
"description", "description",
"file", "file",
) )
autocomplete_fields = ("rolling_stock",)
fieldsets = (
(
None,
{
"fields": (
"private",
"rolling_stock",
"description",
"file",
"size",
)
},
),
)
@admin.register(RollingStockJournal) @admin.register(RollingStockJournal)
@@ -94,19 +115,32 @@ class RollingJournalDocumentAdmin(admin.ModelAdmin):
list_display = ( list_display = (
"__str__", "__str__",
"date", "date",
"rolling_stock",
"private", "private",
) )
list_filter = ( list_filter = (
"date", "date",
"private", "private",
) )
autocomplete_fields = ("rolling_stock",)
search_fields = ( search_fields = (
"rolling_stock__rolling_class__identifier", "rolling_stock__rolling_class__identifier",
"rolling_stock__road_number", "rolling_stock__road_number",
"rolling_stock__item_number", "rolling_stock__item_number",
"log", "log",
) )
fieldsets = (
(
None,
{
"fields": (
"private",
"rolling_stock",
"log",
"date",
)
},
),
)
@admin.register(RollingStock) @admin.register(RollingStock)
@@ -126,7 +160,7 @@ class RollingStockAdmin(SortableAdminBase, admin.ModelAdmin):
"scale", "scale",
"item_number", "item_number",
"company", "company",
"country", "country_flag",
"published", "published",
) )
list_filter = ( list_filter = (
@@ -146,6 +180,12 @@ class RollingStockAdmin(SortableAdminBase, admin.ModelAdmin):
) )
save_as = True save_as = True
@admin.display(description="Country")
def country_flag(self, obj):
return format_html(
'<img src="{}" /> {}'.format(obj.country.flag, obj.country)
)
fieldsets = ( fieldsets = (
( (
None, None,
@@ -187,12 +227,7 @@ class RollingStockAdmin(SortableAdminBase, admin.ModelAdmin):
), ),
( (
"Notes", "Notes",
{ {"classes": ("collapse",), "fields": ("notes",)},
"classes": ("collapse",),
"fields": (
"notes",
)
},
), ),
( (
"Audit", "Audit",
@@ -241,7 +276,8 @@ class RollingStockAdmin(SortableAdminBase, admin.ModelAdmin):
"{}:{}".format(property.property.name, property.value) "{}:{}".format(property.property.name, property.value)
for property in obj.property.all() for property in obj.property.all()
) )
data.append([ data.append(
[
obj.__str__(), obj.__str__(),
obj.rolling_class.company.name, obj.rolling_class.company.name,
obj.rolling_class.identifier, obj.rolling_class.identifier,
@@ -263,7 +299,8 @@ class RollingStockAdmin(SortableAdminBase, admin.ModelAdmin):
obj.purchase_date, obj.purchase_date,
obj.price, obj.price,
properties, properties,
]) ]
)
return generate_csv(header, data, "rolling_stock.csv") return generate_csv(header, data, "rolling_stock.csv")

View File

@@ -0,0 +1,34 @@
# Generated by Django 5.1.4 on 2025-01-18 11:20
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("roster", "0031_alter_rollingstockdocument_unique_together_and_more"),
]
operations = [
migrations.AddField(
model_name="rollingstockdocument",
name="creation_time",
field=models.DateTimeField(
auto_now_add=True, default=django.utils.timezone.now
),
preserve_default=False,
),
migrations.AddField(
model_name="rollingstockdocument",
name="updated_time",
field=models.DateTimeField(auto_now=True),
),
migrations.AlterField(
model_name="rollingstockdocument",
name="private",
field=models.BooleanField(
default=False, help_text="Document will be visible only to logged users"
),
),
]

View File

@@ -42,6 +42,10 @@ class RollingClass(models.Model):
def __str__(self): def __str__(self):
return "{0} {1}".format(self.company, self.identifier) return "{0} {1}".format(self.company, self.identifier)
@property
def country(self):
return self.company.country
class RollingClassProperty(PropertyInstance): class RollingClassProperty(PropertyInstance):
rolling_class = models.ForeignKey( rolling_class = models.ForeignKey(
@@ -127,7 +131,7 @@ class RollingStock(BaseModel):
@property @property
def country(self): def country(self):
return str(self.rolling_class.company.country) return self.rolling_class.company.country
@property @property
def company(self): def company(self):