summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMark Powers <mark@marks.kitchen>2024-07-14 15:30:27 -0500
committerMark Powers <mark@marks.kitchen>2024-07-14 15:30:27 -0500
commitad232020d57d2c77dfd5400dc4a8290b484c8ba2 (patch)
tree3e2121666986c67fe554a1e9046682b7370d412e
Initial commitmain
-rw-r--r--.gitignore1
-rw-r--r--Dockerfile15
-rw-r--r--docker-compose.yaml23
-rw-r--r--journal/__init__.py0
-rw-r--r--journal/api/__init__.py0
-rw-r--r--journal/api/admin.py22
-rw-r--r--journal/api/apps.py6
-rw-r--r--journal/api/migrations/0001_initial.py39
-rw-r--r--journal/api/migrations/0002_source_title_alter_entry_created.py24
-rw-r--r--journal/api/migrations/__init__.py0
-rw-r--r--journal/api/models.py26
-rw-r--r--journal/api/serializers.py23
-rw-r--r--journal/api/tests.py3
-rw-r--r--journal/api/views.py41
-rw-r--r--journal/apps.py6
-rw-r--r--journal/asgi.py16
-rw-r--r--journal/migrations/__init__.py0
-rw-r--r--journal/settings.py134
-rw-r--r--journal/urls.py13
-rw-r--r--journal/wsgi.py16
-rwxr-xr-xmanage.py22
-rw-r--r--requirements.txt8
22 files changed, 438 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..0d20b64
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+*.pyc
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..70f65a6
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,15 @@
+FROM python:3.10.12 as base
+
+WORKDIR /project
+
+VOLUME ["/static"]
+VOLUME ["/media"]
+VOLUME ["/data"]
+
+COPY requirements.txt /project/
+
+RUN pip install -r requirements.txt
+
+COPY . /project
+
+EXPOSE 80 443
diff --git a/docker-compose.yaml b/docker-compose.yaml
new file mode 100644
index 0000000..a72cee7
--- /dev/null
+++ b/docker-compose.yaml
@@ -0,0 +1,23 @@
+version: "3.5"
+
+services:
+ journal_api:
+ container_name: journal_api
+ image: mppowers/journal_api
+ restart: always
+ env_file:
+ - env
+ volumes:
+ - ./media:/media
+ - static:/static
+ - ./data:/data
+ command: ["gunicorn", "--max-requests", "1000", "--max-requests-jitter", "50", "journal.wsgi", "--bind=0.0.0.0:80", "--capture-output", "--access-logfile" ,"-" ]
+
+ static_files:
+ image: nginx
+ restart: always
+ volumes:
+ - static:/usr/share/nginx/html/static
+
+volumes:
+ static:
diff --git a/journal/__init__.py b/journal/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/journal/__init__.py
diff --git a/journal/api/__init__.py b/journal/api/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/journal/api/__init__.py
diff --git a/journal/api/admin.py b/journal/api/admin.py
new file mode 100644
index 0000000..8ba4fdd
--- /dev/null
+++ b/journal/api/admin.py
@@ -0,0 +1,22 @@
+from django.contrib import admin
+
+from . import models
+
+
+class AuthorAdmin(admin.ModelAdmin):
+ list_display = ["author"]
+
+admin.site.register(models.Author, AuthorAdmin)
+
+
+class SourceAdmin(admin.ModelAdmin):
+ list_display = ["title", "author", "url"]
+
+admin.site.register(models.Source, SourceAdmin)
+
+
+class EntryAdmin(admin.ModelAdmin):
+ list_display = ["created", "source"]
+ list_filter = ["source"]
+
+admin.site.register(models.Entry, EntryAdmin)
diff --git a/journal/api/apps.py b/journal/api/apps.py
new file mode 100644
index 0000000..e68ac76
--- /dev/null
+++ b/journal/api/apps.py
@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class ApiConfig(AppConfig):
+ name = 'journal.api'
+ label = 'journal_api'
diff --git a/journal/api/migrations/0001_initial.py b/journal/api/migrations/0001_initial.py
new file mode 100644
index 0000000..ad1f787
--- /dev/null
+++ b/journal/api/migrations/0001_initial.py
@@ -0,0 +1,39 @@
+# Generated by Django 5.0.4 on 2024-04-30 02:08
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Author',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('author', models.CharField(max_length=1024)),
+ ],
+ ),
+ migrations.CreateModel(
+ name='Source',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('url', models.CharField(max_length=1024)),
+ ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='journal_api.author')),
+ ],
+ ),
+ migrations.CreateModel(
+ name='Entry',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('created', models.DateTimeField()),
+ ('html_text', models.TextField()),
+ ('source', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='entries', to='journal_api.source')),
+ ],
+ ),
+ ]
diff --git a/journal/api/migrations/0002_source_title_alter_entry_created.py b/journal/api/migrations/0002_source_title_alter_entry_created.py
new file mode 100644
index 0000000..26ae3b1
--- /dev/null
+++ b/journal/api/migrations/0002_source_title_alter_entry_created.py
@@ -0,0 +1,24 @@
+# Generated by Django 5.0.4 on 2024-04-30 02:13
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('journal_api', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='source',
+ name='title',
+ field=models.CharField(default='', max_length=1024),
+ preserve_default=False,
+ ),
+ migrations.AlterField(
+ model_name='entry',
+ name='created',
+ field=models.DateField(),
+ ),
+ ]
diff --git a/journal/api/migrations/__init__.py b/journal/api/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/journal/api/migrations/__init__.py
diff --git a/journal/api/models.py b/journal/api/models.py
new file mode 100644
index 0000000..fb5dac4
--- /dev/null
+++ b/journal/api/models.py
@@ -0,0 +1,26 @@
+from django.db import models
+
+class Author(models.Model):
+ author = models.CharField(max_length=1024)
+
+ def __str__(self):
+ return self.author
+
+
+class Source(models.Model):
+ url = models.CharField(max_length=1024)
+ title = models.CharField(max_length=1024)
+ author = models.ForeignKey(Author, on_delete=models.CASCADE)
+
+ def __str__(self):
+ return f"{self.author} -{self.title}"
+
+
+class Entry(models.Model):
+ created = models.DateField()
+ html_text = models.TextField()
+ source = models.ForeignKey(
+ Source, on_delete=models.CASCADE, related_name="entries")
+
+ def __str__(self):
+ return f"{self.source} - {self.created}"
diff --git a/journal/api/serializers.py b/journal/api/serializers.py
new file mode 100644
index 0000000..ced248b
--- /dev/null
+++ b/journal/api/serializers.py
@@ -0,0 +1,23 @@
+from . import models
+from rest_framework import serializers
+
+
+class AuthorSerializer(serializers.ModelSerializer):
+ class Meta:
+ exclude = ["id"]
+ model = models.Author
+
+
+class SourceSerializer(serializers.ModelSerializer):
+ class Meta:
+ exclude = ["id"]
+ model = models.Source
+
+ author = AuthorSerializer()
+
+class EntrySerializer(serializers.ModelSerializer):
+ class Meta:
+ exclude = ["id"]
+ model = models.Entry
+
+ source = SourceSerializer()
diff --git a/journal/api/tests.py b/journal/api/tests.py
new file mode 100644
index 0000000..7ce503c
--- /dev/null
+++ b/journal/api/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/journal/api/views.py b/journal/api/views.py
new file mode 100644
index 0000000..a88e560
--- /dev/null
+++ b/journal/api/views.py
@@ -0,0 +1,41 @@
+from rest_framework import viewsets
+from rest_framework import filters
+import datetime
+
+from . import serializers
+from . import models
+
+
+class CustomDateFilterBackend(filters.BaseFilterBackend):
+ def filter_queryset(self, request, queryset, view):
+ if "today" in request.query_params:
+ today = datetime.date.today()
+ queryset = queryset.filter(created__month=today.month, created__day=today.day)
+ day = request.query_params.get("day")
+ if day:
+ try:
+ queryset = queryset.filter(created__day=day)
+ except ValueError:
+ pass
+ month = request.query_params.get("month")
+ if month:
+ try:
+ queryset = queryset.filter(created__month=month)
+ except ValueError:
+ pass
+ year = request.query_params.get("year")
+ if year:
+ try:
+ queryset = queryset.filter(created__year=year)
+ except ValueError:
+ pass
+ return queryset
+
+
+class EntrySerializer(viewsets.ReadOnlyModelViewSet):
+ queryset = models.Entry.objects.all()
+ serializer_class = serializers.EntrySerializer
+ filter_backends = [
+ CustomDateFilterBackend, filters.OrderingFilter, filters.SearchFilter]
+ search_fields = ["html_text"]
+ ordering_fields = ["created"]
diff --git a/journal/apps.py b/journal/apps.py
new file mode 100644
index 0000000..11ef107
--- /dev/null
+++ b/journal/apps.py
@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class ApiConfig(AppConfig):
+ name = 'journal'
+ label = 'journal'
diff --git a/journal/asgi.py b/journal/asgi.py
new file mode 100644
index 0000000..52a0537
--- /dev/null
+++ b/journal/asgi.py
@@ -0,0 +1,16 @@
+"""
+ASGI config for journal project.
+
+It exposes the ASGI callable as a module-level variable named ``application``.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/5.0/howto/deployment/asgi/
+"""
+
+import os
+
+from django.core.asgi import get_asgi_application
+
+os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'journal.settings')
+
+application = get_asgi_application()
diff --git a/journal/migrations/__init__.py b/journal/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/journal/migrations/__init__.py
diff --git a/journal/settings.py b/journal/settings.py
new file mode 100644
index 0000000..73a9678
--- /dev/null
+++ b/journal/settings.py
@@ -0,0 +1,134 @@
+from pathlib import Path
+import os
+
+# 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.0/howto/deployment/checklist/
+
+# SECURITY WARNING: keep the secret key used in production secret!
+SECRET_KEY = os.environ["SECRET_KEY"]
+
+# SECURITY WARNING: don't run with debug turned on in production!
+DEBUG = False
+
+ALLOWED_HOSTS = [os.environ["ALLOWED_HOST"]]
+
+
+# Application definition
+
+INSTALLED_APPS = [
+ 'django.contrib.admin',
+ 'django.contrib.auth',
+ 'django.contrib.contenttypes',
+ 'django.contrib.sessions',
+ 'django.contrib.messages',
+ 'django.contrib.staticfiles',
+ 'rest_framework',
+ 'django_filters',
+ 'journal',
+ 'journal.api',
+]
+
+MIDDLEWARE = [
+ 'django.middleware.security.SecurityMiddleware',
+ 'django.contrib.sessions.middleware.SessionMiddleware',
+ 'django.middleware.common.CommonMiddleware',
+ 'django.middleware.csrf.CsrfViewMiddleware',
+ 'django.contrib.auth.middleware.AuthenticationMiddleware',
+ 'django.contrib.messages.middleware.MessageMiddleware',
+ 'django.middleware.clickjacking.XFrameOptionsMiddleware',
+]
+
+ROOT_URLCONF = 'journal.urls'
+
+TEMPLATES = [
+ {
+ 'BACKEND': 'django.template.backends.django.DjangoTemplates',
+ 'DIRS': [],
+ 'APP_DIRS': True,
+ 'OPTIONS': {
+ 'context_processors': [
+ 'django.template.context_processors.debug',
+ 'django.template.context_processors.request',
+ 'django.contrib.auth.context_processors.auth',
+ 'django.contrib.messages.context_processors.messages',
+ ],
+ },
+ },
+]
+
+WSGI_APPLICATION = 'journal.wsgi.application'
+
+
+# Database
+# https://docs.djangoproject.com/en/5.0/ref/settings/#databases
+
+DATABASES = {
+ 'default': {
+ 'ENGINE': 'django.db.backends.sqlite3',
+ 'NAME': './data/db.sqlite3',
+ }
+}
+
+
+# Password validation
+# https://docs.djangoproject.com/en/5.0/ref/settings/#auth-password-validators
+
+AUTH_PASSWORD_VALIDATORS = [
+ {
+ 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
+ },
+ {
+ 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
+ },
+ {
+ 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
+ },
+ {
+ 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
+ },
+]
+
+
+# Internationalization
+# https://docs.djangoproject.com/en/5.0/topics/i18n/
+
+LANGUAGE_CODE = 'en-us'
+
+TIME_ZONE = 'UTC'
+
+USE_I18N = True
+
+USE_TZ = True
+
+
+# Static files (CSS, JavaScript, Images)
+# https://docs.djangoproject.com/en/5.0/howto/static-files/
+
+STATIC_URL = 'static/'
+
+# Default primary key field type
+# https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field
+
+DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
+
+
+REST_FRAMEWORK = {
+ 'DEFAULT_THROTTLE_CLASSES': [
+ 'rest_framework.throttling.AnonRateThrottle',
+ ],
+ 'DEFAULT_THROTTLE_RATES': {
+ 'anon': '60/minute',
+ }
+}
+
+STATIC_ROOT = "/static"
+STATIC_URL = "/static/"
+STATICFILES_DIRS = (os.path.join(BASE_DIR, "journal", "static"),)
+STATICFILES_FINDERS = (
+ "django.contrib.staticfiles.finders.FileSystemFinder",
+ "django.contrib.staticfiles.finders.AppDirectoriesFinder",
+)
diff --git a/journal/urls.py b/journal/urls.py
new file mode 100644
index 0000000..c5d4439
--- /dev/null
+++ b/journal/urls.py
@@ -0,0 +1,13 @@
+from django.contrib import admin
+from django.urls import path, include
+
+from rest_framework import routers
+from .api import views
+
+router = routers.DefaultRouter()
+router.register("entries", views.EntrySerializer)
+
+urlpatterns = [
+ path('admin/', admin.site.urls),
+ path("", include(router.urls)),
+]
diff --git a/journal/wsgi.py b/journal/wsgi.py
new file mode 100644
index 0000000..b172983
--- /dev/null
+++ b/journal/wsgi.py
@@ -0,0 +1,16 @@
+"""
+WSGI config for journal project.
+
+It exposes the WSGI callable as a module-level variable named ``application``.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/5.0/howto/deployment/wsgi/
+"""
+
+import os
+
+from django.core.wsgi import get_wsgi_application
+
+os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'journal.settings')
+
+application = get_wsgi_application()
diff --git a/manage.py b/manage.py
new file mode 100755
index 0000000..17f384c
--- /dev/null
+++ b/manage.py
@@ -0,0 +1,22 @@
+#!/usr/bin/env python
+"""Django's command-line utility for administrative tasks."""
+import os
+import sys
+
+
+def main():
+ """Run administrative tasks."""
+ os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'journal.settings')
+ try:
+ from django.core.management import execute_from_command_line
+ except ImportError as exc:
+ raise ImportError(
+ "Couldn't import Django. Are you sure it's installed and "
+ "available on your PYTHONPATH environment variable? Did you "
+ "forget to activate a virtual environment?"
+ ) from exc
+ execute_from_command_line(sys.argv)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..fe7ec3c
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,8 @@
+asgiref==3.8.1
+Django==5.0.4
+django-filter==24.2
+djangorestframework==3.15.1
+gunicorn==22.0.0
+packaging==24.0
+sqlparse==0.5.0
+typing_extensions==4.11.0