initial commit
This commit is contained in:
commit
53df5c5035
31
.env.example
Normal file
31
.env.example
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
DJANGO_SUPERUSER_USERNAME=""
|
||||||
|
DJANGO_SUPERUSER_PASSWORD=""
|
||||||
|
DJANGO_SUPERUSER_EMAIL=""
|
||||||
|
|
||||||
|
DOMAIN="localhost:8000"
|
||||||
|
LANGUAGE_CODE="en-AU"
|
||||||
|
TIME_ZONE="Australia/Melbourne"
|
||||||
|
|
||||||
|
# custom recipient email setting
|
||||||
|
ADMIN_EMAIL=""
|
||||||
|
|
||||||
|
# django email settings
|
||||||
|
DEFAULT_FROM_EMAIL="First Second <First.Second@example.org>"
|
||||||
|
EMAIL_HOST="smtp.fastmail.com"
|
||||||
|
EMAIL_HOST_USER=""
|
||||||
|
EMAIL_HOST_PASSWORD=""
|
||||||
|
EMAIL_PORT=465
|
||||||
|
EMAIL_USE_SSL=True
|
||||||
|
|
||||||
|
# database
|
||||||
|
|
||||||
|
POSTGRES_USER="ausglamr"
|
||||||
|
POSTGRES_PASSWORD=""
|
||||||
|
POSTGRES_DB="ausglamr"
|
||||||
|
PGPORT=5432
|
||||||
|
POSTGRES_HOST="db"
|
||||||
|
|
||||||
|
# mastodon
|
||||||
|
|
||||||
|
MASTODON_ACCESS_TOKEN=""
|
||||||
|
MASTODON_DOMAIN="https://example.com"
|
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
data
|
||||||
|
static
|
9
.pylintrc
Normal file
9
.pylintrc
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
[MAIN]
|
||||||
|
ignore=migrations
|
||||||
|
load-plugins=pylint.extensions.no_self_use
|
||||||
|
|
||||||
|
[MESSAGES CONTROL]
|
||||||
|
disable=E1101,E1135,E1136,E0307,R0903,R0901,R0902,W0707,W0406,R0401,R0801,C3001,import-error,C0301
|
||||||
|
|
||||||
|
[FORMAT]
|
||||||
|
max-line-length=88
|
6
Dockerfile
Normal file
6
Dockerfile
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
FROM python:3.9
|
||||||
|
ENV PYTHONUNBUFFERED 1
|
||||||
|
RUN mkdir /app /app/static
|
||||||
|
WORKDIR /app
|
||||||
|
COPY requirements.txt /app/
|
||||||
|
RUN pip install -r requirements.txt --no-cache-dir
|
70
README.md
Normal file
70
README.md
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
# Aus GLAMR
|
||||||
|
|
||||||
|
A django app running on Docker. Replaces _Aus GLAM Blogs_.
|
||||||
|
|
||||||
|
## Admin
|
||||||
|
|
||||||
|
Don't forget to add some Content Warnings for use by the Mastodon bot.
|
||||||
|
|
||||||
|
## CLI tool
|
||||||
|
|
||||||
|
Use `glamr-dev` to make your life easier (thanks to Mouse Reeve for the inspiration):
|
||||||
|
|
||||||
|
* announce
|
||||||
|
* check_feeds
|
||||||
|
* manage [django management command]
|
||||||
|
* makemigrations
|
||||||
|
* migrate
|
||||||
|
* queue_announcements
|
||||||
|
* send_weekly_email
|
||||||
|
|
||||||
|
And for dev work:
|
||||||
|
|
||||||
|
* black
|
||||||
|
* collectstatic
|
||||||
|
* createsuperuser
|
||||||
|
* pylint
|
||||||
|
* resetdb
|
||||||
|
* test
|
||||||
|
|
||||||
|
## Registration
|
||||||
|
|
||||||
|
- users can register a blog, group, event, newsletter, or Call for Papers.
|
||||||
|
- most of these ask for an "owner email" - this is optional but allows us to communicate with the person registering.
|
||||||
|
- all registrations should trigger an email to admin
|
||||||
|
- all must be approved before they are included
|
||||||
|
|
||||||
|
## Management commands
|
||||||
|
|
||||||
|
There are four commands:
|
||||||
|
|
||||||
|
- announce
|
||||||
|
- check_feeds
|
||||||
|
- queue_announcements
|
||||||
|
- send_weekly_email
|
||||||
|
|
||||||
|
These will not be triggered within the app - they should be called via cron jobs.
|
||||||
|
|
||||||
|
### announce
|
||||||
|
|
||||||
|
This announces the next queued announcement on Mastodon.
|
||||||
|
|
||||||
|
Run every 21 mins.
|
||||||
|
|
||||||
|
### check_feeds
|
||||||
|
|
||||||
|
This checks all blog feeds for any new posts, and adds them to the database as long as they don't have an exclusion tag and were not published during a time the blog was suspended.
|
||||||
|
|
||||||
|
Run every hour.
|
||||||
|
|
||||||
|
### queue_announcements
|
||||||
|
|
||||||
|
This queues announcements for events and CFPs. These are announced three times, evenly spaced between when they were added and when the event starts or the CFP closes.
|
||||||
|
|
||||||
|
Run daily.
|
||||||
|
|
||||||
|
### send_weekly_email
|
||||||
|
|
||||||
|
Does what you think. Creates a weekly email of the latest stuff, and send to everyone in Subscribers.
|
||||||
|
|
||||||
|
Run weekly.
|
0
ausglamr/__init__.py
Normal file
0
ausglamr/__init__.py
Normal file
16
ausglamr/asgi.py
Normal file
16
ausglamr/asgi.py
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
"""
|
||||||
|
ASGI config for ausglamr project.
|
||||||
|
|
||||||
|
It exposes the ASGI callable as a module-level variable named ``application``.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/4.2/howto/deployment/asgi/
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.core.asgi import get_asgi_application
|
||||||
|
|
||||||
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "ausglamr.settings")
|
||||||
|
|
||||||
|
application = get_asgi_application()
|
160
ausglamr/settings.py
Normal file
160
ausglamr/settings.py
Normal file
|
@ -0,0 +1,160 @@
|
||||||
|
"""
|
||||||
|
Django settings for ausglamr project.
|
||||||
|
|
||||||
|
Generated by 'django-admin startproject' using Django 4.2.7.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/4.2/topics/settings/
|
||||||
|
|
||||||
|
For the full list of settings and their values, see
|
||||||
|
https://docs.djangoproject.com/en/4.2/ref/settings/
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
from environs import Env
|
||||||
|
|
||||||
|
# .env
|
||||||
|
env = Env()
|
||||||
|
env.read_env()
|
||||||
|
|
||||||
|
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||||
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||||
|
|
||||||
|
|
||||||
|
# Quick-start development settings - unsuitable for production
|
||||||
|
# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/
|
||||||
|
|
||||||
|
# SECURITY WARNING: keep the secret key used in production secret!
|
||||||
|
SECRET_KEY = env("SECRET_KEY")
|
||||||
|
|
||||||
|
# SECURITY WARNING: don't run with debug turned on in production!
|
||||||
|
DEBUG = env("DEBUG")
|
||||||
|
|
||||||
|
ALLOWED_HOSTS = [env("DOMAIN")]
|
||||||
|
CSRF_TRUSTED_ORIGINS = [f'https://{env("DOMAIN")}']
|
||||||
|
|
||||||
|
# Application definition
|
||||||
|
|
||||||
|
INSTALLED_APPS = [
|
||||||
|
"django.contrib.admin",
|
||||||
|
"django.contrib.auth",
|
||||||
|
"django.contrib.contenttypes",
|
||||||
|
"django.contrib.sessions",
|
||||||
|
"django.contrib.messages",
|
||||||
|
"django.contrib.postgres.search",
|
||||||
|
"django.contrib.staticfiles",
|
||||||
|
"blogs",
|
||||||
|
]
|
||||||
|
|
||||||
|
MIDDLEWARE = [
|
||||||
|
"django.middleware.security.SecurityMiddleware",
|
||||||
|
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||||
|
"django.middleware.common.CommonMiddleware",
|
||||||
|
"django.middleware.csrf.CsrfViewMiddleware",
|
||||||
|
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||||
|
"django.contrib.messages.middleware.MessageMiddleware",
|
||||||
|
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||||
|
]
|
||||||
|
|
||||||
|
ROOT_URLCONF = "ausglamr.urls"
|
||||||
|
|
||||||
|
TEMPLATES = [
|
||||||
|
{
|
||||||
|
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
||||||
|
"DIRS": [],
|
||||||
|
"APP_DIRS": True,
|
||||||
|
"OPTIONS": {
|
||||||
|
"context_processors": [
|
||||||
|
"django.template.context_processors.debug",
|
||||||
|
"django.template.context_processors.request",
|
||||||
|
"django.contrib.auth.context_processors.auth",
|
||||||
|
"django.contrib.messages.context_processors.messages",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
WSGI_APPLICATION = "ausglamr.wsgi.application"
|
||||||
|
|
||||||
|
|
||||||
|
# Database
|
||||||
|
# https://docs.djangoproject.com/en/4.2/ref/settings/#databases
|
||||||
|
|
||||||
|
DATABASES = {
|
||||||
|
"default": {
|
||||||
|
"ENGINE": "django.db.backends.postgresql",
|
||||||
|
"NAME": env("POSTGRES_DB", "ausglamr"),
|
||||||
|
"USER": env("POSTGRES_USER", "ausglamr"),
|
||||||
|
"PASSWORD": env("POSTGRES_PASSWORD", "ausglamr"),
|
||||||
|
"HOST": env("POSTGRES_HOST", "db"),
|
||||||
|
"PORT": env.int("PGPORT", 5432),
|
||||||
|
"TEST": {
|
||||||
|
"NAME": f"{env('POSTGRES_DB')}_test",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Password validation
|
||||||
|
# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators
|
||||||
|
|
||||||
|
AUTH_PASSWORD_VALIDATORS = [
|
||||||
|
{
|
||||||
|
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", # pylint: disable=line-too-long
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# Internationalization
|
||||||
|
# https://docs.djangoproject.com/en/4.2/topics/i18n/
|
||||||
|
|
||||||
|
LANGUAGE_CODE = env("LANGUAGE_CODE")
|
||||||
|
TIME_ZONE = env("TIME_ZONE")
|
||||||
|
USE_I18N = True
|
||||||
|
USE_TZ = True
|
||||||
|
|
||||||
|
|
||||||
|
# Static files (CSS, JavaScript, Images)
|
||||||
|
# https://docs.djangoproject.com/en/4.2/howto/static-files/
|
||||||
|
|
||||||
|
STORAGES = {
|
||||||
|
"staticfiles": {
|
||||||
|
"BACKEND": "django.contrib.staticfiles.storage.ManifestStaticFilesStorage",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
MEDIA_URL = "media/"
|
||||||
|
STATIC_URL = "static/"
|
||||||
|
STATIC_ROOT = os.path.join(BASE_DIR, "static/")
|
||||||
|
STATICFILES_DIRS = []
|
||||||
|
|
||||||
|
# Default primary key field type
|
||||||
|
# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field
|
||||||
|
|
||||||
|
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
||||||
|
|
||||||
|
# ENV SETTINGS
|
||||||
|
DOMAIN = env("DOMAIN")
|
||||||
|
ADMIN_EMAIL = env("ADMIN_EMAIL") # custom
|
||||||
|
|
||||||
|
# django email settings
|
||||||
|
DEFAULT_FROM_EMAIL = env("DEFAULT_FROM_EMAIL")
|
||||||
|
EMAIL_HOST = env("EMAIL_HOST")
|
||||||
|
EMAIL_HOST_USER = env("EMAIL_HOST_USER")
|
||||||
|
EMAIL_HOST_PASSWORD = env("EMAIL_HOST_PASSWORD")
|
||||||
|
EMAIL_PORT = env("EMAIL_PORT")
|
||||||
|
EMAIL_USE_SSL = env("EMAIL_USE_SSL")
|
||||||
|
|
||||||
|
# mastodon
|
||||||
|
MASTODON_ACCESS_TOKEN = env("MASTODON_ACCESS_TOKEN")
|
||||||
|
MASTODON_DOMAIN = env("MASTODON_DOMAIN")
|
55
ausglamr/urls.py
Normal file
55
ausglamr/urls.py
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
"""
|
||||||
|
URL configuration for ausglamr project.
|
||||||
|
"""
|
||||||
|
from django.contrib import admin
|
||||||
|
from django.urls import path, re_path
|
||||||
|
|
||||||
|
from blogs import views
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path("admin/", admin.site.urls),
|
||||||
|
path("", views.HomeFeed.as_view(), name="home"),
|
||||||
|
re_path(r"^browse/?$", views.Browse.as_view(), name="browse"),
|
||||||
|
re_path(r"^search/?$", views.Search.as_view(), name="search"),
|
||||||
|
re_path(r"^help/?$", views.Help.as_view(), name="help"),
|
||||||
|
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("events", views.Conferences.as_view(), name="events"),
|
||||||
|
path("cfps", views.CallsForPapers.as_view(), name="cfps"),
|
||||||
|
path("groups", views.Groups.as_view(), name="groups"),
|
||||||
|
path("newsletters", views.Newsletters.as_view(), name="newsletters"),
|
||||||
|
path("register-blog", views.RegisterBlog.as_view(), name="register-blog"),
|
||||||
|
path(
|
||||||
|
"submit-blog-registration",
|
||||||
|
views.ConfirmBlogRegistration.as_view(),
|
||||||
|
name="submit-blog-registration",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"register-event",
|
||||||
|
views.RegisterConference.as_view(),
|
||||||
|
name="register-event",
|
||||||
|
),
|
||||||
|
path("register-cfp", views.RegisterCallForPapers.as_view(), name="register-cfp"),
|
||||||
|
path("register-group", views.RegisterGroup.as_view(), name="register-group"),
|
||||||
|
path(
|
||||||
|
"register-newsletter",
|
||||||
|
views.RegisterNewsletter.as_view(),
|
||||||
|
name="register-newsletter",
|
||||||
|
),
|
||||||
|
path("thankyou/<register_type>", views.Thankyou.as_view(), name="thankyou"),
|
||||||
|
path("subscribe", views.Subscribe.as_view(), name="subscribe"),
|
||||||
|
path("subscribe-email", views.SubscribeEmail.as_view(), name="subscribe-email"),
|
||||||
|
path(
|
||||||
|
"confirm-subscribe-email/<token>/<id>",
|
||||||
|
views.ConfirmEmail.as_view(),
|
||||||
|
name="confirm-subscribe-email",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"unsubscribe-email/<token>/<id>",
|
||||||
|
views.UnsubscribeEmail.as_view(),
|
||||||
|
name="unsubscribe-email",
|
||||||
|
),
|
||||||
|
path("feeds/blogs", views.ArticleFeed(), name="article-feed"),
|
||||||
|
path("feeds/events", views.EventFeed(), name="event-feed"),
|
||||||
|
]
|
16
ausglamr/wsgi.py
Normal file
16
ausglamr/wsgi.py
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
"""
|
||||||
|
WSGI config for ausglamr project.
|
||||||
|
|
||||||
|
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/4.2/howto/deployment/wsgi/
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.core.wsgi import get_wsgi_application
|
||||||
|
|
||||||
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "ausglamr.settings")
|
||||||
|
|
||||||
|
application = get_wsgi_application()
|
0
blogs/__init__.py
Normal file
0
blogs/__init__.py
Normal file
216
blogs/admin.py
Normal file
216
blogs/admin.py
Normal file
|
@ -0,0 +1,216 @@
|
||||||
|
"""admin interface customisations"""
|
||||||
|
|
||||||
|
# pylint: disable=W0613
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.contrib import admin
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from . import models
|
||||||
|
from . import utilities
|
||||||
|
|
||||||
|
|
||||||
|
@admin.action(description="Approve selected")
|
||||||
|
def approve(modeladmin, request, queryset):
|
||||||
|
"""approve selected"""
|
||||||
|
queryset.update(approved=True)
|
||||||
|
|
||||||
|
for instance in queryset:
|
||||||
|
instance.announce()
|
||||||
|
|
||||||
|
if hasattr(instance, "event"): # CFP
|
||||||
|
recipient = instance.event.owner_email
|
||||||
|
if hasattr(instance, "owner_email"): # overrides above in case needed in future
|
||||||
|
recipient = instance.owner_email
|
||||||
|
|
||||||
|
if recipient:
|
||||||
|
if hasattr(instance, "name"):
|
||||||
|
title = instance.name
|
||||||
|
else:
|
||||||
|
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>"
|
||||||
|
|
||||||
|
utilities.send_email(subject, message, recipient)
|
||||||
|
|
||||||
|
|
||||||
|
@admin.action(description="Unapprove selected")
|
||||||
|
def unapprove(modeladmin, request, queryset):
|
||||||
|
"""unapprove selected"""
|
||||||
|
queryset.update(approved=False)
|
||||||
|
|
||||||
|
|
||||||
|
@admin.action(description="Suspend selected blogs")
|
||||||
|
def suspend(modeladmin, request, queryset):
|
||||||
|
"""suspend selected blogs"""
|
||||||
|
queryset.update(suspended=True)
|
||||||
|
|
||||||
|
for instance in queryset:
|
||||||
|
if hasattr(instance, "owner_email"):
|
||||||
|
if hasattr(instance, "name"):
|
||||||
|
title = instance.name
|
||||||
|
else:
|
||||||
|
title = instance.title
|
||||||
|
|
||||||
|
subject = f"⚠️ {title} has been suspended from AusGLAMR!"
|
||||||
|
message = f"<html><body>\
|
||||||
|
<p>Your blog {title} has been suspended from AusGLAMR. It may be unsuspended in future once the issue is resolved. If you would like more information, please reply to this email.</p> \
|
||||||
|
</body></html>"
|
||||||
|
|
||||||
|
utilities.send_email(subject, message, instance.owner_email)
|
||||||
|
|
||||||
|
|
||||||
|
@admin.action(description="Unsuspend selected blogs")
|
||||||
|
def unsuspend(modeladmin, request, queryset):
|
||||||
|
"""unsuspend selected blogs"""
|
||||||
|
queryset.update(suspended=False, suspension_lifted=timezone.now())
|
||||||
|
|
||||||
|
for instance in queryset:
|
||||||
|
if hasattr(instance, "owner_email"):
|
||||||
|
if hasattr(instance, "name"):
|
||||||
|
title = instance.name
|
||||||
|
else:
|
||||||
|
title = instance.title
|
||||||
|
|
||||||
|
subject = f"✅ The AusGLAMR suspension on {title} has been lifted"
|
||||||
|
message = f"<html><body>\
|
||||||
|
<p>The suspension on your blog {title} has been removed on AusGLAMR. Please note that articles published whilst it was suspended will not be added to AusGLAMR retrospectively. If you would like more information, please reply to this email.</p> \
|
||||||
|
</body></html>"
|
||||||
|
|
||||||
|
utilities.send_email(subject, message, instance.owner_email)
|
||||||
|
|
||||||
|
|
||||||
|
@admin.action(description="Confirm selected subscribers")
|
||||||
|
def confirm(modeladmin, request, queryset):
|
||||||
|
"""confirm selected"""
|
||||||
|
queryset.update(confirmed=True)
|
||||||
|
|
||||||
|
|
||||||
|
@admin.action(description="Unconfirm selected subscribers")
|
||||||
|
def unconfirm(modeladmin, request, queryset):
|
||||||
|
"""unconfirm selected"""
|
||||||
|
queryset.update(confirmed=False)
|
||||||
|
|
||||||
|
|
||||||
|
@admin.action(description="Send confirmation request to selected subscribers")
|
||||||
|
def send_conf_request(modeladmin, request, queryset):
|
||||||
|
"""send to selected"""
|
||||||
|
|
||||||
|
for instance in queryset:
|
||||||
|
instance.send_confirmation_email()
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(models.Blog)
|
||||||
|
class Blog(admin.ModelAdmin):
|
||||||
|
"""display settings for blogs"""
|
||||||
|
|
||||||
|
list_display = (
|
||||||
|
"title",
|
||||||
|
"url",
|
||||||
|
"author_name",
|
||||||
|
"approved",
|
||||||
|
"announced",
|
||||||
|
"suspended",
|
||||||
|
"failing",
|
||||||
|
)
|
||||||
|
ordering = ["approved", "-suspended", "-failing"]
|
||||||
|
actions = [approve, unapprove, suspend, unsuspend]
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(models.Article)
|
||||||
|
class Article(admin.ModelAdmin):
|
||||||
|
"""display settings for articles"""
|
||||||
|
|
||||||
|
date_hierarchy = "pubdate"
|
||||||
|
list_display = ("title", "blog_title", "pubdate")
|
||||||
|
|
||||||
|
def blog_title(self, obj): # pylint: disable=no-self-use
|
||||||
|
"""get the title of the parent blog"""
|
||||||
|
return obj.blog.title
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(models.Tag)
|
||||||
|
class Tag(admin.ModelAdmin):
|
||||||
|
"""display settings for tags"""
|
||||||
|
|
||||||
|
list_display = ("name",)
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(models.Event)
|
||||||
|
class Event(admin.ModelAdmin):
|
||||||
|
"""display settings for conferences"""
|
||||||
|
|
||||||
|
list_display = (
|
||||||
|
"name",
|
||||||
|
"approved",
|
||||||
|
"announcements",
|
||||||
|
"category",
|
||||||
|
"description",
|
||||||
|
"start_date",
|
||||||
|
)
|
||||||
|
ordering = ["approved", "announcements"]
|
||||||
|
actions = [approve, unapprove]
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(models.CallForPapers)
|
||||||
|
class CallForPapers(admin.ModelAdmin):
|
||||||
|
"""display settings for CFPs"""
|
||||||
|
|
||||||
|
list_display = ("name", "event", "approved", "closing_date")
|
||||||
|
list_select_related = ("event",)
|
||||||
|
ordering = ["approved", "closing_date"]
|
||||||
|
actions = [approve, unapprove]
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(models.Group)
|
||||||
|
class Group(admin.ModelAdmin):
|
||||||
|
"""display settings for groups"""
|
||||||
|
|
||||||
|
list_display = ("name", "approved", "category", "description")
|
||||||
|
ordering = ["approved", "name"]
|
||||||
|
actions = [approve, unapprove]
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(models.Newsletter)
|
||||||
|
class Newsletter(admin.ModelAdmin):
|
||||||
|
"""display settings for newsletters"""
|
||||||
|
|
||||||
|
list_display = ("name", "approved", "category", "description")
|
||||||
|
ordering = ["approved", "name"]
|
||||||
|
actions = [approve, unapprove]
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(models.ContentWarning)
|
||||||
|
class ContentWarning(admin.ModelAdmin):
|
||||||
|
"""display settings for CWs"""
|
||||||
|
|
||||||
|
list_display = (
|
||||||
|
"match_text",
|
||||||
|
"display",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(models.Announcement)
|
||||||
|
class Announcement(admin.ModelAdmin):
|
||||||
|
"""display settings for announcements"""
|
||||||
|
|
||||||
|
list_display = ("status",)
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(models.SiteMessage)
|
||||||
|
class SiteMessage(admin.ModelAdmin):
|
||||||
|
"""create a message"""
|
||||||
|
|
||||||
|
list_display = ("message",)
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(models.Subscriber)
|
||||||
|
class Subscriber(admin.ModelAdmin):
|
||||||
|
"""email subscribers"""
|
||||||
|
|
||||||
|
list_display = (
|
||||||
|
"email",
|
||||||
|
"confirmed",
|
||||||
|
)
|
||||||
|
actions = [confirm]
|
9
blogs/apps.py
Normal file
9
blogs/apps.py
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
"""django apps"""
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class BlogsConfig(AppConfig):
|
||||||
|
"""default config class"""
|
||||||
|
|
||||||
|
default_auto_field = "django.db.models.BigAutoField"
|
||||||
|
name = "blogs"
|
143
blogs/forms.py
Normal file
143
blogs/forms.py
Normal file
|
@ -0,0 +1,143 @@
|
||||||
|
"""forms for use in views"""
|
||||||
|
|
||||||
|
from django import forms
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from .models import Blog, CallForPapers, Event, Group, Newsletter, Subscriber
|
||||||
|
|
||||||
|
|
||||||
|
class DateInput(forms.DateInput):
|
||||||
|
"""make date input a date picker"""
|
||||||
|
|
||||||
|
input_type = "date"
|
||||||
|
|
||||||
|
|
||||||
|
class RegisterBlogForm(forms.ModelForm):
|
||||||
|
"""form for registering a blog"""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
"""set fields and model"""
|
||||||
|
|
||||||
|
model = Blog
|
||||||
|
fields = ["url", "category", "activitypub_account_name", "owner_email"]
|
||||||
|
|
||||||
|
|
||||||
|
class ConfirmBlogForm(forms.ModelForm):
|
||||||
|
"""confirm all details are correct before final blog submission"""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
"""set fields and model"""
|
||||||
|
|
||||||
|
model = Blog
|
||||||
|
fields = [
|
||||||
|
"url",
|
||||||
|
"feed",
|
||||||
|
"title",
|
||||||
|
"author_name",
|
||||||
|
"description",
|
||||||
|
"category",
|
||||||
|
"activitypub_account_name",
|
||||||
|
"owner_email",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class RegisterConferenceForm(forms.ModelForm):
|
||||||
|
"""form for registering a event"""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
"""set fields and model"""
|
||||||
|
|
||||||
|
model = Event
|
||||||
|
fields = [
|
||||||
|
"name",
|
||||||
|
"url",
|
||||||
|
"category",
|
||||||
|
"description",
|
||||||
|
"start_date",
|
||||||
|
"activitypub_account_name",
|
||||||
|
"owner_email",
|
||||||
|
]
|
||||||
|
widgets = {
|
||||||
|
"start_date": DateInput(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class RegisterCallForPapersForm(forms.ModelForm):
|
||||||
|
"""form for registering a CallForPapers"""
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
# we don't want every event ever created to be listed
|
||||||
|
self.fields["event"].queryset = Event.objects.filter(
|
||||||
|
approved=True, start_date__gte=timezone.now()
|
||||||
|
).order_by("start_date")
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
"""set fields and model"""
|
||||||
|
|
||||||
|
model = CallForPapers
|
||||||
|
fields = ["event", "name", "details", "opening_date", "closing_date"]
|
||||||
|
widgets = {
|
||||||
|
"opening_date": DateInput(),
|
||||||
|
"closing_date": DateInput(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class RegisterGroupForm(forms.ModelForm):
|
||||||
|
"""form for registering a group"""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
"""set fields and model"""
|
||||||
|
|
||||||
|
model = Group
|
||||||
|
fields = [
|
||||||
|
"name",
|
||||||
|
"category",
|
||||||
|
"type",
|
||||||
|
"url",
|
||||||
|
"registration_url",
|
||||||
|
"description",
|
||||||
|
"owner_email",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class RegisterNewsletterForm(forms.ModelForm):
|
||||||
|
"""form for registering a newsletter"""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
"""set fields and model"""
|
||||||
|
|
||||||
|
model = Newsletter
|
||||||
|
fields = [
|
||||||
|
"name",
|
||||||
|
"author",
|
||||||
|
"category",
|
||||||
|
"url",
|
||||||
|
"description",
|
||||||
|
"activitypub_account_name",
|
||||||
|
"owner_email",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class ContactForm(forms.Form):
|
||||||
|
"""form for contacting site admin"""
|
||||||
|
|
||||||
|
from_email = forms.EmailField(label="Email", max_length=200)
|
||||||
|
subject = forms.CharField(label="Subject", max_length=200)
|
||||||
|
message = forms.CharField(widget=forms.Textarea)
|
||||||
|
|
||||||
|
|
||||||
|
class SubscribeViaMastodon(forms.Form):
|
||||||
|
"""form for subscribing to Mastodon bot"""
|
||||||
|
|
||||||
|
username = forms.CharField(label="Username", max_length=200)
|
||||||
|
|
||||||
|
|
||||||
|
class SubscribeEmailForm(forms.ModelForm):
|
||||||
|
"""subscribe via email form"""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
"""set fields and model"""
|
||||||
|
|
||||||
|
model = Subscriber
|
||||||
|
fields = ["email"]
|
0
blogs/management/__init__.py
Normal file
0
blogs/management/__init__.py
Normal file
1
blogs/management/commands/__init__.py
Normal file
1
blogs/management/commands/__init__.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
""" call these commands using 'python manage.py name_of_command' """
|
19
blogs/management/commands/announce.py
Normal file
19
blogs/management/commands/announce.py
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
"""announce something"""
|
||||||
|
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
|
||||||
|
from blogs.models import Announcement
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
"""the announce command"""
|
||||||
|
|
||||||
|
# we could add arguments but we don't really need any
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
"""check for pending announcements and announce the latest"""
|
||||||
|
|
||||||
|
announcement = Announcement.objects.filter().order_by("queued").first()
|
||||||
|
|
||||||
|
if announcement:
|
||||||
|
announcement.announce()
|
118
blogs/management/commands/check_feeds.py
Normal file
118
blogs/management/commands/check_feeds.py
Normal file
|
@ -0,0 +1,118 @@
|
||||||
|
"""call this from cron to run through all the feeds to find new posts"""
|
||||||
|
|
||||||
|
from datetime import datetime, timedelta, timezone
|
||||||
|
|
||||||
|
import feedparser
|
||||||
|
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
from django.db.models import Q
|
||||||
|
from django.utils import timezone as django_timezone
|
||||||
|
|
||||||
|
from blogs import models
|
||||||
|
|
||||||
|
|
||||||
|
def date_to_tz_aware(date_tuple):
|
||||||
|
"""turn a 9-tuple into something usable"""
|
||||||
|
|
||||||
|
# we are assuming all dates are UTC which is a bit dodgy but it works
|
||||||
|
return datetime(*date_tuple[0:7], tzinfo=timezone.utc)
|
||||||
|
|
||||||
|
|
||||||
|
def get_tags(dictionary):
|
||||||
|
"""parse out tags from blog and upsert as tag instances"""
|
||||||
|
tags = []
|
||||||
|
for tag_obj in dictionary:
|
||||||
|
tag = models.Tag.objects.filter(name=tag_obj.term.lower()).first()
|
||||||
|
if not tag:
|
||||||
|
tag = models.Tag.objects.create(name=tag_obj.term.lower())
|
||||||
|
|
||||||
|
tags.append(tag)
|
||||||
|
|
||||||
|
return tags
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
"""the check_feeds command"""
|
||||||
|
|
||||||
|
# we could add arguments but we don't really need any
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
"""check feeds and update database"""
|
||||||
|
|
||||||
|
print(f"checking feeds at {django_timezone.localtime(django_timezone.now())}")
|
||||||
|
|
||||||
|
blogs = models.Blog.objects.filter(approved=True, suspended=False).all()
|
||||||
|
for blog in blogs:
|
||||||
|
try:
|
||||||
|
data = feedparser.parse(blog.feed)
|
||||||
|
|
||||||
|
for article in data.entries:
|
||||||
|
if not models.Article.objects.filter(
|
||||||
|
Q(url=article.link) | Q(guid=article.id)
|
||||||
|
).exists():
|
||||||
|
if (
|
||||||
|
blog.suspension_lifted
|
||||||
|
and blog.suspension_lifted
|
||||||
|
< date_to_tz_aware(article.updated_parsed)
|
||||||
|
):
|
||||||
|
continue # don't ingest posts published during a suspension
|
||||||
|
|
||||||
|
tags = get_tags(
|
||||||
|
getattr(article, "tags", None)
|
||||||
|
or getattr(article, "categories", [])
|
||||||
|
)
|
||||||
|
|
||||||
|
opt_out = False
|
||||||
|
# don't include posts with opt out tags
|
||||||
|
for tag in tags:
|
||||||
|
if (
|
||||||
|
len(
|
||||||
|
{tag.name}
|
||||||
|
& {
|
||||||
|
"notglam",
|
||||||
|
"notglamr",
|
||||||
|
"notausglamblogs",
|
||||||
|
"notausglamr",
|
||||||
|
"notglamblogs",
|
||||||
|
"#notglam",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
> 0
|
||||||
|
):
|
||||||
|
opt_out = True
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not opt_out:
|
||||||
|
author_name = getattr(article, "author", None) or getattr(
|
||||||
|
blog, "author", None
|
||||||
|
)
|
||||||
|
|
||||||
|
instance = models.Article.objects.create(
|
||||||
|
title=article.title,
|
||||||
|
author_name=author_name,
|
||||||
|
url=article.link,
|
||||||
|
description=article.summary,
|
||||||
|
updateddate=date_to_tz_aware(article.updated_parsed),
|
||||||
|
blog=blog,
|
||||||
|
pubdate=date_to_tz_aware(article.published_parsed),
|
||||||
|
guid=article.id,
|
||||||
|
)
|
||||||
|
|
||||||
|
for tag in tags:
|
||||||
|
instance.tags.add(tag)
|
||||||
|
instance.save()
|
||||||
|
|
||||||
|
cutoff = django_timezone.now() - timedelta(days=3)
|
||||||
|
newish = instance.pubdate > cutoff
|
||||||
|
if newish:
|
||||||
|
instance.announce()
|
||||||
|
|
||||||
|
blog.set_success()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
blog.set_failing()
|
||||||
|
print(f"ERROR WITH BLOG {blog.title} - {blog.url}")
|
||||||
|
print(e)
|
||||||
|
|
||||||
|
print(f"completed run at {django_timezone.localtime(django_timezone.now())}")
|
49
blogs/management/commands/queue_announcements.py
Normal file
49
blogs/management/commands/queue_announcements.py
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
"""check whether announcements need to be queued"""
|
||||||
|
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from blogs import models
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
"""the command"""
|
||||||
|
|
||||||
|
# we could add arguments but we don't really need any
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
"""check whether we need to queue announcements and queue them"""
|
||||||
|
|
||||||
|
conferences = models.Event.objects.filter(
|
||||||
|
approved=True,
|
||||||
|
announcements__lt=3,
|
||||||
|
start_date__gte=timezone.now(),
|
||||||
|
)
|
||||||
|
calls = models.CallForPapers.objects.filter(
|
||||||
|
announcements__lt=3,
|
||||||
|
closing_date__gte=timezone.now(),
|
||||||
|
conference__approved=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
for conf in conferences:
|
||||||
|
delta = conf.start_date - timezone.now().date()
|
||||||
|
|
||||||
|
if (
|
||||||
|
conf.announcements < 1
|
||||||
|
or (delta < timedelta(days=7))
|
||||||
|
or (delta < timedelta(months=3) and conf.announcements < 2)
|
||||||
|
):
|
||||||
|
conf.announce()
|
||||||
|
|
||||||
|
for cfp in calls:
|
||||||
|
delta_one = timezone.now().date() - cfp.opening_date
|
||||||
|
delta_two = cfp.closing_date - timezone.now().date()
|
||||||
|
|
||||||
|
if (
|
||||||
|
cfp.announcements < 1
|
||||||
|
or (delta_one > delta_two and cfp.announcements < 2)
|
||||||
|
or (delta_two < timedelta(days=7))
|
||||||
|
):
|
||||||
|
cfp.announce()
|
213
blogs/management/commands/send_weekly_email.py
Normal file
213
blogs/management/commands/send_weekly_email.py
Normal file
|
@ -0,0 +1,213 @@
|
||||||
|
"""send the weekly email"""
|
||||||
|
|
||||||
|
from datetime import timedelta
|
||||||
|
import random
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
|
||||||
|
from blogs import models
|
||||||
|
from blogs.utilities import send_email
|
||||||
|
from blogs.models.utils import GroupType
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
"""the announce command"""
|
||||||
|
|
||||||
|
# we could add arguments but we don't really need any
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
"""find subscribers and send an update"""
|
||||||
|
|
||||||
|
subscribers = models.Subscriber.objects.filter(confirmed=True)
|
||||||
|
|
||||||
|
print(
|
||||||
|
f"Sending weekly emails to {len(subscribers)} subscribers at {timezone.now()}"
|
||||||
|
)
|
||||||
|
|
||||||
|
cutoff = timezone.now() - timedelta(days=7)
|
||||||
|
blogs = models.Blog.objects.filter(approved=True, updateddate__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(
|
||||||
|
conference__approved=True, closing_date__gte=timezone.now().date()
|
||||||
|
)
|
||||||
|
newsletters = models.Newsletter.objects.filter(
|
||||||
|
approved=True, pub_date__gte=cutoff
|
||||||
|
)
|
||||||
|
groups = models.Group.objects.filter(approved=True, pub_date__gte=cutoff)
|
||||||
|
|
||||||
|
new_blogs = ""
|
||||||
|
for blog in blogs:
|
||||||
|
title_string = f"<h4><a href='{blog.url}'>{blog.title}</a></h4>"
|
||||||
|
author_string = (
|
||||||
|
f"<p><em>{blog.author_name}</em></p>" if blog.author_name else ""
|
||||||
|
)
|
||||||
|
description_string = (
|
||||||
|
f"<p style='margin-bottom:24px;'>{blog.description}</p>"
|
||||||
|
)
|
||||||
|
|
||||||
|
string_list = [title_string, author_string, description_string]
|
||||||
|
string = "".join(string_list)
|
||||||
|
|
||||||
|
new_blogs = new_blogs + string
|
||||||
|
|
||||||
|
if new_blogs != "":
|
||||||
|
new_blogs = (
|
||||||
|
"<h3 style='margin-top:20px;'>New Blogs</h3>" + new_blogs + "<hr/>"
|
||||||
|
)
|
||||||
|
|
||||||
|
new_articles = ""
|
||||||
|
for post in articles:
|
||||||
|
title_string = f"<h4><a href='{post.url}'>{post.title}</a></h4>"
|
||||||
|
author_string = (
|
||||||
|
f"<p><em>{post.author_name}</em></p>" if post.author_name else ""
|
||||||
|
)
|
||||||
|
description_string = (
|
||||||
|
f"<p style='margin-bottom:24px;'>{post.description}</p>"
|
||||||
|
)
|
||||||
|
|
||||||
|
string_list = [title_string, author_string, description_string]
|
||||||
|
string = "".join(string_list)
|
||||||
|
|
||||||
|
new_articles = new_articles + string
|
||||||
|
|
||||||
|
if new_articles != "":
|
||||||
|
new_articles = (
|
||||||
|
"<h3 style='margin-top:20px;'>New Articles</h3>"
|
||||||
|
+ new_articles
|
||||||
|
+ "<hr/>"
|
||||||
|
)
|
||||||
|
|
||||||
|
coming_events = ""
|
||||||
|
for event in events:
|
||||||
|
s_date = event.start_date
|
||||||
|
title_string = f"<h4><a href='{event.url}'>{event.name}</a></h4>"
|
||||||
|
date_string = (
|
||||||
|
f"<p><em>{s_date:%a} {s_date.day} {s_date:%B} {s_date:%Y}</em></p>"
|
||||||
|
)
|
||||||
|
description_string = (
|
||||||
|
f"<p style='margin-bottom:24px;'>{event.description}</p>"
|
||||||
|
)
|
||||||
|
|
||||||
|
string_list = [title_string, date_string, description_string]
|
||||||
|
string = "".join(string_list)
|
||||||
|
|
||||||
|
coming_events = coming_events + string
|
||||||
|
|
||||||
|
if coming_events != "":
|
||||||
|
coming_events = (
|
||||||
|
"<h3 style='margin-top:20px;'>Upcoming Events</h3>"
|
||||||
|
+ coming_events
|
||||||
|
+ "<hr/>"
|
||||||
|
)
|
||||||
|
|
||||||
|
open_cfps = ""
|
||||||
|
for instance in cfps:
|
||||||
|
c_date = instance.closing_date
|
||||||
|
title_string = (
|
||||||
|
f"<h4><a href='{instance.event.url}'>{instance.name}</a></h4>"
|
||||||
|
)
|
||||||
|
dates_string = f"<p><strong>Closes:</strong><em>{c_date:%a} {c_date.day} {c_date:%B}</em></p>"
|
||||||
|
description_string = (
|
||||||
|
f"<p style='margin-bottom:24px;'>{instance.details}</p>"
|
||||||
|
)
|
||||||
|
|
||||||
|
string_list = [title_string, dates_string, description_string]
|
||||||
|
string = "".join(string_list)
|
||||||
|
|
||||||
|
open_cfps = open_cfps + string
|
||||||
|
|
||||||
|
if open_cfps != "":
|
||||||
|
open_cfps = (
|
||||||
|
"<h3 style='margin-top:20px;'>Open Calls</h3>" + open_cfps + "<hr/>"
|
||||||
|
)
|
||||||
|
|
||||||
|
new_newsletters = ""
|
||||||
|
for instance in newsletters:
|
||||||
|
title_string = f"<h4><a href='{instance.url}'>{instance.name}</a></h4>"
|
||||||
|
author_string = (
|
||||||
|
f"<p><em>{instance.author}</em></p>" if instance.author else ""
|
||||||
|
)
|
||||||
|
description_string = (
|
||||||
|
f"<p style='margin-bottom:24px;'>{instance.description}</p>"
|
||||||
|
)
|
||||||
|
string_list = [title_string, author_string, description_string]
|
||||||
|
string = "".join(string_list)
|
||||||
|
|
||||||
|
new_newsletters = new_newsletters + string
|
||||||
|
|
||||||
|
if new_newsletters != "":
|
||||||
|
new_newsletters = (
|
||||||
|
"<h3 style='margin-top:20px;'>New Newsletters</h3>"
|
||||||
|
+ new_newsletters
|
||||||
|
+ "<hr/>"
|
||||||
|
)
|
||||||
|
|
||||||
|
new_groups = ""
|
||||||
|
for instance in groups:
|
||||||
|
group_type = GroupType(instance.type).label
|
||||||
|
title_string = f"<h4><a href='{instance.url}'>{instance.name}</a></h4>"
|
||||||
|
register_string = f"<p><em><a href='{instance.registration_url}'>Register</a> to join this {group_type}</em></p>"
|
||||||
|
description_string = (
|
||||||
|
f"<p style='margin-bottom:24px;'>{instance.description}</p>"
|
||||||
|
)
|
||||||
|
string_list = [title_string, register_string, description_string]
|
||||||
|
string = "".join(string_list)
|
||||||
|
|
||||||
|
new_groups = new_groups + string
|
||||||
|
|
||||||
|
if new_groups != "":
|
||||||
|
new_groups = (
|
||||||
|
"<h3 style='margin-top:20px;'>New Groups</h3>" + new_groups + "<hr/>"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Now let's put it all together...
|
||||||
|
dt = timezone.now()
|
||||||
|
choices = [
|
||||||
|
"🍓",
|
||||||
|
"🍒",
|
||||||
|
"🍎",
|
||||||
|
"🍊",
|
||||||
|
"🍍",
|
||||||
|
"🍋",
|
||||||
|
"🍉",
|
||||||
|
"🥝",
|
||||||
|
"🥦",
|
||||||
|
"🥒",
|
||||||
|
"🥕",
|
||||||
|
"🍏",
|
||||||
|
"🍅",
|
||||||
|
"🥬",
|
||||||
|
"🫐",
|
||||||
|
"🍐",
|
||||||
|
"🥗",
|
||||||
|
"☕️",
|
||||||
|
"🚚",
|
||||||
|
"📬",
|
||||||
|
"🍣",
|
||||||
|
]
|
||||||
|
emoji = random.choice(choices)
|
||||||
|
subject = f"{emoji} Fresh Aus GLAMR updates for the week of {dt.day} {dt:%B} {dt.year}"
|
||||||
|
sections = [
|
||||||
|
new_articles,
|
||||||
|
new_blogs,
|
||||||
|
new_newsletters,
|
||||||
|
new_groups,
|
||||||
|
open_cfps,
|
||||||
|
coming_events,
|
||||||
|
]
|
||||||
|
body = "".join(sections)
|
||||||
|
|
||||||
|
for subscriber in subscribers:
|
||||||
|
opt_out = f"https://{settings.DOMAIN}/unsubscribe-email/{subscriber.token}/{subscriber.id}"
|
||||||
|
start = "<html><body>"
|
||||||
|
footer = f"<div style='padding: 20px; width: 100vw; background-color:#eee; margin-top: 100px;text-align:center;'><em><p>This email was sent to <strong>{subscriber.email}</strong> because you subscribed to email updates from <a href='https://{settings.DOMAIN}'>Aus GLAMR</a>.</p><p>You can <a href='{opt_out}'>unsubscribe</a> at any time.</p></em></div>"
|
||||||
|
end = "</body></html>"
|
||||||
|
parts = [start, body, footer, end]
|
||||||
|
message = "".join(parts)
|
||||||
|
|
||||||
|
send_email(subject, message, subscriber.email)
|
||||||
|
|
||||||
|
print(f"Weekly emails completed {timezone.now()}")
|
369
blogs/migrations/0001_initial.py
Normal file
369
blogs/migrations/0001_initial.py
Normal file
|
@ -0,0 +1,369 @@
|
||||||
|
# Generated by Django 4.2.7 on 2024-01-03 03:35
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import django.utils.timezone
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = []
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="Announcement",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("status", models.TextField()),
|
||||||
|
("summary", models.TextField(null=True)),
|
||||||
|
("queued", models.DateTimeField(default=django.utils.timezone.now)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="Blog",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("title", models.CharField(max_length=2000)),
|
||||||
|
("author_name", models.CharField(max_length=1000, null=True)),
|
||||||
|
("url", models.URLField(max_length=2000, unique=True)),
|
||||||
|
("description", models.TextField(blank=True, null=True)),
|
||||||
|
("updateddate", models.DateTimeField()),
|
||||||
|
("feed", models.URLField(max_length=2000)),
|
||||||
|
(
|
||||||
|
"category",
|
||||||
|
models.CharField(
|
||||||
|
choices=[
|
||||||
|
("GAL", "Galleries"),
|
||||||
|
("LIB", "Libraries"),
|
||||||
|
("ARC", "Archives"),
|
||||||
|
("MUS", "Museums"),
|
||||||
|
("REC", "Records"),
|
||||||
|
("DH", "Digital Humanities"),
|
||||||
|
("GLAM", "GLAMR"),
|
||||||
|
],
|
||||||
|
max_length=4,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("approved", models.BooleanField(default=False)),
|
||||||
|
("announced", models.BooleanField(default=False)),
|
||||||
|
("failing", models.BooleanField(blank=True, default=False, null=True)),
|
||||||
|
(
|
||||||
|
"suspended",
|
||||||
|
models.BooleanField(blank=True, default=False, null=True),
|
||||||
|
),
|
||||||
|
("suspension_lifted", models.DateTimeField(blank=True, null=True)),
|
||||||
|
(
|
||||||
|
"activitypub_account_name",
|
||||||
|
models.CharField(blank=True, max_length=200, null=True),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"owner_email",
|
||||||
|
models.EmailField(blank=True, max_length=254, null=True),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"abstract": False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="ContentWarning",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("match_text", models.CharField(max_length=999, null=True)),
|
||||||
|
("display", models.CharField(max_length=999, null=True)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="Event",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("name", models.CharField(max_length=999)),
|
||||||
|
(
|
||||||
|
"category",
|
||||||
|
models.CharField(
|
||||||
|
choices=[
|
||||||
|
("GAL", "Galleries"),
|
||||||
|
("LIB", "Libraries"),
|
||||||
|
("ARC", "Archives"),
|
||||||
|
("MUS", "Museums"),
|
||||||
|
("REC", "Records"),
|
||||||
|
("DH", "Digital Humanities"),
|
||||||
|
("GLAM", "GLAMR"),
|
||||||
|
],
|
||||||
|
max_length=4,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("url", models.URLField(max_length=400, unique=True)),
|
||||||
|
("description", models.TextField(blank=True, null=True)),
|
||||||
|
("pub_date", models.DateTimeField()),
|
||||||
|
("start_date", models.DateField()),
|
||||||
|
(
|
||||||
|
"announcements",
|
||||||
|
models.IntegerField(blank=True, default=0, null=True),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"activitypub_account_name",
|
||||||
|
models.CharField(blank=True, max_length=200, null=True),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"owner_email",
|
||||||
|
models.EmailField(blank=True, max_length=254, null=True),
|
||||||
|
),
|
||||||
|
("approved", models.BooleanField(default=False)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="Group",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("name", models.CharField(max_length=999)),
|
||||||
|
(
|
||||||
|
"category",
|
||||||
|
models.CharField(
|
||||||
|
choices=[
|
||||||
|
("GAL", "Galleries"),
|
||||||
|
("LIB", "Libraries"),
|
||||||
|
("ARC", "Archives"),
|
||||||
|
("MUS", "Museums"),
|
||||||
|
("REC", "Records"),
|
||||||
|
("DH", "Digital Humanities"),
|
||||||
|
("GLAM", "GLAMR"),
|
||||||
|
],
|
||||||
|
max_length=4,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"type",
|
||||||
|
models.CharField(
|
||||||
|
choices=[
|
||||||
|
("DISC", "Discord server"),
|
||||||
|
("DCRS", "Discourse community"),
|
||||||
|
("EML", "email list"),
|
||||||
|
("GOOG", "Google group"),
|
||||||
|
("KBIN", "KBin server"),
|
||||||
|
("LEMM", "Lemmy server"),
|
||||||
|
("MAS", "Mastodon server"),
|
||||||
|
("RED", "subreddit"),
|
||||||
|
("SLAC", "Slack channel"),
|
||||||
|
("ZLIP", "Zulip server"),
|
||||||
|
("OTHR", "group"),
|
||||||
|
],
|
||||||
|
max_length=4,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("url", models.URLField(max_length=400, unique=True)),
|
||||||
|
("registration_url", models.URLField(max_length=400, unique=True)),
|
||||||
|
("description", models.TextField(blank=True, null=True)),
|
||||||
|
(
|
||||||
|
"owner_email",
|
||||||
|
models.EmailField(blank=True, max_length=254, null=True),
|
||||||
|
),
|
||||||
|
("announced", models.BooleanField(default=False)),
|
||||||
|
("approved", models.BooleanField(default=False)),
|
||||||
|
("pub_date", models.DateTimeField(default=None, null=True)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="Newsletter",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("name", models.CharField(max_length=999)),
|
||||||
|
("author", models.CharField(max_length=999)),
|
||||||
|
(
|
||||||
|
"category",
|
||||||
|
models.CharField(
|
||||||
|
choices=[
|
||||||
|
("GAL", "Galleries"),
|
||||||
|
("LIB", "Libraries"),
|
||||||
|
("ARC", "Archives"),
|
||||||
|
("MUS", "Museums"),
|
||||||
|
("REC", "Records"),
|
||||||
|
("DH", "Digital Humanities"),
|
||||||
|
("GLAM", "GLAMR"),
|
||||||
|
],
|
||||||
|
max_length=4,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("url", models.URLField(max_length=400, unique=True)),
|
||||||
|
("description", models.TextField(blank=True, null=True)),
|
||||||
|
(
|
||||||
|
"activitypub_account_name",
|
||||||
|
models.CharField(blank=True, max_length=200, null=True),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"owner_email",
|
||||||
|
models.EmailField(blank=True, max_length=254, null=True),
|
||||||
|
),
|
||||||
|
("announced", models.BooleanField(default=False)),
|
||||||
|
("approved", models.BooleanField(default=False)),
|
||||||
|
("pub_date", models.DateTimeField(default=None, null=True)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="SiteMessage",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("message", models.TextField(max_length=999)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="Subscriber",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"email",
|
||||||
|
models.EmailField(
|
||||||
|
blank=True, max_length=254, null=True, unique=True
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("confirmed", models.BooleanField(default=False, editable=False)),
|
||||||
|
("token", models.UUIDField(default=uuid.uuid4, editable=False)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="Tag",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("name", models.CharField(max_length=100, unique=True)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="CallForPapers",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("name", models.CharField(max_length=999)),
|
||||||
|
("details", models.TextField(blank=True, null=True)),
|
||||||
|
("pub_date", models.DateTimeField(default=None, null=True)),
|
||||||
|
("opening_date", models.DateField()),
|
||||||
|
("closing_date", models.DateField()),
|
||||||
|
("announcements", models.IntegerField(default=0, null=True)),
|
||||||
|
("approved", models.BooleanField(default=False)),
|
||||||
|
(
|
||||||
|
"event",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="cfp",
|
||||||
|
to="blogs.event",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="Article",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("title", models.CharField(max_length=2000)),
|
||||||
|
("author_name", models.CharField(max_length=1000, null=True)),
|
||||||
|
("url", models.URLField(max_length=2000, unique=True)),
|
||||||
|
("description", models.TextField(blank=True, null=True)),
|
||||||
|
("updateddate", models.DateTimeField()),
|
||||||
|
("pubdate", models.DateTimeField()),
|
||||||
|
("guid", models.CharField(max_length=2000)),
|
||||||
|
(
|
||||||
|
"blog",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="articles",
|
||||||
|
to="blogs.blog",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"tags",
|
||||||
|
models.ManyToManyField(related_name="articles", to="blogs.tag"),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"abstract": False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
0
blogs/migrations/__init__.py
Normal file
0
blogs/migrations/__init__.py
Normal file
8
blogs/models/__init__.py
Normal file
8
blogs/models/__init__.py
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
"""blogs app models"""
|
||||||
|
|
||||||
|
from .blog import Article, Blog, Tag
|
||||||
|
from .event import Event, CallForPapers
|
||||||
|
from .group import Group
|
||||||
|
from .newsletter import Newsletter
|
||||||
|
from .utils import Announcement, Category, ContentWarning, SiteMessage
|
||||||
|
from .subscriber import Subscriber
|
128
blogs/models/blog.py
Normal file
128
blogs/models/blog.py
Normal file
|
@ -0,0 +1,128 @@
|
||||||
|
"""blog models"""
|
||||||
|
|
||||||
|
from django.db import models
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from .utils import Announcement, Category, ContentWarning
|
||||||
|
|
||||||
|
|
||||||
|
class BlogData(models.Model):
|
||||||
|
"""Base bloggy data"""
|
||||||
|
|
||||||
|
title = models.CharField(max_length=2000)
|
||||||
|
author_name = models.CharField(max_length=1000, null=True)
|
||||||
|
url = models.URLField(max_length=2000, unique=True)
|
||||||
|
description = models.TextField(null=True, blank=True)
|
||||||
|
updateddate = models.DateTimeField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
"""This is an abstract model for common data"""
|
||||||
|
|
||||||
|
abstract = True
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
"""display for admin dropdowns"""
|
||||||
|
return self.title
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
"""override"""
|
||||||
|
|
||||||
|
return self.url
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
if not self.updateddate:
|
||||||
|
self.updateddate = timezone.now()
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class Blog(BlogData):
|
||||||
|
"""A blog"""
|
||||||
|
|
||||||
|
feed = models.URLField(max_length=2000)
|
||||||
|
category = models.CharField(choices=Category.choices, max_length=4)
|
||||||
|
approved = models.BooleanField(default=False)
|
||||||
|
announced = models.BooleanField(default=False)
|
||||||
|
failing = models.BooleanField(default=False, blank=True, null=True)
|
||||||
|
suspended = models.BooleanField(default=False, blank=True, null=True)
|
||||||
|
suspension_lifted = models.DateTimeField(blank=True, null=True)
|
||||||
|
activitypub_account_name = models.CharField(max_length=200, blank=True, null=True)
|
||||||
|
owner_email = models.EmailField(blank=True, null=True)
|
||||||
|
|
||||||
|
def announce(self):
|
||||||
|
"""queue announcement"""
|
||||||
|
|
||||||
|
if self.activitypub_account_name:
|
||||||
|
author = f" by {self.activitypub_account_name}"
|
||||||
|
elif self.author_name:
|
||||||
|
author = f" by {self.author_name}"
|
||||||
|
else:
|
||||||
|
author = ""
|
||||||
|
|
||||||
|
category = Category(self.category).label
|
||||||
|
status = f"{self.title}{author} has been added to Aus GLAMR! \
|
||||||
|
\n\nIt's about {category}\n\n{self.url}"
|
||||||
|
|
||||||
|
Announcement.objects.create(status=status)
|
||||||
|
self.announced = True
|
||||||
|
super().save()
|
||||||
|
|
||||||
|
def set_failing(self):
|
||||||
|
"""set the blog feed as failing"""
|
||||||
|
|
||||||
|
self.failing = True
|
||||||
|
super().save()
|
||||||
|
|
||||||
|
def set_success(self):
|
||||||
|
"""set failing to false"""
|
||||||
|
|
||||||
|
self.failing = False
|
||||||
|
super().save()
|
||||||
|
|
||||||
|
|
||||||
|
class Article(BlogData):
|
||||||
|
"""A blog post"""
|
||||||
|
|
||||||
|
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")
|
||||||
|
|
||||||
|
# pylint: disable=undefined-variable
|
||||||
|
def announce(self):
|
||||||
|
"""queue a blog post announcement"""
|
||||||
|
|
||||||
|
summary = []
|
||||||
|
warnings = ContentWarning.objects.all()
|
||||||
|
for warning in warnings:
|
||||||
|
for text in [self.title, self.description]:
|
||||||
|
label = warning.is_in(text)
|
||||||
|
if label:
|
||||||
|
summmary.append(label)
|
||||||
|
for tag in self.tags: # pylint: disable=E1133
|
||||||
|
label = warning.is_in(tag.name)
|
||||||
|
if label:
|
||||||
|
summmary.append(label)
|
||||||
|
|
||||||
|
summary_text = ", ".join(summary) if len(summary) > 0 else None
|
||||||
|
author = self.blog.activitypub_account_name or self.author_name
|
||||||
|
|
||||||
|
if self.blog.activitypub_account_name:
|
||||||
|
author = f"{self.blog.activitypub_account_name} "
|
||||||
|
elif self.author_name:
|
||||||
|
author = f"{self.author_name} "
|
||||||
|
else:
|
||||||
|
author = ""
|
||||||
|
|
||||||
|
status = f"{self.title} ({author}on {self.blog.title})\n\n{self.url}"
|
||||||
|
|
||||||
|
Announcement.objects.create(status=status, summary=summary_text)
|
||||||
|
|
||||||
|
|
||||||
|
class Tag(models.Model):
|
||||||
|
"""An article tag"""
|
||||||
|
|
||||||
|
name = models.CharField(max_length=100, unique=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
"""display for admin dropdowns"""
|
||||||
|
return self.name
|
85
blogs/models/event.py
Normal file
85
blogs/models/event.py
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
"""event models"""
|
||||||
|
|
||||||
|
from django.db import models
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from .utils import Announcement, Category
|
||||||
|
|
||||||
|
|
||||||
|
class Event(models.Model):
|
||||||
|
"""a event"""
|
||||||
|
|
||||||
|
name = models.CharField(max_length=999)
|
||||||
|
category = models.CharField(choices=Category.choices, max_length=4)
|
||||||
|
url = models.URLField(max_length=400, unique=True)
|
||||||
|
description = models.TextField(null=True, blank=True)
|
||||||
|
pub_date = models.DateTimeField() # for RSS feed
|
||||||
|
start_date = models.DateField()
|
||||||
|
announcements = models.IntegerField(null=True, blank=True, default=0)
|
||||||
|
activitypub_account_name = models.CharField(max_length=200, blank=True, null=True)
|
||||||
|
owner_email = models.EmailField(blank=True, null=True)
|
||||||
|
approved = models.BooleanField(default=False)
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
if not self.pub_date:
|
||||||
|
self.pub_date = timezone.now()
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
"""display for admin dropdowns"""
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
"""override for rss feed"""
|
||||||
|
return self.url
|
||||||
|
|
||||||
|
def announce(self):
|
||||||
|
"""announce a event"""
|
||||||
|
|
||||||
|
date = self.start_date.strftime("%a %d %b %Y")
|
||||||
|
category = Category(self.category).label
|
||||||
|
name = self.name
|
||||||
|
if self.activitypub_account_name:
|
||||||
|
name = f"{self.name} ({self.activitypub_account_name})"
|
||||||
|
|
||||||
|
status = (
|
||||||
|
f"{name} is a event about {category}, starting on {date}!\n\n{self.url}"
|
||||||
|
)
|
||||||
|
|
||||||
|
Announcement.objects.create(status=status)
|
||||||
|
self.announcements = self.announcements + 1
|
||||||
|
super().save()
|
||||||
|
|
||||||
|
|
||||||
|
class CallForPapers(models.Model):
|
||||||
|
"""a event call for papers/presentations"""
|
||||||
|
|
||||||
|
name = models.CharField(
|
||||||
|
max_length=999
|
||||||
|
) # "Call for papers", "call for participation" etc
|
||||||
|
details = models.TextField(null=True, blank=True)
|
||||||
|
pub_date = models.DateTimeField(null=True, default=None)
|
||||||
|
opening_date = models.DateField()
|
||||||
|
closing_date = models.DateField()
|
||||||
|
announcements = models.IntegerField(null=True, default=0)
|
||||||
|
event = models.ForeignKey(Event, on_delete=models.CASCADE, related_name="cfp")
|
||||||
|
approved = models.BooleanField(default=False)
|
||||||
|
|
||||||
|
def announce(self):
|
||||||
|
"""create a call for papers announcement"""
|
||||||
|
|
||||||
|
opening_date = self.opening_date.strftime("%a %d %b %Y")
|
||||||
|
closing_date = self.closing_date.strftime("%a %d %b %Y")
|
||||||
|
|
||||||
|
status = f"{self.event.name} {self.name } is open from {opening_date}, closing on {closing_date}!\n\nMore info at {self.event.url}"
|
||||||
|
|
||||||
|
if self.event.approved:
|
||||||
|
Announcement.objects.create(status=status)
|
||||||
|
self.announcements = self.announcements + 1
|
||||||
|
super().save()
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
"""save a CFP with pub_date"""
|
||||||
|
if not self.pub_date:
|
||||||
|
self.pub_date = timezone.now()
|
||||||
|
super().save(*args, **kwargs)
|
37
blogs/models/group.py
Normal file
37
blogs/models/group.py
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
"""group models"""
|
||||||
|
|
||||||
|
from django.db import models
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from .utils import Announcement, Category, GroupType
|
||||||
|
|
||||||
|
|
||||||
|
class Group(models.Model):
|
||||||
|
"""a group on email, discord, slack etc"""
|
||||||
|
|
||||||
|
name = models.CharField(max_length=999)
|
||||||
|
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)
|
||||||
|
owner_email = models.EmailField(blank=True, null=True)
|
||||||
|
announced = models.BooleanField(default=False)
|
||||||
|
approved = models.BooleanField(default=False)
|
||||||
|
pub_date = models.DateTimeField(null=True, default=None)
|
||||||
|
|
||||||
|
def announce(self):
|
||||||
|
"""create a group announcement"""
|
||||||
|
|
||||||
|
category = Category(self.category).label
|
||||||
|
type = GroupType(self.type).label
|
||||||
|
status = f"{self.name} is a {type} about {category}!\n\nJoin them: {self.registration_url}"
|
||||||
|
|
||||||
|
Announcement.objects.create(status=status)
|
||||||
|
self.announced = True
|
||||||
|
super().save()
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
if not self.pub_date:
|
||||||
|
self.pub_date = timezone.now()
|
||||||
|
super().save(*args, **kwargs)
|
41
blogs/models/newsletter.py
Normal file
41
blogs/models/newsletter.py
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
"""newsletter models"""
|
||||||
|
|
||||||
|
from django.db import models
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from .utils import Announcement, Category
|
||||||
|
|
||||||
|
|
||||||
|
class Newsletter(models.Model):
|
||||||
|
"""a newsletter"""
|
||||||
|
|
||||||
|
name = models.CharField(max_length=999)
|
||||||
|
author = models.CharField(max_length=999)
|
||||||
|
category = models.CharField(choices=Category.choices, max_length=4)
|
||||||
|
url = models.URLField(max_length=400, unique=True)
|
||||||
|
|
||||||
|
description = models.TextField(null=True, blank=True)
|
||||||
|
activitypub_account_name = models.CharField(max_length=200, blank=True, null=True)
|
||||||
|
owner_email = models.EmailField(blank=True, null=True)
|
||||||
|
announced = models.BooleanField(default=False)
|
||||||
|
approved = models.BooleanField(default=False)
|
||||||
|
pub_date = models.DateTimeField(null=True, default=None)
|
||||||
|
|
||||||
|
def announce(self):
|
||||||
|
"""create a event announcement"""
|
||||||
|
|
||||||
|
category = Category(self.category).label
|
||||||
|
name = self.name
|
||||||
|
if self.activitypub_account_name:
|
||||||
|
name = f"{self.name} ({self.activitypub_account_name})"
|
||||||
|
|
||||||
|
status = f"{name} is a newsletter about {category} from {self.author}. Check it out:\n\n{self.url}"
|
||||||
|
|
||||||
|
Announcement.objects.create(status=status)
|
||||||
|
self.announced = True
|
||||||
|
super().save()
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
if not self.pub_date:
|
||||||
|
self.pub_date = timezone.now()
|
||||||
|
super().save(*args, **kwargs)
|
41
blogs/models/subscriber.py
Normal file
41
blogs/models/subscriber.py
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
"""email subscriptions"""
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
from blogs.utilities import send_email
|
||||||
|
|
||||||
|
|
||||||
|
class Subscriber(models.Model):
|
||||||
|
"""a person who wants to receive weekly emails"""
|
||||||
|
|
||||||
|
email = models.EmailField(blank=True, null=True, unique=True)
|
||||||
|
confirmed = models.BooleanField(default=False, editable=False)
|
||||||
|
token = models.UUIDField(default=uuid.uuid4, editable=False)
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
"""always reset the token on save"""
|
||||||
|
|
||||||
|
self.token = uuid.uuid4()
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
def send_confirmation_email(self):
|
||||||
|
"""send an email requesting confirmation"""
|
||||||
|
|
||||||
|
subject = "Please confirm your email address"
|
||||||
|
recipient = self.email
|
||||||
|
url = f"{settings.DOMAIN}/confirm-subscribe-email/{self.token}/{self.id}"
|
||||||
|
opt_out = f"{settings.DOMAIN}/unsubscribe-email/{self.token}/{self.id}"
|
||||||
|
start = "<html><body>"
|
||||||
|
body = f"<p>Please <a href='{url}'>confirm your email address</a> to receive weekly updates.</p>"
|
||||||
|
footer = (
|
||||||
|
f"<p><em>You can <a href='{opt_out}'>unsubscribe</a> at any time.</em></p>"
|
||||||
|
)
|
||||||
|
end = "</body></html>"
|
||||||
|
|
||||||
|
parts = [start, body, footer, end]
|
||||||
|
message = "".join(parts)
|
||||||
|
|
||||||
|
send_email(subject, message, recipient)
|
84
blogs/models/utils.py
Normal file
84
blogs/models/utils.py
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
"""utility models for use in other models"""
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import models
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
|
class Category(models.TextChoices):
|
||||||
|
"""what GLAMR are you"""
|
||||||
|
|
||||||
|
GALLERIES = "GAL", _("Galleries")
|
||||||
|
LIBRARIES = "LIB", _("Libraries")
|
||||||
|
ARCHIVES = "ARC", _("Archives")
|
||||||
|
MUSEUMS = "MUS", _("Museums")
|
||||||
|
RECORDS = "REC", _("Records")
|
||||||
|
DIGITAL_HUMANITIES = "DH", _("Digital Humanities")
|
||||||
|
GLAM = "GLAM", _("GLAMR")
|
||||||
|
|
||||||
|
|
||||||
|
class GroupType(models.TextChoices):
|
||||||
|
"""what GLAMR are you"""
|
||||||
|
|
||||||
|
DISCORD = "DISC", _("Discord server")
|
||||||
|
DISCOURSE = "DCRS", _("Discourse community")
|
||||||
|
EMAIL = "EML", _("email list")
|
||||||
|
GOOGLE = "GOOG", _("Google group")
|
||||||
|
KBIN = "KBIN", _("KBin server")
|
||||||
|
LEMMY = "LEMM", _("Lemmy server")
|
||||||
|
MASTODON = "MAS", _("Mastodon server")
|
||||||
|
REDDIT = "RED", _("subreddit")
|
||||||
|
SLACK = "SLAC", _("Slack channel")
|
||||||
|
ZULIP = "ZLIP", _("Zulip server")
|
||||||
|
OTHER = "OTHR", _("group")
|
||||||
|
|
||||||
|
|
||||||
|
class Announcement(models.Model):
|
||||||
|
"""an announcement on Mastodon"""
|
||||||
|
|
||||||
|
status = models.TextField()
|
||||||
|
summary = models.TextField(null=True)
|
||||||
|
queued = models.DateTimeField(default=timezone.now)
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
if not self.queued:
|
||||||
|
self.queued = timezone.now()
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
def announce(self):
|
||||||
|
"""tell the world about it"""
|
||||||
|
|
||||||
|
key = settings.MASTODON_ACCESS_TOKEN
|
||||||
|
headers = {"Authorization": f"Bearer {key}"}
|
||||||
|
params = {"status": self.status}
|
||||||
|
if self.summary:
|
||||||
|
params["spoiler_text"] = self.summary
|
||||||
|
|
||||||
|
url = f"{settings.MASTODON_DOMAIN}/api/v1/statuses"
|
||||||
|
r = requests.post(url, data=params, headers=headers, timeout=(4, 13))
|
||||||
|
if r.status_code == 200:
|
||||||
|
self.delete()
|
||||||
|
|
||||||
|
|
||||||
|
class ContentWarning(models.Model):
|
||||||
|
"""content warnings"""
|
||||||
|
|
||||||
|
match_text = models.CharField(max_length=999, null=True)
|
||||||
|
display = models.CharField(max_length=999, null=True)
|
||||||
|
|
||||||
|
def is_in(self, text):
|
||||||
|
"""check some text and return a CW if needed"""
|
||||||
|
|
||||||
|
warning = None
|
||||||
|
if self.match_text in text.lower():
|
||||||
|
warning = self.display
|
||||||
|
return warning
|
||||||
|
|
||||||
|
|
||||||
|
class SiteMessage(models.Model):
|
||||||
|
"""A message to be displayed somewhere"""
|
||||||
|
|
||||||
|
message = models.TextField(max_length=999)
|
54
blogs/templates/blogs/confirm-register.html
Normal file
54
blogs/templates/blogs/confirm-register.html
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
{% extends "layout.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<div class="loader" hidden>
|
||||||
|
<div class="lds-heart"><div></div></div>
|
||||||
|
<div class="lds-text">Processing...</div>
|
||||||
|
</div>
|
||||||
|
{% if error %}
|
||||||
|
<div class="alert failure">
|
||||||
|
{{ error }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<form action="{% url 'submit-blog-registration' %}" method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="columns eight">
|
||||||
|
|
||||||
|
<input hidden type="text" name="register_type" value="blog">
|
||||||
|
<label for="{{ form.title.id_for_label }}">Title:</label>
|
||||||
|
<input type="text" name="title" id="title" value="{{ blog_info.title }}" class="u-full-width" required="">
|
||||||
|
|
||||||
|
<label for="{{ form.author_name.id_for_label }}">Author:</label>
|
||||||
|
<input type="text" name="author_name" id="author_name" value="{{ blog_info.author_name }}" class="u-full-width" required="">
|
||||||
|
|
||||||
|
<label for="{{ form.description.id_for_label }}">Description:</label>
|
||||||
|
<textarea name="description" id="description" class="u-full-width">{{ blog_info.description }}</textarea>
|
||||||
|
|
||||||
|
{{ form.url.errors }}
|
||||||
|
<label for="{{ form.url.id_for_label }}">URL:</label>
|
||||||
|
{{ form.url }}
|
||||||
|
|
||||||
|
<label for="{{ form.feed.id_for_label }}">Feed:</label>
|
||||||
|
<input type="text" name="feed" id="feed" value="{{ blog_info.feed }}" class="u-full-width" required="" placeholder="https://example.com/rss.xml">
|
||||||
|
|
||||||
|
{{ form.category.errors }}
|
||||||
|
<label for="{{ form.category.id_for_label }}">Category:</label>
|
||||||
|
{{ form.category }}
|
||||||
|
|
||||||
|
{{ form.activitypub_account_name.errors }}
|
||||||
|
<label for="{{ form.activitypub_account_name.id_for_label }}">Activitypub account name:</label>
|
||||||
|
{{ form.activitypub_account_name }}
|
||||||
|
|
||||||
|
{{ form.owner_email.errors }}
|
||||||
|
<label for="{{ form.owner_email.id_for_label }}">Owner email:</label>
|
||||||
|
{{ form.owner_email }}
|
||||||
|
|
||||||
|
<input class="button-primary u-pull-right" type="submit" value="Register!">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% endblock %}
|
20
blogs/templates/blogs/register.html
Normal file
20
blogs/templates/blogs/register.html
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
{% extends "layout.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<div class="loader" hidden>
|
||||||
|
<div class="lds-heart"><div></div></div>
|
||||||
|
<div class="lds-text">Finding feed data...</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form action="{% url 'register-blog' %}" method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="columns eight">
|
||||||
|
{{ form }}
|
||||||
|
<input id="submit" class="button-primary u-pull-right" type="submit" value="Register!">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% endblock %}
|
16
blogs/templates/blogs/submit-registration.html
Normal file
16
blogs/templates/blogs/submit-registration.html
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
{% extends "layout.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<form action="{% url 'submit-blog-registration' %}" method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="columns eight">
|
||||||
|
{{ form }}
|
||||||
|
<input class="button-primary u-pull-right" type="submit" value="Register!">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% endblock %}
|
19
blogs/templates/browse/blogs.html
Normal file
19
blogs/templates/browse/blogs.html
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
{% extends "layout.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="listing l-three header">
|
||||||
|
<div>Title</div>
|
||||||
|
<div>Author</div>
|
||||||
|
<div>Category</div>
|
||||||
|
</div>
|
||||||
|
<hr/>
|
||||||
|
{% for blog in blogs %}
|
||||||
|
<div class="listing l-three">
|
||||||
|
<div><a href="{{blog.url}}">{{blog.title}}</a></div>
|
||||||
|
<div>{{blog.author_name}}</div>
|
||||||
|
<div><div class="end badge badge_{{blog.category}}">{{blog.category_name}}</div></div>
|
||||||
|
</div>
|
||||||
|
<hr/>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{% endblock %}
|
25
blogs/templates/browse/cfp.html
Normal file
25
blogs/templates/browse/cfp.html
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
{% extends "layout.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<div class="listing l-three header">
|
||||||
|
<span>Details</span>
|
||||||
|
<span>Event</span>
|
||||||
|
<span>Closes</span>
|
||||||
|
</div>
|
||||||
|
<hr/>
|
||||||
|
|
||||||
|
{% for cfp in cfps %}
|
||||||
|
<div class="listing l-three">
|
||||||
|
<div>
|
||||||
|
<p><strong>{{cfp.name}}</strong></p>
|
||||||
|
<p>{{cfp.details}}</p>
|
||||||
|
</div>
|
||||||
|
<span><a href="{{cfp.event.url}}">{{cfp.event.name}}</a></span>
|
||||||
|
<span class="closing-date">{{cfp.closing_date}}</span>
|
||||||
|
</div>
|
||||||
|
<hr/>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
|
||||||
|
{% endblock %}
|
31
blogs/templates/browse/events.html
Normal file
31
blogs/templates/browse/events.html
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
{% extends "layout.html" %}
|
||||||
|
|
||||||
|
{% 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>
|
||||||
|
<hr/>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
|
||||||
|
{% endblock %}
|
20
blogs/templates/browse/groups.html
Normal file
20
blogs/templates/browse/groups.html
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
{% extends "layout.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="listing l-four header">
|
||||||
|
<span>Name</span>
|
||||||
|
<span>Category</span>
|
||||||
|
<span>Description</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>
|
||||||
|
<span><a href="{{group.registration_url}}">Join the {{group.reg_type}}</a></span>
|
||||||
|
</div>
|
||||||
|
<hr/>
|
||||||
|
{% endfor %}
|
||||||
|
{% endblock %}
|
18
blogs/templates/browse/newsletters.html
Normal file
18
blogs/templates/browse/newsletters.html
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
{% extends "layout.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="listing l-three header">
|
||||||
|
<span>Title</span>
|
||||||
|
<span>Author</span>
|
||||||
|
<span>Category</span>
|
||||||
|
</div>
|
||||||
|
<hr/>
|
||||||
|
{% for pub in news %}
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
<hr/>
|
||||||
|
{% endfor %}
|
||||||
|
{% endblock %}
|
63
blogs/templates/browse/tags.html
Normal file
63
blogs/templates/browse/tags.html
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
{% extends "layout.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{% if items %}
|
||||||
|
{% for item in items %}
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-title">
|
||||||
|
<a href="{{ item.url }}"><h3>{% if item.title %} {{ item.title }} {% else %} {{item.name}} {% endif %}</h3></a>
|
||||||
|
<p class="meta">
|
||||||
|
{% if item.author_name or item.author %}
|
||||||
|
<span class="author_name">{% if item.author_name%} {{ item.author_name }} {% else %} {{ item.author_name }} {% endif %}</span> |
|
||||||
|
{% endif %}
|
||||||
|
{% if item.blog %}
|
||||||
|
<span class="blog_title">{{ item.blog.title }}</span>
|
||||||
|
{% elif item.description %}
|
||||||
|
<p class="">{{ item.description }}</p>
|
||||||
|
{% elif item.details %}
|
||||||
|
<p class="">{{ item.details }}</p>
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
|
{% if item.tags %}
|
||||||
|
{% for tag in item.tags.all %}
|
||||||
|
<a href="{% url 'browse' %}?q={{ tag.name|urlencode }}"><span class="badge">{{ tag.name }}</span></a>
|
||||||
|
{% endfor %}
|
||||||
|
{% elif item.category_name %}
|
||||||
|
<span class="badge badge_{{item.category}}">{{item.category_name}}</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
<section class=" pagination row">
|
||||||
|
<span class="four columns">
|
||||||
|
{% if items.has_previous %}
|
||||||
|
<a href="?q={{ query }}&page=1">« first</a> |
|
||||||
|
<a href="?q={{ query }}&page={{ items.previous_page_number }}">previous</a>
|
||||||
|
{% else %}
|
||||||
|
<span class="inactive">« first | previous</span>
|
||||||
|
{% endif %}
|
||||||
|
</span>
|
||||||
|
<span class="current four columns">
|
||||||
|
Page {{ items.number }} of {{ items.paginator.num_pages }}
|
||||||
|
</span>
|
||||||
|
<span class="current four columns">
|
||||||
|
{% if items.has_next %}
|
||||||
|
<a href="?q={{ query }}&page={{ items.next_page_number }}">next</a> |
|
||||||
|
<a href="?q={{ query }}&page={{ items.paginator.num_pages }}">last »</a>
|
||||||
|
{% else %}
|
||||||
|
<span class="inactive">next | last »</span>
|
||||||
|
{% endif %}
|
||||||
|
</span>
|
||||||
|
</section>
|
||||||
|
{% elif query %}
|
||||||
|
<p><em>No items found for "{{ query }}"</em></p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<section class="trending">
|
||||||
|
<h4>Top tags</h4>
|
||||||
|
{% for tag in trending %}
|
||||||
|
<a href="{% url 'browse' %}?q={{ tag.name|urlencode }}"><span class="badge">{{ tag.name }}</span></a>
|
||||||
|
{% endfor %}
|
||||||
|
</section>
|
||||||
|
{% endblock %}
|
5
blogs/templates/confirm-email.html
Normal file
5
blogs/templates/confirm-email.html
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
{% extends "layout.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<p>Your email address has been confirmed! You can opt-out at any time by clicking the link at the bottom of the weekly emails.</p>
|
||||||
|
{% endblock %}
|
27
blogs/templates/contact.html
Normal file
27
blogs/templates/contact.html
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
{% extends "layout.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="loader" hidden>
|
||||||
|
<div class="lds-heart"><div></div></div>
|
||||||
|
<div class="lds-text">Sending...</div>
|
||||||
|
</div>
|
||||||
|
<form action="{% url 'contact' %}" method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="columns eight">
|
||||||
|
<label for="{{ form.from_email.id_for_label }}">Your email address:</label>
|
||||||
|
{% if errors %} {{ form.from_email.errors }} {% endif %}
|
||||||
|
{{form.from_email}}
|
||||||
|
<label for="{{ form.subject.id_for_label }}">Subject:</label>
|
||||||
|
{% if errors %} {{ form.subject.errors }} {% endif %}
|
||||||
|
{{form.subject}}
|
||||||
|
{% if errors %} {{ form.message.errors }} {% endif %}
|
||||||
|
<label for="{{ form.message.id_for_label }}">Message:</label>
|
||||||
|
<textarea name="message" id="message" class="u-full-width"></textarea>
|
||||||
|
<input class="button-primary u-pull-right" type="submit" value="Register!">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
|
|
54
blogs/templates/contribute.html
Normal file
54
blogs/templates/contribute.html
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
{% extends "layout.html" %}
|
||||||
|
|
||||||
|
<main class="container pop-boxes">
|
||||||
|
{% block intro %}{% endblock %}
|
||||||
|
{% block title %}
|
||||||
|
<h2 class="page-title">{{ title }}</h2>
|
||||||
|
{% endblock %}
|
||||||
|
<section>
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<p>You can contribute to <em>Aus GLAMR</em> by registering a blog, newsletter, event, or discussion group. Creators or content should be Australasian-based.</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="contribute">
|
||||||
|
|
||||||
|
<section class="pop">
|
||||||
|
<h2>Register a blog</h2>
|
||||||
|
<p>We have a capacious definition of "blog". If it's a website that often or sometimes has Strong GLAMR Themes, with an RSS or Atom feed, you can register it here.</p>
|
||||||
|
<a class="button" href="{% url 'register-blog' %}">Register blog</a>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="pop">
|
||||||
|
<h2>Register a group</h2>
|
||||||
|
<p>A "group" could be an email list, a subreddit, Mastodon server, etc. If it's a many-to-many electronic medium, it's probably a discussion group.</p>
|
||||||
|
<a class="button" href="{% url 'register-group' %}">Register group</a>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="pop">
|
||||||
|
<h2>Register an event</h2>
|
||||||
|
<p>Conference, convention, seminar, workshop, talk, meet-up...
|
||||||
|
</br>Whatever you call it, you know what we mean.</p>
|
||||||
|
<a class="button" href="{% url 'register-event' %}">Register event</a>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="pop">
|
||||||
|
<h2>Register a newsletter</h2>
|
||||||
|
<p>Whether it uses Ghost, Write.as, Mailchimp or something else, if it's an email newsletter with some kind of GLAMR focus you can register it here.</p>
|
||||||
|
<a class="button" href="{% url 'register-newsletter' %}">Register newsletter</a>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<section class="pop">
|
||||||
|
<h2>Register or update a Call for Papers</h2>
|
||||||
|
<p>When you register your event you can also register a "call for papers", "call for proposals" or whatever it's called for your event. Forgot? Extended or re-opened the call? Use this form.</p>
|
||||||
|
<a class="button" href="{% url 'register-cfp' %}">Register CFP</a>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</section>
|
||||||
|
{% endblock %}
|
||||||
|
</section>
|
66
blogs/templates/events/cfp.html
Normal file
66
blogs/templates/events/cfp.html
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
{% extends "layout.html" %}
|
||||||
|
|
||||||
|
{% block intro %}
|
||||||
|
{% if conf_name %}
|
||||||
|
<div class="alert success">
|
||||||
|
{{ conf_name }} has been registered!
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block title %}
|
||||||
|
{% if conf_name %}
|
||||||
|
<h2>Register a Call for Papers for {{ conf_name }}</h2>
|
||||||
|
{% else %}
|
||||||
|
<h2>Register a Call for Papers</h2>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<div class="loader" hidden>
|
||||||
|
<div class="lds-heart"><div></div></div>
|
||||||
|
<div class="lds-text">Processing...</div>
|
||||||
|
</div>
|
||||||
|
<form action="{% url 'register-cfp' %}" method="post">
|
||||||
|
{% 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>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% endblock %}
|
19
blogs/templates/events/register.html
Normal file
19
blogs/templates/events/register.html
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
{% extends "layout.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<div class="loader" hidden>
|
||||||
|
<div class="lds-heart"><div></div></div>
|
||||||
|
<div class="lds-text">Processing...</div>
|
||||||
|
</div>
|
||||||
|
<form action="{% url 'register-event' %}" method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="columns eight">
|
||||||
|
{{ form }}
|
||||||
|
<input class="button-primary u-pull-right" type="submit" value="Register!">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% endblock %}
|
49
blogs/templates/help.html
Normal file
49
blogs/templates/help.html
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
{% extends "layout.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<section class="help">
|
||||||
|
{% with 'contributing' as anchor %}
|
||||||
|
{% url 'help' as the_url %}
|
||||||
|
<h4 id="{{anchor}}">Adding something to the site</h4><a href="{{ the_url }}/#{{anchor}}">🔗</a>
|
||||||
|
<p>If you want to add a blog, event, group, or newsletter, head to <a href="/contribute">Contribute</a>.</p>
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
{% with 'updating' as anchor %}
|
||||||
|
{% url 'help' as the_url %}
|
||||||
|
<h4 id="{{anchor}}">Updating or removing a listing</h4><a href="{{ the_url }}/#{{anchor}}">🔗</a>
|
||||||
|
<p>Made a mistake or have more information to add? Blog got hacked? Changed careers? Just fill in the <a href="/contact">contact form</a>.</p>
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
{% with 'opting-out' as anchor %}
|
||||||
|
{% url 'help' as the_url %}
|
||||||
|
<h4 id="{{anchor}}">Opting out for a blog post</h4><a href="{{ the_url }}/#{{anchor}}">🔗</a>
|
||||||
|
<p>If you have registered your blog but want to opt-out of being indexed and announced for an individual post, you can add any of the following as tags using your blog publishing software, and your post will be ignored.</p>
|
||||||
|
<ul>
|
||||||
|
<li>notglam</li>
|
||||||
|
<li>notglamr</li>
|
||||||
|
<li>notausglamr</li>
|
||||||
|
<li>notausglamblogs</li>
|
||||||
|
</ul>
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
{% with 'aus-glam-blogs' as anchor %}
|
||||||
|
{% url 'help' as the_url %}
|
||||||
|
<h4 id="{{anchor}}">What happened to Aus GLAM Blogs?</h4><a href="{{ the_url }}/#{{anchor}}">🔗</a>
|
||||||
|
<p><em>Aus GLAM Blogs</em> was focussed on blogs specifically. <em>Aus GLAMR</em> is the successor, now including more GLAMR content. All the blog articles from the original site have been migrated and will be inluded in your search results.</p>
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
{% with 'pocket' as anchor %}
|
||||||
|
{% url 'help' as the_url %}
|
||||||
|
<h4 id="{{anchor}}">What happened to the Pocket integration?</h4><a href="{{ the_url }}/#{{anchor}}">🔗</a>
|
||||||
|
<p>The integration with Pocket from Aus GLAM Blogs is no longer available. Please consider using the RSS feed or email subscription instead.</p>
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
{% with 'activitypub' as anchor %}
|
||||||
|
{% url 'help' as the_url %}
|
||||||
|
<h4 id="{{anchor}}">Activitypub account name!!???</h4><a href="{{ the_url }}/#{{anchor}}">🔗</a>
|
||||||
|
<p>ActivityPub is the protocol used by Mastodon, BlueSky, BookWyrm, Threads, and other "fediverse" social media. If you include an account name, it will be mentioned when your registered thing is announced by the AusGLAMR Mastodon bot. If you'd like to join the "fediverse", try <a href="https://ausglam.space">Aus GLAM Space</a>.</p>
|
||||||
|
{% endwith %}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{% endblock %}
|
36
blogs/templates/index.html
Normal file
36
blogs/templates/index.html
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
{% extends "layout.html" %}
|
||||||
|
|
||||||
|
{% block intro %}
|
||||||
|
<section>
|
||||||
|
<p><em>Aus GLAMR</em> is the place to find out what's happening in the Australasian cultural memory professions. Whether you work in galleries, libraries, archives, museums, or records, you'll find the latest blog posts, conferences and events, newsletters, and discussion groups here!</p>
|
||||||
|
<p>Browse one of the lists, <a href="{% url 'search' %}">search by keywords</a>, or <a href="{% url 'contribute' %}">contribute by registering</a> your blog, event, group or newsletter.</p>
|
||||||
|
<p>You can stay up to date by following the Mastodon bot or subscribing to email updates or to one of the RSS feeds - check out the <a href="{% url 'subscribe' %}">subscribe page</a>.</p>
|
||||||
|
<p>Please note that whilst off-topic or egregiously offensive content may be refused or removed, inclusion on this site does not imply endorsement by Hugh or newCardigan of the content of any particular listed publication or event.</p>
|
||||||
|
</section>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{% for article in latest %}
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-title">
|
||||||
|
<a href="{{ article.url }}"><h3>{{ article.title }}</h3></a>
|
||||||
|
<p class="meta">
|
||||||
|
{% if article.author_name %}
|
||||||
|
<span class="author_name">{{ article.author_name }}</span> |
|
||||||
|
{% endif %}
|
||||||
|
<span class="blog_title">{{ article.blog.title }}</span>
|
||||||
|
</p>
|
||||||
|
{% for tag in article.tags.all %}
|
||||||
|
<a href="{% url 'browse' %}?q={{ tag.name|urlencode }}"><span class="badge">{{ tag.name }}</span></a>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% if article.description %}
|
||||||
|
<div class="card-body">
|
||||||
|
{% autoescape off %}
|
||||||
|
<p>{{ article.description|truncatewords_html:60 }}</p>
|
||||||
|
{% endautoescape %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endblock %}
|
65
blogs/templates/layout.html
Normal file
65
blogs/templates/layout.html
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
{% load static %}
|
||||||
|
{% load glamr_tags %}
|
||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en-AU">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>{{ title }}</title>
|
||||||
|
<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" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<div class="container">
|
||||||
|
<a href="/"><h1>Aus GLAMR</h1></a>
|
||||||
|
<a href="{% url 'blogs' %}"><span class="menu-item">Blogs</span>></a>
|
||||||
|
<a href="/events"><span class="menu-item">Events</span>></a>
|
||||||
|
<a href="{% url 'groups' %}"><span class="menu-item">Groups</span>></a>
|
||||||
|
<a href="{% url 'newsletters' %}"><span class="menu-item">Newsletters</span>></a>
|
||||||
|
</div>
|
||||||
|
<div class="core-menu">
|
||||||
|
<div class="container">
|
||||||
|
<a href="{% url 'contribute' %}"><span class="menu-item">Contribute</span></a>
|
||||||
|
<a href="{% url 'subscribe' %}"><span class="menu-item">Subscribe</span>></a>
|
||||||
|
<a href="{% url 'search' %}"><span class="menu-item">Search</span></a>
|
||||||
|
<a href="{% url 'help' %}"><span class="menu-item">Help</span>></a>
|
||||||
|
<a href="{% url 'contact' %}"><span class="menu-item">Contact</span>></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<section class="container">
|
||||||
|
{% site_messages %}
|
||||||
|
</section>
|
||||||
|
{% block container %}
|
||||||
|
<main class="container">
|
||||||
|
{% block intro %}{% endblock %}
|
||||||
|
{% block title %}
|
||||||
|
<h2 class="page-title">{{ title }}</h2>
|
||||||
|
{% endblock %}
|
||||||
|
<section>
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
</section>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
</main>
|
||||||
|
<footer>
|
||||||
|
<section class="row">
|
||||||
|
<section class="one-half column left">
|
||||||
|
<div class="container">
|
||||||
|
<p>Love GLAMR data? You might also like the <a href="https://librarymap.hugh.run">Australian Library Map</a></p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section class="one-half column">
|
||||||
|
<div class="container">
|
||||||
|
<p>Made & maintained by <a href="https://www.hughrundle.net">Hugh Rundle</a> and hosted by <a href="https://newcardigan.org">newCardigan</a></p>
|
||||||
|
<p>Built with <a href="https://djangoproject.com">Django</a> and 💖</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
</footer>
|
||||||
|
<script src="/static/js/glamr.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
7
blogs/templates/messages.html
Normal file
7
blogs/templates/messages.html
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
<section class="message-box">
|
||||||
|
{% for msg in messages %}
|
||||||
|
<div class="alert info">
|
||||||
|
{{ msg.message }}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</section>
|
20
blogs/templates/register-group.html
Normal file
20
blogs/templates/register-group.html
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
{% extends "layout.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<div class="loader" hidden>
|
||||||
|
<div class="lds-heart"><div></div></div>
|
||||||
|
<div class="lds-text">Processing...</div>
|
||||||
|
</div>
|
||||||
|
<form action="{% url 'register-group' %}" method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="columns eight">
|
||||||
|
{{ form }}
|
||||||
|
<input class="button-primary u-pull-right" type="submit" value="Register!">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% endblock %}
|
19
blogs/templates/register-newsletter.html
Normal file
19
blogs/templates/register-newsletter.html
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
{% extends "layout.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<div class="loader" hidden>
|
||||||
|
<div class="lds-heart"><div></div></div>
|
||||||
|
<div class="lds-text">Processing...</div>
|
||||||
|
</div>
|
||||||
|
<form action="{% url 'register-newsletter' %}" method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="columns eight">
|
||||||
|
{{ form }}
|
||||||
|
<input class="button-primary u-pull-right" type="submit" value="Register!">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% endblock %}
|
63
blogs/templates/search.html
Normal file
63
blogs/templates/search.html
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
{% extends "layout.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<form action="{% url 'search' %}" method="get">
|
||||||
|
<label for="keywords">Search for keywords across blog posts, groups, newsletters, and conferences</label>
|
||||||
|
<input type="text" name="q" value="{% if query %}{{ query }}{% endif %}">
|
||||||
|
<button class="button" type="submit">Search!</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% if items %}
|
||||||
|
{% for item in items %}
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-title">
|
||||||
|
<a href="{{ item.url }}"><h3>{% if item.title %} {{ item.title }} {% else %} {{item.name}} {% endif %}</h3></a>
|
||||||
|
<p class="meta">
|
||||||
|
{% if item.author_name or item.author %}
|
||||||
|
<span class="author_name">{% if item.author_name%} {{ item.author_name }} {% else %} {{ item.author_name }} {% endif %}</span> |
|
||||||
|
{% endif %}
|
||||||
|
{% if item.blog %}
|
||||||
|
<span class="blog_title">{{ item.blog.title }}</span>
|
||||||
|
{% elif item.description %}
|
||||||
|
<p class="">{{ item.description }}</p>
|
||||||
|
{% elif item.details %}
|
||||||
|
<p class="">{{ item.details }}</p>
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
|
{% if item.tags %}
|
||||||
|
{% for tag in item.tags.all %}
|
||||||
|
<a href="{% url 'browse' %}?q={{ tag.name|urlencode }}"><span class="badge">{{ tag.name }}</span></a>
|
||||||
|
{% endfor %}
|
||||||
|
{% elif item.category_name %}
|
||||||
|
<span class="badge badge_{{item.category}}">{{item.category_name}}</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
<section class=" pagination row">
|
||||||
|
<span class="four columns">
|
||||||
|
{% if items.has_previous %}
|
||||||
|
<a href="?q={{ query }}&page=1">« first</a> |
|
||||||
|
<a href="?q={{ query }}&page={{ items.previous_page_number }}">previous</a>
|
||||||
|
{% else %}
|
||||||
|
<span class="inactive">« first | previous</span>
|
||||||
|
{% endif %}
|
||||||
|
</span>
|
||||||
|
<span class="current four columns">
|
||||||
|
Page {{ items.number }} of {{ items.paginator.num_pages }}
|
||||||
|
</span>
|
||||||
|
<span class="current four columns">
|
||||||
|
{% if items.has_next %}
|
||||||
|
<a href="?q={{ query }}&page={{ items.next_page_number }}">next</a> |
|
||||||
|
<a href="?q={{ query }}&page={{ items.paginator.num_pages }}">last »</a>
|
||||||
|
{% else %}
|
||||||
|
<span class="inactive">next | last »</span>
|
||||||
|
{% endif %}
|
||||||
|
</span>
|
||||||
|
</section>
|
||||||
|
{% elif query %}
|
||||||
|
<p><em>No items found for "{{ query }}"</em></p>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
20
blogs/templates/subscribe-email.html
Normal file
20
blogs/templates/subscribe-email.html
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
{% extends "layout.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<div class="loader" hidden>
|
||||||
|
<div class="lds-heart"><div></div></div>
|
||||||
|
<div class="lds-text">Processing...</div>
|
||||||
|
</div>
|
||||||
|
<form action="{% url 'subscribe-email' %}" method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="columns eight">
|
||||||
|
{{ form }}
|
||||||
|
<input class="button-primary u-pull-right" type="submit" value="Register!">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% endblock %}
|
51
blogs/templates/subscribe.html
Normal file
51
blogs/templates/subscribe.html
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
{% extends "layout.html" %}
|
||||||
|
|
||||||
|
{% block container %}
|
||||||
|
<main class="container pop-boxes">
|
||||||
|
{% block intro %}{% endblock %}
|
||||||
|
{% block title %}
|
||||||
|
<h2 class="page-title">{{ title }}</h2>
|
||||||
|
{% endblock %}
|
||||||
|
<section>
|
||||||
|
{% block content %}
|
||||||
|
<section class="subscribe">
|
||||||
|
<section class="pop">
|
||||||
|
<hgroup>
|
||||||
|
<h3>Subscribe via email</h3>
|
||||||
|
</hgroup>
|
||||||
|
<p>Get a weekly update listing the latest blog posts, open calls for papers, and upcoming conferences.</p>
|
||||||
|
<div>
|
||||||
|
<a class="button" href="{% url 'subscribe-email' %}">Subscribe via email</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="pop">
|
||||||
|
<hgroup>
|
||||||
|
<h3>Subscribe via ActivityPub</h3>
|
||||||
|
</hgroup>
|
||||||
|
<p>Follow the <code class="code">@blogs@ausglam.space</code> bot to see an announcement every time a new blog post , event, or call for papers is published.</p>
|
||||||
|
<form method="post" action="{% url 'subscribe' %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
<label for="username">Enter address you want to follow from</label>
|
||||||
|
{{ form.errors.username }}
|
||||||
|
<input type="text" name="username" placeholder="@user@example.social">
|
||||||
|
<button class="button u-pull-right" type="submit">Follow</button>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="pop">
|
||||||
|
<hgroup>
|
||||||
|
<h3>Subscribe via RSS</h3>
|
||||||
|
</hgroup>
|
||||||
|
<p>Subscribe to the blogs feed in your favourite RSS reader to get a combined feed with every new article.</p>
|
||||||
|
<p>Or subscribe to the events feed to be the first to know about upcoming events.</p>
|
||||||
|
<div>
|
||||||
|
<a class="button" href="{% url 'article-feed' %}">blog posts</a>
|
||||||
|
<a class="button" href="{% url 'event-feed' %}">events</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</section>
|
||||||
|
{% endblock %}
|
||||||
|
</section>
|
||||||
|
{% endblock %}
|
16
blogs/templates/thanks.html
Normal file
16
blogs/templates/thanks.html
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
{% extends "layout.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
{% if register_type == "message" %}
|
||||||
|
<p>Thanks for getting in touch!</p>
|
||||||
|
{% else %}
|
||||||
|
<p>Thankyou for registering your {{ register_type }}.</p>
|
||||||
|
|
||||||
|
{% 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>
|
||||||
|
{% else %}
|
||||||
|
<p>Your {{ register_type }} registration will be reviewed before being added to <em>Aus GLAMR</em>.</p>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
7
blogs/templates/unsubscribe.html
Normal file
7
blogs/templates/unsubscribe.html
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
{% extends "layout.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<p>You have been unsubscribed from weekly emails.</p>
|
||||||
|
|
||||||
|
{% endblock %}
|
0
blogs/templatetags/__init__.py
Normal file
0
blogs/templatetags/__init__.py
Normal file
12
blogs/templatetags/glamr_tags.py
Normal file
12
blogs/templatetags/glamr_tags.py
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
"""custom tags"""
|
||||||
|
|
||||||
|
from django import template
|
||||||
|
from blogs import models
|
||||||
|
|
||||||
|
register = template.Library()
|
||||||
|
|
||||||
|
|
||||||
|
@register.inclusion_tag("messages.html")
|
||||||
|
def site_messages():
|
||||||
|
"""display site messages"""
|
||||||
|
return {"messages": models.SiteMessage.objects.all()}
|
66
blogs/tests/test_management.py
Normal file
66
blogs/tests/test_management.py
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
"""test management commands"""
|
||||||
|
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
from django.core.management import call_command
|
||||||
|
from django.test import TestCase
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from blogs import models, management
|
||||||
|
|
||||||
|
|
||||||
|
class ManagementTestCase(TestCase):
|
||||||
|
"""test management commands"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
"""set up some things to announce"""
|
||||||
|
|
||||||
|
self.conf = models.Event.objects.create(
|
||||||
|
name="Amazing Conf",
|
||||||
|
url="https://test.com",
|
||||||
|
category="LIB",
|
||||||
|
start_date=timezone.now() + timedelta(days=3),
|
||||||
|
activitypub_account_name="@conf@conf.conf",
|
||||||
|
approved=True,
|
||||||
|
announcements=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.cfp = models.CallForPapers.objects.create(
|
||||||
|
event=self.conf,
|
||||||
|
name="Call for Papers for Amazing Conf",
|
||||||
|
opening_date=timezone.now() + timedelta(days=30),
|
||||||
|
closing_date=timezone.now() + timedelta(days=1),
|
||||||
|
)
|
||||||
|
|
||||||
|
self.cfp = models.CallForPapers.objects.create(
|
||||||
|
event=self.conf,
|
||||||
|
name="Call for posters",
|
||||||
|
opening_date=timezone.now() - timedelta(days=30),
|
||||||
|
closing_date=timezone.now() - timedelta(days=1),
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_queue_announcements(self):
|
||||||
|
"""both in one"""
|
||||||
|
|
||||||
|
call_command("queue_announcements")
|
||||||
|
|
||||||
|
# call for posters is in the past, so should not be announced
|
||||||
|
self.assertEqual(models.Announcement.objects.count(), 2)
|
||||||
|
|
||||||
|
# event
|
||||||
|
announcement = models.Announcement.objects.first()
|
||||||
|
start_date = timezone.now() + timedelta(days=3)
|
||||||
|
date = start_date.strftime("%a %d %b %Y")
|
||||||
|
status = f"Amazing Conf (@conf@conf.conf) is a event about Libraries, starting on {date}!\n\nhttps://test.com"
|
||||||
|
|
||||||
|
self.assertEqual(announcement.status, status)
|
||||||
|
|
||||||
|
# cfp
|
||||||
|
announcement = models.Announcement.objects.last()
|
||||||
|
opening_date = timezone.now() + timedelta(days=30)
|
||||||
|
closing_date = timezone.now() + timedelta(days=1)
|
||||||
|
opening_date_str = opening_date.strftime("%a %d %b %Y")
|
||||||
|
closing_date_str = closing_date.strftime("%a %d %b %Y")
|
||||||
|
status = f"Amazing Conf Call for Papers for Amazing Conf is open from {opening_date_str}, closing on {closing_date_str}!\n\nMore info at https://test.com"
|
||||||
|
|
||||||
|
self.assertEqual(announcement.status, status)
|
179
blogs/tests/test_models.py
Normal file
179
blogs/tests/test_models.py
Normal file
|
@ -0,0 +1,179 @@
|
||||||
|
"""model tests"""
|
||||||
|
|
||||||
|
from datetime import date
|
||||||
|
|
||||||
|
from django.test import TestCase
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from blogs import models
|
||||||
|
|
||||||
|
|
||||||
|
class BlogTestCase(TestCase):
|
||||||
|
"""test cases for Blog model"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
"""set up test blog"""
|
||||||
|
self.blog = models.Blog.objects.create(
|
||||||
|
title="mya awesome blog",
|
||||||
|
url="https://test.com",
|
||||||
|
feed="https://test.com/feed.xml",
|
||||||
|
category="LIB",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_get_absolute_url(self):
|
||||||
|
"""get_absolute_url class function"""
|
||||||
|
|
||||||
|
self.assertEqual(self.blog.get_absolute_url(), "https://test.com")
|
||||||
|
|
||||||
|
def test_set_success(self):
|
||||||
|
"""set_success class function"""
|
||||||
|
|
||||||
|
self.blog.failing = True
|
||||||
|
self.blog.save()
|
||||||
|
self.blog.set_success()
|
||||||
|
|
||||||
|
self.assertEqual(self.blog.failing, False)
|
||||||
|
|
||||||
|
def test_set_failing(self):
|
||||||
|
"""set_failing class function"""
|
||||||
|
|
||||||
|
self.blog.failing = False
|
||||||
|
self.blog.save()
|
||||||
|
self.blog.set_failing()
|
||||||
|
self.assertEqual(self.blog.failing, True)
|
||||||
|
|
||||||
|
def test_announce_article(self):
|
||||||
|
"""announcing a blog article"""
|
||||||
|
|
||||||
|
article = models.Article.objects.create(
|
||||||
|
title="My article",
|
||||||
|
author_name="Hugh",
|
||||||
|
url="https://example.blog/1",
|
||||||
|
blog=self.blog,
|
||||||
|
pubdate=timezone.now(),
|
||||||
|
guid="123-123-123",
|
||||||
|
)
|
||||||
|
|
||||||
|
article.announce()
|
||||||
|
status = f"My article (Hugh on mya awesome blog)\n\nhttps://example.blog/1"
|
||||||
|
self.assertTrue(models.Announcement.objects.filter(status=status).exists())
|
||||||
|
|
||||||
|
|
||||||
|
class ConferenceTestCase(TestCase):
|
||||||
|
"""test event functions"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
"""set up test conf"""
|
||||||
|
self.conf = models.Event.objects.create(
|
||||||
|
name="Awesome Conf",
|
||||||
|
url="https://test.com",
|
||||||
|
category="LIB",
|
||||||
|
start_date=date.fromisoformat("2030-12-01"),
|
||||||
|
activitypub_account_name="@conf@conf.conf",
|
||||||
|
approved=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.cfp = models.CallForPapers.objects.create(
|
||||||
|
event=self.conf,
|
||||||
|
name="Call for Tests",
|
||||||
|
opening_date=date.fromisoformat("2030-11-01"),
|
||||||
|
closing_date=date.fromisoformat("2030-11-30"),
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_announce(self):
|
||||||
|
"""test announcing a conf"""
|
||||||
|
|
||||||
|
self.conf.announce()
|
||||||
|
|
||||||
|
announcement = models.Announcement.objects.first()
|
||||||
|
self.assertEqual(
|
||||||
|
announcement.status,
|
||||||
|
f"Awesome Conf (@conf@conf.conf) is a event about Libraries, starting on Sun 01 Dec 2030!\n\nhttps://test.com",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_announce_cfp(self):
|
||||||
|
"""test announcing a conf CFP"""
|
||||||
|
|
||||||
|
self.cfp.announce()
|
||||||
|
|
||||||
|
announcement = models.Announcement.objects.first()
|
||||||
|
self.assertEqual(
|
||||||
|
announcement.status,
|
||||||
|
f"Awesome Conf Call for Tests is open from Fri 01 Nov 2030, closing on Sat 30 Nov 2030!\n\nMore info at https://test.com",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class GroupTestCase(TestCase):
|
||||||
|
"""test group functions"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
"""set up test gropu"""
|
||||||
|
self.group = models.Group.objects.create(
|
||||||
|
name="Awesome group",
|
||||||
|
url="https://test.com",
|
||||||
|
category="LIB",
|
||||||
|
type="KBIN",
|
||||||
|
registration_url="https://test.com/reg",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_announce(self):
|
||||||
|
"""test announcing a group"""
|
||||||
|
|
||||||
|
self.group.announce()
|
||||||
|
|
||||||
|
announcement = models.Announcement.objects.first()
|
||||||
|
self.assertEqual(
|
||||||
|
announcement.status,
|
||||||
|
f"Awesome group is a KBin server about Libraries!\n\nJoin them: https://test.com/reg",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class NewsletterTestCase(TestCase):
|
||||||
|
"""test newsletter functions"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
"""set up test newsletter"""
|
||||||
|
self.news = models.Newsletter.objects.create(
|
||||||
|
name="Awesome news",
|
||||||
|
author="Hugh",
|
||||||
|
url="https://test.com",
|
||||||
|
category="ARC",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_announce(self):
|
||||||
|
"""test announcing a group"""
|
||||||
|
|
||||||
|
self.news.announce()
|
||||||
|
|
||||||
|
announcement = models.Announcement.objects.first()
|
||||||
|
self.assertEqual(
|
||||||
|
announcement.status,
|
||||||
|
f"Awesome news is a newsletter about Archives from Hugh. Check it out:\n\nhttps://test.com",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class UtilsTestCase(TestCase):
|
||||||
|
"""test utility functions"""
|
||||||
|
|
||||||
|
def test_announcement(self):
|
||||||
|
"""test announcing"""
|
||||||
|
|
||||||
|
# TODO: mock mastodon somehow
|
||||||
|
|
||||||
|
# announcement = models.Announcement.objects.create(
|
||||||
|
# status="I have something to say!",
|
||||||
|
# summary="boasting",
|
||||||
|
# )
|
||||||
|
|
||||||
|
# announcement.announce()
|
||||||
|
|
||||||
|
# self.assertEqual(models.Announcement.objects.count(), 0)
|
||||||
|
|
||||||
|
def test_content_warning(self):
|
||||||
|
"""test CWs"""
|
||||||
|
|
||||||
|
warning = models.ContentWarning.objects.create(
|
||||||
|
match_text="horrible thing", display="bad shit"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertTrue(warning.is_in("I saw a horrible thingy"))
|
8
blogs/tests/test_utilities.py
Normal file
8
blogs/tests/test_utilities.py
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
"""test utility functions"""
|
||||||
|
|
||||||
|
# TODO:
|
||||||
|
"""
|
||||||
|
get_feed_info
|
||||||
|
get_blog_info
|
||||||
|
get_webfinger_subscribe_uri
|
||||||
|
"""
|
224
blogs/tests/test_views.py
Normal file
224
blogs/tests/test_views.py
Normal file
|
@ -0,0 +1,224 @@
|
||||||
|
"""test views"""
|
||||||
|
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from django.contrib.auth.models import AnonymousUser
|
||||||
|
from django.core import mail
|
||||||
|
from django.test import Client, TestCase
|
||||||
|
from django.test.client import RequestFactory
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from blogs import forms, models, views
|
||||||
|
|
||||||
|
|
||||||
|
class PublicTests(TestCase):
|
||||||
|
"""Public views test cases"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
start_date = timezone.now()
|
||||||
|
|
||||||
|
self.factory = RequestFactory()
|
||||||
|
self.glam_conf = models.Event.objects.create(
|
||||||
|
name="Awesome conf",
|
||||||
|
url="https://awesome.conf",
|
||||||
|
category="GLAM",
|
||||||
|
description="An awesome conf",
|
||||||
|
start_date=start_date,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_public_views_load(self):
|
||||||
|
"""
|
||||||
|
Do public views load correctly?
|
||||||
|
"""
|
||||||
|
|
||||||
|
home = self.client.get(reverse("home"))
|
||||||
|
self.assertEqual(home.status_code, 200)
|
||||||
|
|
||||||
|
browse = self.client.get(reverse("browse"))
|
||||||
|
self.assertEqual(browse.status_code, 200)
|
||||||
|
|
||||||
|
search = self.client.get(reverse("search"))
|
||||||
|
self.assertEqual(search.status_code, 200)
|
||||||
|
|
||||||
|
help = self.client.get(reverse("help"))
|
||||||
|
self.assertEqual(help.status_code, 200)
|
||||||
|
|
||||||
|
contribute = self.client.get(reverse("contribute"))
|
||||||
|
self.assertEqual(contribute.status_code, 200)
|
||||||
|
|
||||||
|
contact = self.client.get(reverse("contact"))
|
||||||
|
self.assertEqual(contact.status_code, 200)
|
||||||
|
|
||||||
|
blogs_response = self.client.get(reverse("blogs"))
|
||||||
|
self.assertEqual(blogs_response.status_code, 200)
|
||||||
|
|
||||||
|
confs_response = self.client.get(reverse("conferences"))
|
||||||
|
self.assertEqual(confs_response.status_code, 200)
|
||||||
|
|
||||||
|
groups_response = self.client.get(reverse("groups"))
|
||||||
|
self.assertEqual(groups_response.status_code, 200)
|
||||||
|
|
||||||
|
news_response = self.client.get(reverse("newsletters"))
|
||||||
|
self.assertEqual(news_response.status_code, 200)
|
||||||
|
|
||||||
|
rblog_response = self.client.get(reverse("register-blog"))
|
||||||
|
self.assertEqual(rblog_response.status_code, 200)
|
||||||
|
|
||||||
|
submit = self.client.get(reverse("submit-blog-registration"))
|
||||||
|
self.assertEqual(submit.status_code, 200) # 301?
|
||||||
|
|
||||||
|
rconf_response = self.client.get(reverse("register-event"))
|
||||||
|
self.assertEqual(rconf_response.status_code, 200)
|
||||||
|
|
||||||
|
rcfp_response = self.client.get(reverse("register-cfp"))
|
||||||
|
self.assertEqual(rcfp_response.status_code, 200)
|
||||||
|
|
||||||
|
rgroup_response = self.client.get(reverse("register-group"))
|
||||||
|
self.assertEqual(rgroup_response.status_code, 200)
|
||||||
|
|
||||||
|
rnews_response = self.client.get(reverse("register-newsletter"))
|
||||||
|
self.assertEqual(rnews_response.status_code, 200)
|
||||||
|
|
||||||
|
rnews_response = self.client.get(
|
||||||
|
reverse("thankyou", args=({"register_type": "blog"}))
|
||||||
|
)
|
||||||
|
self.assertEqual(rnews_response.status_code, 200)
|
||||||
|
|
||||||
|
subscribe = self.client.get(reverse("subscribe"))
|
||||||
|
self.assertEqual(subscribe.status_code, 200)
|
||||||
|
|
||||||
|
af = self.client.get(reverse("article-feed"))
|
||||||
|
self.assertEqual(af.status_code, 200)
|
||||||
|
|
||||||
|
cf = self.client.get(reverse("event-feed"))
|
||||||
|
self.assertEqual(cf.status_code, 200)
|
||||||
|
|
||||||
|
def test_confirm_register_blog(self):
|
||||||
|
"""post final event registration form"""
|
||||||
|
|
||||||
|
view = views.ConfirmBlogRegistration.as_view()
|
||||||
|
form = forms.ConfirmBlogForm()
|
||||||
|
form.data["title"] = "My blog"
|
||||||
|
form.data["author_name"] = "Bob Bobson"
|
||||||
|
form.data["url"] = "https://www.example.com"
|
||||||
|
form.data["feed"] = "https://www.example.com/feed"
|
||||||
|
form.data["category"] = "LIB"
|
||||||
|
|
||||||
|
request = self.factory.post("/submit-blog-registration", form.data)
|
||||||
|
request.user = AnonymousUser()
|
||||||
|
|
||||||
|
view(request)
|
||||||
|
|
||||||
|
exists = models.Blog.objects.filter(title="My blog").exists()
|
||||||
|
self.assertTrue(exists)
|
||||||
|
|
||||||
|
def test_register_conference(self):
|
||||||
|
"""post event registration form"""
|
||||||
|
|
||||||
|
view = views.RegisterConference.as_view()
|
||||||
|
form = forms.RegisterConferenceForm()
|
||||||
|
form.data["name"] = "My event"
|
||||||
|
form.data["description"] = "A conf for gallerists"
|
||||||
|
form.data["url"] = "https://awesome.conf/cfp"
|
||||||
|
form.data["category"] = "GAL"
|
||||||
|
form.data["start_date"] = "30/01/2024"
|
||||||
|
|
||||||
|
request = self.factory.post("register-event/", form.data)
|
||||||
|
|
||||||
|
request.user = AnonymousUser()
|
||||||
|
|
||||||
|
view(request)
|
||||||
|
|
||||||
|
exists = models.Event.objects.filter(name="My event").exists()
|
||||||
|
self.assertTrue(exists)
|
||||||
|
|
||||||
|
def test_register_cfp(self):
|
||||||
|
"""post CFP registration form"""
|
||||||
|
|
||||||
|
view = views.RegisterCallForPapers.as_view()
|
||||||
|
form = forms.RegisterCallForPapersForm()
|
||||||
|
form.data["event"] = self.glam_conf.id
|
||||||
|
form.data["name"] = "Call for Papers"
|
||||||
|
form.data["url"] = "https://www.example.com"
|
||||||
|
form.data["category"] = "GLAM"
|
||||||
|
form.data["opening_date"] = "01/01/2024"
|
||||||
|
form.data["closing_date"] = "28/01/2024"
|
||||||
|
|
||||||
|
request = self.factory.post("register-cfp/", form.data)
|
||||||
|
request.user = AnonymousUser()
|
||||||
|
|
||||||
|
view(request)
|
||||||
|
|
||||||
|
exists = models.CallForPapers.objects.filter(name="Call for Papers").exists()
|
||||||
|
self.assertTrue(exists)
|
||||||
|
|
||||||
|
def test_register_group(self):
|
||||||
|
"""post group registration form"""
|
||||||
|
|
||||||
|
view = views.RegisterGroup.as_view()
|
||||||
|
form = forms.RegisterGroupForm()
|
||||||
|
form.data["name"] = "GLAMR testers"
|
||||||
|
form.data["category"] = "GLAM"
|
||||||
|
form.data["type"] = "KBIN"
|
||||||
|
form.data["url"] = "https://kibin.test"
|
||||||
|
form.data["registration_url"] = "https://kbin.test/glamr"
|
||||||
|
form.data["description"] = "GLAMR testers"
|
||||||
|
|
||||||
|
request = self.factory.post("register-group/", form.data)
|
||||||
|
request.user = AnonymousUser()
|
||||||
|
|
||||||
|
view(request)
|
||||||
|
|
||||||
|
exists = models.Group.objects.filter(name="GLAMR testers").exists()
|
||||||
|
self.assertTrue(exists)
|
||||||
|
|
||||||
|
def test_register_newsletter(self):
|
||||||
|
"""post newsletter registration form"""
|
||||||
|
|
||||||
|
view = views.RegisterNewsletter.as_view()
|
||||||
|
form = forms.RegisterNewsletterForm()
|
||||||
|
form.data["name"] = "My newsletter"
|
||||||
|
form.data["author"] = "Bob Bobson"
|
||||||
|
form.data["url"] = "https://www.example.com"
|
||||||
|
form.data["category"] = "LIB"
|
||||||
|
|
||||||
|
request = self.factory.post("register-newsletter/", form.data)
|
||||||
|
request.user = AnonymousUser()
|
||||||
|
|
||||||
|
view(request)
|
||||||
|
|
||||||
|
exists = models.Newsletter.objects.filter(name="My newsletter").exists()
|
||||||
|
self.assertTrue(exists)
|
||||||
|
|
||||||
|
def test_contact(self):
|
||||||
|
"""post message"""
|
||||||
|
|
||||||
|
view = views.Contact.as_view()
|
||||||
|
form = forms.ContactForm()
|
||||||
|
form.data["from_email"] = "example@example.mail"
|
||||||
|
form.data["subject"] = "Hello"
|
||||||
|
form.data["message"] = "Hi there"
|
||||||
|
|
||||||
|
request = self.factory.post("contact/", form.data)
|
||||||
|
request.user = AnonymousUser()
|
||||||
|
|
||||||
|
view(request)
|
||||||
|
|
||||||
|
# Test that one message has been sent.
|
||||||
|
self.assertEqual(len(mail.outbox), 1)
|
||||||
|
|
||||||
|
# Verify that the subject of the first message is correct.
|
||||||
|
self.assertEqual(mail.outbox[0].subject, "Message via Aus GLAMR: Hello")
|
||||||
|
|
||||||
|
def test_search(self):
|
||||||
|
"""post search query"""
|
||||||
|
|
||||||
|
# TODO
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_browse(self):
|
||||||
|
"""post browse tags query"""
|
||||||
|
|
||||||
|
# TODO
|
||||||
|
pass
|
124
blogs/utilities.py
Normal file
124
blogs/utilities.py
Normal file
|
@ -0,0 +1,124 @@
|
||||||
|
"""useful functions that are not associated with models or views"""
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
import feedparser
|
||||||
|
import requests
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.mail import EmailMessage
|
||||||
|
from django.utils.encoding import iri_to_uri
|
||||||
|
|
||||||
|
headers = {"user-agent": "Aus-GLAM-Blogs/0.0.1"}
|
||||||
|
timeout = (4, 13)
|
||||||
|
|
||||||
|
|
||||||
|
def get_feed_info(feed):
|
||||||
|
"""parse a feed for basic blog info"""
|
||||||
|
|
||||||
|
b = feedparser.parse(feed)
|
||||||
|
blog = {}
|
||||||
|
blog["feed"] = feed
|
||||||
|
blog["title"] = getattr(b.feed, "title", "")
|
||||||
|
blog["author_name"] = getattr(b.feed, "author", None)
|
||||||
|
blog["description"] = getattr(b.feed, "subtitle", "")
|
||||||
|
blog["last_updated"] = getattr(b.feed, "updated_parsed", "")
|
||||||
|
|
||||||
|
return blog
|
||||||
|
|
||||||
|
|
||||||
|
def get_blog_info(url):
|
||||||
|
"""given a url, return info from the site
|
||||||
|
including the feed URL"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
r = requests.get(url, headers=headers, timeout=timeout)
|
||||||
|
r.raise_for_status()
|
||||||
|
|
||||||
|
soup = BeautifulSoup(r.text, "html.parser")
|
||||||
|
links = soup.find_all(type=["application/rss+xml", "application/atom+xml"])
|
||||||
|
blog_info = {}
|
||||||
|
|
||||||
|
try:
|
||||||
|
author = soup.select_one('meta[name="author"]').get("content")
|
||||||
|
except AttributeError:
|
||||||
|
try:
|
||||||
|
author = soup.select_one('meta[name="creator"]').get("content")
|
||||||
|
except AttributeError:
|
||||||
|
author = ""
|
||||||
|
|
||||||
|
if len(links) > 0:
|
||||||
|
blog_info = get_feed_info(links[0].get("href"))
|
||||||
|
|
||||||
|
blog_info["title"] = blog_info["title"] or soup.title.string
|
||||||
|
blog_info["author_name"] = blog_info["author_name"] or author
|
||||||
|
|
||||||
|
return blog_info
|
||||||
|
|
||||||
|
except requests.Timeout:
|
||||||
|
print(f"TIMEOUT error registering {url}, trying longer timeout")
|
||||||
|
r = requests.get(url, headers=headers, timeout=(31, 31))
|
||||||
|
r.raise_for_status() # let it flow through, a timeout here means the site is unreasonably slow
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"CONNECTION ERROR when registering {url}")
|
||||||
|
print(e)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def get_webfinger_subscribe_uri(username):
|
||||||
|
"""given a username, return the url needed to follow the user"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
regex = re.match(r"(?:.*@)(.*)", username)
|
||||||
|
domain = regex.group(1)
|
||||||
|
if username[0] == "@":
|
||||||
|
username = username[1:]
|
||||||
|
webfinger_url = (
|
||||||
|
f"https://{domain}/.well-known/webfinger/?resource=acct:{username}"
|
||||||
|
)
|
||||||
|
|
||||||
|
r = requests.get(webfinger_url, headers=headers, timeout=timeout)
|
||||||
|
r.raise_for_status()
|
||||||
|
|
||||||
|
except requests.Timeout:
|
||||||
|
print(f"TIMEOUT error finding {username}, trying longer timeout")
|
||||||
|
r = requests.get(webfinger_url, headers=headers, timeout=(31, 31))
|
||||||
|
r.raise_for_status() # let it flow through, a timeout here means the site is unreasonably slow
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"CONNECTION ERROR when subscribing via {username}")
|
||||||
|
print(e)
|
||||||
|
return None
|
||||||
|
|
||||||
|
data = r.json()
|
||||||
|
|
||||||
|
user_id = False
|
||||||
|
template = False
|
||||||
|
|
||||||
|
for link in data["links"]:
|
||||||
|
if link["rel"] == "self":
|
||||||
|
user_id = link["href"]
|
||||||
|
if link["rel"] == "http://ostatus.org/schema/1.0/subscribe":
|
||||||
|
template = link["template"]
|
||||||
|
|
||||||
|
if user_id and template:
|
||||||
|
encoded_user = iri_to_uri("blogs@ausglam.space")
|
||||||
|
uri = template.replace("{uri}", encoded_user)
|
||||||
|
return uri
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def send_email(subject, message, recipient):
|
||||||
|
"""send an email"""
|
||||||
|
|
||||||
|
msg = EmailMessage(
|
||||||
|
subject,
|
||||||
|
message,
|
||||||
|
settings.DEFAULT_FROM_EMAIL,
|
||||||
|
[recipient],
|
||||||
|
)
|
||||||
|
msg.content_subtype = "html"
|
||||||
|
msg.send()
|
28
blogs/views/__init__.py
Normal file
28
blogs/views/__init__.py
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
"""available views"""
|
||||||
|
|
||||||
|
from .public import (
|
||||||
|
ConfirmBlogRegistration,
|
||||||
|
HomeFeed,
|
||||||
|
Blogs,
|
||||||
|
Browse,
|
||||||
|
Contribute,
|
||||||
|
Contact,
|
||||||
|
Conferences,
|
||||||
|
ConfirmEmail,
|
||||||
|
CallsForPapers,
|
||||||
|
Groups,
|
||||||
|
Help,
|
||||||
|
Newsletters,
|
||||||
|
RegisterBlog,
|
||||||
|
RegisterCallForPapers,
|
||||||
|
RegisterConference,
|
||||||
|
RegisterGroup,
|
||||||
|
RegisterNewsletter,
|
||||||
|
Search,
|
||||||
|
Subscribe,
|
||||||
|
SubscribeEmail,
|
||||||
|
Thankyou,
|
||||||
|
UnsubscribeEmail,
|
||||||
|
)
|
||||||
|
|
||||||
|
from .feeds import ArticleFeed, EventFeed
|
73
blogs/views/feeds.py
Normal file
73
blogs/views/feeds.py
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
"""rss feeds"""
|
||||||
|
|
||||||
|
from django.contrib.syndication.views import Feed
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from blogs.models.blog import Article
|
||||||
|
from blogs.models.event import Event
|
||||||
|
|
||||||
|
# pylint: disable=R6301
|
||||||
|
|
||||||
|
|
||||||
|
class ArticleFeed(Feed):
|
||||||
|
"""Combined RSS feed for all the articles"""
|
||||||
|
|
||||||
|
title = "Aus GLAMR Blogs"
|
||||||
|
link = "/feeds/blogs"
|
||||||
|
description = "Posts from Australasian Galleries, Libraries, Archives, Museums, Records and associated blogs"
|
||||||
|
|
||||||
|
def items(self):
|
||||||
|
"""each article in the feed"""
|
||||||
|
return Article.objects.order_by("-pubdate")[:20]
|
||||||
|
|
||||||
|
def item_title(self, item):
|
||||||
|
"""article title"""
|
||||||
|
return item.title
|
||||||
|
|
||||||
|
def item_description(self, item):
|
||||||
|
"""article description"""
|
||||||
|
return getattr(item, "description", None) or getattr(item, "summary", None)
|
||||||
|
|
||||||
|
def item_author_name(self, item):
|
||||||
|
"""article author"""
|
||||||
|
return item.author_name
|
||||||
|
|
||||||
|
def item_pubdate(self, item):
|
||||||
|
"""article publication date"""
|
||||||
|
return item.pubdate
|
||||||
|
|
||||||
|
def item_categories(self, item):
|
||||||
|
"""article tags"""
|
||||||
|
categories = []
|
||||||
|
for tag in item.tags.all():
|
||||||
|
categories.append(tag.name)
|
||||||
|
return categories
|
||||||
|
|
||||||
|
|
||||||
|
class EventFeed(Feed):
|
||||||
|
"""Combined RSS feed for all the articles"""
|
||||||
|
|
||||||
|
title = "Aus GLAMR events"
|
||||||
|
link = "/feeds/events"
|
||||||
|
description = "Australasian events for Galleries, Libraries, Archives, Museums, Records workers"
|
||||||
|
|
||||||
|
def items(self):
|
||||||
|
"""event items for the feed"""
|
||||||
|
return Event.objects.order_by("-start_date")[:20]
|
||||||
|
|
||||||
|
def item_title(self, item):
|
||||||
|
"""event name"""
|
||||||
|
date = item.start_date.strftime("%d %b %Y")
|
||||||
|
return f"{item.name} ({date})"
|
||||||
|
|
||||||
|
def item_description(self, item):
|
||||||
|
"""event description"""
|
||||||
|
return item.description
|
||||||
|
|
||||||
|
def item_pubdate(self, item):
|
||||||
|
"""date event was registered"""
|
||||||
|
return item.pub_date
|
||||||
|
|
||||||
|
def item_categories(self, item):
|
||||||
|
"""event GLAMR category"""
|
||||||
|
return [_(item.category)]
|
570
blogs/views/public.py
Normal file
570
blogs/views/public.py
Normal file
|
@ -0,0 +1,570 @@
|
||||||
|
"""public views (no need to log in)"""
|
||||||
|
|
||||||
|
# pylint: disable=R6301
|
||||||
|
import os
|
||||||
|
|
||||||
|
from itertools import chain
|
||||||
|
from operator import attrgetter
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.shortcuts import get_object_or_404
|
||||||
|
from django.contrib.postgres.search import SearchRank, SearchVector
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from django.core.mail import EmailMessage
|
||||||
|
from django.core.paginator import Paginator
|
||||||
|
from django.db.models import Count
|
||||||
|
from django.shortcuts import render, redirect
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.views import View
|
||||||
|
|
||||||
|
from blogs import forms, models
|
||||||
|
from blogs.utilities import get_blog_info, get_webfinger_subscribe_uri
|
||||||
|
|
||||||
|
class HomeFeed(View):
|
||||||
|
"""the home feed when someone visits the site"""
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
"""display home page"""
|
||||||
|
|
||||||
|
path = os.path.join(settings.BASE_DIR, "static/")
|
||||||
|
|
||||||
|
print(path)
|
||||||
|
|
||||||
|
latest = models.Article.objects.order_by("-pubdate")[:10]
|
||||||
|
|
||||||
|
data = {"title": "Latest blog posts", "latest": latest}
|
||||||
|
return render(request, "index.html", data)
|
||||||
|
|
||||||
|
|
||||||
|
class Blogs(View):
|
||||||
|
"""browse the list of blogs"""
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
"""here they are"""
|
||||||
|
|
||||||
|
blogs = models.Blog.objects.filter(approved=True)
|
||||||
|
for blog in blogs:
|
||||||
|
blog.category_name = models.Category(blog.category).label
|
||||||
|
data = {"title": "Blogs and websites", "blogs": blogs}
|
||||||
|
return render(request, "browse/blogs.html", data)
|
||||||
|
|
||||||
|
|
||||||
|
class Conferences(View):
|
||||||
|
"""browse the list of conferences"""
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
"""here they are"""
|
||||||
|
now = timezone.now()
|
||||||
|
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}"
|
||||||
|
|
||||||
|
data = {"title": "Upcoming events", "cons": cons}
|
||||||
|
return render(request, "browse/events.html", data)
|
||||||
|
|
||||||
|
|
||||||
|
class CallsForPapers(View):
|
||||||
|
"""browse the list of CFPs"""
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
"""here they are"""
|
||||||
|
now = timezone.now()
|
||||||
|
cfps = models.CallForPapers.objects.filter(
|
||||||
|
approved=True, closing_date__gte=now
|
||||||
|
).order_by("closing_date")
|
||||||
|
data = {"title": "Calls for Papers open now", "cfps": cfps}
|
||||||
|
return render(request, "browse/cfp.html", data)
|
||||||
|
|
||||||
|
|
||||||
|
class Groups(View):
|
||||||
|
"""browse the list of groups"""
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
"""here they are"""
|
||||||
|
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
|
||||||
|
data = {"title": "Groups and discussion lists", "groups": groups}
|
||||||
|
return render(request, "browse/groups.html", data)
|
||||||
|
|
||||||
|
|
||||||
|
class Newsletters(View):
|
||||||
|
"""browse the list of groups"""
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
"""here they are"""
|
||||||
|
news = models.Newsletter.objects.filter(approved=True).order_by("name")
|
||||||
|
for letter in news:
|
||||||
|
letter.category_name = models.Category(letter.category).label
|
||||||
|
data = {"title": "Newsletters", "news": news}
|
||||||
|
return render(request, "browse/newsletters.html", data)
|
||||||
|
|
||||||
|
|
||||||
|
class RegisterBlog(View):
|
||||||
|
"""register a blog"""
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
"""the registration page with a form"""
|
||||||
|
|
||||||
|
form = forms.RegisterBlogForm()
|
||||||
|
data = {"title": "Register your blog", "form": form}
|
||||||
|
return render(request, "blogs/register.html", data)
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
"""receive POSTED RegisterBlogForm"""
|
||||||
|
|
||||||
|
form = forms.RegisterBlogForm(request.POST)
|
||||||
|
if form.is_valid():
|
||||||
|
try:
|
||||||
|
blog_info = get_blog_info(form.cleaned_data["url"])
|
||||||
|
except KeyError:
|
||||||
|
return render(
|
||||||
|
request,
|
||||||
|
"blogs/register.html",
|
||||||
|
{"title": "Complete blog registration", "form": form},
|
||||||
|
)
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"title": "Complete blog registration",
|
||||||
|
"form": form,
|
||||||
|
}
|
||||||
|
|
||||||
|
if blog_info:
|
||||||
|
data["blog_info"] = blog_info
|
||||||
|
|
||||||
|
else:
|
||||||
|
data["error"] = "Could not auto-discover your feed info, please enter manually"
|
||||||
|
|
||||||
|
return render(request, "blogs/confirm-register.html", data)
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"title": "Register your blog",
|
||||||
|
"form": form
|
||||||
|
}
|
||||||
|
return render(request, "blogs/register.html", data)
|
||||||
|
|
||||||
|
|
||||||
|
class ConfirmBlogRegistration(View):
|
||||||
|
"""submit validated and pre-filled registration form"""
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
"""the confirm registration page"""
|
||||||
|
|
||||||
|
form = forms.ConfirmBlogForm(request.POST)
|
||||||
|
data = {"title": "Complete blog registration", "form": form}
|
||||||
|
|
||||||
|
return render(request, "blogs/confirm-register.html", data)
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
"""the final form has been submitted!"""
|
||||||
|
|
||||||
|
form = forms.ConfirmBlogForm(request.POST)
|
||||||
|
if form.is_valid():
|
||||||
|
blog = form.save()
|
||||||
|
send_email("blog", blog)
|
||||||
|
return redirect("/thankyou/blog")
|
||||||
|
|
||||||
|
data = {"title": "Oops!", "form": form}
|
||||||
|
return render(request, "blogs/confirm-register.html", data)
|
||||||
|
|
||||||
|
|
||||||
|
class RegisterConference(View):
|
||||||
|
"""register a event"""
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
"""the registration page with a form"""
|
||||||
|
|
||||||
|
form = forms.RegisterConferenceForm()
|
||||||
|
data = {"title": "Register your event", "form": form}
|
||||||
|
return render(request, "events/register.html", data)
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
"""receive form"""
|
||||||
|
|
||||||
|
form = forms.RegisterConferenceForm(request.POST)
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
class RegisterCallForPapers(View):
|
||||||
|
"""register a RegisterCallForPapers"""
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
"""the registration page with a form"""
|
||||||
|
|
||||||
|
form = forms.RegisterCallForPapersForm()
|
||||||
|
data = {"title": "Register your Call For Papers", "form": form}
|
||||||
|
|
||||||
|
return render(request, "events/cfp.html", data)
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
"""receive POSTED RegisterCallForPapersForm"""
|
||||||
|
|
||||||
|
form = forms.RegisterCallForPapersForm(request.POST)
|
||||||
|
|
||||||
|
if form.is_valid():
|
||||||
|
cfp = form.save()
|
||||||
|
send_email("Call for Papers", cfp)
|
||||||
|
return redirect("/thankyou/Call For Papers")
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"title": "Register your Call For Papers",
|
||||||
|
"form": form,
|
||||||
|
"errors": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
return render(request, "events/cfp.html", data)
|
||||||
|
|
||||||
|
|
||||||
|
class RegisterGroup(View):
|
||||||
|
"""register a group"""
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
"""the registration page with a form"""
|
||||||
|
|
||||||
|
form = forms.RegisterGroupForm()
|
||||||
|
data = {"title": "Register your group", "form": form}
|
||||||
|
|
||||||
|
return render(request, "register-group.html", data)
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
"""receive POSTED form"""
|
||||||
|
|
||||||
|
form = forms.RegisterGroupForm(request.POST)
|
||||||
|
|
||||||
|
if form.is_valid():
|
||||||
|
group = form.save()
|
||||||
|
send_email("group", group)
|
||||||
|
return redirect("/thankyou/group")
|
||||||
|
|
||||||
|
data = {"title": "Oops!", "form": form, "errors": True}
|
||||||
|
|
||||||
|
return render(request, "register-group.html", data)
|
||||||
|
|
||||||
|
|
||||||
|
class RegisterNewsletter(View):
|
||||||
|
"""register a newsletter"""
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
"""the registration page with a form"""
|
||||||
|
|
||||||
|
form = forms.RegisterNewsletterForm()
|
||||||
|
data = {"title": "Register your newsletter", "form": form}
|
||||||
|
|
||||||
|
return render(request, "register-newsletter.html", data)
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
"""receive POSTED form"""
|
||||||
|
|
||||||
|
form = forms.RegisterNewsletterForm(request.POST)
|
||||||
|
|
||||||
|
if form.is_valid():
|
||||||
|
newsletter = form.save()
|
||||||
|
send_email("newsletter", newsletter)
|
||||||
|
return redirect("/thankyou/newsletter")
|
||||||
|
|
||||||
|
data = {"title": "Oops!", "form": form, "errors": True}
|
||||||
|
|
||||||
|
return render(request, "register-newsletter.html", data)
|
||||||
|
|
||||||
|
|
||||||
|
class Search(View):
|
||||||
|
"""search functions"""
|
||||||
|
|
||||||
|
def get(self, request, articles=None):
|
||||||
|
"""display search page"""
|
||||||
|
|
||||||
|
query = request.GET.get("q")
|
||||||
|
|
||||||
|
article_vector = (
|
||||||
|
SearchVector("tags__name", weight="A")
|
||||||
|
+ SearchVector("title", weight="B")
|
||||||
|
+ SearchVector("description", weight="C")
|
||||||
|
)
|
||||||
|
|
||||||
|
articles = (
|
||||||
|
models.Article.objects.annotate(rank=SearchRank(article_vector, query))
|
||||||
|
.filter(rank__gte=0.1)
|
||||||
|
.order_by("-rank")
|
||||||
|
)
|
||||||
|
|
||||||
|
conference_vector = SearchVector("name", weight="A") + SearchVector(
|
||||||
|
"description", weight="C"
|
||||||
|
)
|
||||||
|
|
||||||
|
conferences = (
|
||||||
|
models.Event.objects.annotate(rank=SearchRank(conference_vector, query))
|
||||||
|
.filter(rank__gte=0.1)
|
||||||
|
.order_by("-rank")
|
||||||
|
)
|
||||||
|
|
||||||
|
cfp_vector = SearchVector("name", weight="B") + SearchVector(
|
||||||
|
"details", weight="C"
|
||||||
|
)
|
||||||
|
|
||||||
|
cfps = (
|
||||||
|
models.CallForPapers.objects.annotate(rank=SearchRank(cfp_vector, query))
|
||||||
|
.filter(rank__gte=0.1)
|
||||||
|
.order_by("-rank")
|
||||||
|
)
|
||||||
|
|
||||||
|
news_vector = SearchVector("name", weight="A") + SearchVector(
|
||||||
|
"description", weight="C"
|
||||||
|
)
|
||||||
|
|
||||||
|
newsletters = (
|
||||||
|
models.Newsletter.objects.annotate(rank=SearchRank(news_vector, query))
|
||||||
|
.filter(rank__gte=0.1)
|
||||||
|
.order_by("-rank")
|
||||||
|
)
|
||||||
|
|
||||||
|
group_vector = SearchVector("name", weight="A") + SearchVector(
|
||||||
|
"description", weight="C"
|
||||||
|
)
|
||||||
|
|
||||||
|
groups = (
|
||||||
|
models.Event.objects.annotate(rank=SearchRank(group_vector, query))
|
||||||
|
.filter(rank__gte=0.1)
|
||||||
|
.order_by("-rank")
|
||||||
|
)
|
||||||
|
|
||||||
|
combined = sorted(
|
||||||
|
chain(articles, conferences, cfps, newsletters, groups),
|
||||||
|
key=attrgetter("rank"),
|
||||||
|
reverse=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
for item in combined:
|
||||||
|
if hasattr(item, "category"):
|
||||||
|
item.category_name = models.Category(item.category).label
|
||||||
|
if hasattr(item, "event"):
|
||||||
|
item.category = models.Category(item.event.category)
|
||||||
|
item.category_name = models.Category(item.event.category).label
|
||||||
|
|
||||||
|
paginator = Paginator(combined, 10)
|
||||||
|
page_number = request.GET.get("page")
|
||||||
|
paged = paginator.get_page(page_number)
|
||||||
|
|
||||||
|
data = {"title": "Search Aus GLAMR", "items": paged, "query": query}
|
||||||
|
|
||||||
|
return render(request, "search.html", data)
|
||||||
|
|
||||||
|
|
||||||
|
class Browse(View):
|
||||||
|
"""browse by clicking on a tag"""
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
"""display browse results"""
|
||||||
|
|
||||||
|
query = request.GET.get("q")
|
||||||
|
results = models.Article.objects.filter(tags__name=query).order_by("-pubdate")
|
||||||
|
trending = models.Tag.objects.annotate(count=Count("articles")).order_by(
|
||||||
|
"-count"
|
||||||
|
)[:10]
|
||||||
|
|
||||||
|
paginator = Paginator(results, 10)
|
||||||
|
page_number = request.GET.get("page")
|
||||||
|
paged = paginator.get_page(page_number)
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"title": f"Articles tagged '{query}'",
|
||||||
|
"trending": trending,
|
||||||
|
"items": paged,
|
||||||
|
"query": query,
|
||||||
|
}
|
||||||
|
|
||||||
|
return render(request, "browse/tags.html", data)
|
||||||
|
|
||||||
|
|
||||||
|
class Subscribe(View):
|
||||||
|
"""Subscribe page showing RSS feed link and mastodon follow form"""
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
"""display subscribe page"""
|
||||||
|
|
||||||
|
return render(request, "subscribe.html", {})
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
"""subscribe to Mastodon account"""
|
||||||
|
|
||||||
|
form = forms.SubscribeViaMastodon(request.POST)
|
||||||
|
if form.is_valid():
|
||||||
|
try:
|
||||||
|
username = form.cleaned_data["username"]
|
||||||
|
uri = get_webfinger_subscribe_uri(username)
|
||||||
|
|
||||||
|
if uri:
|
||||||
|
return redirect(uri)
|
||||||
|
|
||||||
|
form.add_error(
|
||||||
|
"username", "Enter a valid username e.g. @example@ausglam.space"
|
||||||
|
)
|
||||||
|
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return render(request, "subscribe.html", {"form": form})
|
||||||
|
|
||||||
|
|
||||||
|
class SubscribeEmail(View):
|
||||||
|
"""Subscribe to weekly emails"""
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
"""display subscribe page"""
|
||||||
|
|
||||||
|
form = forms.SubscribeEmailForm()
|
||||||
|
data = {"title": "Get weekly email updates", "form": form}
|
||||||
|
|
||||||
|
return render(request, "subscribe-email.html", data)
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
"""subscribe to Mastodon account"""
|
||||||
|
|
||||||
|
form = forms.SubscribeEmailForm(request.POST)
|
||||||
|
if form.is_valid():
|
||||||
|
user = form.save()
|
||||||
|
user.send_confirmation_email()
|
||||||
|
return redirect("/thankyou/email%20address")
|
||||||
|
|
||||||
|
return render(request, "subscribe.html", {"form": form})
|
||||||
|
|
||||||
|
|
||||||
|
class ConfirmEmail(View):
|
||||||
|
"""Hit this when click to confirm email subscription"""
|
||||||
|
|
||||||
|
def get(self, request, token, user_id):
|
||||||
|
"""display confirmation page"""
|
||||||
|
|
||||||
|
user = get_object_or_404(models.Subscriber, id=user_id, token=token)
|
||||||
|
|
||||||
|
user.confirmed = True
|
||||||
|
user.save()
|
||||||
|
|
||||||
|
return render(request, "confirm-email.html", {"title": "Confirmed!"})
|
||||||
|
|
||||||
|
|
||||||
|
class UnsubscribeEmail(View):
|
||||||
|
"""Hit this when click to confirm email subscription"""
|
||||||
|
|
||||||
|
def get(self, request, token, user_id):
|
||||||
|
"""unsubscribe conf page"""
|
||||||
|
|
||||||
|
user = get_object_or_404(models.Subscriber, id=user_id, token=token)
|
||||||
|
user.delete()
|
||||||
|
|
||||||
|
return render(request, "unsubscribe.html", {"title": "Sorry to see you go"})
|
||||||
|
|
||||||
|
|
||||||
|
class Contribute(View):
|
||||||
|
"""help page"""
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
"""display contribute page"""
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"title": "Contribute",
|
||||||
|
}
|
||||||
|
return render(request, "contribute.html", data)
|
||||||
|
|
||||||
|
|
||||||
|
class Help(View):
|
||||||
|
"""help page"""
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
"""display help page"""
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"title": "Help",
|
||||||
|
}
|
||||||
|
return render(request, "help.html", data)
|
||||||
|
|
||||||
|
|
||||||
|
class Contact(View):
|
||||||
|
"""contact Hugh"""
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
"""the contact page with a form"""
|
||||||
|
|
||||||
|
form = forms.ContactForm()
|
||||||
|
data = {"title": "Get in touch", "form": form}
|
||||||
|
|
||||||
|
return render(request, "contact.html", data)
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
"""receive POSTED form"""
|
||||||
|
|
||||||
|
form = forms.ContactForm(request.POST)
|
||||||
|
|
||||||
|
if form.is_valid():
|
||||||
|
from_email = form.cleaned_data["from_email"]
|
||||||
|
subject = form.cleaned_data["subject"]
|
||||||
|
message = form.cleaned_data["message"]
|
||||||
|
send_contact_email(from_email, subject, message)
|
||||||
|
return redirect("/thankyou/message")
|
||||||
|
|
||||||
|
data = {"title": "Oops!", "form": form, "errors": True}
|
||||||
|
|
||||||
|
return render(request, "contact.html", data)
|
||||||
|
|
||||||
|
|
||||||
|
class Thankyou(View):
|
||||||
|
"""thankyou for registering page"""
|
||||||
|
|
||||||
|
def get(self, request, register_type):
|
||||||
|
"""display thankyou page"""
|
||||||
|
|
||||||
|
data = {"title": "Thanks!", "register_type": register_type}
|
||||||
|
return render(request, "thanks.html", data)
|
||||||
|
|
||||||
|
|
||||||
|
def send_email(instance, obj):
|
||||||
|
"""send an email alert to admin"""
|
||||||
|
|
||||||
|
html_message = f"<html><body>\
|
||||||
|
<p>A new {instance} has been registered:</p>\
|
||||||
|
<strong>{obj.title if hasattr(obj, 'title') else obj.name}</strong></p>\
|
||||||
|
<p>To approve or reject visit <a href='https://{settings.DOMAIN}/admin'>{settings.DOMAIN}/admin</a></p>\
|
||||||
|
</body></html>"
|
||||||
|
|
||||||
|
msg = EmailMessage(
|
||||||
|
f"📥 Someone has registered a new {instance}!",
|
||||||
|
html_message,
|
||||||
|
settings.DEFAULT_FROM_EMAIL,
|
||||||
|
[settings.ADMIN_EMAIL],
|
||||||
|
)
|
||||||
|
msg.content_subtype = "html"
|
||||||
|
msg.send()
|
||||||
|
|
||||||
|
|
||||||
|
def send_contact_email(from_email, subject, message):
|
||||||
|
"""email the message"""
|
||||||
|
|
||||||
|
html_message = f"<html><body><p>{message}</p></body></html>"
|
||||||
|
|
||||||
|
msg = EmailMessage(
|
||||||
|
f"Message via Aus GLAMR: {subject}",
|
||||||
|
html_message,
|
||||||
|
f"{from_email}",
|
||||||
|
[settings.ADMIN_EMAIL],
|
||||||
|
)
|
||||||
|
msg.content_subtype = "html"
|
||||||
|
msg.send()
|
26
docker-compose.yml
Normal file
26
docker-compose.yml
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
version: '3'
|
||||||
|
|
||||||
|
services:
|
||||||
|
db:
|
||||||
|
image: postgres:13
|
||||||
|
env_file: .env
|
||||||
|
volumes:
|
||||||
|
- pgdata:/var/lib/postgresql/data
|
||||||
|
networks:
|
||||||
|
- main
|
||||||
|
web:
|
||||||
|
build: .
|
||||||
|
env_file: .env
|
||||||
|
command: python manage.py runserver 0.0.0.0:8000
|
||||||
|
volumes:
|
||||||
|
- .:/app
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
|
networks:
|
||||||
|
- main
|
||||||
|
ports:
|
||||||
|
- "8000:8000"
|
||||||
|
volumes:
|
||||||
|
pgdata:
|
||||||
|
networks:
|
||||||
|
main:
|
78
glamr-dev
Executable file
78
glamr-dev
Executable file
|
@ -0,0 +1,78 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# Thanks Mouse Reeve for letting me steal your idea.
|
||||||
|
|
||||||
|
# exit on errors
|
||||||
|
set -e
|
||||||
|
|
||||||
|
function runweb {
|
||||||
|
docker compose run --rm web "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
function execdb {
|
||||||
|
docker compose exec db $@
|
||||||
|
}
|
||||||
|
|
||||||
|
function migrate {
|
||||||
|
runweb python manage.py migrate "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
CMD=$1
|
||||||
|
if [ -n "$CMD" ]; then
|
||||||
|
shift
|
||||||
|
fi
|
||||||
|
|
||||||
|
# show commands as they're executed
|
||||||
|
set -x
|
||||||
|
|
||||||
|
case "$CMD" in
|
||||||
|
|
||||||
|
announce)
|
||||||
|
runweb python manage.py announce
|
||||||
|
;;
|
||||||
|
black)
|
||||||
|
docker compose run --rm web black ausglamr blogs
|
||||||
|
;;
|
||||||
|
check_feeds)
|
||||||
|
runweb python manage.py check_feeds
|
||||||
|
;;
|
||||||
|
collectstatic)
|
||||||
|
runweb python manage.py collectstatic
|
||||||
|
;;
|
||||||
|
createsuperuser)
|
||||||
|
runweb python manage.py createsuperuser --no-input
|
||||||
|
;;
|
||||||
|
dbshell)
|
||||||
|
execdb psql -U ${POSTGRES_USER} ${POSTGRES_DB}
|
||||||
|
;;
|
||||||
|
manage)
|
||||||
|
runweb python manage.py "$@"
|
||||||
|
;;
|
||||||
|
makemigrations)
|
||||||
|
runweb python manage.py makemigrations "$@"
|
||||||
|
;;
|
||||||
|
migrate)
|
||||||
|
migrate "$@"
|
||||||
|
;;
|
||||||
|
pylint)
|
||||||
|
docker compose run --rm web pylint ausglamr blogs
|
||||||
|
;;
|
||||||
|
queue_announcements)
|
||||||
|
runweb python manage.py queue_announcements
|
||||||
|
;;
|
||||||
|
resetdb)
|
||||||
|
docker compose rm -svf
|
||||||
|
docker volume rm -f ausglamr_pgdata
|
||||||
|
migrate
|
||||||
|
;;
|
||||||
|
send_weekly_email)
|
||||||
|
runweb python manage.py send_weekly_email
|
||||||
|
;;
|
||||||
|
test)
|
||||||
|
runweb python manage.py test "$@"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
set +x
|
||||||
|
echo "That is not a command"
|
||||||
|
;;
|
||||||
|
esac
|
22
manage.py
Executable file
22
manage.py
Executable file
|
@ -0,0 +1,22 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
"""Django's command-line utility for administrative tasks."""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Run administrative tasks."""
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'ausglamr.settings')
|
||||||
|
try:
|
||||||
|
from django.core.management import execute_from_command_line
|
||||||
|
except ImportError as exc:
|
||||||
|
raise ImportError(
|
||||||
|
"Couldn't import Django. Are you sure it's installed and "
|
||||||
|
"available on your PYTHONPATH environment variable? Did you "
|
||||||
|
"forget to activate a virtual environment?"
|
||||||
|
) from exc
|
||||||
|
execute_from_command_line(sys.argv)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
11
requirements.txt
Normal file
11
requirements.txt
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
beautifulsoup4==4.12.2
|
||||||
|
Django==4.2.7
|
||||||
|
environs==9.5.0
|
||||||
|
feedparser==6.0.10
|
||||||
|
psycopg2==2.9.5
|
||||||
|
requests==2.31.0
|
||||||
|
|
||||||
|
# dev
|
||||||
|
|
||||||
|
black==23.12.0
|
||||||
|
pylint==3.0.3
|
Loading…
Reference in a new issue