diff --git a/CHANGELOG.md b/CHANGELOG.md index 38eb6827f..c7e4ae59e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ A summary of changes made to the codebase, grouped per deployment. ### New features +- Added a MAKE lore wiki where MAKE members can write and edit articles about anything lore in the MAKE universe - The apply button in the English header, which previously read "søk verv", has now been translated to English ("apply to MAKE") ### Improvements diff --git a/src/internal/admin.py b/src/internal/admin.py index ef5c45fcf..7a4951d4d 100644 --- a/src/internal/admin.py +++ b/src/internal/admin.py @@ -5,7 +5,7 @@ from util import html_utils from util.admin_utils import DefaultAdminWidgetsMixin, UserSearchFieldsMixin, search_escaped_and_unescaped -from .models import Member, Quote, Secret, SystemAccess +from .models import Lore, Member, Quote, Secret, SystemAccess class MemberAdmin(DefaultAdminWidgetsMixin, SimpleHistoryAdmin): @@ -93,7 +93,13 @@ class QuoteAdmin(DefaultAdminWidgetsMixin, UserSearchFieldsMixin, admin.ModelAdm autocomplete_fields = ('author',) +class LoreAdmin(admin.ModelAdmin): + ordering = ('title',) + exclude = ('slug',) + + admin.site.register(Member, MemberAdmin) admin.site.register(SystemAccess, SystemAccessAdmin) admin.site.register(Secret, SecretAdmin) admin.site.register(Quote, QuoteAdmin) +admin.site.register(Lore, LoreAdmin) diff --git a/src/internal/forms.py b/src/internal/forms.py index 8918a8528..9bd1889db 100644 --- a/src/internal/forms.py +++ b/src/internal/forms.py @@ -1,12 +1,14 @@ from django import forms from django.db import models +from django.utils.text import slugify from django.utils.translation import gettext_lazy as _ from card import utils as card_utils from card.formfields import CardNumberField +from ckeditor.widgets import CKEditorWidget from users.models import User from web.widgets import SemanticDateInput, SemanticMultipleSelectInput, SemanticSearchableChoiceInput -from .models import Member, Quote, Secret, SystemAccess +from .models import Lore, Member, Quote, Secret, SystemAccess class AddMemberForm(forms.ModelForm): @@ -176,3 +178,35 @@ class Meta: widgets = { 'date': SemanticDateInput(), } + + +class LoreForm(forms.ModelForm): + class Meta: + model = Lore + fields = ('title', 'content') + widgets = { + 'text': CKEditorWidget() + } + + def clean(self): + cleaned_data = self.cleaned_data + title = cleaned_data.get('title') + title = title.replace('ø', 'o') + title = title.replace('æ', 'ae') + slug = slugify(title) + try: + article = self.instance + except NameError: + article = None + if article: + if (article.slug != slug) and (Lore.objects.filter(slug=slug).exists()): + self.add_error('title', _("An article with this title already exists. Please merge your text with the existing one!")) + elif slug == '': + self.add_error('title', _("Please make a title consisting of actual letters!")) + elif not article: + if Lore.objects.filter(slug=slug).exists(): + self.add_error('title', _("An article with this title already exists. Please merge your text with the existing one!")) + elif slug == '': + self.add_error('title', _("Please make a title consisting of actual letters!")) + else: + return cleaned_data diff --git a/src/internal/migrations/0027_lore.py b/src/internal/migrations/0027_lore.py new file mode 100644 index 000000000..8d642fe26 --- /dev/null +++ b/src/internal/migrations/0027_lore.py @@ -0,0 +1,27 @@ +# Generated by Django 4.1.7 on 2023-11-17 16:00 + +import ckeditor_uploader.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('internal', '0026_add_secret_permissions'), + ] + + operations = [ + migrations.CreateModel( + name='Lore', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=200, unique=True, verbose_name='title')), + ('slug', models.SlugField(unique=True)), + ('content', ckeditor_uploader.fields.RichTextUploadingField(max_length=150000, verbose_name='text')), + ], + options={ + 'verbose_name': 'lore article', + 'verbose_name_plural': 'lore articles', + }, + ), + ] diff --git a/src/internal/models.py b/src/internal/models.py index 02afa34cd..348504f0d 100644 --- a/src/internal/models.py +++ b/src/internal/models.py @@ -5,7 +5,7 @@ from django.db.models import F from django.db.models.functions import Lower from django.utils import timezone -from django.utils.text import capfirst +from django.utils.text import capfirst, slugify from django.utils.translation import gettext_lazy as _ from phonenumber_field.modelfields import PhoneNumberField from phonenumber_field.phonenumber import PhoneNumber @@ -314,3 +314,24 @@ class Meta: def __str__(self): return _("“{quote}” —{quoted}").format(quote=self.quote, quoted=self.quoted) + + +class Lore(models.Model): + title = models.CharField(max_length=200, unique=True, verbose_name=_("title")) + slug = models.SlugField(unique=True) + content = RichTextUploadingField(max_length=150000, verbose_name=_("text")) + + def __str__(self): + return self.title + + def save(self, *args, **kwargs): + title = self.title + self.title = capfirst(title) + title = title.replace('ø', 'o') + title = title.replace('æ', 'ae') + self.slug = slugify(title) + super().save(*args, **kwargs) + + class Meta: + verbose_name = _("lore article") + verbose_name_plural = _("lore articles") diff --git a/src/internal/static/internal/css/lore_list.css b/src/internal/static/internal/css/lore_list.css new file mode 100644 index 000000000..b8d4baba8 --- /dev/null +++ b/src/internal/static/internal/css/lore_list.css @@ -0,0 +1,159 @@ +#lore-page { + position: relative; + max-width: 100vw; +} + +#lore-wiki-list { + float: left; + width: 20%; + padding: 3rem 0 40px 40px; + position: relative; +} + +.lore-article-links a { + color: black; +} + +ul.lore-article-links { + list-style-type: none; + list-style-position: inside; + padding: 0; +} + +.lore-article-links .selected-lore-article { + font-weight: bold; +} + +#lore-article { + float: left; + width: 80%; + padding: 3rem 25% 0 5%; + position: relative; +} + +#lore-article p { + text-align: justify; +} + +.lore-container img { + margin: 20px 30px; +} + +#lore-article .lore-container { + margin-bottom: 70px; +} + +#lore-wiki-list .plus.icon { + font-size: 90%; +} + +#lore-burger-icon + h2 .plus.icon { + font-size: 80%; +} + +#lore-article .trash.icon, +#lore-article .pencil.icon { + font-size: 120%; +} + +#lore-article .lore-heading { + display: block; + max-width: 100vw; +} + +/* The burger menu used on the lore page is loosely based on https://alvarotrigo.com/blog/hamburger-menu-css-responsive/ */ + +#burger-input { + display: none; +} + +#lore-burger-icon { + display: none; + width: 15px; + height: 15px; + cursor: pointer; + border: none; + margin-top:6px; + background: linear-gradient( + to bottom, + black, black 20%, + white 20%, white 40%, + black 40%, black 60%, + white 60%, white 80%, + black 80%, black 100% + ); + position: absolute; + top: 3rem; + left: 15%; +} + +#lore-burger-icon + h2 { + position: absolute; + top: 3rem; + left: 15%; + margin: 0 0 0 30px; + display: none; +} + +#lore-burger-menu { + display: none; + max-width: 100vw; + padding: 6rem 15% 0 15%; +} + +@media screen and (max-width: 992px) { + + #lore-burger-icon { + display: inline; + } + + #lore-burger-icon + h2 { + display: block; + } + + #burger-input:not(:checked) ~ #lore-article { + position: absolute; + top: 6rem; + } + + #burger-input:checked + #lore-burger-icon { + clip-path: polygon(15% 0%, 0% 15%, 35% 50%, 0% 85%, 15% 100%, 50% 65%, 85% 100%, 100% 85%, 65% 50%, 100% 15%, 85% 0%, 50% 35%); + background: black; + } + + #burger-input:checked + #lore-burger-icon ~ #lore-burger-menu { + display: block; + top: 6rem; + } + + #lore-wiki-list { + display: none; + float: none; + max-width: 100vw; + padding: 2rem 15% 0 15%; + } + + #lore-article { + float: none; + width: 100vw; + padding: 3rem 15% 2rem 15%; + position: relative; + } +} + +@media screen and (max-width: 450px) { + + #lore-article { + float: none; + width: 100%; + padding: 3rem 15% 2rem 15%; + position: relative; + } + + .lore-container img { + float: none; + width: 100%; + height: unset; + margin: 10px 0; + } +} diff --git a/src/internal/static/internal/js/lore_list.js b/src/internal/static/internal/js/lore_list.js new file mode 100644 index 000000000..19bf7ed3b --- /dev/null +++ b/src/internal/static/internal/js/lore_list.js @@ -0,0 +1,10 @@ +$(document).ready(function() { + $('img').each(function() { + if ($(this).css('float') === 'left') { + $(this).css('margin-left', '0'); + } + if ($(this).css('float') === 'right') { + $(this).css('margin-right', '0'); + } + }); +}); diff --git a/src/internal/templates/internal/header.html b/src/internal/templates/internal/header.html index bcf274d8d..c278ca7b2 100644 --- a/src/internal/templates/internal/header.html +++ b/src/internal/templates/internal/header.html @@ -20,6 +20,10 @@
{% translate "Quotes" %}
+ +
{% translate "MAKE Lore" %}
+
+