Compare commits

..

29 Commits

Author SHA1 Message Date
Calum Andrew Morrell ba4a6822f2 Merge pull request 'dev' (#1) from dev into master
Reviewed-on: #1
2025-05-06 08:24:41 +00:00
Calum Andrew Morrell 005a4a6ca1 Added gunicorn in preparation of rolling out to semele. 2025-05-06 09:16:19 +01:00
Calum Andrew Morrell 651d0de029 Updated templates for articles app to include tailwind styling. 2025-05-05 19:44:42 +01:00
Calum Andrew Morrell b50d44530d Updated flatpages default template. 2025-05-05 19:29:37 +01:00
Calum Andrew Morrell 4c9b8d6c38 Updated template files for the core website to include tailwind styling. 2025-05-05 19:27:43 +01:00
Calum Andrew Morrell f7f45160d6 Added a fixme to articles model. 2025-02-17 00:15:24 +00:00
Calum Andrew Morrell c135a81f97 Added sites & flatpages into project settings and uncommented entries for tagulous & markdownx. 2025-02-17 00:13:07 +00:00
Calum Andrew Morrell 22148202b2 Linked core urls into project. 2025-02-17 00:08:02 +00:00
Calum Andrew Morrell 1d7d4ff442 Added default flatpage template. 2025-02-17 00:06:43 +00:00
Calum Andrew Morrell 3f4188e4b7 Added urls for the core app. 2025-02-17 00:05:23 +00:00
Calum Andrew Morrell c30f39ca24 Added pagination template. 2025-02-17 00:02:51 +00:00
Calum Andrew Morrell 017aad2ede Added header template. 2025-02-17 00:01:27 +00:00
Calum Andrew Morrell bd4c7cab70 Added footer template. 2025-02-16 23:49:43 +00:00
Calum Andrew Morrell 5ccb53fb5d Added base template. 2025-02-16 23:48:56 +00:00
Calum Andrew Morrell 7b8a57fc92 Added homepage template. 2025-02-16 23:47:37 +00:00
Calum Andrew Morrell c6b834e5b7 Added initial homepage view. 2025-02-16 23:39:00 +00:00
Calum Andrew Morrell d665992b22 Created core app for homepage and generic templates. 2025-02-16 23:33:27 +00:00
Calum Andrew Morrell 6aec5a5659 Migrated articles app. 2025-02-16 23:31:05 +00:00
Calum Andrew Morrell 0a3ef1070f Created initial templates for articles app with basic layout and no styling. 2025-02-16 23:09:43 +00:00
Calum Andrew Morrell 835de18952 Linked articles app and markdownx into project urls. 2025-02-16 22:49:48 +00:00
Calum Andrew Morrell c397468cc3 Added urls for articles app. 2025-02-16 22:46:31 +00:00
Calum Andrew Morrell 2722fbfcd7 Added article, category and taglist views. 2025-02-16 22:38:59 +00:00
Calum Andrew Morrell 89ae91ac35 Corrected db storage of blank TextFields. 2025-02-16 22:37:27 +00:00
Calum Andrew Morrell 8ff2bda49e Linked articles and categories into site admin. 2025-02-16 22:32:14 +00:00
Calum Andrew Morrell 558757318f Added article and category models to articles app. 2025-02-16 22:31:51 +00:00
Calum Andrew Morrell 4dc5935374 Added required applications to uv config. 2025-02-16 22:26:19 +00:00
Calum Andrew Morrell bad5592a9c Created app for articles. 2025-02-16 22:01:41 +00:00
Calum Andrew Morrell e7148362f0 Initial settings configuration. 2025-02-16 21:56:54 +00:00
Calum Andrew Morrell 557dc279c7 Added basic readme file. 2025-02-16 15:28:57 +00:00
35 changed files with 2549 additions and 18 deletions

16
.env-template Normal file
View File

@ -0,0 +1,16 @@
DJANGO_SECRET_KEY=''
DEBUG='True'
ALLOWED_HOSTS=[]
DB_NAME=''
DB_USER=''
DB_PASSWORD=''
DB_HOST='localhost'
DB_PORT=5432
EMAIL_HOST=''
EMAIL_PORT=
EMAIL_USE_TLS='True'
EMAIL_HOST_USER=''
EMAIL_HOST_PASSWORD=''

8
README.rst Normal file
View File

@ -0,0 +1,8 @@
For Cod sake do not use this project as is!
===========================================
I'm making this public on the off chance someone, at an early stage of learning Django, comes across it and finds something useful in the code. This project is designed specifically for my needs and is not intended to be portable to other people without considerable restructuring effort. Besides this, my coding skills are basic to say the least and as soon as the learner viewing the project improves their knowledge they'll be able to work out all the many errors I've made.
I will decide on a license and add it in due course.
Calum.

0
articles/__init__.py Normal file
View File

22
articles/admin.py Normal file
View File

@ -0,0 +1,22 @@
import tagulous.admin
from django.contrib import admin
from markdownx.admin import MarkdownxModelAdmin
from .models import Article, Category
@admin.register(Category)
class CategoryAdmin(admin.ModelAdmin):
list_display = ['title', 'slug']
prepopulated_fields = {'slug': ('title',)}
class ArticleAdmin(MarkdownxModelAdmin):
list_display = ['title', 'subtitle', 'category', 'tags', 'created', 'updated', 'is_published', 'is_featured']
list_filter = ['is_published', 'is_featured', 'category', 'tags']
ordering = ['is_published', '-created']
prepopulated_fields = {'slug': ('title',)}
list_display_links = ['title', 'subtitle']
tagulous.admin.register(Article, ArticleAdmin)

6
articles/apps.py Normal file
View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class ArticlesConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'articles'

View File

@ -0,0 +1,72 @@
# Generated by Django 5.1.6 on 2025-02-16 23:10
import django.db.models.deletion
import markdownx.models
import tagulous.models.fields
import tagulous.models.models
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Category',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=40, unique=True)),
('slug', models.SlugField(max_length=40, unique=True)),
('introduction', models.TextField(blank=True)),
],
options={
'ordering': ['title'],
},
),
migrations.CreateModel(
name='Tagulous_Article_tags',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255, unique=True)),
('slug', models.SlugField()),
('count',
models.IntegerField(default=0, help_text='Internal counter of how many times this tag is in use')),
('protected',
models.BooleanField(default=False, help_text='Will not be deleted when the count reaches 0')),
],
options={
'ordering': ('name',),
'abstract': False,
'unique_together': {('slug',)},
},
bases=(tagulous.models.models.BaseTagModel, models.Model),
),
migrations.CreateModel(
name='Article',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=200)),
('subtitle', models.CharField(blank=True, max_length=200, null=True)),
('slug', models.SlugField(max_length=200, unique=True)),
('introduction', models.TextField(blank=True)),
('body', markdownx.models.MarkdownxField()),
('summary', markdownx.models.MarkdownxField(blank=True)),
('created', models.DateTimeField(auto_now_add=True)),
('updated', models.DateTimeField(auto_now_add=True)),
('is_published', models.BooleanField(default=False)),
('is_updated', models.BooleanField(default=False)),
('is_featured', models.BooleanField(default=False)),
('category', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='articles',
to='articles.category')),
('tags', tagulous.models.fields.TagField(_set_tag_meta=True, force_lowercase=True,
help_text='Enter a comma-separated tag string',
to='articles.tagulous_article_tags')),
],
options={
'ordering': ['-created'],
},
),
]

