diff --git a/.gitignore b/.gitignore index cfe8779..e1dba2e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ /build +*/uploads __pycache__ *.pyc .venv +*.sqlite3 diff --git a/README.md b/README.md index d08d610..d7112f5 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,19 @@ # wedding-site-backend +Web backend for hosting a wedding website. + +## Run development server: + +```sh +source .venv/bin/activate +cd wedding_site +python manage.py runserver +``` + +## Do Migrations + +```sh +cd wedding_site +python manage.py makemigrations primary +python manage.py migrate +``` \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index dddfeed..2e2e3ab 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,4 @@ django markdown +pillow +beautifulsoup4 diff --git a/wedding_site/manage.py b/wedding_site/manage.py new file mode 100755 index 0000000..586897d --- /dev/null +++ b/wedding_site/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', 'wedding_site.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/wedding_site/primary/__init__.py b/wedding_site/primary/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/wedding_site/primary/admin.py b/wedding_site/primary/admin.py new file mode 100644 index 0000000..e49b3c3 --- /dev/null +++ b/wedding_site/primary/admin.py @@ -0,0 +1,9 @@ +from django.contrib import admin + +# Register your models here. + +from .models import Site, Page, Image + +admin.site.register(Site) +admin.site.register(Page) +admin.site.register(Image) \ No newline at end of file diff --git a/wedding_site/primary/apps.py b/wedding_site/primary/apps.py new file mode 100644 index 0000000..bffd2c7 --- /dev/null +++ b/wedding_site/primary/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class PrimaryConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'primary' diff --git a/wedding_site/primary/migrations/0001_initial.py b/wedding_site/primary/migrations/0001_initial.py new file mode 100644 index 0000000..fad6d38 --- /dev/null +++ b/wedding_site/primary/migrations/0001_initial.py @@ -0,0 +1,40 @@ +# Generated by Django 5.0.2 on 2024-02-10 17:58 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Image', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=200)), + ('alt_text', models.TextField()), + ], + ), + migrations.CreateModel( + name='Site', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=200)), + ('index', models.TextField()), + ], + ), + migrations.CreateModel( + name='Page', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('content', models.TextField()), + ('name', models.CharField(max_length=200)), + ('site', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='primary.site')), + ], + ), + ] diff --git a/wedding_site/primary/migrations/0002_page_navigable_page_published_site_password.py b/wedding_site/primary/migrations/0002_page_navigable_page_published_site_password.py new file mode 100644 index 0000000..6488e51 --- /dev/null +++ b/wedding_site/primary/migrations/0002_page_navigable_page_published_site_password.py @@ -0,0 +1,28 @@ +# Generated by Django 5.0.2 on 2024-02-10 18:04 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('primary', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='page', + name='navigable', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='page', + name='published', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='site', + name='password', + field=models.CharField(default='', max_length=200), + ), + ] diff --git a/wedding_site/primary/migrations/0003_site_name.py b/wedding_site/primary/migrations/0003_site_name.py new file mode 100644 index 0000000..134e77c --- /dev/null +++ b/wedding_site/primary/migrations/0003_site_name.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0.2 on 2024-02-10 18:09 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('primary', '0002_page_navigable_page_published_site_password'), + ] + + operations = [ + migrations.AddField( + model_name='site', + name='name', + field=models.CharField(default='', max_length=200), + ), + ] diff --git a/wedding_site/primary/migrations/0004_template.py b/wedding_site/primary/migrations/0004_template.py new file mode 100644 index 0000000..6f215a2 --- /dev/null +++ b/wedding_site/primary/migrations/0004_template.py @@ -0,0 +1,21 @@ +# Generated by Django 5.0.2 on 2024-02-11 11:56 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('primary', '0003_site_name'), + ] + + operations = [ + migrations.CreateModel( + name='Template', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(default='', max_length=200)), + ('content', models.TextField()), + ], + ), + ] diff --git a/wedding_site/primary/migrations/0005_image_content_image_height_image_width.py b/wedding_site/primary/migrations/0005_image_content_image_height_image_width.py new file mode 100644 index 0000000..28c202e --- /dev/null +++ b/wedding_site/primary/migrations/0005_image_content_image_height_image_width.py @@ -0,0 +1,28 @@ +# Generated by Django 5.0.2 on 2024-02-11 12:22 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('primary', '0004_template'), + ] + + operations = [ + migrations.AddField( + model_name='image', + name='content', + field=models.ImageField(default=None, height_field='height', upload_to='uploads', width_field='width'), + ), + migrations.AddField( + model_name='image', + name='height', + field=models.IntegerField(default=0), + ), + migrations.AddField( + model_name='image', + name='width', + field=models.IntegerField(default=0), + ), + ] diff --git a/wedding_site/primary/migrations/0006_site_header.py b/wedding_site/primary/migrations/0006_site_header.py new file mode 100644 index 0000000..eb129e7 --- /dev/null +++ b/wedding_site/primary/migrations/0006_site_header.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0.2 on 2024-02-11 12:55 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('primary', '0005_image_content_image_height_image_width'), + ] + + operations = [ + migrations.AddField( + model_name='site', + name='header', + field=models.TextField(default=''), + ), + ] diff --git a/wedding_site/primary/migrations/0007_site_page_header.py b/wedding_site/primary/migrations/0007_site_page_header.py new file mode 100644 index 0000000..19ef19c --- /dev/null +++ b/wedding_site/primary/migrations/0007_site_page_header.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0.2 on 2024-02-11 12:57 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('primary', '0006_site_header'), + ] + + operations = [ + migrations.AddField( + model_name='site', + name='page_header', + field=models.TextField(default=''), + ), + ] diff --git a/wedding_site/primary/migrations/0008_delete_template.py b/wedding_site/primary/migrations/0008_delete_template.py new file mode 100644 index 0000000..e059df4 --- /dev/null +++ b/wedding_site/primary/migrations/0008_delete_template.py @@ -0,0 +1,16 @@ +# Generated by Django 5.0.2 on 2024-02-11 13:13 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('primary', '0007_site_page_header'), + ] + + operations = [ + migrations.DeleteModel( + name='Template', + ), + ] diff --git a/wedding_site/primary/migrations/0009_page_pretty_name.py b/wedding_site/primary/migrations/0009_page_pretty_name.py new file mode 100644 index 0000000..58475cf --- /dev/null +++ b/wedding_site/primary/migrations/0009_page_pretty_name.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0.2 on 2024-02-11 13:17 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('primary', '0008_delete_template'), + ] + + operations = [ + migrations.AddField( + model_name='page', + name='pretty_name', + field=models.CharField(default='', max_length=200), + ), + ] diff --git a/wedding_site/primary/migrations/__init__.py b/wedding_site/primary/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/wedding_site/primary/models.py b/wedding_site/primary/models.py new file mode 100644 index 0000000..a73c009 --- /dev/null +++ b/wedding_site/primary/models.py @@ -0,0 +1,34 @@ +from django.db import models + +class Site(models.Model): + name = models.CharField(max_length=200, default="") + title = models.CharField(max_length=200) + header = models.TextField(default="") + page_header = models.TextField(default="") + index = models.TextField() + password = models.CharField(max_length=200, default="") + + def __str__(self): + return self.name + +class Page(models.Model): + site = models.ForeignKey(Site, on_delete=models.CASCADE) + content = models.TextField() + name = models.CharField(max_length=200) + pretty_name = models.CharField(max_length=200, default="") + published = models.BooleanField(default=False) + navigable = models.BooleanField(default=False) + + def __str__(self): + return self.name + +class Image(models.Model): + name = models.CharField(max_length=200) + content = models.ImageField(upload_to="uploads", + width_field="width", height_field="height", default=None) + width = models.IntegerField(default=0) + height = models.IntegerField(default=0) + alt_text = models.TextField() + + def __str__(self): + return self.name diff --git a/wedding_site/primary/tests.py b/wedding_site/primary/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/wedding_site/primary/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/wedding_site/primary/urls.py b/wedding_site/primary/urls.py new file mode 100644 index 0000000..3fe02fa --- /dev/null +++ b/wedding_site/primary/urls.py @@ -0,0 +1,12 @@ + +from django.urls import path + +from . import views + +urlpatterns = [ + path("", views.index, name="index"), + path("schedule", views.schedule, name="schedule"), + path("thingstodo", views.things_to_do, name="thingstodo"), + path("travel", views.travel, name="travel"), +] + diff --git a/wedding_site/primary/views.py b/wedding_site/primary/views.py new file mode 100644 index 0000000..c315607 --- /dev/null +++ b/wedding_site/primary/views.py @@ -0,0 +1,112 @@ +from django.shortcuts import render +from django.http import HttpResponse + +from django.template import Template +from django.template import Context +import markdown +from bs4 import BeautifulSoup + +from .models import Site, Page + +_TEMPLATE = """ + + + + {{site_header}} + + + {{page_body}} + + + +""" + +_BODY = """ +
SAVE the DATE
+ +
JAMES AND FIONNUALA
+ +
+
+ + + +
+
+""" + +_HEADER = """ + {{site.title}} + + +""" + +def get_site_header(site): + template = Template(_HEADER) + context = Context({"site": site}) + return template.render(context) + +def get_index_body(site): + template = Template(_BODY) + context = Context({"site": site}) + return template.render(context) + +def index(request): + site = Site.objects.get(pk=1) + + template = Template(_TEMPLATE) + context = Context({"site": site, + "site_header" : get_site_header(site), + "page_body" : get_index_body(site)}) + + soup = BeautifulSoup(template.render(context)) + return HttpResponse(soup.prettify()) + +def schedule(request): + return get_page("Schedule") + +def things_to_do(request): + return get_page("ThingsToDo") + +def travel(request): + return get_page("Travel") + + +_PAGE_HEADER = """ +
+ +
+""" + +def get_page_header(site: Site): + + pages = site.page_set.filter(navigable=True) + template = Template(_PAGE_HEADER) + context = Context({"pages", pages}) + return template.render(context) + +def get_page_body(site: Site, page: Page): + + header = get_page_header(site) + body = markdown.markdown(page.content) + return header + body + +def get_page(name:str): + site = Site.objects.get(pk=1) + page = Page.objects.get(name=name) + + template = Template(_TEMPLATE) + context = Context({"site": site, + "site_header" : get_site_header(site), + "page_body" : get_page_body(site, page)}) + + soup = BeautifulSoup(template.render(context)) + return HttpResponse(soup.prettify()) \ No newline at end of file diff --git a/wedding_site/wedding_site/__init__.py b/wedding_site/wedding_site/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/wedding_site/wedding_site/asgi.py b/wedding_site/wedding_site/asgi.py new file mode 100644 index 0000000..9357dee --- /dev/null +++ b/wedding_site/wedding_site/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for wedding_site 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', 'wedding_site.settings') + +application = get_asgi_application() diff --git a/wedding_site/wedding_site/settings.py b/wedding_site/wedding_site/settings.py new file mode 100644 index 0000000..022b19a --- /dev/null +++ b/wedding_site/wedding_site/settings.py @@ -0,0 +1,124 @@ +""" +Django settings for wedding_site project. + +Generated by 'django-admin startproject' using Django 5.0.2. + +For more information on this file, see +https://docs.djangoproject.com/en/5.0/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/5.0/ref/settings/ +""" + +from pathlib import Path + +# 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 = 'django-insecure-bj9fez3qztt5e2lrzpgh%%nat@w^kn!k@l92l=+#%wm)4)p^5m' + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = [] + + +# Application definition + +INSTALLED_APPS = [ + 'primary.apps.PrimaryConfig', + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', +] + +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 = 'wedding_site.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 = 'wedding_site.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/5.0/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': BASE_DIR / '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' diff --git a/wedding_site/wedding_site/urls.py b/wedding_site/wedding_site/urls.py new file mode 100644 index 0000000..2e78c8d --- /dev/null +++ b/wedding_site/wedding_site/urls.py @@ -0,0 +1,23 @@ +""" +URL configuration for wedding_site project. + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/5.0/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" +from django.contrib import admin +from django.urls import path, include + +urlpatterns = [ + path("", include('primary.urls')), + path('admin/', admin.site.urls), +] diff --git a/wedding_site/wedding_site/wsgi.py b/wedding_site/wedding_site/wsgi.py new file mode 100644 index 0000000..682259b --- /dev/null +++ b/wedding_site/wedding_site/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for wedding_site 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', 'wedding_site.settings') + +application = get_wsgi_application()