diff --git a/app/item/admin.py b/app/item/admin.py index 94acd42..3d41227 100644 --- a/app/item/admin.py +++ b/app/item/admin.py @@ -9,6 +9,10 @@ class ItemAdmin(admin.ModelAdmin): list_filter = ( ("published", admin.EmptyFieldListFilter), ) + raw_id_fields = ['user'] + + def get_changeform_initial_data(self, request): + return {'user': request.user} admin.site.register(models.Item, ItemAdmin) @@ -25,5 +29,6 @@ class CommentAdmin(admin.ModelAdmin): list_filter = ( ("published", admin.EmptyFieldListFilter), ) + raw_id_fields = ['item', 'user'] admin.site.register(models.Comment, CommentAdmin) diff --git a/app/item/models.py b/app/item/models.py index 9584121..de681d0 100644 --- a/app/item/models.py +++ b/app/item/models.py @@ -26,14 +26,14 @@ class Item(models.Model): created = models.DateTimeField(auto_now_add=True) modified = models.DateTimeField(auto_now=True) - user = models.ForeignKey(User, null=True, related_name='items', on_delete=models.CASCADE) - url = models.CharField(max_length=1024, unique=True) title = models.CharField(max_length=1024) - description = models.TextField(default="", blank=True) + url = models.CharField(max_length=1024, unique=True) + description = models.TextField(default="", blank=True, editable=False) published = models.DateTimeField(default=timezone.now, null=True, blank=True) - announced = models.DateTimeField(null=True, default=None, blank=True) + announced = models.DateTimeField(null=True, default=None, blank=True, editable=False) data = models.JSONField(default=dict, editable=False) + user = models.ForeignKey(User, null=True, related_name='items', on_delete=models.CASCADE) def save(self, *args, **kwargs): if self.url and not self.data: @@ -105,14 +105,16 @@ class Comment(models.Model): modified = models.DateTimeField(auto_now=True) item = models.ForeignKey(Item, related_name='comments', on_delete=models.CASCADE) - user = models.ForeignKey(User, null=True, related_name='comments', on_delete=models.CASCADE, blank=True) - session_key = models.CharField(max_length=60, null=True, default=None, blank=True) + text = models.TextField(default="") + + name = models.CharField(max_length=1024, blank=True) + email = models.CharField(max_length=1024, blank=True) + + user = models.ForeignKey(User, null=True, related_name='comments', on_delete=models.CASCADE, blank=True) + session_key = models.CharField(max_length=60, null=True, default=None, blank=True, editable=False) - name = models.CharField(max_length=1024) - email = models.CharField(max_length=1024) - text = models.TextField(default="", blank=True) data = models.JSONField(default=dict, editable=False) - published = models.DateTimeField(null=True, default=None) + published = models.DateTimeField(null=True, default=None, blank=True) class Meta: permissions = [ diff --git a/app/item/tasks.py b/app/item/tasks.py index 7f47746..b6562c2 100644 --- a/app/item/tasks.py +++ b/app/item/tasks.py @@ -12,7 +12,7 @@ from ..celery import app from . import models -@app.task(queue="default") +@app.task(queue="default", ignore_results=True) def announce_items(): if not getattr(settings, 'SIGNAL_ANNOUNCE_GROUP'): return @@ -42,18 +42,27 @@ def announce_items(): os.unlink(f.name) -@app.task(queue="default") +@app.task(queue="default", ignore_results=True) def notify_moderators(id, link): comment = models.Comment.objects.filter(id=id).first() if comment: - message = "%s commented on %s (%s)\n\n%s" % (comment.name, comment.item.title, link, comment.text) + message = "%s commented on %s (%s)\n\n%s" % ( + comment.name, comment.item.title, link, comment.text + ) r = rpc.send(message, group=settings.SIGNAL_MODERATORS_GROUP) if r and "timestamp" in r: comment.data["moderator_ts"] = r["timestamp"] comment.save() + if comment.published: + account = settings.SIGNAL_ACCOUNT + group = settings.SIGNAL_MODERATORS_GROUP + rpc.send_reaction( + account, comment.data["moderator_ts"], "🎉", group=group + ) else: print("failed to notify", r) + @app.on_after_finalize.connect def setup_periodic_tasks(sender, **kwargs): sender.add_periodic_task(crontab(minute="*/2"), announce_items.s()) diff --git a/app/item/views.py b/app/item/views.py index 8c993a3..f216d9c 100644 --- a/app/item/views.py +++ b/app/item/views.py @@ -1,3 +1,5 @@ +import xml.etree.ElementTree as ET + from datetime import date, datetime, timedelta import json @@ -6,6 +8,7 @@ from django.shortcuts import render from django.db.models import Q from django.utils.html import mark_safe from django.conf import settings +from django.http import HttpResponse from . import models from . import tasks @@ -14,7 +17,7 @@ from .utils import render_to_json def index(request): - context = {} + context = {"settings": settings} week, archive = models.Item.public() context['items'] = week context['archive'] = archive.exists() @@ -22,7 +25,7 @@ def index(request): def archive(request): - context = {} + context = {"settings": settings} qs = models.Item.public() week, archive = models.Item.public() context['items'] = archive @@ -30,7 +33,7 @@ def archive(request): def item(request, id): - context = {} + context = {"settings": settings} item = models.Item.objects.get(id=id) context['item'] = item qs = item.comments.order_by('created') @@ -71,9 +74,8 @@ def comment(request): comment.session_key = request.session.session_key comment.text = data['text'] comment.save() - if not comment.published: - link = request.build_absolute_uri(comment.item.get_absolute_url()) - tasks.notify_moderators.delay(comment.id, link) + link = request.build_absolute_uri(comment.item.get_absolute_url()) + tasks.notify_moderators.delay(comment.id, link) response = comment.json() return render_to_json(response) @@ -88,8 +90,61 @@ def publish_comment(request): if comment.data.get("moderator_ts"): account = settings.SIGNAL_ACCOUNT group = settings.SIGNAL_MODERATORS_GROUP - send_reaction(account, comment.data["moderator_ts"], "🎉", group=group) + send_reaction( + account, comment.data["moderator_ts"], "🎉", group=group + ) response['status'] = 'ok' else: response['error'] = 'permission denied' return render_to_json(response) + + +def atom_xml(request): + feed = ET.Element("feed") + feed.attrib['xmlns'] = 'http://www.w3.org/2005/Atom' + feed.attrib['xmlns:media'] = 'http://search.yahoo.com/mrss/' + feed.attrib['xml:lang'] = 'en' + title = ET.SubElement(feed, "title") + title.text = settings.SITENAME + title.attrib['type'] = 'text' + link = ET.SubElement(feed, "link") + link.attrib['rel'] = 'self' + link.attrib['type'] = 'application/atom+xml' + atom_link = request.build_absolute_uri('/atom.xml') + link.attrib['href'] = atom_link + el = ET.SubElement(feed, 'id') + el.text = atom_link + + week, archive = models.Item.public() + for item in week: + page_link = request.build_absolute_uri(item.get_absolute_url()) + + entry = ET.Element("entry") + title = ET.SubElement(entry, "title") + title.text = item.title + link = ET.SubElement(entry, "link") + link.attrib['rel'] = 'alternate' + link.attrib['href'] = page_link + updated = ET.SubElement(entry, "updated") + updated.text = item.modified.strftime("%Y-%m-%dT%H:%M:%SZ") + published = ET.SubElement(entry, "published") + published.text = item.created.strftime("%Y-%m-%dT%H:%M:%SZ") + el = ET.SubElement(entry, "id") + el.text = page_link + print(item.data) + if 'title' in item.data and 'thumbnail' in item.data: + html = f''' +

+ {item.data['title']} +

+ + '''.strip() + content = ET.SubElement(entry, "content") + content.attrib['type'] = 'html' + content.text = html + feed.append(entry) + return HttpResponse( + '\n' + ET.tostring(feed).decode(), + 'application/atom+xml' + ) + diff --git a/app/page/__init__.py b/app/page/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/page/admin.py b/app/page/admin.py new file mode 100644 index 0000000..d38f4ce --- /dev/null +++ b/app/page/admin.py @@ -0,0 +1,7 @@ +from django.contrib import admin +from . import models + +@admin.decorators.register(models.Page) +class PageAdmin(admin.ModelAdmin): + list_display = ('slug', 'title', 'created', 'modified') + search_fields = ['title', 'slug'] diff --git a/app/page/apps.py b/app/page/apps.py new file mode 100644 index 0000000..77a4bc1 --- /dev/null +++ b/app/page/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class PageConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "app.page" diff --git a/app/page/migrations/0001_initial.py b/app/page/migrations/0001_initial.py new file mode 100644 index 0000000..f3a7a00 --- /dev/null +++ b/app/page/migrations/0001_initial.py @@ -0,0 +1,34 @@ +# Generated by Django 4.2.3 on 2023-07-25 18:51 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name="Page", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("created", models.DateTimeField(auto_now_add=True)), + ("modified", models.DateTimeField(auto_now=True)), + ( + "slug", + models.CharField(max_length=255, unique=True, verbose_name="Slug"), + ), + ("title", models.CharField(max_length=1024)), + ("content", models.TextField(blank=True, default="")), + ], + ), + ] diff --git a/app/page/migrations/__init__.py b/app/page/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/page/models.py b/app/page/models.py new file mode 100644 index 0000000..603a7bb --- /dev/null +++ b/app/page/models.py @@ -0,0 +1,16 @@ +from django.db import models + + +class Page(models.Model): + created = models.DateTimeField(auto_now_add=True) + modified = models.DateTimeField(auto_now=True) + slug = models.CharField('Slug', max_length=255, unique=True) + + title = models.CharField(max_length=1024) + content = models.TextField(default='', blank=True) + + def __str__(self): + return self.slug + + def get_absolute_url(self): + return '/%s' % self.slug diff --git a/app/page/tests.py b/app/page/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/app/page/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/app/page/views.py b/app/page/views.py new file mode 100644 index 0000000..7c3da49 --- /dev/null +++ b/app/page/views.py @@ -0,0 +1,13 @@ +from django.conf import settings +from django.shortcuts import render, redirect, get_object_or_404 + +from . import models + + +def page(request, slug): + page = get_object_or_404(models.Page, slug=slug) + context = { + 'settings': settings, + 'page': page, + } + return render(request, 'page.html', context) diff --git a/app/settings/common.py b/app/settings/common.py index 32d240d..8c7a9cd 100644 --- a/app/settings/common.py +++ b/app/settings/common.py @@ -60,6 +60,7 @@ INSTALLED_APPS = [ "app.user", "app.item", "app.signalbot", + "app.page", ] MIDDLEWARE = [ @@ -86,6 +87,9 @@ TEMPLATES = [ "django.contrib.auth.context_processors.auth", "django.contrib.messages.context_processors.messages", ], + "builtins": [ + "django.templatetags.static", + ] }, }, ] @@ -159,3 +163,5 @@ CELERY_RESULT_BACKEND = 'django-db' SIGNAL_MODERATORS = [] RATELIMIT_CACHE_BACKEND = "app.brake_backend.BrakeBackend" + +SITENAME = "phantas.ma" diff --git a/app/static/css/site.scss b/app/static/css/site.scss index 4a8e4da..948c32b 100644 --- a/app/static/css/site.scss +++ b/app/static/css/site.scss @@ -50,3 +50,24 @@ header { } } +.burger { + cursor: pointer; + padding-left: 4px; +} + +nav.overlay { + position: absolute; + width: 100%; + height: 100vh; + top: 42px; + left: 0; + background: rgb(16, 16, 16); + opacity: 0; + z-index: 100; + padding: 4px; + display: none; + &.active { + display: block; + opacity: 0.9; + } +} diff --git a/app/static/css/style.css b/app/static/css/style.css index 61dbb57..27dc2a7 100644 --- a/app/static/css/style.css +++ b/app/static/css/style.css @@ -35,7 +35,7 @@ video, .poster { .content { display: flex; flex-direction: column; - min-height: 100vh; + min-height: max(100vh, 100%); max-width: 1000px; margin: auto; } diff --git a/app/static/js/utils.js b/app/static/js/utils.js index d77024f..cbaec6a 100644 --- a/app/static/js/utils.js +++ b/app/static/js/utils.js @@ -155,6 +155,9 @@ const getVideoURL = function(id, resolution, part, track, streamId) { .replace('{resolution}', resolution) .replace('{uid}', uid) .replace('{uid42}', uid % 42); + if (!prefix) { + prefix = pandoraURL + } return prefix + '/' + getVideoURLName(id, resolution, part, track, streamId); }; diff --git a/app/templates/base.html b/app/templates/base.html index c4ded3f..9eb72e1 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -1,4 +1,4 @@ -{% load static sass_tags compress %} +{% load sass_tags compress %} @@ -15,8 +15,35 @@ {% block header %}
- phantas.ma + [=] {{ settings.SITENAME }}
+ + + {% endblock %} {% block main %}
diff --git a/app/templates/index.html b/app/templates/index.html index 5e6ed39..4c5492f 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -1,4 +1,7 @@ {% extends "base.html" %} +{% block head %} + +{% endblock %} {% block content %} diff --git a/app/templates/item.html b/app/templates/item.html index 3c08f1d..c348234 100644 --- a/app/templates/item.html +++ b/app/templates/item.html @@ -57,7 +57,7 @@ pandora.url = new URL('{{ item.url|escapejs }}'); pandora.comment = '{{ item.id | escapejs }}'; pandora.hostname = pandora.url.hostname - pandoraURL = `https://${pandora.hostname}` + pandoraURL = `${pandora.url.protocol}//${pandora.hostname}` diff --git a/app/urls.py b/app/urls.py index ee205dc..807c972 100644 --- a/app/urls.py +++ b/app/urls.py @@ -19,9 +19,14 @@ from django.urls import path, re_path from .item import views as item_views from .user import views as user_views +from .page import views as page_views +from . import views urlpatterns = [ path("admin/", admin.site.urls), + path('robots.txt', views.robots_txt, name='robots_txt'), + path('sitemap.xml', views.sitemap_xml, name='sitemap_xml'), + path('atom.xml', item_views.atom_xml, name='atom_xml'), path('login/', user_views.login, name='login'), path('logout/', user_views.logout, name='logout'), path('register/', user_views.register, name='register'), @@ -29,5 +34,6 @@ urlpatterns = [ path('comment/publish/', item_views.publish_comment, name='publish-comment'), path('comment/', item_views.comment, name='comment'), path('/', item_views.item, name='item'), + path('/', page_views.page, name='page'), path('', item_views.index, name='index'), ] diff --git a/app/user/views.py b/app/user/views.py index fcef36a..20f1739 100644 --- a/app/user/views.py +++ b/app/user/views.py @@ -2,6 +2,7 @@ import json from django.shortcuts import render from django.shortcuts import redirect +from django.conf import settings import django.contrib.auth from django.contrib.auth import get_user_model @@ -15,35 +16,55 @@ User = get_user_model() @ratelimit(method="POST", block=True, rate="5/m") def register(request): response = {} - data = json.loads(request.body) - if User.objects.filter(username__iexact=data['username']).exists(): - response['error'] = 'username not allowed' - elif User.objects.filter(email__iexact=data['email']).exists(): - response['error'] = 'username not allowed' - elif not data['password']: - response['error'] = 'password too simple' - if not response: - user = User(username=data['username'], email=data['email'].lower()) - user.set_password(data['password']) - user.is_active = True - user.save() - user = django.contrib.auth.authenticate(username=data['username'], password=data['password']) - django.contrib.auth.login(request, user) - response['user'] = user.username - return render_to_json(response) + if request.method == "POST": + data = json.loads(request.body) + if User.objects.filter(username__iexact=data['username']).exists(): + response['error'] = 'username not allowed' + elif User.objects.filter(email__iexact=data['email']).exists(): + response['error'] = 'username not allowed' + elif not data['password']: + response['error'] = 'password too simple' + if not response: + user = User(username=data['username'], email=data['email'].lower()) + user.set_password(data['password']) + user.is_active = True + user.save() + user = django.contrib.auth.authenticate(username=data['username'], password=data['password']) + django.contrib.auth.login(request, user) + response['user'] = user.username + return render_to_json(response) + else: + context = {'settings': settings} + return render(request, 'register.html', context) @ratelimit(method="POST", block=True, rate="5/m") def login(request): + context = {'settings': settings} response = {} - data = json.loads(request.body) - user = django.contrib.auth.authenticate(username=data['username'], password=data['password']) - if user is not None and user.is_active: - django.contrib.auth.login(request, user) - response['user'] = user.username + request_type = 'json' + if request.method == "POST": + if "username" in request.POST and "password" in request.POST: + data = request.POST + request_type = 'html' + else: + data = json.loads(request.body) + user = django.contrib.auth.authenticate(username=data['username'], password=data['password']) + if user is not None and user.is_active: + django.contrib.auth.login(request, user) + response['user'] = user.username + if request_type == 'html': + return redirect('/') + else: + response['error'] = 'login failed' + if request_type == 'html': + context['error'] = response['error'] + return render(request, 'login.html', context) + return render_to_json(response) else: - response['error'] = 'login failed' - return render_to_json(response) + if request.user.is_authenticated: + return redirect('/') + return render(request, 'login.html', context) def logout(request):