View File

66
articles/models.py Normal file
View File

@ -0,0 +1,66 @@
from datetime import datetime
from django.db import models
from django.urls import reverse_lazy
from django.utils.text import slugify
from markdownx.models import MarkdownxField
from markdownx.utils import markdownify
from tagulous.models import TagField
class Category(models.Model):
title = models.CharField(max_length=40, unique=True)
slug = models.SlugField(max_length=40, unique=True)
introduction = models.TextField(blank=True)
def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
if not self.slug:
self.slug = slugify(self.title)
return super(Category, self).save()
def get_absolute_url(self):
return reverse_lazy('articles:list_category', args=[self.slug])
def __str__(self):
return self.title
class Meta:
ordering = ['title']
class Article(models.Model):
title = models.CharField(max_length=200)
subtitle = models.CharField(max_length=200, blank=True, null=True)
slug = models.SlugField(max_length=200, unique=True)
category = models.ForeignKey(Category, on_delete=models.PROTECT, related_name='articles')
introduction = models.TextField(blank=True)
body = MarkdownxField()
summary = MarkdownxField(blank=True)
# FIXME: tags with spaces do not match any articles when clicked on.
tags = TagField(force_lowercase=True,
get_absolute_url=lambda tag: reverse_lazy('articles:list_tag', kwargs={'tag': tag.slug}))
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now_add=True)
is_published = models.BooleanField(default=False)
is_updated = models.BooleanField(default=False)
is_featured = models.BooleanField(default=False)
def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
if not self.slug:
self.slug = slugify(self.title)
if self.is_updated:
self.updated = datetime.now()
self.is_updated = False
return super(Article, self).save()
def get_absolute_url(self):
return reverse_lazy('articles:detail', args=[self.category.slug, self.slug])
def body_as_markdown(self):
return markdownify(self.body)
def __str__(self):
return self.title
class Meta:
ordering = ['-created']

View File

