diff --git a/arduino/CommandStation-EX b/arduino/CommandStation-EX index 2db2b0e..3b16299 160000 --- a/arduino/CommandStation-EX +++ b/arduino/CommandStation-EX @@ -1 +1 @@ -Subproject commit 2db2b0ecc60f0b20f01f186fcac7971d437fdf2c +Subproject commit 3b162996ad42546486b812e22d3ed6daee857d19 diff --git a/ram/bookshelf/migrations/0001_initial.py b/ram/bookshelf/migrations/0001_initial.py index 61c8b35..7a537a3 100644 --- a/ram/bookshelf/migrations/0001_initial.py +++ b/ram/bookshelf/migrations/0001_initial.py @@ -1,6 +1,7 @@ # Generated by Django 4.2.5 on 2023-10-01 20:16 -import ckeditor_uploader.fields +# ckeditor removal +# import ckeditor_uploader.fields from django.db import migrations, models import django.db.models.deletion import uuid @@ -47,7 +48,8 @@ class Migration(migrations.Migration): ("ISBN", models.CharField(max_length=13, unique=True)), ("publication_year", models.SmallIntegerField(blank=True, null=True)), ("purchase_date", models.DateField(blank=True, null=True)), - ("notes", ckeditor_uploader.fields.RichTextUploadingField(blank=True)), + # ("notes", ckeditor_uploader.fields.RichTextUploadingField(blank=True)), + ("notes", models.TextField(blank=True)), ("creation_time", models.DateTimeField(auto_now_add=True)), ("updated_time", models.DateTimeField(auto_now=True)), ("authors", models.ManyToManyField(to="bookshelf.author")), diff --git a/ram/bookshelf/migrations/0012_alter_book_notes.py b/ram/bookshelf/migrations/0012_alter_book_notes.py new file mode 100644 index 0000000..73b8e08 --- /dev/null +++ b/ram/bookshelf/migrations/0012_alter_book_notes.py @@ -0,0 +1,19 @@ +# Generated by Django 5.0.2 on 2024-02-17 12:19 + +import tinymce.models +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("bookshelf", "0011_alter_book_language"), + ] + + operations = [ + migrations.AlterField( + model_name="book", + name="notes", + field=tinymce.models.HTMLField(blank=True), + ), + ] diff --git a/ram/bookshelf/models.py b/ram/bookshelf/models.py index 5c025a1..6c2dcf8 100644 --- a/ram/bookshelf/models.py +++ b/ram/bookshelf/models.py @@ -6,7 +6,7 @@ from django.conf import settings from django.urls import reverse from django_countries.fields import CountryField -from ckeditor_uploader.fields import RichTextUploadingField +from tinymce import models as tinymce from metadata.models import Tag from ram.utils import DeduplicatedStorage @@ -56,7 +56,7 @@ class Book(models.Model): tags = models.ManyToManyField( Tag, related_name="bookshelf", blank=True ) - notes = RichTextUploadingField(blank=True) + notes = tinymce.HTMLField(blank=True) creation_time = models.DateTimeField(auto_now_add=True) updated_time = models.DateTimeField(auto_now=True) diff --git a/ram/consist/migrations/0005_alter_consist_notes.py b/ram/consist/migrations/0005_alter_consist_notes.py index 0d128d2..c9dfbdd 100644 --- a/ram/consist/migrations/0005_alter_consist_notes.py +++ b/ram/consist/migrations/0005_alter_consist_notes.py @@ -1,6 +1,7 @@ # Generated by Django 4.1 on 2022-08-23 15:54 -import ckeditor_uploader.fields +# ckeditor removal +# import ckeditor_uploader.fields from django.db import migrations @@ -11,9 +12,9 @@ class Migration(migrations.Migration): ] operations = [ - migrations.AlterField( - model_name="consist", - name="notes", - field=ckeditor_uploader.fields.RichTextUploadingField(blank=True), - ), + # migrations.AlterField( + # model_name="consist", + # name="notes", + # field=ckeditor_uploader.fields.RichTextUploadingField(blank=True), + # ), ] diff --git a/ram/consist/migrations/0010_alter_consist_notes.py b/ram/consist/migrations/0010_alter_consist_notes.py new file mode 100644 index 0000000..39dbfa2 --- /dev/null +++ b/ram/consist/migrations/0010_alter_consist_notes.py @@ -0,0 +1,19 @@ +# Generated by Django 5.0.2 on 2024-02-17 12:19 + +import tinymce.models +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("consist", "0009_alter_consist_image"), + ] + + operations = [ + migrations.AlterField( + model_name="consist", + name="notes", + field=tinymce.models.HTMLField(blank=True), + ), + ] diff --git a/ram/consist/models.py b/ram/consist/models.py index f0e27c0..0ea0a9f 100644 --- a/ram/consist/models.py +++ b/ram/consist/models.py @@ -4,7 +4,7 @@ from uuid import uuid4 from django.db import models from django.urls import reverse -from ckeditor_uploader.fields import RichTextUploadingField +from tinymce import models as tinymce from ram.utils import DeduplicatedStorage from metadata.models import Company, Tag @@ -26,7 +26,7 @@ class Consist(models.Model): null=True, blank=True, ) - notes = RichTextUploadingField(blank=True) + notes = tinymce.HTMLField(blank=True) creation_time = models.DateTimeField(auto_now_add=True) updated_time = models.DateTimeField(auto_now=True) diff --git a/ram/portal/migrations/0011_alter_flatpage_content_alter_siteconfiguration_about_and_more.py b/ram/portal/migrations/0011_alter_flatpage_content_alter_siteconfiguration_about_and_more.py index 7dd7fe7..4bbe106 100644 --- a/ram/portal/migrations/0011_alter_flatpage_content_alter_siteconfiguration_about_and_more.py +++ b/ram/portal/migrations/0011_alter_flatpage_content_alter_siteconfiguration_about_and_more.py @@ -1,7 +1,8 @@ # Generated by Django 4.1 on 2022-08-23 15:54 -import ckeditor.fields -import ckeditor_uploader.fields +# ckeditor dependency removal +# import ckeditor.fields +# import ckeditor_uploader.fields from django.db import migrations @@ -12,24 +13,24 @@ class Migration(migrations.Migration): ] operations = [ - migrations.AlterField( - model_name="flatpage", - name="content", - field=ckeditor_uploader.fields.RichTextUploadingField(), - ), - migrations.AlterField( - model_name="siteconfiguration", - name="about", - field=ckeditor.fields.RichTextField(blank=True), - ), - migrations.AlterField( - model_name="siteconfiguration", - name="footer", - field=ckeditor.fields.RichTextField(blank=True), - ), - migrations.AlterField( - model_name="siteconfiguration", - name="footer_extended", - field=ckeditor.fields.RichTextField(blank=True), - ), +# migrations.AlterField( +# model_name="flatpage", +# name="content", +# field=ckeditor_uploader.fields.RichTextUploadingField(), +# ), +# migrations.AlterField( +# model_name="siteconfiguration", +# name="about", +# field=ckeditor.fields.RichTextField(blank=True), +# ), +# migrations.AlterField( +# model_name="siteconfiguration", +# name="footer", +# field=ckeditor.fields.RichTextField(blank=True), +# ), +# migrations.AlterField( +# model_name="siteconfiguration", +# name="footer_extended", +# field=ckeditor.fields.RichTextField(blank=True), +# ), ] diff --git a/ram/portal/migrations/0017_alter_flatpage_content_alter_siteconfiguration_about_and_more.py b/ram/portal/migrations/0017_alter_flatpage_content_alter_siteconfiguration_about_and_more.py new file mode 100644 index 0000000..8a9e11d --- /dev/null +++ b/ram/portal/migrations/0017_alter_flatpage_content_alter_siteconfiguration_about_and_more.py @@ -0,0 +1,34 @@ +# Generated by Django 5.0.2 on 2024-02-17 12:19 + +import tinymce.models +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("portal", "0016_remove_siteconfiguration_site_name"), + ] + + operations = [ + migrations.AlterField( + model_name="flatpage", + name="content", + field=tinymce.models.HTMLField(), + ), + migrations.AlterField( + model_name="siteconfiguration", + name="about", + field=tinymce.models.HTMLField(blank=True), + ), + migrations.AlterField( + model_name="siteconfiguration", + name="footer", + field=tinymce.models.HTMLField(blank=True), + ), + migrations.AlterField( + model_name="siteconfiguration", + name="footer_extended", + field=tinymce.models.HTMLField(blank=True), + ), + ] diff --git a/ram/portal/models.py b/ram/portal/models.py index 43a34d7..52c2014 100644 --- a/ram/portal/models.py +++ b/ram/portal/models.py @@ -6,8 +6,7 @@ from django.dispatch.dispatcher import receiver from django.utils.safestring import mark_safe from solo.models import SingletonModel -from ckeditor.fields import RichTextField -from ckeditor_uploader.fields import RichTextUploadingField +from tinymce import models as tinymce from ram import __version__ as app_version from ram.utils import slugify @@ -15,7 +14,7 @@ from ram.utils import slugify class SiteConfiguration(SingletonModel): site_author = models.CharField(max_length=256, blank=True) - about = RichTextField(blank=True) + about = tinymce.HTMLField(blank=True) items_per_page = models.CharField( max_length=2, choices=[(str(x * 3), str(x * 3)) for x in range(2, 11)], @@ -30,8 +29,8 @@ class SiteConfiguration(SingletonModel): ], default="type", ) - footer = RichTextField(blank=True) - footer_extended = RichTextField(blank=True) + footer = tinymce.HTMLField(blank=True) + footer_extended = tinymce.HTMLField(blank=True) show_version = models.BooleanField(default=True) use_cdn = models.BooleanField(default=True) extra_head = models.TextField(blank=True) @@ -56,7 +55,7 @@ class Flatpage(models.Model): name = models.CharField(max_length=256, unique=True) path = models.CharField(max_length=256, unique=True) published = models.BooleanField(default=False) - content = RichTextUploadingField() + content = tinymce.HTMLField() creation_time = models.DateTimeField(auto_now_add=True) updated_time = models.DateTimeField(auto_now=True) diff --git a/ram/ram/__init__.py b/ram/ram/__init__.py index 5578c39..3c53cf0 100644 --- a/ram/ram/__init__.py +++ b/ram/ram/__init__.py @@ -1,4 +1,4 @@ from ram.utils import git_suffix -__version__ = "0.9.6" +__version__ = "0.10.0" __version__ += git_suffix(__file__) diff --git a/ram/ram/settings.py b/ram/ram/settings.py index b4122de..9b0a516 100644 --- a/ram/ram/settings.py +++ b/ram/ram/settings.py @@ -44,8 +44,7 @@ INSTALLED_APPS = [ "adminsortable2", "django_countries", "solo", - "ckeditor", - "ckeditor_uploader", + "tinymce", "rest_framework", "ram", "portal", @@ -60,7 +59,7 @@ MIDDLEWARE = [ "django.middleware.security.SecurityMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", "django.middleware.common.CommonMiddleware", - 'django.middleware.csrf.CsrfViewMiddleware', + "django.middleware.csrf.CsrfViewMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", @@ -142,7 +141,23 @@ DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" MEDIA_URL = "media/" MEDIA_ROOT = STORAGE_DIR / "media" -CKEDITOR_UPLOAD_PATH = "uploads/" + +TINYMCE_DEFAULT_CONFIG = { + "height": "500px", + "menubar": False, + "plugins": "autolink lists link image charmap preview anchor " + "searchreplace visualblocks code fullscreen insertdatetime media " + "table paste code", + "toolbar": "undo redo | " + "bold italic underline strikethrough removeformat | " + "fontsizeselect formatselect | " + "alignleft aligncenter alignright alignjustify | " + "outdent indent numlist bullist | " + "insertfile image media pageembed template link anchor codesample | " + "charmap | " + "fullscreen preview code", + "images_upload_url": "/tinymce/upload_image", +} COUNTRIES_OVERRIDE = { "EU": "Europe", diff --git a/ram/ram/urls.py b/ram/ram/urls.py index ae2b14a..d5c851c 100644 --- a/ram/ram/urls.py +++ b/ram/ram/urls.py @@ -13,6 +13,7 @@ Including another URLconf 1. Import the include() function: from django.urls import include, path 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ + from django.apps import apps from django.conf import settings from django.shortcuts import redirect @@ -20,9 +21,12 @@ from django.conf.urls.static import static from django.contrib import admin from django.urls import include, path +from ram.views import UploadImage + urlpatterns = [ path("", lambda r: redirect("portal/")), - path("ckeditor/", include("ckeditor_uploader.urls")), + path("tinymce/", include("tinymce.urls")), + path("tinymce/upload_image", UploadImage.as_view(), name="upload_image"), path("portal/", include("portal.urls")), path("admin/", admin.site.urls), path("api/v1/consist/", include("consist.urls")), diff --git a/ram/ram/views.py b/ram/ram/views.py new file mode 100644 index 0000000..ea044e9 --- /dev/null +++ b/ram/ram/views.py @@ -0,0 +1,62 @@ +import os +import datetime +import posixpath + +from pathlib import Path +from PIL import Image, UnidentifiedImageError + +from django.views import View +from django.conf import settings +from django.http import ( + HttpResponseBadRequest, + HttpResponseForbidden, + JsonResponse, +) +from django.utils.text import slugify as slugify +from django.utils.decorators import method_decorator +from django.views.decorators.csrf import csrf_exempt + + +@method_decorator(csrf_exempt, name="dispatch") +class UploadImage(View): + def post(self, request, application=None, model=None): + if not request.user.is_authenticated: + raise HttpResponseForbidden() + + file_obj = request.FILES["file"] + file_name, file_extension = os.path.splitext(file_obj.name) + file_name = slugify(file_name) + file_extension + + try: + Image.open(file_obj) + except UnidentifiedImageError: + return HttpResponseBadRequest() + + today = datetime.date.today() + container = ( + "uploads", + today.strftime("%Y"), + today.strftime("%m"), + today.strftime("%d"), + ) + + dir_path = os.path.join(settings.MEDIA_ROOT, *(p for p in container)) + file_path = os.path.normpath(os.path.join(dir_path, file_name)) + # even if we apply slugify to the file name, add more hardening + # to avoid any path transversal risk + if not file_path.startswith(str(settings.MEDIA_ROOT)): + return HttpResponseBadRequest() + + Path(dir_path).mkdir(parents=True, exist_ok=True) + with open(file_path, "wb+") as f: + for chunk in file_obj.chunks(): + f.write(chunk) + + return JsonResponse( + { + "message": "Image uploaded successfully", + "location": posixpath.join( + settings.MEDIA_URL, *(p for p in container), file_name + ), + } + ) diff --git a/ram/roster/migrations/0010_alter_rollingstock_notes.py b/ram/roster/migrations/0010_alter_rollingstock_notes.py index d5be0fd..a88ca1a 100644 --- a/ram/roster/migrations/0010_alter_rollingstock_notes.py +++ b/ram/roster/migrations/0010_alter_rollingstock_notes.py @@ -1,6 +1,7 @@ # Generated by Django 4.1 on 2022-08-23 15:54 -import ckeditor_uploader.fields +# ckeditor removal +# import ckeditor_uploader.fields from django.db import migrations @@ -11,9 +12,9 @@ class Migration(migrations.Migration): ] operations = [ - migrations.AlterField( - model_name="rollingstock", - name="notes", - field=ckeditor_uploader.fields.RichTextUploadingField(blank=True), - ), + # migrations.AlterField( + # model_name="rollingstock", + # name="notes", + # field=ckeditor_uploader.fields.RichTextUploadingField(blank=True), + # ), ] diff --git a/ram/roster/migrations/0012_rollingstockjournal.py b/ram/roster/migrations/0012_rollingstockjournal.py index 98e013b..269ee06 100644 --- a/ram/roster/migrations/0012_rollingstockjournal.py +++ b/ram/roster/migrations/0012_rollingstockjournal.py @@ -1,6 +1,7 @@ # Generated by Django 4.1 on 2022-08-27 12:43 -import ckeditor_uploader.fields +# ckeditor removal +# import ckeditor_uploader.fields from django.db import migrations, models import django.db.models.deletion @@ -25,7 +26,8 @@ class Migration(migrations.Migration): ), ), ("date", models.DateField()), - ("log", ckeditor_uploader.fields.RichTextUploadingField()), + # ("log", ckeditor_uploader.fields.RichTextUploadingField()), + ("log", models.TextField()), ("private", models.BooleanField(default=False)), ("creation_time", models.DateTimeField(auto_now_add=True)), ("updated_time", models.DateTimeField(auto_now=True)), diff --git a/ram/roster/migrations/0022_alter_rollingstock_notes_and_more.py b/ram/roster/migrations/0022_alter_rollingstock_notes_and_more.py new file mode 100644 index 0000000..e015dc1 --- /dev/null +++ b/ram/roster/migrations/0022_alter_rollingstock_notes_and_more.py @@ -0,0 +1,24 @@ +# Generated by Django 5.0.2 on 2024-02-17 12:19 + +import tinymce.models +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("roster", "0021_alter_rollingstockdocument_file_and_more"), + ] + + operations = [ + migrations.AlterField( + model_name="rollingstock", + name="notes", + field=tinymce.models.HTMLField(blank=True), + ), + migrations.AlterField( + model_name="rollingstockjournal", + name="log", + field=tinymce.models.HTMLField(), + ), + ] diff --git a/ram/roster/models.py b/ram/roster/models.py index 5ae18d8..80b9f86 100644 --- a/ram/roster/models.py +++ b/ram/roster/models.py @@ -7,7 +7,7 @@ from django.urls import reverse from django.conf import settings from django.dispatch import receiver -from ckeditor_uploader.fields import RichTextUploadingField +from tinymce import models as tinymce from ram.models import Document, Image, PropertyInstance from ram.utils import DeduplicatedStorage @@ -85,7 +85,7 @@ class RollingStock(models.Model): era = models.CharField(max_length=32, blank=True) production_year = models.SmallIntegerField(null=True, blank=True) purchase_date = models.DateField(null=True, blank=True) - notes = RichTextUploadingField(blank=True) + notes = tinymce.HTMLField(blank=True) tags = models.ManyToManyField( Tag, related_name="rolling_stock", blank=True ) @@ -175,7 +175,7 @@ class RollingStockJournal(models.Model): blank=False, ) date = models.DateField() - log = RichTextUploadingField() + log = tinymce.HTMLField() private = models.BooleanField(default=False) creation_time = models.DateTimeField(auto_now_add=True) updated_time = models.DateTimeField(auto_now=True) diff --git a/requirements.txt b/requirements.txt index 3717bce..f749874 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,7 +7,7 @@ django-solo django-countries django-health-check django-admin-sortable2 -django-ckeditor +django-tinymce # Optional: # psycopg2-binary # Optional: # pySerial # Required by django-countries and not always installed