diff --git a/.github/workflows/django.yml b/.github/workflows/django.yml
index 4c3e7ee..059f4c9 100644
--- a/.github/workflows/django.yml
+++ b/.github/workflows/django.yml
@@ -13,7 +13,7 @@ jobs:
strategy:
max-parallel: 2
matrix:
- python-version: ['3.12', '3.13']
+ python-version: ['3.13', '3.14']
steps:
- uses: actions/checkout@v3
diff --git a/ram/bookshelf/admin.py b/ram/bookshelf/admin.py
index ba57d98..4fea47f 100644
--- a/ram/bookshelf/admin.py
+++ b/ram/bookshelf/admin.py
@@ -2,7 +2,7 @@ import html
from django.conf import settings
from django.contrib import admin
-from django.utils.html import format_html, strip_tags
+from django.utils.html import format_html, format_html_join, strip_tags
from adminsortable2.admin import SortableAdminBase, SortableInlineAdminMixin
from ram.admin import publish, unpublish
@@ -133,13 +133,14 @@ class BookAdmin(SortableAdminBase, admin.ModelAdmin):
@admin.display(description="Invoices")
def invoices(self, obj):
if obj.invoice.exists():
- html = "
".join(
- "{}".format(
- i.file.url, i
- ) for i in obj.invoice.all())
+ html = format_html_join(
+ "
",
+ "{}",
+ ((i.file.url, i) for i in obj.invoice.all())
+ )
else:
html = "-"
- return format_html(html)
+ return html
@admin.display(description="Publisher")
def get_publisher(self, obj):
@@ -217,7 +218,7 @@ class PublisherAdmin(admin.ModelAdmin):
@admin.display(description="Country")
def country_flag(self, obj):
return format_html(
- ' {}'.format(obj.country.flag, obj.country.name)
+ '
{}', obj.country.flag, obj.country.name
)
@@ -295,13 +296,14 @@ class CatalogAdmin(SortableAdminBase, admin.ModelAdmin):
@admin.display(description="Invoices")
def invoices(self, obj):
if obj.invoice.exists():
- html = "
".join(
- "{}".format(
- i.file.url, i
- ) for i in obj.invoice.all())
+ html = format_html_join(
+ "
",
+ "{}",
+ ((i.file.url, i) for i in obj.invoice.all())
+ )
else:
html = "-"
- return format_html(html)
+ return html
def download_csv(modeladmin, request, queryset):
header = [
diff --git a/ram/bookshelf/migrations/0016_basebook_book_catalogue.py b/ram/bookshelf/migrations/0016_basebook_book_catalogue.py
index 5e44540..e465782 100644
--- a/ram/bookshelf/migrations/0016_basebook_book_catalogue.py
+++ b/ram/bookshelf/migrations/0016_basebook_book_catalogue.py
@@ -1,7 +1,8 @@
# Generated by Django 5.1.2 on 2024-11-27 16:35
import django.db.models.deletion
-from django.db import migrations, models
+from django.db import migrations, models, connection
+from django.db.utils import ProgrammingError, OperationalError
def basebook_to_book(apps, schema_editor):
@@ -16,6 +17,19 @@ def basebook_to_book(apps, schema_editor):
b.authors.set(row.old_authors.all())
+def drop_temporary_tables(apps, schema_editor):
+ try:
+ with connection.cursor() as cursor:
+ cursor.execute(
+ 'DROP TABLE IF EXISTS bookshelf_basebook_old_authors'
+ )
+ cursor.execute(
+ 'DROP TABLE IF EXISTS bookshelf_basebook_authors'
+ )
+ except (ProgrammingError, OperationalError):
+ pass
+
+
class Migration(migrations.Migration):
dependencies = [
@@ -101,10 +115,6 @@ class Migration(migrations.Migration):
model_name="basebook",
name="old_title",
),
- migrations.RemoveField(
- model_name="basebook",
- name="old_authors",
- ),
migrations.RemoveField(
model_name="basebook",
name="old_publisher",
@@ -138,4 +148,16 @@ class Migration(migrations.Migration):
},
bases=("bookshelf.basebook",),
),
+ # Required by Dajngo 6.0 on SQLite
+ migrations.SeparateDatabaseAndState(
+ state_operations=[
+ migrations.RemoveField(
+ model_name="basebook",
+ name="old_authors",
+ ),
+ ],
+ database_operations=[
+ migrations.RunPython(drop_temporary_tables)
+ ]
+ ),
]
diff --git a/ram/bookshelf/migrations/0024_alter_basebook_language.py b/ram/bookshelf/migrations/0024_alter_basebook_language.py
new file mode 100644
index 0000000..b7f5c5c
--- /dev/null
+++ b/ram/bookshelf/migrations/0024_alter_basebook_language.py
@@ -0,0 +1,123 @@
+# Generated by Django 6.0 on 2025-12-03 22:07
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("bookshelf", "0023_delete_basebookdocument"),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name="basebook",
+ name="language",
+ field=models.CharField(
+ choices=[
+ ("af", "Afrikaans"),
+ ("ar", "Arabic"),
+ ("ar-dz", "Algerian Arabic"),
+ ("ast", "Asturian"),
+ ("az", "Azerbaijani"),
+ ("bg", "Bulgarian"),
+ ("be", "Belarusian"),
+ ("bn", "Bengali"),
+ ("br", "Breton"),
+ ("bs", "Bosnian"),
+ ("ca", "Catalan"),
+ ("ckb", "Central Kurdish (Sorani)"),
+ ("cs", "Czech"),
+ ("cy", "Welsh"),
+ ("da", "Danish"),
+ ("de", "German"),
+ ("dsb", "Lower Sorbian"),
+ ("el", "Greek"),
+ ("en", "English"),
+ ("en-au", "Australian English"),
+ ("en-gb", "British English"),
+ ("eo", "Esperanto"),
+ ("es", "Spanish"),
+ ("es-ar", "Argentinian Spanish"),
+ ("es-co", "Colombian Spanish"),
+ ("es-mx", "Mexican Spanish"),
+ ("es-ni", "Nicaraguan Spanish"),
+ ("es-ve", "Venezuelan Spanish"),
+ ("et", "Estonian"),
+ ("eu", "Basque"),
+ ("fa", "Persian"),
+ ("fi", "Finnish"),
+ ("fr", "French"),
+ ("fy", "Frisian"),
+ ("ga", "Irish"),
+ ("gd", "Scottish Gaelic"),
+ ("gl", "Galician"),
+ ("he", "Hebrew"),
+ ("hi", "Hindi"),
+ ("hr", "Croatian"),
+ ("hsb", "Upper Sorbian"),
+ ("ht", "Haitian Creole"),
+ ("hu", "Hungarian"),
+ ("hy", "Armenian"),
+ ("ia", "Interlingua"),
+ ("id", "Indonesian"),
+ ("ig", "Igbo"),
+ ("io", "Ido"),
+ ("is", "Icelandic"),
+ ("it", "Italian"),
+ ("ja", "Japanese"),
+ ("ka", "Georgian"),
+ ("kab", "Kabyle"),
+ ("kk", "Kazakh"),
+ ("km", "Khmer"),
+ ("kn", "Kannada"),
+ ("ko", "Korean"),
+ ("ky", "Kyrgyz"),
+ ("lb", "Luxembourgish"),
+ ("lt", "Lithuanian"),
+ ("lv", "Latvian"),
+ ("mk", "Macedonian"),
+ ("ml", "Malayalam"),
+ ("mn", "Mongolian"),
+ ("mr", "Marathi"),
+ ("ms", "Malay"),
+ ("my", "Burmese"),
+ ("nb", "Norwegian Bokmål"),
+ ("ne", "Nepali"),
+ ("nl", "Dutch"),
+ ("nn", "Norwegian Nynorsk"),
+ ("os", "Ossetic"),
+ ("pa", "Punjabi"),
+ ("pl", "Polish"),
+ ("pt", "Portuguese"),
+ ("pt-br", "Brazilian Portuguese"),
+ ("ro", "Romanian"),
+ ("ru", "Russian"),
+ ("sk", "Slovak"),
+ ("sl", "Slovenian"),
+ ("sq", "Albanian"),
+ ("sr", "Serbian"),
+ ("sr-latn", "Serbian Latin"),
+ ("sv", "Swedish"),
+ ("sw", "Swahili"),
+ ("ta", "Tamil"),
+ ("te", "Telugu"),
+ ("tg", "Tajik"),
+ ("th", "Thai"),
+ ("tk", "Turkmen"),
+ ("tr", "Turkish"),
+ ("tt", "Tatar"),
+ ("udm", "Udmurt"),
+ ("ug", "Uyghur"),
+ ("uk", "Ukrainian"),
+ ("ur", "Urdu"),
+ ("uz", "Uzbek"),
+ ("vi", "Vietnamese"),
+ ("zh-hans", "Simplified Chinese"),
+ ("zh-hant", "Traditional Chinese"),
+ ],
+ default="en",
+ max_length=7,
+ ),
+ ),
+ ]
diff --git a/ram/consist/admin.py b/ram/consist/admin.py
index 472b6a0..135760a 100644
--- a/ram/consist/admin.py
+++ b/ram/consist/admin.py
@@ -54,7 +54,7 @@ class ConsistAdmin(SortableAdminBase, admin.ModelAdmin):
@admin.display(description="Country")
def country_flag(self, obj):
return format_html(
- ' {}'.format(obj.country.flag, obj.country)
+ '
{}', obj.country.flag, obj.country
)
fieldsets = (
diff --git a/ram/metadata/admin.py b/ram/metadata/admin.py
index c017394..d3f30b5 100644
--- a/ram/metadata/admin.py
+++ b/ram/metadata/admin.py
@@ -54,7 +54,7 @@ class CompanyAdmin(admin.ModelAdmin):
@admin.display(description="Country")
def country_flag(self, obj):
return format_html(
- '
{}'.format(obj.country.flag, obj.country.name)
+ '
{}', obj.country.flag, obj.country.name
)
@@ -68,7 +68,7 @@ class ManufacturerAdmin(admin.ModelAdmin):
@admin.display(description="Country")
def country_flag(self, obj):
return format_html(
- '
{}'.format(obj.country.flag, obj.country.name)
+ '
{}', obj.country.flag, obj.country.name
)
diff --git a/ram/portal/templates/includes/login.html b/ram/portal/templates/includes/login.html
index f9d69a7..6001cbc 100644
--- a/ram/portal/templates/includes/login.html
+++ b/ram/portal/templates/includes/login.html
@@ -11,9 +11,10 @@