Compare commits

...

16 commits

Author SHA1 Message Date
Danielle McLean 5348dc9f82
v1.12.5 2024-05-19 16:04:00 +10:00
Danielle McLean e36ad27d49
Enable import sorting with Ruff 2024-05-19 16:00:14 +10:00
Danielle McLean d21d4bda83
Paginate without errors if a page doesn't exist 2024-05-19 15:59:43 +10:00
Danielle McLean 8d8aa4749b
Update year range in LICENSE 2024-05-19 13:04:19 +10:00
Danielle McLean 3baf75e59e
Remove unused CI config files and the like 2024-05-19 13:03:57 +10:00
Danielle McLean 880b899e81
Update Highlight.js to 11.9.0 2024-03-13 19:14:35 +11:00
Danielle McLean 6061d6f600
Update Tippy.js to v6.3.7 2024-03-13 19:12:36 +11:00
Danielle McLean a680a6501c
Remove defunct oEmbed converter service 2024-03-13 19:04:35 +11:00
Danielle McLean 625b5d963a
Remove unused django-analytical plugin 2024-03-13 19:03:58 +11:00
Danielle McLean 9d11cc7576
Swap from Poetry to PDM 2024-03-13 18:10:51 +11:00
Danielle McLean c49e17db90
Upgrade Bootstrap to v5
This is just an in-place upgrade to produce a roughly unchanged page
design. Ideally I'm going to need to install Sass and use that, because
Bootstrap 5 relies a bit more heavily on using its Sass sources if you
want to customise things (which I do), but for now loading standard
Bootstrap from the CDN is fine.

I still prefer Stylus over both Sass and LESS, but the industry seem to
have decided on using Sass, which probably means I'll be better off
porting my customisations to Sass in the long run. Oh well.
2024-03-13 17:10:38 +11:00
Danielle McLean 7696ff45db
Upgrade to Font Awesome v6 2024-03-13 16:57:00 +11:00
Danielle McLean 731f177d18
Bump package versions to get stuff working again 2024-03-13 15:58:54 +11:00
Danielle McLean 0061111ad8
Ensure User.avatar is optional 2024-03-13 15:58:24 +11:00
Danielle McLean 6b53c00d7c
Remove deprecated reference to HiredisParser 2024-03-13 15:54:42 +11:00
Danielle McLean 1490a95735
Fix submodule not to use deprecated git:// protocol 2024-03-13 15:28:37 +11:00
20 changed files with 1301 additions and 2597 deletions

6
.gitignore vendored
View file

