various interface changes

This commit is contained in:
Hugh Rundle 2024-01-09 18:11:22 +11:00
parent 7307ec3931
commit 2445fadf63
Signed by: hugh
GPG key ID: A7E35779918253F9
21 changed files with 172 additions and 137 deletions

View file

@ -5,15 +5,18 @@ A django app running on Docker. Replaces _Aus GLAM Blogs_.
## Deploy
* `docker compose build`
* `./glamr-dev createsuperuser`
* `./glamr-dev makemigrations`
* `./glamr-dev makemigrations` (may get a DB connection error here, ignore it or run again)
* `./glamr-dev migrate`
* `./glamr-dev createsuperuser`
* `docker compose up`
* set up database backups (as cron jobs):
* `/usr/bin/docker exec -u root ausglamr-db-1 pg_dump -v -Fc -U ausglamr -d "ausglamr" -f /tmp/ausglamr_backup.dump`
* `/usr/bin/docker cp ausglamr-db-1:/tmp/ausglamr_backup.dump /home/hugh/backups/`
* `./glamr-dev backup`
* `/snap/bin/docker exec -u root ausglamr_db_1 pg_dump -v -Fc -U ausglamr -d "ausglamr" -f /tmp/ausglamr_backup.dump`
* `/snap/bin/docker cp ausglamr_db_1:/tmp/ausglamr_backup.dump /home/hugh/backups/`
* `mv /home/hugh/backups/ausglamr_backup.dump /home/hugh/backups/ausglamr_backup_$(date +'%a').dump`
* set up cron jobs for management commands as below

View file

@ -32,11 +32,11 @@ SECRET_KEY = env("SECRET_KEY")
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = env("DEBUG")
ALLOWED_HOSTS = [env("DOMAIN")]
CSRF_COOKIE_SECURE=True
SESSION_COOKIE_SECURE=True
ALLOWED_HOSTS = ['.localhost', '127.0.0.1', '[::1]', env("DOMAIN")]
CSRF_COOKIE_SECURE = True
SESSION_COOKIE_SECURE = True
CSRF_TRUSTED_ORIGINS = [f'https://{env("DOMAIN")}']
CONN_MAX_AGE=None # persistent DB connection
CONN_MAX_AGE = None # persistent DB connection
# Application definition

View file

