diff --git a/ram/bookshelf/migrations/0026_alter_basebook_language_alter_magazine_image_and_more.py b/ram/bookshelf/migrations/0026_alter_basebook_language_alter_magazine_image_and_more.py
new file mode 100644
index 0000000..189c84b
--- /dev/null
+++ b/ram/bookshelf/migrations/0026_alter_basebook_language_alter_magazine_image_and_more.py
@@ -0,0 +1,244 @@
+# Generated by Django 6.0 on 2025-12-10 20:59
+
+import bookshelf.models
+import ram.utils
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("bookshelf", "0025_magazine_magazineissue"),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name="basebook",
+ name="language",
+ field=models.CharField(
+ choices=[
+ ("af", "Afrikaans"),
+ ("sq", "Albanian"),
+ ("ar-dz", "Algerian Arabic"),
+ ("ar", "Arabic"),
+ ("es-ar", "Argentinian Spanish"),
+ ("hy", "Armenian"),
+ ("ast", "Asturian"),
+ ("en-au", "Australian English"),
+ ("az", "Azerbaijani"),
+ ("eu", "Basque"),
+ ("be", "Belarusian"),
+ ("bn", "Bengali"),
+ ("bs", "Bosnian"),
+ ("pt-br", "Brazilian Portuguese"),
+ ("br", "Breton"),
+ ("en-gb", "British English"),
+ ("bg", "Bulgarian"),
+ ("my", "Burmese"),
+ ("ca", "Catalan"),
+ ("ckb", "Central Kurdish (Sorani)"),
+ ("es-co", "Colombian Spanish"),
+ ("hr", "Croatian"),
+ ("cs", "Czech"),
+ ("da", "Danish"),
+ ("nl", "Dutch"),
+ ("en", "English"),
+ ("eo", "Esperanto"),
+ ("et", "Estonian"),
+ ("fi", "Finnish"),
+ ("fr", "French"),
+ ("fy", "Frisian"),
+ ("gl", "Galician"),
+ ("ka", "Georgian"),
+ ("de", "German"),
+ ("el", "Greek"),
+ ("ht", "Haitian Creole"),
+ ("he", "Hebrew"),
+ ("hi", "Hindi"),
+ ("hu", "Hungarian"),
+ ("is", "Icelandic"),
+ ("io", "Ido"),
+ ("ig", "Igbo"),
+ ("id", "Indonesian"),
+ ("ia", "Interlingua"),
+ ("ga", "Irish"),
+ ("it", "Italian"),
+ ("ja", "Japanese"),
+ ("kab", "Kabyle"),
+ ("kn", "Kannada"),
+ ("kk", "Kazakh"),
+ ("km", "Khmer"),
+ ("ko", "Korean"),
+ ("ky", "Kyrgyz"),
+ ("lv", "Latvian"),
+ ("lt", "Lithuanian"),
+ ("dsb", "Lower Sorbian"),
+ ("lb", "Luxembourgish"),
+ ("mk", "Macedonian"),
+ ("ms", "Malay"),
+ ("ml", "Malayalam"),
+ ("mr", "Marathi"),
+ ("es-mx", "Mexican Spanish"),
+ ("mn", "Mongolian"),
+ ("ne", "Nepali"),
+ ("es-ni", "Nicaraguan Spanish"),
+ ("nb", "Norwegian Bokmål"),
+ ("nn", "Norwegian Nynorsk"),
+ ("os", "Ossetic"),
+ ("fa", "Persian"),
+ ("pl", "Polish"),
+ ("pt", "Portuguese"),
+ ("pa", "Punjabi"),
+ ("ro", "Romanian"),
+ ("ru", "Russian"),
+ ("gd", "Scottish Gaelic"),
+ ("sr", "Serbian"),
+ ("sr-latn", "Serbian Latin"),
+ ("zh-hans", "Simplified Chinese"),
+ ("sk", "Slovak"),
+ ("sl", "Slovenian"),
+ ("es", "Spanish"),
+ ("sw", "Swahili"),
+ ("sv", "Swedish"),
+ ("tg", "Tajik"),
+ ("ta", "Tamil"),
+ ("tt", "Tatar"),
+ ("te", "Telugu"),
+ ("th", "Thai"),
+ ("zh-hant", "Traditional Chinese"),
+ ("tr", "Turkish"),
+ ("tk", "Turkmen"),
+ ("udm", "Udmurt"),
+ ("uk", "Ukrainian"),
+ ("hsb", "Upper Sorbian"),
+ ("ur", "Urdu"),
+ ("ug", "Uyghur"),
+ ("uz", "Uzbek"),
+ ("es-ve", "Venezuelan Spanish"),
+ ("vi", "Vietnamese"),
+ ("cy", "Welsh"),
+ ],
+ default="en",
+ max_length=7,
+ ),
+ ),
+ migrations.AlterField(
+ model_name="magazine",
+ name="image",
+ field=models.ImageField(
+ blank=True,
+ storage=ram.utils.DeduplicatedStorage,
+ upload_to=bookshelf.models.magazine_image_upload,
+ ),
+ ),
+ migrations.AlterField(
+ model_name="magazine",
+ name="language",
+ field=models.CharField(
+ choices=[
+ ("af", "Afrikaans"),
+ ("sq", "Albanian"),
+ ("ar-dz", "Algerian Arabic"),
+ ("ar", "Arabic"),
+ ("es-ar", "Argentinian Spanish"),
+ ("hy", "Armenian"),
+ ("ast", "Asturian"),
+ ("en-au", "Australian English"),
+ ("az", "Azerbaijani"),
+ ("eu", "Basque"),
+ ("be", "Belarusian"),
+ ("bn", "Bengali"),
+ ("bs", "Bosnian"),
+ ("pt-br", "Brazilian Portuguese"),
+ ("br", "Breton"),
+ ("en-gb", "British English"),
+ ("bg", "Bulgarian"),
+ ("my", "Burmese"),
+ ("ca", "Catalan"),
+ ("ckb", "Central Kurdish (Sorani)"),
+ ("es-co", "Colombian Spanish"),
+ ("hr", "Croatian"),
+ ("cs", "Czech"),
+ ("da", "Danish"),
+ ("nl", "Dutch"),
+ ("en", "English"),
+ ("eo", "Esperanto"),
+ ("et", "Estonian"),
+ ("fi", "Finnish"),
+ ("fr", "French"),
+ ("fy", "Frisian"),
+ ("gl", "Galician"),
+ ("ka", "Georgian"),
+ ("de", "German"),
+ ("el", "Greek"),
+ ("ht", "Haitian Creole"),
+ ("he", "Hebrew"),
+ ("hi", "Hindi"),
+ ("hu", "Hungarian"),
+ ("is", "Icelandic"),
+ ("io", "Ido"),
+ ("ig", "Igbo"),
+ ("id", "Indonesian"),
+ ("ia", "Interlingua"),
+ ("ga", "Irish"),
+ ("it", "Italian"),
+ ("ja", "Japanese"),
+ ("kab", "Kabyle"),
+ ("kn", "Kannada"),
+ ("kk", "Kazakh"),
+ ("km", "Khmer"),
+ ("ko", "Korean"),
+ ("ky", "Kyrgyz"),
+ ("lv", "Latvian"),
+ ("lt", "Lithuanian"),
+ ("dsb", "Lower Sorbian"),
+ ("lb", "Luxembourgish"),
+ ("mk", "Macedonian"),
+ ("ms", "Malay"),
+ ("ml", "Malayalam"),
+ ("mr", "Marathi"),
+ ("es-mx", "Mexican Spanish"),
+ ("mn", "Mongolian"),
+ ("ne", "Nepali"),
+ ("es-ni", "Nicaraguan Spanish"),
+ ("nb", "Norwegian Bokmål"),
+ ("nn", "Norwegian Nynorsk"),
+ ("os", "Ossetic"),
+ ("fa", "Persian"),
+ ("pl", "Polish"),
+ ("pt", "Portuguese"),
+ ("pa", "Punjabi"),
+ ("ro", "Romanian"),
+ ("ru", "Russian"),
+ ("gd", "Scottish Gaelic"),
+ ("sr", "Serbian"),
+ ("sr-latn", "Serbian Latin"),
+ ("zh-hans", "Simplified Chinese"),
+ ("sk", "Slovak"),
+ ("sl", "Slovenian"),
+ ("es", "Spanish"),
+ ("sw", "Swahili"),
+ ("sv", "Swedish"),
+ ("tg", "Tajik"),
+ ("ta", "Tamil"),
+ ("tt", "Tatar"),
+ ("te", "Telugu"),
+ ("th", "Thai"),
+ ("zh-hant", "Traditional Chinese"),
+ ("tr", "Turkish"),
+ ("tk", "Turkmen"),
+ ("udm", "Udmurt"),
+ ("uk", "Ukrainian"),
+ ("hsb", "Upper Sorbian"),
+ ("ur", "Urdu"),
+ ("ug", "Uyghur"),
+ ("uz", "Uzbek"),
+ ("es-ve", "Venezuelan Spanish"),
+ ("vi", "Vietnamese"),
+ ("cy", "Welsh"),
+ ],
+ default="en",
+ max_length=7,
+ ),
+ ),
+ ]
diff --git a/ram/bookshelf/models.py b/ram/bookshelf/models.py
index 0aa0be8..58de694 100644
--- a/ram/bookshelf/models.py
+++ b/ram/bookshelf/models.py
@@ -43,8 +43,8 @@ class BaseBook(BaseModel):
ISBN = models.CharField(max_length=17, blank=True) # 13 + dashes
language = models.CharField(
max_length=7,
- choices=settings.LANGUAGES,
- default='en'
+ choices=sorted(settings.LANGUAGES, key=lambda s: s[1]),
+ default="en",
)
number_of_pages = models.SmallIntegerField(null=True, blank=True)
publication_year = models.SmallIntegerField(null=True, blank=True)
@@ -81,6 +81,15 @@ def book_image_upload(instance, filename):
)
+def magazine_image_upload(instance, filename):
+ return os.path.join(
+ "images",
+ "magazines",
+ str(instance.uuid),
+ filename
+ )
+
+
class BaseBookImage(Image):
book = models.ForeignKey(
BaseBook, on_delete=models.CASCADE, related_name="image"
@@ -163,12 +172,12 @@ class Magazine(BaseModel):
ISBN = models.CharField(max_length=17, blank=True) # 13 + dashes
image = models.ImageField(
blank=True,
- upload_to=book_image_upload,
+ upload_to=magazine_image_upload,
storage=DeduplicatedStorage,
)
language = models.CharField(
max_length=7,
- choices=settings.LANGUAGES,
+ choices=sorted(settings.LANGUAGES, key=lambda s: s[1]),
default='en'
)
tags = models.ManyToManyField(
@@ -192,8 +201,8 @@ class Magazine(BaseModel):
def get_absolute_url(self):
return reverse(
- "bookshelf_item",
- kwargs={"selector": "magazine", "uuid": self.uuid}
+ "magazine",
+ kwargs={"uuid": self.uuid}
)
@@ -224,3 +233,16 @@ class MagazineIssue(BaseBook):
def preview(self):
return self.image.first().image_thumbnail(100)
+
+ @property
+ def publisher(self):
+ return self.magazine.publisher
+
+ def get_absolute_url(self):
+ return reverse(
+ "issue",
+ kwargs={
+ "uuid": self.uuid,
+ "magazine": self.magazine.uuid
+ }
+ )
diff --git a/ram/portal/templates/bookshelf/book.html b/ram/portal/templates/bookshelf/book.html
index 778fcd4..34c66d1 100644
--- a/ram/portal/templates/bookshelf/book.html
+++ b/ram/portal/templates/bookshelf/book.html
@@ -89,7 +89,30 @@
| Publisher |
- {{ book.publisher }} |
+
+ {{ book.publisher }}
+ {% if book.publisher.website %} {% endif %}
+ |
+
+ {% elif type == "magazineissue" %}
+
+ | Magazine |
+ {{ book.magazine }} |
+
+
+ | Publisher |
+
+ {{ book.publisher }}
+ {% if book.publisher.website %} {% endif %}
+ |
+
+
+ | Issue |
+ {{ book.issue_number }} |
+
+
+ | Date |
+ {{ book.publication_year|default:"-" }} / {{ book.publication_month|default:"-" }} |
{% endif %}
@@ -104,10 +127,12 @@
| Number of pages |
{{ book.number_of_pages|default:"-" }} |
+ {% if type == "boook" or type == "catalog" %}
| Publication year |
{{ book.publication_year|default:"-" }} |
+ {% endif %}
{% if book.description %}
| Description |
diff --git a/ram/portal/templates/bookshelf/bookshelf_menu.html b/ram/portal/templates/bookshelf/bookshelf_menu.html
index 7321045..a10e207 100644
--- a/ram/portal/templates/bookshelf/bookshelf_menu.html
+++ b/ram/portal/templates/bookshelf/bookshelf_menu.html
@@ -10,6 +10,9 @@
{% if catalogs_menu %}
Catalogs
{% endif %}
+ {% if magazines_menu %}
+ Magazines
+ {% endif %}
{% endif %}
diff --git a/ram/portal/templates/cards.html b/ram/portal/templates/cards.html
index d80659b..fc64027 100644
--- a/ram/portal/templates/cards.html
+++ b/ram/portal/templates/cards.html
@@ -18,6 +18,8 @@
{% include "cards/consist.html" %}
{% elif d.type == "manufacturer" %}
{% include "cards/manufacturer.html" %}
+ {% elif d.type == "magazine" or d.type == "magazineissue" %}
+ {% include "cards/magazine.html" %}
{% elif d.type == "book" or d.type == "catalog" %}
{% include "cards/book.html" %}
{% endif %}
diff --git a/ram/portal/templates/cards/book.html b/ram/portal/templates/cards/book.html
index 6205aee..5110276 100644
--- a/ram/portal/templates/cards/book.html
+++ b/ram/portal/templates/cards/book.html
@@ -24,8 +24,7 @@
|
- {% if d.type == "catalog" %}Catalog
- {% elif d.type == "book" %}Book{% endif %}
+ {{ d.type | capfirst }}
{% if not d.item.published %}
Unpublished
@@ -53,7 +52,7 @@
|
| Publisher |
- {{ d.item.publisher }} |
+ {{ d.item.publisher }} |
{% endif %}
diff --git a/ram/portal/templates/cards/consist.html b/ram/portal/templates/cards/consist.html
index 5d22611..e3da341 100644
--- a/ram/portal/templates/cards/consist.html
+++ b/ram/portal/templates/cards/consist.html
@@ -63,7 +63,7 @@
Show all data
- {% if request.user.is_staff %}
Edit{% endif %}
+ {% if request.user.is_staff %}
Edit{% endif %}
diff --git a/ram/portal/templates/cards/magazine.html b/ram/portal/templates/cards/magazine.html
new file mode 100644
index 0000000..8e90c29
--- /dev/null
+++ b/ram/portal/templates/cards/magazine.html
@@ -0,0 +1,97 @@
+{% load static %}
+
diff --git a/ram/portal/templates/cards/manufacturer.html b/ram/portal/templates/cards/manufacturer.html
index c7e41e0..fbcfd3f 100644
--- a/ram/portal/templates/cards/manufacturer.html
+++ b/ram/portal/templates/cards/manufacturer.html
@@ -31,7 +31,7 @@
diff --git a/ram/portal/templates/magazine.html b/ram/portal/templates/magazine.html
new file mode 100644
index 0000000..05f7630
--- /dev/null
+++ b/ram/portal/templates/magazine.html
@@ -0,0 +1,120 @@
+{% extends "cards.html" %}
+ {% block header %}
+ {{ block.super }}
+ {% if magazine.tags.all %}
+ Tags:
+ {% for t in magazine.tags.all %}
+ {{ t.name }}{# new line is required #}
+ {% endfor %}
+
+ {% if not magazine.published %}
+ Unpublished |
+ {% endif %}
+ Updated {{ magazine.updated_time | date:"M d, Y H:i" }}
+ {% endif %}
+ {% endblock %}
+ {% block carousel %}
+ {% if magazine.image %}
+
+
+
+
+

+
+
+
+
+ {% endif %}
+ {% endblock %}
+ {% block pagination %}
+ {% if data.has_other_pages %}
+
+ {% endif %}
+ {% endblock %}
+ {% block extra_content %}
+
+
+
+
+
+
+
+
+
+
+ |
+ Magazine
+ |
+
+
+
+
+ | Name |
+ {{ magazine }} |
+
+
+ | Publisher |
+
+ {{ magazine.publisher }}
+ {% if magazine.publisher.website %} {% endif %}
+ |
+
+
+ | ISBN |
+ {{ magazine.ISBN | default:"-" }} |
+
+ {% if magazine.description %}
+
+ | Description |
+ {{ magazine.description | safe }} |
+
+ {% endif %}
+
+
+
+
+
+ {% if request.user.is_staff %}
Edit{% endif %}
+
+
+
+
+ {% endblock %}
diff --git a/ram/portal/templatetags/show_menu.py b/ram/portal/templatetags/show_menu.py
index 35421dd..4ae0fd9 100644
--- a/ram/portal/templatetags/show_menu.py
+++ b/ram/portal/templatetags/show_menu.py
@@ -1,6 +1,6 @@
from django import template
from portal.models import Flatpage
-from bookshelf.models import Book, Catalog
+from bookshelf.models import Book, Catalog, Magazine
register = template.Library()
@@ -8,10 +8,14 @@ register = template.Library()
@register.inclusion_tag('bookshelf/bookshelf_menu.html')
def show_bookshelf_menu():
# FIXME: Filter out unpublished books and catalogs?
+ books = Book.objects.exists()
+ catalogs = Catalog.objects.exists()
+ magazines = Magazine.objects.exists()
return {
- "bookshelf_menu": (Book.objects.exists() or Catalog.objects.exists()),
- "books_menu": Book.objects.exists(),
- "catalogs_menu": Catalog.objects.exists(),
+ "bookshelf_menu": (books or catalogs or magazines),
+ "books_menu": books,
+ "catalogs_menu": catalogs,
+ "magazines_menu": magazines,
}
diff --git a/ram/portal/urls.py b/ram/portal/urls.py
index b12943e..d517a55 100644
--- a/ram/portal/urls.py
+++ b/ram/portal/urls.py
@@ -15,6 +15,9 @@ from portal.views import (
Types,
Books,
Catalogs,
+ Magazines,
+ GetMagazine,
+ GetMagazineIssue,
GetBookCatalog,
SearchObjects,
)
@@ -98,6 +101,31 @@ urlpatterns = [
Books.as_view(),
name="books_pagination"
),
+ path(
+ "bookshelf/magazine/",
+ GetMagazine.as_view(),
+ name="magazine"
+ ),
+ path(
+ "bookshelf/magazine//page/",
+ GetMagazine.as_view(),
+ name="magazine_pagination",
+ ),
+ path(
+ "bookshelf/magazine//issue/",
+ GetMagazineIssue.as_view(),
+ name="issue",
+ ),
+ path(
+ "bookshelf/magazines",
+ Magazines.as_view(),
+ name="magazines"
+ ),
+ path(
+ "bookshelf/magazines/page/",
+ Magazines.as_view(),
+ name="magazines_pagination"
+ ),
path(
"bookshelf//",
GetBookCatalog.as_view(),
diff --git a/ram/portal/views.py b/ram/portal/views.py
index 7b90bc2..8141761 100644
--- a/ram/portal/views.py
+++ b/ram/portal/views.py
@@ -16,7 +16,7 @@ from portal.utils import get_site_conf
from portal.models import Flatpage
from roster.models import RollingStock
from consist.models import Consist
-from bookshelf.models import Book, Catalog
+from bookshelf.models import Book, Catalog, Magazine, MagazineIssue
from metadata.models import (
Company,
Manufacturer,
@@ -73,7 +73,8 @@ class GetData(View):
.filter(self.filter)
)
- def get(self, request, page=1):
+ def get(self, request, filter=Q(), page=1):
+ self.filter = filter
data = []
for item in self.get_data(request):
data.append({"type": self.item_type, "item": item})
@@ -491,7 +492,8 @@ class Manufacturers(GetData):
def get_data(self, request):
return (
- Manufacturer.objects.filter(self.filter).annotate(
+ Manufacturer.objects.filter(self.filter)
+ .annotate(
num_rollingstock=(
Count(
"rollingstock",
@@ -592,9 +594,7 @@ class Scales(GetData):
num_consists=Count(
"consist",
filter=Q(
- consist__in=Consist.objects.get_published(
- request.user
- )
+ consist__in=Consist.objects.get_published(request.user)
),
distinct=True,
),
@@ -637,6 +637,85 @@ class Catalogs(GetData):
return Catalog.objects.get_published(request.user).all()
+class Magazines(GetData):
+ title = "Magazines"
+ item_type = "magazine"
+
+ def get_data(self, request):
+ return (
+ Magazine.objects.get_published(request.user)
+ .all()
+ .annotate(
+ issues=Count(
+ "issue",
+ filter=Q(
+ issue__in=(
+ MagazineIssue.objects.get_published(request.user)
+ )
+ ),
+ )
+ )
+ )
+
+
+class GetMagazine(View):
+ def get(self, request, uuid, page=1):
+ try:
+ magazine = Magazine.objects.get_published(request.user).get(
+ uuid=uuid
+ )
+ except ObjectDoesNotExist:
+ raise Http404
+ data = [
+ {
+ "type": "magazineissue",
+ "item": i,
+ }
+ for i in magazine.issue.get_published(request.user).all()
+ ]
+ paginator = Paginator(data, get_items_per_page())
+ data = paginator.get_page(page)
+ page_range = paginator.get_elided_page_range(
+ data.number, on_each_side=1, on_ends=1
+ )
+
+ return render(
+ request,
+ "magazine.html",
+ {
+ "title": magazine,
+ "magazine": magazine,
+ "data": data,
+ "matches": paginator.count,
+ "page_range": page_range,
+ },
+ )
+
+
+class GetMagazineIssue(View):
+ def get(self, request, uuid, magazine, page=1):
+ try:
+ issue = MagazineIssue.objects.get_published(request.user).get(
+ uuid=uuid,
+ magazine__uuid=magazine,
+ )
+ except ObjectDoesNotExist:
+ raise Http404
+ properties = issue.property.get_public(request.user)
+ documents = issue.document.get_public(request.user)
+ return render(
+ request,
+ "bookshelf/book.html",
+ {
+ "title": issue,
+ "book": issue,
+ "documents": documents,
+ "properties": properties,
+ "type": "magazineissue",
+ },
+ )
+
+
class GetBookCatalog(View):
def get_object(self, request, uuid, selector):
if selector == "book":
diff --git a/ram/ram/__init__.py b/ram/ram/__init__.py
index 4e27f29..01e66b5 100644
--- a/ram/ram/__init__.py
+++ b/ram/ram/__init__.py
@@ -1,4 +1,4 @@
from ram.utils import git_suffix
-__version__ = "0.18.00"
+__version__ = "0.18.1"
__version__ += git_suffix(__file__)