@ -1,4 +1,3 @@
# Created by https://www.gitignore.io/api/django
### Django ###
@ -15,9 +14,10 @@ media
# <django-project-name>/staticfiles/
# End of https://www.gitignore.io/api/django
/.pdm-python
/.env
/.mypy_cache
/.pytest_cache
/*.egg-info/
/static
node_modules
/node_modules

View file

@ -1,21 +0,0 @@
image: python:3.6
services:
- postgres:latest
variables:
GIT_SUBMODULE_STRATEGY: normal
PIP_CACHE_DIR: $CI_PROJECT_DIR/.cache/pip
PIPENV_CACHE_DIR: $CI_PROJECT_DIR/.cache/pipenv
POSTGRES_HOST: postgres
POSTGRES_DB: nice_marmot
POSTGRES_USER: runner
POSTGRES_PASSWORD: ''
cache:
paths:
- .cache
test:
script:
- pip install pipenv
- pipenv sync --dev
- pipenv run pytest

2
.gitmodules vendored
View file

@ -1,3 +1,3 @@
[submodule "lemoncurry/static/base16-materialtheme-scheme"]
path = lemoncurry/static/base16-materialtheme-scheme
url = git://github.com/ntpeters/base16-materialtheme-scheme.git
url = https://github.com/ntpeters/base16-materialtheme-scheme

View file

@ -1,41 +0,0 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0
hooks:
- id: check-ast
- id: check-builtin-literals
- id: check-case-conflict
- id: check-docstring-first
- id: check-executables-have-shebangs
- id: check-json
- id: check-merge-conflict
- id: check-symlinks
- id: check-toml
- id: check-vcs-permalinks
- id: check-yaml
- id: destroyed-symlinks
- id: end-of-file-fixer
- id: fix-byte-order-marker
- id: mixed-line-ending
args:
- --fix=lf
- id: trailing-whitespace
- repo: https://github.com/psf/black
rev: 23.7.0
hooks:
- id: black
language_version: python3.11
- repo: local
hooks:
- id: pytest
name: Check pytest unit tests pass
entry: poetry run pytest
pass_filenames: false
language: system
types: [python]
- id: mypy
name: Check mypy static types match
entry: poetry run mypy . --ignore-missing-imports
pass_filenames: false
language: system
types: [python]

View file

@ -1,3 +0,0 @@
requirements:
- Pipfile
- Pipfile.lock

View file

@ -1,16 +0,0 @@
language: python
cache:
directories:
- $PIP_CACHE_DIR
- $PIPENV_CACHE_DIR
env:
global:
- PIP_CACHE_DIR=$HOME/.cache/pip
- PIPENV_CACHE_DIR=$HOME/.cache/pipenv
python:
- '3.6'
install:
- pip install pipenv
- pipenv install --dev
script:
- pipenv run pytest

View file

@ -1,4 +0,0 @@
# vim: set ft=yaml :
host: 00dani.dev
port: 443
cname: dev.00dani.me

View file

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2017 - 2018 Danielle McLean
Copyright (c) 2017 - 2024 Danielle McLean
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View file

@ -1,7 +1,6 @@
{% extends 'lemoncurry/layout.html' %}
{% block head %}
<link rel="shortlink" href="{{ entry.short_url }}" />
<link rel="alternate" type="application/json+oembed" href="https://wirres.net/oembed/oembed/php?url={{ entry.absolute_url | urlencode }}" />
{% endblock %}
{% block styles %}

View file

@ -3,7 +3,7 @@
<article class="h-entry media">
{{i}}<aside class="info">
{{i}}<a class="p-author h-card" href="{{ entry.author.url }}">
{{i}}<img class="u-photo img-fluid" src="{{ entry.author.avatar.url }}" alt="{{ entry.author.name }}" />
{{i}}{% if entry.author.avatar %}<img class="u-photo img-fluid" src="{{ entry.author.avatar.url }}" alt="{{ entry.author.name }}" />{% endif %}
{{i}}<span class="p-name sr-only">{{ entry.author.name }}</span>
{{i}}</a>
{{i}}<a class="u-uid u-url" href="{{ entry.url }}">

View file

@ -1,35 +1,32 @@
from django.core.paginator import Paginator
from typing import Callable
from django.core.paginator import Page, Paginator
from django.shortcuts import redirect
from lemoncurry.middleware import ResponseException
def paginate(queryset, reverse, page):
class Page:
def __init__(self, i):
self.i = i
def paginate(queryset, reverse: Callable[[int], str], page: int | None) -> Page:
def redirect_to_page(i: int):
raise ResponseException(redirect(reverse(i)))
@property
def url(self):
return reverse(self.i)
@property
def current(self):
return self.i == entries.number
# If the first page was requested, redirect to the clean version of the URL
# with no page suffix.
if page == 1:
raise ResponseException(redirect(Page(1).url))
def reversible(p: Page) -> Page:
p.reverse = reverse
return p
paginator = Paginator(queryset, 10)
entries = paginator.page(page or 1)
entries.pages = tuple(Page(i) for i in paginator.page_range)
# If no page number was specified, return page one.
if page is None:
return reversible(paginator.page(1))
if entries.has_previous():
entries.prev = Page(entries.previous_page_number())
if entries.has_next():
entries.next = Page(entries.next_page_number())
# If the first page was explicitly requested, or the page number was negative, redirect to page one with no URL suffix.
if page <= 1:
redirect_to_page(1)
return entries
# If the page requested is larger than the last page, then redirect to the last page.
if page > paginator.num_pages:
redirect_to_page(paginator.num_pages)
# Just return the current page! Hooray!
return reversible(paginator.page(page))

View file

@ -62,17 +62,19 @@
</div>
<div id="verified-success" hidden>
this client has been <strong>verified</strong> using <code>{{ '<link rel="redirect_uri">' | escape }}</code> - they are who they claim to be!
this client has been <strong>verified</strong> using <code>{{ '<link rel="redirect_uri">' | escape }}</code> <br/>- they are who they claim to be!
</div>
<div id="verified-warning" hidden>
this client could <strong>not</strong> be verified using <code>{{ '<link rel="redirect_uri">' | escape }}</code> - check the redirect uri carefully yourself!
this client could <strong>not</strong> be verified using <code>{{ '<link rel="redirect_uri">' | escape }}</code> <br/>- check the redirect uri carefully yourself!
</div>
{% endblock %}
{% block foot %}
<script type="text/javascript">
tippy('[data-tippy-theme]', {
tippy('[data-tippy-html]', {
arrow: true,
allowHTML: true,
maxWidth: 500,
content: function(element) {
return document.querySelector(element.getAttribute('data-tippy-html')).innerHTML;
}

View file

@ -2,10 +2,10 @@
img
height 2em
margin-right .5em
.tippy-tooltip
&.success-theme
.tippy-box
&[data-theme~='success']
color $base0B
&.warning-theme
&[data-theme~='warning']
color $base0A
.verified-success
color $base0B

View file

@ -1,5 +1,5 @@
<!doctype html>
<html{% block html_attr %} dir="ltr" lang="en"{% endblock %}>
<html{% block html_attr %} dir="ltr" lang="en" data-bs-theme="dark"{% endblock %}>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
@ -27,10 +27,10 @@
<link rel="{{ i.rel }}" type="{{ i.mime }}" sizes="{{ i.sizes }}" href="{{ i.url }}" />
{% endfor %}
<link rel="stylesheet" type="text/css" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"
integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.13.1/styles/monokai.min.css"
integrity="sha384-bHqbpRh/XW+phptvH9nQvMKHwPH1ZbOxpIeAB2D2OIEL4Ni7aZzZgMFpsRra+v1g" crossorigin="anonymous">
<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
<link rel="stylesheet" type="text/css" href="https://unpkg.com/@highlightjs/cdn-assets@11.9.0/styles/monokai.min.css"
integrity="sha384-88Jvj9Q2LiBDwL7w3yciRTcH5q2zzvMFYIm4xX9/evqxJsxA33Xk9XYKcvUlPITo" crossorigin="anonymous">
<link rel="stylesheet" type="text/css" href="https://unpkg.com/openwebicons@1.6.0/css/openwebicons.min.css"
integrity="sha384-NkRWM9o4Kfak7GwS+un+sProBBpj02vc/e1EoXvdCUSdRk0muOfkKJ5NtpueAuka" crossorigin="anonymous">
<link rel="stylesheet" type="text/css" href="https://unpkg.com/tippy.js@3.4.1/dist/tippy.css"
@ -40,12 +40,11 @@
{% block styles %}{% endblock %}
{% endcompress %}
<script type="text/javascript" defer src="https://use.fontawesome.com/releases/v5.8.1/js/all.js"
integrity="sha384-g5uSoOSBd7KkhAMlnQILrecXvzst9TdC09/VM+pjDTCM+1il8RHz5fKANTFFb+gQ" crossorigin="anonymous"></script>
<script type="text/javascript" defer src="https://kit.fontawesome.com/a3aade9b41.js" crossorigin="anonymous"></script>
</head>
<body{% block body_attr %}{% endblock %}>
<header>
<nav class="navbar navbar-expand-md navbar-dark">
<nav class="navbar navbar-expand-md"><div class="container-fluid">
<a class="navbar-brand" rel="home" href="{{ url('home:index') }}">{{ request.site.name }}</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbar"
aria-controls="navbar" aria-expanded="false" aria-label="Toggle navigation">
@ -97,7 +96,7 @@
</div>
{% endactiveurl %}
</nav>
</div></nav>
</header>
<main>
@ -111,29 +110,29 @@
<nav>
<ul class="pagination">
{% if entries.prev %}
{% if entries.has_previous() %}
<li class="page-item">
<a class="page-link" rel="prev" href="{{ entries.prev.url }}">
<a class="page-link" rel="prev" href="{{ entries.reverse(entries.previous_page_number()) }}">
<i class="fas fa-step-backward" aria-hidden="true"></i> <span class="sr-only">previous page</span>
</a>
</li>
{% endif %}
{% for page in entries.pages %}
{% if page.current %}
{% for i in entries.paginator.page_range %}
{% if i == entries.number %}
<li class="page-item active">
<span class="page-link">{{ page.i }} <span class="sr-only">(current page)</span></span>
<span class="page-link">{{ i }} <span class="sr-only">(current page)</span></span>
</li>
{% else %}
<li class="page-item">
<a class="page-link" href="{{ page.url }}">{{ page.i }}</a>
<a class="page-link" href="{{ entries.reverse(i) }}">{{ i }}</a>
</li>
{% endif %}
{% endfor %}
{% if entries.next %}
{% if entries.has_next() %}
<li class="page-item">
<a class="page-link" rel="next" href="{{ entries.next.url }}">
<a class="page-link" rel="next" href="{{ entries.reverse(entries.next_page_number()) }}">
<i class="fas fa-step-forward" aria-hidden="true"></i> <span class="sr-only">next page</span>
</a>
</li>
@ -147,14 +146,14 @@
<script src="https://code.jquery.com/jquery-3.3.1.min.js" crossorigin="anonymous"
integrity="sha384-tsQFqpEReu7ZLhBV2VZlAu7zcOV+rXbYlF2cqB8txI/8aZajjp4Bqd+V6D5IgvKT"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" crossorigin="anonymous"
integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" crossorigin="anonymous"
integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.13.1/highlight.min.js" crossorigin="anonymous"
integrity="sha384-BlPof9RtjBqeJFskKv3sK3dh4Wk70iKlpIe92FeVN+6qxaGUOUu+mZNpALZ+K7ya"></script>
<script src="https://unpkg.com/tippy.js@3.4.1/dist/tippy.standalone.min.js" crossorigin="anonymous"
integrity="sha384-x7dGoSfOWUdyPccAel9dkWte6n8GxDWbByavEixRzW0O9xvPGzg3y0qzZBwGNUw9"></script>
<script src="https://unpkg.com/@highlightjs/cdn-assets@11.9.0/highlight.min.js" crossorigin="anonymous"
integrity="sha384-F/bZzf7p3Joyp5psL90p/p89AZJsndkSoGwRpXcZhleCWhd8SnRuoYo4d0yirjJp"></script>
<script src="https://unpkg.com/@popperjs/core@2.11.8/dist/umd/popper.min.js" crossorigin="anonymous"
integrity="sha384-I7E8VVD/ismYTF4hNIPjVp/Zjvgyol6VFvRkX/vR+Vc4jQkC+hVqc2pM8ODewa9r"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.min.js" crossorigin="anonymous"
integrity="sha384-0pUGZvbkm6XF6gxjEnlmuGrJXVbNuzT9qBBavbLwCsOGabYfZo0T0to5eqruptLy"></script>
<script src="https://unpkg.com/tippy.js@6.3.7/dist/tippy-bundle.umd.js" crossorigin="anonymous"
integrity="sha384-dtMr4wkcxQWUqsJFgElu4AttgIhOsjr2vYIzP2mv0MZbD/uJ6OHxFdbgE3MOKabN"></script>
{% compress js %}
<script type="text/javascript">
hljs.initHighlightingOnLoad();

View file

@ -75,7 +75,6 @@ INSTALLED_APPS = [
"django.contrib.sitemaps",
"django.contrib.messages",
"django.contrib.staticfiles",
"analytical",
"annoying",
"compressor",
"computed_property",
@ -154,7 +153,6 @@ CACHES = {
"LOCATION": "redis://127.0.0.1:6380/0",
"KEY_PREFIX": "lemoncurry",
"OPTIONS": {
"PARSER_CLASS": "redis.connection.HiredisParser",
"SERIALIZER": "lemoncurry.msgpack.MSGPackModernSerializer",
},
"VERSION": 2,

View file

@ -5,6 +5,7 @@ html
a
color $base0D
text-decoration none
&:hover
color $base0C
@ -31,13 +32,13 @@ code, pre, .code, .pre
text-decoration none
line-height 1
for placement in top bottom left right
.tippy-popper[x-placement^={placement}] .tippy-tooltip.dark-theme .tippy-arrow
border-{placement}-color $base03
.tippy-tooltip.dark-theme
.tippy-box[data-theme~='dark']
background-color $base03
color $base04
text-align center
for placement in top bottom left right
&[data-placement^={placement}] > .tippy-arrow::before
border-{placement}-color $base03
body
display flex
@ -103,6 +104,12 @@ ul.pagination
background-color $base02
border 1px solid rgba(0,0,0,.125)
.media
display flex
> .media-body
flex-grow 1
margin-left 3px
.card
background-color $base02

1156
pdm.lock Normal file

File diff suppressed because it is too large Load diff

2380
poetry.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,67 +1,78 @@
[tool.poetry]
[project]
name = "lemoncurry"
version = "1.12.4"
version = "1.12.5"
description = "Indieweb-compatible personal website"
authors = ["Danielle McLean <dani@00dani.me>"]
license = "MIT"
[tool.poetry.dependencies]
python = "^3.9"
accept-types = "*"
ago = "*"
argon2-cffi = "*"
bleach = "*"
cachecontrol = "*"
django = "<4,>=3"
django-activeurl = "*"
django-analytical = "*"
django-annoying = "*"
django-compressor = "*"
django-computed-property = "*"
django-cors-headers = "*"
django-debug-toolbar = "*"
django-extensions = "*"
django-meta = "*"
django-model-utils = "*"
django-otp = "*"
django-otp-agents = "*"
django-push = "*"
django-randomslugfield = "*"
django-redis = "*"
django-rq = "*"
docutils = "*"
gevent = "*"
gunicorn = {extras = ["gevent"], version = "*"}
hiredis = "*"
jinja2 = "*"
markdown = "*"
mf2py = "*"
mf2util = "*"
msgpack = "*"
pillow = "*"
psycopg2-binary = "*"
python-baseconv = "*"
python-magic = "*"
python-slugify = "*"
pyup-django = "*"
pyyaml = "*"
qrcode = "*"
ronkyuu = "*"
xrd = "*"
greenlet = "^2.0.2"
authors = [
{name = "Danielle McLean", email = "dani@00dani.me"},
]
license = {text = "MIT"}
requires-python = "<4.0,>=3.12"
[tool.poetry.dev-dependencies]
mypy = "*"
ptpython = "*"
pytest-django = "*"
types-bleach = "*"
types-markdown = "*"
types-python-slugify = "*"
types-pyyaml = "*"
types-requests = "*"
watchdog = "*"
werkzeug = "*"
dependencies = [
"accept-types",
"ago",
"argon2-cffi",
"bleach",
"cachecontrol",
"django<4,>=3",
"django-activeurl",
"django-annoying",
"django-compressor",
"django-computed-property",
"django-cors-headers",
"django-debug-toolbar",
"django-extensions",
"django-meta",
"django-model-utils",
"django-otp",
"django-otp-agents",
"django-push",
"django-randomslugfield",
"django-redis",
"django-rq",
"docutils",
"gevent",
"gunicorn[gevent]",
"hiredis",
"jinja2",
"markdown",
"mf2py",
"mf2util",
"msgpack",
"pillow",
"psycopg2-binary",
"python-baseconv",
"python-magic",
"python-slugify",
"pyup-django",
"pyyaml",
"qrcode",
"ronkyuu",
"xrd",
"greenlet",
]
[tool.pdm.dev-dependencies]
dev = [
"mypy",
"ptpython",
"pytest-django",
"types-bleach",
"types-markdown",
"types-python-slugify",
"types-pyyaml",
"types-requests",
"watchdog",
"werkzeug",
]
[tool.pdm.build]
includes = []
[tool.ruff.lint]
select = ["E4", "E7", "E9", "F", "I"]
[build-system]
requires = ["poetry>=0.12"]
build-backend = "poetry.masonry.api"
requires = ["pdm-backend"]
build-backend = "pdm.backend"

View file

@ -128,7 +128,7 @@ class User(ModelMeta, AbstractUser):
@property
def avatar_url(self):
return self.avatar.url
return self.avatar.url if self.avatar else None
@cached_property
def facebook_id(self):
@ -154,7 +154,7 @@ class User(ModelMeta, AbstractUser):
"url": self.full_url,
"name": self.name,
"email": self.email,
"image": urljoin(base, self.avatar.url),
"image": urljoin(base, self.avatar.url) if self.avatar else None,
"givenName": self.first_name,
"familyName": self.last_name,
"sameAs": [profile.url for profile in self.profiles.all()],