@ -15,10 +15,14 @@ urlpatterns = [
path("contribute", views.Contribute.as_view(), name="contribute"),
path("contact", views.Contact.as_view(), name="contact"),
path("blogs", views.Blogs.as_view(), name="blogs"),
path("blogs/<category>", views.Blogs.as_view(), name="blog-category "),
path("events", views.Conferences.as_view(), name="events"),
path("cfps", views.CallsForPapers.as_view(), name="cfps"),
path("events/<category>", views.Conferences.as_view(), name="events-category"),
re_path(r"^cfps/?$", views.CallsForPapers.as_view(), name="cfps"),
path("groups", views.Groups.as_view(), name="groups"),
path("groups/<category>", views.Groups.as_view(), name="group-category"),
path("newsletters", views.Newsletters.as_view(), name="newsletters"),
path("newsletters/<category>", views.Newsletters.as_view(), name="newsletters-category"),
path("register-blog", views.RegisterBlog.as_view(), name="register-blog"),
path(
"submit-blog-registration",

View file

@ -20,9 +20,7 @@ def approve(modeladmin, request, queryset):
if hasattr(instance, "event"): # CFP
recipient = instance.event.contact_email
if hasattr(
instance, "contact_email"
): # overrides above in case needed in future
if hasattr(instance, "contact_email"):
recipient = instance.contact_email
if recipient:
@ -32,7 +30,10 @@ def approve(modeladmin, request, queryset):
title = instance.title
subject = f"{title} has been approved on AusGLAMR!"
message = f"<html><body><p>{title} has been approved on <a href='https://{settings.DOMAIN}'>AusGLAMR</a>. Hooray!</p></body></html>"
message = f"<html><body><p>{title} has been approved on <a href='https://{settings.DOMAIN}'>AusGLAMR</a>. Hooray!</p>"
if type(instance).__name__ == "Event":
message += f"<p>You can now optionally <a href='https://{settings.DOMAIN}/register-cfp'>register a call for papers</a>.</p>"
message += "</body></html>"
utilities.send_email(subject, message, recipient)

View file

@ -28,7 +28,9 @@ class Command(BaseCommand):
)
cutoff = timezone.now() - timedelta(days=7)
blogs = models.Blog.objects.filter(approved=True, updateddate__gte=cutoff)
blogs = models.Blog.objects.filter(
approved=True, active=True, added__gte=cutoff
)
articles = models.Article.objects.filter(pubdate__gte=cutoff)
events = models.Event.objects.filter(approved=True, pub_date__gte=cutoff)
cfps = models.CallForPapers.objects.filter(

View file

@ -25,7 +25,7 @@ class BlogData(models.Model):
"""Base bloggy data"""
title = models.CharField(max_length=2000)
author_name = models.CharField(max_length=1000, null=True)
author_name = models.CharField(max_length=1000, null=True, blank=True)
url = models.URLField(max_length=2000, unique=True)
description = models.TextField(null=True, blank=True)
updateddate = models.DateTimeField()
@ -55,6 +55,7 @@ class Blog(BlogData):
feed = models.URLField(max_length=2000)
category = models.CharField(choices=Category.choices, max_length=4)
added = models.DateTimeField()
approved = models.BooleanField(default=False)
announced = models.BooleanField(default=False)
failing = models.BooleanField(default=False, blank=True, null=True)
@ -66,6 +67,11 @@ class Blog(BlogData):
)
contact_email = models.EmailField(blank=True, null=True)
def save(self, *args, **kwargs):
if not self.added:
self.added = timezone.now()
super().save(*args, **kwargs)
def announce(self):
"""queue announcement"""
@ -108,7 +114,7 @@ class Article(BlogData):
blog = models.ForeignKey(Blog, on_delete=models.CASCADE, related_name="articles")
pubdate = models.DateTimeField()
guid = models.CharField(max_length=2000)
tags = models.ManyToManyField("Tag", related_name="articles")
tags = models.ManyToManyField("Tag", related_name="articles", null=True, blank=True)
# pylint: disable=undefined-variable
def announce(self):

View file

@ -9,10 +9,10 @@ from .utils import Announcement, Category
class Event(models.Model):
"""a event"""
name = models.CharField(max_length=999)
name = models.CharField(max_length=100)
category = models.CharField(choices=Category.choices, max_length=4)
url = models.URLField(max_length=400, unique=True)
description = models.TextField(null=True, blank=True)
description = models.TextField(null=True, blank=True, max_length=250)
pub_date = models.DateTimeField() # for RSS feed
start_date = models.DateField()
announcements = models.IntegerField(null=True, blank=True, default=0)
@ -55,9 +55,9 @@ class CallForPapers(models.Model):
"""a event call for papers/presentations"""
name = models.CharField(
max_length=999
max_length=100
) # "Call for papers", "call for participation" etc
details = models.TextField(null=True, blank=True)
details = models.TextField(null=True, blank=True, max_length=250)
pub_date = models.DateTimeField(null=True, default=None)
opening_date = models.DateField()
closing_date = models.DateField()

View file

@ -9,12 +9,12 @@ from .utils import Announcement, Category, GroupType
class Group(models.Model):
"""a group on email, discord, slack etc"""
name = models.CharField(max_length=999)
name = models.CharField(max_length=100)
category = models.CharField(choices=Category.choices, max_length=4)
type = models.CharField(choices=GroupType.choices, max_length=4)
url = models.URLField(max_length=400, unique=True)
registration_url = models.URLField(max_length=400, unique=True)
description = models.TextField(null=True, blank=True)
description = models.TextField(null=True, blank=True, max_length=250)
contact_email = models.EmailField(blank=True, null=True)
announced = models.BooleanField(default=False)
approved = models.BooleanField(default=False)

View file

@ -9,12 +9,12 @@ from .utils import Announcement, Category
class Newsletter(models.Model):
"""a newsletter"""
name = models.CharField(max_length=999)
author = models.CharField(max_length=999)
name = models.CharField(max_length=100)
author = models.CharField(max_length=100)
category = models.CharField(choices=Category.choices, max_length=4)
url = models.URLField(max_length=400, unique=True)
description = models.TextField(null=True, blank=True)
description = models.TextField(null=True, blank=True, max_length=250)
activitypub_account_name = models.CharField(max_length=200, blank=True, null=True)
contact_email = models.EmailField(blank=True, null=True)
announced = models.BooleanField(default=False)

View file

@ -1,20 +1,28 @@
{% extends "layout.html" %}
{% block content %}
<div class="listing l-three header">
<div>Title</div>
<div>Category</div>
<div>Last updated</div>
<div>
<a href="{% url 'register-blog' %}"><button class="button button-first">Add a Blog</button></a>
</div>
<div class="listing l-three header">
<div>Title</div>
<div>Category</div>
<div>Last updated</div>
</div>
<hr/>
{% for blog in blogs %}
<div class="listing l-three">
<div>
<p><a href="{{blog.url}}">{{blog.title}}</a></p>
<p>{{blog.description}}</p>
</div>
<hr/>
{% for blog in blogs %}
<div class="listing l-three">
<div><a href="{{blog.url}}">{{blog.title}}</a><p>{{blog.description}}</p></div>
<div><div class="end badge badge_{{blog.category}}">{{blog.category_name}}</div></div>
<div>{{ blog.updateddate|date:"D d M Y" }}
</div>
<div><div class="end badge badge_{{blog.category}}"><a href="/blogs/{{blog.category}}">{{blog.category_name}}</a></div></div>
<div>{{ blog.updateddate|date:"D d M Y" }}
</div>
<hr/>
{% endfor %}
</div>
<hr/>
{% empty %}
<p>Oh no! There are no blogs currently registered. Try checking out <a href="{% url 'newsletters' %}">some newsletters</a>.</p>
{% endfor %}
{% endblock %}

View file

@ -10,7 +10,7 @@
<hr/>
{% for cfp in cfps %}
<div class="listing l-three">
<div class="listing l-three" id="cfp_{{cfp.id}}">
<div>
<p><strong>{{cfp.name}}</strong></p>
<p>{{cfp.details}}</p>
@ -19,7 +19,9 @@
<span class="closing-date">{{cfp.closing_date}}</span>
</div>
<hr/>
{% endfor %}
{% empty %}
<p>Oh no! There are no Calls for Papers currently open. Try checking out <a href="{% url 'newsletters' %}">some newsletters</a>.</p>
{% endfor %}
{% endblock %}

View file

@ -2,30 +2,33 @@
{% block content %}
<div class="listing l-four header">
<span>Name</span>
<span>Category</span>
<span>Start Date</span>
<span>Description</span>
</div>
<hr/>
{% for con in cons %}
<div>{{con.call_for_papers.closing_date}}</div>
<div class="listing l-four">
<span><a href="{{con.url}}">{{con.name}}</a></span>
<span><span class="badge badge_{{con.category}}">{{con.category_name}}</span></span>
<span class="start-date">{{con.start_date}}</span>
<div>
<div>{{con.description}}</div>
{% if con.call_for_papers %}
<div><em><a href="{% url 'cfps' %}">{{ con.call_for_papers }}</a></em></div>
{% endif %}
</div>
<div>
<a href="{% url 'register-event' %}"><button class="button button-first">Add an Event</button></a>
<a href="{% url 'register-cfp' %}"><button class="button">Add a CFP</button></a>
</div>
<div class="listing l-three header">
<span>Description</span>
<span>Category</span>
<span>Start Date</span>
</div>
<hr/>
{% for con in cons %}
<div class="listing l-three">
<div>
<p><a href="{{con.url}}">{{con.name}}</a></p>
<div>
<p>{{con.description}}</p>
{% if con.call_for_papers %}
<p><em><a href="/cfps/#cfp_{{con.call_for_papers.id}}">{{con.call_for_papers.name}} closes {{ con.call_for_papers.closing_date|date:"D d M" }}</a></em></p>
{% endif %}
</div>
<hr/>
{% endfor %}
</div>
<span><span class="badge badge_{{con.category}}"><a href="/events/{{con.category}}">{{con.category_name}}</a></span></span>
<span class="start-date">{{con.start_date}}</span>
</div>
<hr/>
{% empty %}
<p>Oh no! There are no events currently registered. Try checking out <a href="{% url 'blogs' %}">some blogs</a>.</p>
{% endfor %}
{% endblock %}

View file

@ -1,20 +1,26 @@
{% extends "layout.html" %}
{% block content %}
<div class="listing l-four header">
<span>Name</span>
<span>Category</span>
<div>
<a href="{% url 'register-group' %}"><button class="button button-first">Add a Group</button></a>
</div>
<div class="listing l-three header">
<span>Description</span>
<span>Category</span>
<span>Registration Link</span>
</div>
<hr/>
{% for group in groups %}
<div class="listing l-four">
<span><a href="{{group.url}}">{{group.name}}</a></span>
<span><span class="badge badge_{{group.category}}">{{group.category_name}}</span></span>
<span>{{group.description}}</span>
<div class="listing l-three">
<span>
<p><a href="{{group.url}}">{{group.name}}</a></p>
<p><span>{{group.description}}</span></p>
</span>
<span><span class="badge badge_{{group.category}}"><a href="/groups/{{group.category}}">{{group.category_name}}</a></span></span>
<span><a href="{{group.registration_url}}">Join the {{group.reg_type}}</a></span>
</div>
<hr/>
{% empty %}
<p>Oh no! There are no groups currently registered. Try checking out <a href="{% url 'newsletters' %}">some newsletters</a>.</p>
{% endfor %}
{% endblock %}

View file

@ -1,6 +1,9 @@
{% extends "layout.html" %}
{% block content %}
<div>
<a href="{% url 'register-newsletter' %}"><button class="button button-first">Add a Newsletter</button></a>
</div>
<div class="listing l-three header">
<span>Title</span>
<span>Author</span>
@ -11,8 +14,10 @@
<div class="listing l-three">
<span><a href="{{pub.url}}">{{pub.name}}</a></span>
<span>{{pub.author}}</span>
<span><span class="badge badge_{{pub.category}}">{{pub.category_name}}</span></span>
<span><span class="badge badge_{{pub.category}}"><a href="/newsletters/{{pub.category}}">{{pub.category_name}}</a></span></span>
</div>
<hr/>
{% empty %}
<p>Oh no! There are no newsletter currently registered. Try checking out <a href="{% url 'blogs' %}">some blogs</a>.</p>
{% endfor %}
{% endblock %}

View file

@ -27,36 +27,7 @@
{% csrf_token %}
<div class="row">
<div class="columns eight">
{% if conf_name %}
<input hidden type="text" name="event" value="{{ form.event.value }}">
{% else %}
<label for="{{ form.event.id_for_label }}">Event:</label>
{% if errors %} {{ form.event.errors }} {% endif %}
{{ form.event }}
{% endif %}
<label for="{{ form.name.id_for_label }}">Name:</label>
{% if errors %} {{ form.name.errors }} {% endif %}
<p class="hint">e.g. "Call for Papers", "Request for submissions" etc</p>
<input type="text" name="name" id="name" class="u-full-width" required="">
{% if errors %} {{ form.details.errors }} {% endif %}
<label for="{{ form.details.id_for_label }}">Details:</label>
<textarea name="details" id="details" class="u-full-width"></textarea>
<div class="row">
<span class="columns four">
{% if errors %} {{ form.opening_date.errors }} {% endif %}
<label for="{{ form.opening_date.id_for_label }}">opening_date:</label>
{% if errors %} {{ form.opening_date.errors }} {% endif %}
{{ form.opening_date }}
</span>
<span class="columns four">
<label for="{{ form.closing_date.id_for_label }}">closing_date:</label>
{% if errors %} {{ form.closing_date.errors }} {% endif %}
{{ form.closing_date }}
</span>
</div>
{{ form }}
<input class="button-primary u-pull-right" type="submit" value="Register!">
<a href="/"><button class="button u-pull-right button-first" type="button">No thanks</button></a>
</div>

View file

@ -7,6 +7,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ title }}</title>
<link rel="icon" type="image/svg" href="{% static 'favicon.svg' %}" />
<link rel="stylesheet" href="{% static 'css/normalize.css' %}" type="text/css" />
<link rel="stylesheet" href="{% static 'css/skeleton.css' %}" type="text/css" />
<link rel="stylesheet" href="{% static 'css/custom.css' %}" type="text/css" />

