commit
ba4a6822f2
|
|
@ -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=''
|
||||
|
|
@ -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,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)
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class ArticlesConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'articles'
|
||||
|
|
@ -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'],
|
||||
},
|
||||
),
|
||||
]
|
||||
|
|
@ -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']
|
||||
|
|
@ -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 %}
|
||||
|
|
@ -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>
|
||||
• {{ 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 %}
|
||||
|
|
@ -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 %}
|
||||
|
|
@ -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 %}
|
||||
|
|
@ -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 %}
|
||||
|
|
@ -0,0 +1 @@
|
|||
# Create your tests here.
|
||||
|
|
@ -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'),
|
||||
]
|
||||
|
|
@ -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()
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,0 +1 @@
|
|||
# Register your models here.
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class CoreConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'core'
|
||||
|
|
@ -0,0 +1 @@
|
|||
# Create your models here.
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
• {{ article.created | date:'jS \o\f F, Y' }}
|
||||
</span>
|
||||
<span>{% include 'articles/includes/display_tags.html' %}</span>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</main>
|
||||
{% endblock %}
|
||||
|
|
@ -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 %}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
<footer class="bg-blue-900 p-8 text-white font-mono">
|
||||
<p class="text-center">All content © Calum Andrew Morrell</p>
|
||||
</footer>
|
||||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
<nav>
|
||||
<hr>
|
||||
<ul class="flex gap-x-2 justify-center">
|
||||
{% if page_obj.has_previous %}
|
||||
<li><a href="?page=1">« 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 »</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
|
|
@ -0,0 +1 @@
|
|||
# Create your tests here.
|
||||
|
|
@ -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'),
|
||||
]
|
||||
|
|
@ -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
|
||||
|
|
@ -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
119
uv.lock
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Reference in New Issue