@ -0,0 +1,28 @@
{% extends 'base.html' %}
{% block content %}
<article class="max-w-5xl mx-auto my-4 px-8">
<h3 class="text-6xl font-extrabold">{{ article.title }}</h3>
{% if article.subtitle %}
<h5 class="text-4xl italic font-medium">{{ article.subtitle }}</h5>
{% endif %}
<p class="text-sm">Added on the {{ article.created | date:'jS \o\f F, Y' }}
{% if article.created.date != article.updated.date %}
and updated on the {{ article.updated | date:'jS \o\f F, Y' }}
{% endif %}
</p>
<hr class="my-2">
{% if article.introduction %}
<p class="text-justify italic">{{ article.introduction|linebreaksbr }}</p>
<hr class="my-2">
{% endif %}
<div class="prose md:prose-lg lg:prose-xl max-w-none prose-img:rounded-2xl">
{{ article.body_as_markdown|safe }}
</div>
<hr class="my-2">
<div class="flex justify-between text-neutral-400">
<span>Posted in the <a class="hover:text-red-400" href="{% url 'articles:list_category' category=article.category.slug %}">{{ article.category }}</a> category</span>
{# <span>{% include 'articles/includes/display_tags.html' %}</span>#}
</div>
</article>
{% endblock %}

View File

@ -0,0 +1,52 @@
{% extends 'base.html' %}
{% block content %}
<main>
{% if object_list %}
{% if category %}
<h3 class="text-xl sm:text-4xl lg:text-6xl my-4 text-center">{{ category }} Articles</h3>
{% if category.introduction %}
<div class="flex justify-center mb-4">
<p class="sm:w-1/2 lg:w-1/3 mx-8 text-justify">{{ category.introduction }}</p>
</div>
{% endif %}
{% elif tag %}
<h3 class="text-xl sm:text-4xl lg:text-6xl my-4 text-center">Articles for tag: {{ tag }}</h3>
{% else %}
<h3 class="text-xl sm:text-4xl lg:text-6xl my-4 text-center">Recent Articles</h3>
{% endif %}
<hr class="mx-8">
<div class="grid gap-12 grid-cols-1 md:grid-cols-2 lg:grid-cols-3 m-8">
{% for article in object_list %}
<div class="pb-8 break-inside-avoid-column">
<a class="text-3xl font-semibold hover:text-red-400" href="{{ article.get_absolute_url }}">{{ article.title }}</a>
{% if article.subtitle %}
<h5 class="text-xl italic font-medium">{{ article.subtitle }}</h5>
{% endif %}
{% if article.introduction %}
<hr class="my-2">
<p class="text-justify">{{ article.introduction|linebreaksbr }}</p>
{% endif %}
<hr class="my-2">
<div class="flex justify-between text-neutral-400">
<span>
<a class="hover:text-red-400" href="{% url 'articles:list_category' category=article.category.slug %}">{{ article.category }} </a>
&bull; {{ article.created | date:'jS \o\f F, Y' }}
</span>
<span>{% include 'articles/includes/display_tags.html' %}</span>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="text-center m-8">
<h3 class="text-4xl mb-4">Oops!</h3>
<h5 class="text-lg">It looks like there are no articles for that {% if category %}category{% elif tag %}tag{% endif %} yet.</h5>
<p class="text-neutral-400">(sorry not sorry)</p>
</div>
{% endif %}
{% if page_obj.has_previous or page_obj.has_next %}
{% include 'pagination.html' %}
{% endif %}
</main>
{% endblock %}

View File

@ -0,0 +1,21 @@
{% extends 'base.html' %}
{% block content %}
<main>
{% if object_list %}
<h3 class="text-6xl my-4 text-center">Category List</h3>
<hr class="mx-8">
<ul class="grid gap-12 grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 m-8">
{% for category in object_list %}
<li>
<a class="text-2xl font-semibold hover:text-red-400" href="{{ category.get_absolute_url }}">{{ category.title }}</a>
{% if category.introduction %}
<hr class="my-2">
<p class="text-justify">{{ category.introduction }}</p>
{% endif %}
</li>
{% endfor %}
</ul>
{% endif %}
</main>
{% endblock %}

View File

@ -0,0 +1,3 @@
{% for tag in article.tags.all %}
<a href="{{ tag.get_absolute_url }}">{{ tag }}</a>{% if tag != article.tags.last %},{% endif %}
{% endfor %}

View File

@ -0,0 +1,17 @@
{% extends 'base.html' %}
{% block content %}
<main>
{% if object_list %}
<h3 class="text-6xl my-4 text-center">List of Tags</h3>
<hr class="mx-8">
<ul class="grid gap-12 grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 m-8">
{% for tag in object_list %}
<li>
<a class="text-2xl font-semibold hover:text-red-400" href="{{ tag.get_absolute_url }}">{{ tag }}</a>
</li>
{% endfor %}
</ul>
{% endif %}
</main>
{% endblock %}

1
articles/tests.py Normal file
View File

@ -0,0 +1 @@
# Create your tests here.

14
articles/urls.py Normal file
View File

@ -0,0 +1,14 @@
from django.urls import path
from . import views
app_name = 'articles'
urlpatterns = [
path('', views.ArticleList.as_view(), name='list'),
path('categories/', views.CategoryList.as_view(), name='categories'),
path('categories/<slug:category>/', views.ArticleList.as_view(), name='list_category'),
path('tags/', views.TagList.as_view(), name='tags'),
path('tags/<slug:tag>/', views.ArticleList.as_view(), name='list_tag'),
path('<slug:category>/<slug:slug>/', views.ArticleDetail.as_view(), name='detail'),
]

42
articles/views.py Normal file
View File

@ -0,0 +1,42 @@
from django.shortcuts import get_object_or_404
from django.views.generic import DetailView, ListView
from .models import Article, Category
class ArticleList(ListView):
category = None
tag = None
paginate_by = 9
def get_queryset(self):
category_slug = self.kwargs.get('category', None)
self.tag = self.kwargs.get('tag', None)
if category_slug:
self.category = get_object_or_404(Category, slug=category_slug)
return Article.objects.filter(category=self.category).filter(is_published=True)
elif self.tag:
return Article.objects.filter(tags=self.tag).filter(is_published=True)
else:
return Article.objects.filter(is_published=True)
def get_context_data(self, *, object_list=None, **kwargs):
context = super(ArticleList, self).get_context_data(**kwargs)
context['category'] = self.category
context['tag'] = self.tag
return context
class ArticleDetail(DetailView):
model = Article
class CategoryList(ListView):
model = Category
class TagList(ListView):
template_name = 'articles/tag_list.html'
def get_queryset(self):
return Article.tags.tag_model.objects.all()

View File

@ -9,24 +9,26 @@ https://docs.djangoproject.com/en/5.1/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/5.1/ref/settings/
"""
import os
from pathlib import Path
from dotenv import load_dotenv
load_dotenv()
# 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/5.1/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'django-insecure-e7m1a6dwy1^-^zp^*_(ql^c8y5!^0$kf4jwarw^3ny5b(d^t1r'
SECRET_KEY = os.getenv('DJANGO_SECRET_KEY')
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = []
DEBUG = os.getenv('DEBUG')
ALLOWED_HOSTS = eval(os.getenv('ALLOWED_HOSTS'))
# Application definition
@ -37,6 +39,11 @@ INSTALLED_APPS = [
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django.contrib.sites',
'django.contrib.flatpages',
'core.apps.CoreConfig',
'articles.apps.ArticlesConfig',
'markdownx',
]
MIDDLEWARE = [
@ -54,8 +61,7 @@ ROOT_URLCONF = 'config.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [BASE_DIR / 'templates']
,
'DIRS': [BASE_DIR / 'templates'],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
@ -70,18 +76,20 @@ TEMPLATES = [
WSGI_APPLICATION = 'config.wsgi.application'
# Database
# https://docs.djangoproject.com/en/5.1/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
'ENGINE': 'django.db.backends.postgresql',
'NAME': os.getenv('DB_NAME'),
'USER': os.getenv('DB_USER'),
'PASSWORD': os.getenv('DB_PASSWORD'),
'HOST': os.getenv('DB_HOST'),
'PORT': os.getenv('DB_PORT'),
}
}
# Password validation
# https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators
@ -100,11 +108,10 @@ AUTH_PASSWORD_VALIDATORS = [
},
]
# Internationalization
# https://docs.djangoproject.com/en/5.1/topics/i18n/
LANGUAGE_CODE = 'en-us'
LANGUAGE_CODE = 'en-gb'
TIME_ZONE = 'UTC'
@ -112,11 +119,42 @@ USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/5.1/howto/static-files/
STATIC_URL = 'static/'
STATIC_ROOT = BASE_DIR / 'static/'
MEDIA_URL = 'media/'
MEDIA_ROOT = BASE_DIR / 'media/'
# Site id to allow flatpages to function correctly
SITE_ID = 1
# User model and authentication
# LOGIN_REDIRECT_URL = ''
# LOGOUT_REDIRECT_URL = ''
# Email backend configuration
# DEFAULT_FROM_EMAIL = 'calum@drulum.com'
# EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
# EMAIL_HOST = os.getenv('EMAIL_HOST')
# EMAIL_PORT = os.getenv('EMAIL_PORT')
# EMAIL_USE_TLS = (os.getenv('EMAIL_USE_TLS') == 'True')
# EMAIL_HOST_USER = os.getenv('EMAIL_HOST_USER')
# EMAIL_HOST_PASSWORD = os.getenv('EMAIL_HOST_PASSWORD')
# Tagulous config
SERIALIZATION_MODULES = {
'xml': 'tagulous.serializers.xml_serializer',
'json': 'tagulous.serializers.json',
'python': 'tagulous.serializers.python',
'yaml': 'tagulous.serializers.pyyaml',
}
# Markdownx config
MARKDOWNX_MARKDOWN_EXTENSIONS = [
'markdown.extensions.extra',
]
# Default primary key field type
# https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field

View File

@ -15,8 +15,11 @@ Including another URLconf
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('core.urls', namespace='core')),
path('articles/', include('articles.urls', namespace='articles')),
path('markdownx/', include('markdownx.urls')),
]

0
core/__init__.py Normal file
View File

1
core/admin.py Normal file
View File

@ -0,0 +1 @@
# Register your models here.

6
core/apps.py Normal file
View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class CoreConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'core'

View File

1
core/models.py Normal file
View File

@ -0,0 +1 @@
# Create your models here.

1840
core/static/css/base.css Normal file

File diff suppressed because it is too large Load Diff

18
core/templates/base.html Normal file
View File

@ -0,0 +1,18 @@
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Drulum{% endblock %}</title>
<link rel="stylesheet" href="{% static 'css/base.css' %}">
</head>
<body class="font-serif">
{% block page %}
{% block header %}{% include 'header.html' %}{% endblock %}
{% block content %}{% endblock %}
{% block footer %}{% include 'footer.html' %}{% endblock %}
{% endblock %}
</body>
</html>

View File

@ -0,0 +1,46 @@
{% extends 'base.html' %}
{% block content %}
<main>
<div class="max-w-3xl mx-auto my-8">
<h1 class="text-xl sm:text-4xl lg:text-6xl my-4 text-center">Oh my word, this is basic!</h1>
<p class="text-justify mx-8">I got bored and decided to develop this site from scratch, again. See the <a
class="hover:text-red-400" href="{% url 'core:about-website' %}">About This Website</a> page if
you're at all interested in what I'm doing and why. The short version is that I'll be developing
features for the website as I create content for it. In other words, it's going to take a long time
before it's <em>pretty</em>.</p>
<p class="text-justify mx-8 mt-4">Longer term this site will present my own photographs and photography
tuition services along with articles on photography, philosophy, politics, health & mental health, and
whatever else I feel like.</p>
</div>
<hr class="mx-8">
{% if featured_articles %}
<h1 class="text-xl sm:text-4xl lg:text-6xl my-4 text-center">Featured Articles</h1>
<hr class="mx-8">
<div class="grid gap-12 grid-cols-1 md:grid-cols-2 lg:grid-cols-3 m-8">
{% for article in featured_articles %}
<div class="pb-8 break-inside-avoid-column">
<a class="text-3xl font-semibold hover:text-red-400"
href="{{ article.get_absolute_url }}">{{ article.title }}</a>
{% if article.subtitle %}
<h5 class="text-xl italic font-medium">{{ article.subtitle }}</h5>
{% endif %}
{% if article.introduction %}
<hr class="my-2">
<p class="text-justify">{{ article.introduction|linebreaksbr }}</p>
{% endif %}
<hr class="my-2">
<div class="flex justify-between text-neutral-400">
<span>
<a class="hover:text-red-400"
href="{% url 'articles:list_category' category=article.category.slug %}">{{ article.category }} </a>
&bull; {{ article.created | date:'jS \o\f F, Y' }}
</span>
<span>{% include 'articles/includes/display_tags.html' %}</span>
</div>
</div>
{% endfor %}
</div>
{% endif %}
</main>
{% endblock %}

View File

@ -0,0 +1,8 @@
{% extends 'base.html' %}
{% block content %}
<h1 class="text-xl sm:text-4xl lg:text-6xl my-4 text-center">{{ flatpage.title }}</h1>
<div class="text-center mb-4">
{{ flatpage.content }}
</div>
{% endblock %}

View File

@ -0,0 +1,3 @@
<footer class="bg-blue-900 p-8 text-white font-mono">
<p class="text-center">All content &copy; Calum Andrew Morrell</p>
</footer>

View File

@ -0,0 +1,32 @@
<header class="grid grid-cols-1 md:grid-cols-[400px_1fr] md:gap-x-4 bg-blue-900 p-8 text-white">
<div class="row-span-3">
<h3 class="font-sans text-sm"><a href="{% url 'core:homepage' %}">the digital home of</a></h3>
<h1 class="text-4xl font-bold"><a href="{% url 'core:homepage' %}">Calum Andrew Morrell</a></h1>
{# <h2 class="font-mono">Photography · Writing · Audio · Video</h2>#}
</div>
<div class="font-sans md:row-span-3 md:grid md:grid-cols-1">
<nav class="flex gap-x-4 md:justify-end">
<a class="{% if 'about-me' in request.path %}text-cyan-400 uppercase{% endif %} hover:text-red-400"
href="{% url 'core:about-me' %}">about me</a>
<a class="{% if 'about-website' in request.path %}text-cyan-400 uppercase{% endif %} hover:text-red-400"
href="{% url 'core:about-website' %}">about this website</a>
<a class="{% if 'contact-me' in request.path %}text-cyan-400 uppercase{% endif %} hover:text-red-400"
href="{% url 'core:contact-me' %}">contact me</a>
</nav>
<nav class="flex gap-x-4 md:row-span-2 md:items-end text-lg">
<a class="{% if request.path == '/' %}text-cyan-400 uppercase{% endif %} hover:text-red-400"
href="{% url 'core:homepage' %}">Home</a>
<a class="{% if 'articles' in request.path %}text-cyan-400 uppercase{% endif %} hover:text-red-400"
href="{% url 'articles:list' %}">Articles</a>
{# <a class="{% if 'photography' in request.path %}text-cyan-400 uppercase{% endif %} hover:text-red-400"#}
{# href="{% url 'showcase:project_list' %}">Photography</a>#}
{# <a class="{% if 'bear' in request.path %}text-cyan-400 uppercase{% endif %} hover:text-red-400"#}
{# href="{% url 'bear:latest' %}">A Bear Aware</a>#}
{# <a class="{% if 'websites' in request.path %}text-cyan-400 uppercase{% endif %} hover:text-red-400"#}
{# href="{% url 'links:list' %}">Websites</a>#}
{# <a class="{% if 'categories' in request.path %}text-cyan-400 uppercase{% endif %} hover:text-red-400" href="{% url 'articles:categories' %}">Categories</a>#}
{# <a class="{% if 'showcase' in request.path %}text-cyan-400 uppercase{% endif %} hover:text-red-400" href="#">Showcase</a>#}
{# <a class="{% if 'locations' in request.path %}text-cyan-400 uppercase{% endif %} hover:text-red-400" href="#">Locations</a>#}
</nav>
</div>
</header>

View File

@ -0,0 +1,17 @@
<nav>
<hr>
<ul class="flex gap-x-2 justify-center">
{% if page_obj.has_previous %}
<li><a href="?page=1">&laquo; first</a></li>
<li><a href="?page={{ page_obj.previous_page_number }}">previous</a></li>
{% endif %}
{% for page in page_obj.paginator.page_range %}
<li {% if page == page_obj.number %}class="font-bold text-blue-600"{% endif %}><a
href="?page={{ page }}">{{ page }}</a></li>
{% endfor %}
{% if page_obj.has_next %}
<li><a href="?page={{ page_obj.next_page_number }}">next</a></li>
<li><a href="?page={{ page_obj.paginator.num_pages }}">last &raquo;</a></li>
{% endif %}
</ul>
</nav>

1
core/tests.py Normal file
View File

@ -0,0 +1 @@
# Create your tests here.

13
core/urls.py Normal file
View File

@ -0,0 +1,13 @@
from django.contrib.flatpages.views import flatpage
from django.urls import path
from . import views
app_name = 'core'
urlpatterns = [
path('', views.Homepage.as_view(), name='homepage'),
path('about/me/', flatpage, {'url': '/about/me/'}, name='about-me'),
path('about/website/', flatpage, {'url': '/about/website/'}, name='about-website'),
path('about/contact/', flatpage, {'url': '/about/contact/'}, name='contact-me'),
]

14
core/views.py Normal file
View File

@ -0,0 +1,14 @@
from django.views.generic import TemplateView
from articles.models import Article
class Homepage(TemplateView):
template_name = 'core/homepage.html'
def get_context_data(self, **kwargs):
context = super(Homepage, self).get_context_data()
context['featured_articles'] = Article.objects.filter(is_featured=True)
return context
# TODO: place a random website link on the homepage

View File

@ -1,8 +1,13 @@
[project]
name = "drulum"
version = "0.1.0"
description = "Add your description here"
description = "This is the Django project for drulum.com"
requires-python = ">=3.13"
dependencies = [
"django>=5.1.6",
"django-markdownx>=4.0.7",
"django-tagulous>=2.1.0",
"gunicorn>=23.0.0",
"psycopg>=3.2.4",
"python-dotenv>=1.0.1",
]

119
uv.lock
View File

@ -1,4 +1,5 @@
version = 1
revision = 1
requires-python = ">=3.13"
[[package]]
@ -24,16 +25,132 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/75/6f/d2c216d00975e2604b10940937b0ba6b2c2d9b3cc0cc633e414ae3f14b2e/Django-5.1.6-py3-none-any.whl", hash = "sha256:8d203400bc2952fbfb287c2bbda630297d654920c72a73cc82a9ad7926feaad5", size = 8277066 },
]
[[package]]
name = "django-markdownx"
version = "4.0.7"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "django" },
{ name = "markdown" },
{ name = "pillow" },
]
sdist = { url = "https://files.pythonhosted.org/packages/61/b9/e33e16c721ca429c42cdf52f5f52f78023a576eb4f01294385446a3adaee/django-markdownx-4.0.7.tar.gz", hash = "sha256:38aa331c2ca0bee218b77f462361b5393e4727962bc6021939c09048363cb6ea", size = 35697 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/14/0e/1413c3228ef262df351094621e0e1c550a68a7960c23719198b41a6097bc/django_markdownx-4.0.7-py2.py3-none-any.whl", hash = "sha256:c1975ae3053481d4c111abd38997a5b5bb89235a1e3215f995d835942925fe7b", size = 44136 },
]
[[package]]
name = "django-tagulous"
version = "2.1.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "django" },
]
sdist = { url = "https://files.pythonhosted.org/packages/5d/97/f9a157716e3ce00b9d0ae596d9452bc6bb993066337a885a94c96ac7169c/django_tagulous-2.1.0.tar.gz", hash = "sha256:f629b54ad720052092785b0dce056dc6a68c7b63f8126075af9c25848b250bfd", size = 291575 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/dd/7b/14c1c0a8e5378ccebe906676084ada7c7679ad76659a44cb5a55413ba5bc/django_tagulous-2.1.0-py3-none-any.whl", hash = "sha256:5ebba5a51f049f6df5f9d2a30eef431c0bf7cd35758aa0a42fc3351be4d239cd", size = 286294 },
]
[[package]]
name = "drulum"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "django" },
{ name = "django-markdownx" },
{ name = "django-tagulous" },
{ name = "gunicorn" },
{ name = "psycopg" },
{ name = "python-dotenv" },
]
[package.metadata]
requires-dist = [{ name = "django", specifier = ">=5.1.6" }]
requires-dist = [
{ name = "django", specifier = ">=5.1.6" },
{ name = "django-markdownx", specifier = ">=4.0.7" },
{ name = "django-tagulous", specifier = ">=2.1.0" },
{ name = "gunicorn", specifier = ">=23.0.0" },
{ name = "psycopg", specifier = ">=3.2.4" },
{ name = "python-dotenv", specifier = ">=1.0.1" },
]
[[package]]
name = "gunicorn"
version = "23.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "packaging" },
]
sdist = { url = "https://files.pythonhosted.org/packages/34/72/9614c465dc206155d93eff0ca20d42e1e35afc533971379482de953521a4/gunicorn-23.0.0.tar.gz", hash = "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec", size = 375031 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/cb/7d/6dac2a6e1eba33ee43f318edbed4ff29151a49b5d37f080aad1e6469bca4/gunicorn-23.0.0-py3-none-any.whl", hash = "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d", size = 85029 },
]
[[package]]
name = "markdown"
version = "3.7"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/54/28/3af612670f82f4c056911fbbbb42760255801b3068c48de792d354ff4472/markdown-3.7.tar.gz", hash = "sha256:2ae2471477cfd02dbbf038d5d9bc226d40def84b4fe2986e49b59b6b472bbed2", size = 357086 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3f/08/83871f3c50fc983b88547c196d11cf8c3340e37c32d2e9d6152abe2c61f7/Markdown-3.7-py3-none-any.whl", hash = "sha256:7eb6df5690b81a1d7942992c97fad2938e956e79df20cbc6186e9c3a77b1c803", size = 106349 },
]
[[package]]
name = "packaging"
version = "25.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469 },
]
[[package]]
name = "pillow"
version = "11.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f3/af/c097e544e7bd278333db77933e535098c259609c4eb3b85381109602fb5b/pillow-11.1.0.tar.gz", hash = "sha256:368da70808b36d73b4b390a8ffac11069f8a5c85f29eff1f1b01bcf3ef5b2a20", size = 46742715 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b3/31/9ca79cafdce364fd5c980cd3416c20ce1bebd235b470d262f9d24d810184/pillow-11.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ae98e14432d458fc3de11a77ccb3ae65ddce70f730e7c76140653048c71bfcbc", size = 3226640 },
{ url = "https://files.pythonhosted.org/packages/ac/0f/ff07ad45a1f172a497aa393b13a9d81a32e1477ef0e869d030e3c1532521/pillow-11.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cc1331b6d5a6e144aeb5e626f4375f5b7ae9934ba620c0ac6b3e43d5e683a0f0", size = 3101437 },
{ url = "https://files.pythonhosted.org/packages/08/2f/9906fca87a68d29ec4530be1f893149e0cb64a86d1f9f70a7cfcdfe8ae44/pillow-11.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:758e9d4ef15d3560214cddbc97b8ef3ef86ce04d62ddac17ad39ba87e89bd3b1", size = 4326605 },
{ url = "https://files.pythonhosted.org/packages/b0/0f/f3547ee15b145bc5c8b336401b2d4c9d9da67da9dcb572d7c0d4103d2c69/pillow-11.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b523466b1a31d0dcef7c5be1f20b942919b62fd6e9a9be199d035509cbefc0ec", size = 4411173 },
{ url = "https://files.pythonhosted.org/packages/b1/df/bf8176aa5db515c5de584c5e00df9bab0713548fd780c82a86cba2c2fedb/pillow-11.1.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:9044b5e4f7083f209c4e35aa5dd54b1dd5b112b108648f5c902ad586d4f945c5", size = 4369145 },
{ url = "https://files.pythonhosted.org/packages/de/7c/7433122d1cfadc740f577cb55526fdc39129a648ac65ce64db2eb7209277/pillow-11.1.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:3764d53e09cdedd91bee65c2527815d315c6b90d7b8b79759cc48d7bf5d4f114", size = 4496340 },
{ url = "https://files.pythonhosted.org/packages/25/46/dd94b93ca6bd555588835f2504bd90c00d5438fe131cf01cfa0c5131a19d/pillow-11.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:31eba6bbdd27dde97b0174ddf0297d7a9c3a507a8a1480e1e60ef914fe23d352", size = 4296906 },
{ url = "https://files.pythonhosted.org/packages/a8/28/2f9d32014dfc7753e586db9add35b8a41b7a3b46540e965cb6d6bc607bd2/pillow-11.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b5d658fbd9f0d6eea113aea286b21d3cd4d3fd978157cbf2447a6035916506d3", size = 4431759 },
{ url = "https://files.pythonhosted.org/packages/33/48/19c2cbe7403870fbe8b7737d19eb013f46299cdfe4501573367f6396c775/pillow-11.1.0-cp313-cp313-win32.whl", hash = "sha256:f86d3a7a9af5d826744fabf4afd15b9dfef44fe69a98541f666f66fbb8d3fef9", size = 2291657 },
{ url = "https://files.pythonhosted.org/packages/3b/ad/285c556747d34c399f332ba7c1a595ba245796ef3e22eae190f5364bb62b/pillow-11.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:593c5fd6be85da83656b93ffcccc2312d2d149d251e98588b14fbc288fd8909c", size = 2626304 },
{ url = "https://files.pythonhosted.org/packages/e5/7b/ef35a71163bf36db06e9c8729608f78dedf032fc8313d19bd4be5c2588f3/pillow-11.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:11633d58b6ee5733bde153a8dafd25e505ea3d32e261accd388827ee987baf65", size = 2375117 },
{ url = "https://files.pythonhosted.org/packages/79/30/77f54228401e84d6791354888549b45824ab0ffde659bafa67956303a09f/pillow-11.1.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:70ca5ef3b3b1c4a0812b5c63c57c23b63e53bc38e758b37a951e5bc466449861", size = 3230060 },
{ url = "https://files.pythonhosted.org/packages/ce/b1/56723b74b07dd64c1010fee011951ea9c35a43d8020acd03111f14298225/pillow-11.1.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8000376f139d4d38d6851eb149b321a52bb8893a88dae8ee7d95840431977081", size = 3106192 },
{ url = "https://files.pythonhosted.org/packages/e1/cd/7bf7180e08f80a4dcc6b4c3a0aa9e0b0ae57168562726a05dc8aa8fa66b0/pillow-11.1.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ee85f0696a17dd28fbcfceb59f9510aa71934b483d1f5601d1030c3c8304f3c", size = 4446805 },
{ url = "https://files.pythonhosted.org/packages/97/42/87c856ea30c8ed97e8efbe672b58c8304dee0573f8c7cab62ae9e31db6ae/pillow-11.1.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:dd0e081319328928531df7a0e63621caf67652c8464303fd102141b785ef9547", size = 4530623 },
{ url = "https://files.pythonhosted.org/packages/ff/41/026879e90c84a88e33fb00cc6bd915ac2743c67e87a18f80270dfe3c2041/pillow-11.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e63e4e5081de46517099dc30abe418122f54531a6ae2ebc8680bcd7096860eab", size = 4465191 },
{ url = "https://files.pythonhosted.org/packages/e5/fb/a7960e838bc5df57a2ce23183bfd2290d97c33028b96bde332a9057834d3/pillow-11.1.0-cp313-cp313t-win32.whl", hash = "sha256:dda60aa465b861324e65a78c9f5cf0f4bc713e4309f83bc387be158b077963d9", size = 2295494 },
{ url = "https://files.pythonhosted.org/packages/d7/6c/6ec83ee2f6f0fda8d4cf89045c6be4b0373ebfc363ba8538f8c999f63fcd/pillow-11.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ad5db5781c774ab9a9b2c4302bbf0c1014960a0a7be63278d13ae6fdf88126fe", size = 2631595 },
{ url = "https://files.pythonhosted.org/packages/cf/6c/41c21c6c8af92b9fea313aa47c75de49e2f9a467964ee33eb0135d47eb64/pillow-11.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:67cd427c68926108778a9005f2a04adbd5e67c442ed21d95389fe1d595458756", size = 2377651 },
]
[[package]]
name = "psycopg"
version = "3.2.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "tzdata", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e0/f2/954b1467b3e2ca5945b83b5e320268be1f4df486c3e8ffc90f4e4b707979/psycopg-3.2.4.tar.gz", hash = "sha256:f26f1346d6bf1ef5f5ef1714dd405c67fb365cfd1c6cea07de1792747b167b92", size = 156109 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/40/49/15114d5f7ee68983f4e1a24d47e75334568960352a07c6f0e796e912685d/psycopg-3.2.4-py3-none-any.whl", hash = "sha256:43665368ccd48180744cab26b74332f46b63b7e06e8ce0775547a3533883d381", size = 198716 },
]
[[package]]
name = "python-dotenv"
version = "1.0.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/bc/57/e84d88dfe0aec03b7a2d4327012c1627ab5f03652216c63d49846d7a6c58/python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca", size = 39115 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/6a/3e/b68c118422ec867fa7ab88444e1274aa40681c606d59ac27de5a5588f082/python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a", size = 19863 },
]
[[package]]
name = "sqlparse"