View file

@ -9,6 +9,8 @@
{% if register_type == "email address" %}
<p>Check your inbox to confirm your subscription. You can opt-out at any time by clicking the link at the bottom of the weekly emails.</p>
{% elif register_type == "conference" %}
<p>Once your conference is approved, you will be able to register a <a href="{% url 'cfps' %}">Call for Papers</a> if you would like to do so.</p>
{% else %}
<p>Your {{ register_type }} registration will be reviewed before being added to <em>Aus GLAMR</em>.</p>
{% endif %}

View file

@ -205,7 +205,6 @@ class CommandsTestCase(TestCase):
# should not be announced
self.assertEqual(models.Announcement.objects.count(), 0)
def test_check_feeds_exclude_tag(self):
"""test parse a feed with exclude tag"""

View file

@ -6,6 +6,7 @@ from operator import attrgetter
from django.conf import settings
from django.shortcuts import get_object_or_404
from django.contrib.postgres.aggregates import ArrayAgg
from django.contrib.postgres.search import SearchRank, SearchVector
from django.utils.translation import gettext_lazy as _
from django.core.mail import EmailMessage
@ -34,12 +35,21 @@ class HomeFeed(View):
class Blogs(View):
"""browse the list of blogs"""
def get(self, request):
def get(self, request, category=None):
"""here they are"""
blogs = models.Blog.objects.filter(approved=True, active=True).order_by(
"-updateddate"
)
if category:
blogs = models.Blog.objects.filter(approved=True, active=True, category=category).order_by(
"-updateddate"
)
else:
blogs = models.Blog.objects.filter(approved=True, active=True).order_by(
"-updateddate"
)
for blog in blogs:
blog.category_name = models.Category(blog.category).label
data = {"title": "Blogs and websites", "blogs": blogs}
@ -49,18 +59,22 @@ class Blogs(View):
class Conferences(View):
"""browse the list of conferences"""
def get(self, request):
def get(self, request, category=None):
"""here they are"""
now = timezone.now()
cons = models.Event.objects.filter(approved=True, start_date__gte=now).order_by(
"start_date"
)
if category:
cons = models.Event.objects.filter(approved=True, start_date__gte=now, category=category).order_by(
"start_date"
)
else:
cons = models.Event.objects.filter(approved=True, start_date__gte=now).order_by(
"start_date"
)
for con in cons:
con.category_name = models.Category(con.category).label
con.call_for_papers = con.cfp.all().last()
if con.call_for_papers and (con.call_for_papers.closing_date > now.date()):
date = con.call_for_papers.closing_date.strftime("%a %d %b %Y")
con.call_for_papers = f"{con.call_for_papers.name} closes {date}"
con.call_for_papers = con.cfp.filter(closing_date__gte=timezone.now()).order_by("-closing_date").last()
data = {"title": "Upcoming events", "cons": cons}
return render(request, "browse/events.html", data)
@ -82,9 +96,15 @@ class CallsForPapers(View):
class Groups(View):
"""browse the list of groups"""
def get(self, request):
def get(self, request, category=None):
"""here they are"""
groups = models.Group.objects.filter(approved=True).order_by("name")
if category:
groups = models.Group.objects.filter(approved=True, category=category).order_by("name")
else:
groups = models.Group.objects.filter(approved=True).order_by("name")
for group in groups:
group.category_name = models.Category(group.category).label
group.reg_type = models.utils.GroupType(group.type).label
@ -188,17 +208,7 @@ class RegisterConference(View):
if form.is_valid():
conf = form.save()
send_email("event", conf)
cfp_form = forms.RegisterCallForPapersForm({"event": conf.id})
data = {
"title": "Register your Call for Papers",
"form": cfp_form,
"conf_name": conf.name,
}
return render(request, "events/cfp.html", data)
data = {"title": "Complete blog registration", "form": form, "errors": True}
return render(request, "events/register.html", data)
return redirect("/thankyou/conference")
class RegisterCallForPapers(View):
@ -292,13 +302,16 @@ class Search(View):
query = request.GET.get("q")
article_vector = (
SearchVector("tags__name", weight="A")
SearchVector("tagnames", weight="A")
+ SearchVector("title", weight="B")
+ SearchVector("description", weight="C")
)
articles = (
models.Article.objects.annotate(rank=SearchRank(article_vector, query))
models.Article.objects.annotate(
tagnames=ArrayAgg("tags__name", distinct=True), # see above: this prevents many-to-many objects like tags causing the same article to appear in the results multiple times
rank=SearchRank(article_vector, query)
)
.filter(rank__gte=0.1)
.order_by("-rank")
)
@ -308,7 +321,8 @@ class Search(View):
)
events = (
models.Event.objects.filter(approved=True).annotate(rank=SearchRank(conference_vector, query))
models.Event.objects.filter(approved=True)
.annotate(rank=SearchRank(conference_vector, query))
.filter(rank__gte=0.1)
.order_by("-rank")
)
@ -318,7 +332,8 @@ class Search(View):
)
cfps = (
models.CallForPapers.objects.filter(approved=True).annotate(rank=SearchRank(cfp_vector, query))
models.CallForPapers.objects.filter(approved=True)
.annotate(rank=SearchRank(cfp_vector, query))
.filter(rank__gte=0.1)
.order_by("-rank")
)
@ -328,7 +343,8 @@ class Search(View):
)
newsletters = (
models.Newsletter.objects.filter(approved=True).annotate(rank=SearchRank(news_vector, query))
models.Newsletter.objects.filter(approved=True)
.annotate(rank=SearchRank(news_vector, query))
.filter(rank__gte=0.1)
.order_by("-rank")
)
@ -338,7 +354,8 @@ class Search(View):
)
groups = (
models.Event.objects.filter(approved=True).annotate(rank=SearchRank(group_vector, query))
models.Group.objects.filter(approved=True)
.annotate(rank=SearchRank(group_vector, query))
.filter(rank__gte=0.1)
.order_by("-rank")
)

View file

@ -11,8 +11,8 @@ services:
web:
build: .
env_file: .env
# command: python manage.py runserver 0.0.0.0:8000
command: gunicorn --env DJANGO_SETTINGS_MODULE=ausglamr.settings ausglamr.wsgi --workers=10 --threads=4 -b 0.0.0.0:8000
command: python manage.py runserver 0.0.0.0:8000
# command: gunicorn --env DJANGO_SETTINGS_MODULE=ausglamr.settings ausglamr.wsgi --workers=10 --threads=4 -b 0.0.0.0:8000
volumes:
- .:/app
depends_on:

View file

@ -30,6 +30,11 @@ case "$CMD" in
announce)
runweb python manage.py announce
;;
backup)
/snap/bin/docker exec -u root ausglamr_db_1 pg_dump -v -Fc -U ausglamr -d "ausglamr" -f /tmp/ausglamr_backup.dump
/snap/bin/docker cp ausglamr_db_1:/tmp/ausglamr_backup.dump /home/hugh/backups/
mv /home/hugh/backups/ausglamr_backup.dump /home/hugh/backups/ausglamr_backup_$(date +'%a').dump
;;
black)
docker compose run --rm web black ausglamr blogs
;;