From eb38722a535104d9e743aa188cc977dd06b8d82e Mon Sep 17 00:00:00 2001 From: Disco DeDisco Date: Tue, 13 Jan 2026 00:35:28 -0500 Subject: [PATCH] ansible playbook commenced; deployment aimed at staging.earthmanrpg.me --- .gitignore | 353 +++++++++--------- Dockerfile | 38 +- infra/deploy-playbook.yaml | 26 ++ src/apps/dashboard/admin.py | 6 +- src/apps/dashboard/apps.py | 10 +- src/apps/dashboard/migrations/0001_initial.py | 40 +- .../dashboard/migrations/0002_item_text.py | 36 +- .../migrations/0003_list_item_list.py | 50 +-- src/apps/dashboard/models.py | 18 +- src/apps/dashboard/tests.py | 222 +++++------ src/apps/dashboard/urls.py | 16 +- src/apps/dashboard/views.py | 40 +- src/core/asgi.py | 32 +- src/core/settings.py | 282 +++++++------- src/core/urls.py | 18 +- src/core/wsgi.py | 32 +- src/functional_tests/tests.py | 214 +++++------ src/manage.py | 44 +-- src/templates/apps/dashboard/home.html | 12 +- src/templates/apps/dashboard/list.html | 30 +- src/templates/core/base.html | 88 ++--- 21 files changed, 817 insertions(+), 790 deletions(-) create mode 100644 infra/deploy-playbook.yaml diff --git a/.gitignore b/.gitignore index 714ac5d..facdbef 100644 --- a/.gitignore +++ b/.gitignore @@ -1,176 +1,177 @@ -# Created by https://www.toptal.com/developers/gitignore/api/django -# Edit at https://www.toptal.com/developers/gitignore?templates=django - -### Django ### -*.log -*.pot -*.pyc -__pycache__/ -local_settings.py -db.sqlite3 -db.sqlite3-journal -container.db.sqlite3 -media -static - -# If your build process includes running collectstatic, then you probably don't need or want to include staticfiles/ -# in your Git repository. Update and uncomment the following line accordingly. -# /staticfiles/ - -### Django.Python Stack ### -# Byte-compiled / optimized / DLL files -*.py[cod] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.nox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -*.py,cover -.hypothesis/ -.pytest_cache/ -cover/ - -# Translations -*.mo - -# Django stuff: - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -.pybuilder/ -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# IPython -profile_default/ -ipython_config.py - -# pyenv -# For a library or package, you might want to ignore these files since the code is -# intended to run in multiple environments; otherwise, check them in: -# .python-version - -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -#Pipfile.lock - -# poetry -# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. -# This is especially recommended for binary packages to ensure reproducibility, and is more -# commonly ignored for libraries. -# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control -#poetry.lock - -# pdm -# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. -#pdm.lock -# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it -# in version control. -# https://pdm.fming.dev/#use-with-ide -.pdm.toml - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm -__pypackages__/ - -# Celery stuff -celerybeat-schedule -celerybeat.pid - -# SageMath parsed files -*.sage.py - -# Environments -.env -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ - -# pytype static type analyzer -.pytype/ - -# Cython debug symbols -cython_debug/ - -# PyCharm -# JetBrains specific template is maintained in a separate JetBrains.gitignore that can -# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore -# and can be added to the global gitignore or merged into this file. For a more nuclear -# option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ - -# End of https://www.toptal.com/developers/gitignore/api/django +# Created by https://www.toptal.com/developers/gitignore/api/django +# Edit at https://www.toptal.com/developers/gitignore?templates=django + +### Django ### +*.log +*.pot +*.pyc +__pycache__/ +local_settings.py +db.sqlite3 +db.sqlite3-journal +container.db.sqlite3 +media +static + +# If your build process includes running collectstatic, then you probably don't need or want to include staticfiles/ +# in your Git repository. Update and uncomment the following line accordingly. +# /staticfiles/ + +### Django.Python Stack ### +# Byte-compiled / optimized / DLL files +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo + +# Django stuff: + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +.venv-wsl +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +# End of https://www.toptal.com/developers/gitignore/api/django diff --git a/Dockerfile b/Dockerfile index f3ce0a7..394702d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,20 +1,20 @@ -FROM python:3.13-slim - -RUN python -m venv /.venv -ENV PATH="/.venv/bin:$PATH" - -COPY requirements.txt /tmp/requirements.txt -RUN pip install -r /tmp/requirements.txt - -COPY src /src - -WORKDIR /src - -RUN python manage.py collectstatic - -ENV DJANGO_DEBUG_FALSE=1 - -RUN adduser --uid 1234 nonroot -# USER nonroot - +FROM python:3.13-slim + +RUN python -m venv /.venv +ENV PATH="/.venv/bin:$PATH" + +COPY requirements.txt /tmp/requirements.txt +RUN pip install -r /tmp/requirements.txt + +COPY src /src + +WORKDIR /src + +RUN python manage.py collectstatic + +ENV DJANGO_DEBUG_FALSE=1 + +RUN adduser --uid 1234 nonroot +# USER nonroot + CMD ["gunicorn", "--bind", ":8888", "core.wsgi:application"] \ No newline at end of file diff --git a/infra/deploy-playbook.yaml b/infra/deploy-playbook.yaml new file mode 100644 index 0000000..4cd242c --- /dev/null +++ b/infra/deploy-playbook.yaml @@ -0,0 +1,26 @@ +- hosts: all + + tasks: + - name: Install docker + ansible.builtin.apt: + name: docker.io + state: latest + update_cache: true + become: true + + - name: Add our user to the docker group, so we don't need sudo/become + ansible.builtin.user: + name: '{{ ansible_user }}' + groups: docker + append: true # don't remove any existing groups + become: true + + - name: Reset ssh connection to allow the user/group change to take effect + ansible.builtin.meta: reset_connection + + - name: Run test container + community.docker.docker_container: + name: testcontainer + state: started + image: busybox + command: echo howdy partner \ No newline at end of file diff --git a/src/apps/dashboard/admin.py b/src/apps/dashboard/admin.py index 8c38f3f..ea5d68b 100644 --- a/src/apps/dashboard/admin.py +++ b/src/apps/dashboard/admin.py @@ -1,3 +1,3 @@ -from django.contrib import admin - -# Register your models here. +from django.contrib import admin + +# Register your models here. diff --git a/src/apps/dashboard/apps.py b/src/apps/dashboard/apps.py index e0fb62c..7f81e17 100644 --- a/src/apps/dashboard/apps.py +++ b/src/apps/dashboard/apps.py @@ -1,5 +1,5 @@ -from django.apps import AppConfig - - -class DashboardConfig(AppConfig): - name = 'apps.dashboard' +from django.apps import AppConfig + + +class DashboardConfig(AppConfig): + name = 'apps.dashboard' diff --git a/src/apps/dashboard/migrations/0001_initial.py b/src/apps/dashboard/migrations/0001_initial.py index 87994eb..8ed54c1 100644 --- a/src/apps/dashboard/migrations/0001_initial.py +++ b/src/apps/dashboard/migrations/0001_initial.py @@ -1,20 +1,20 @@ -# Generated by Django 6.0 on 2025-12-31 05:18 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ] - - operations = [ - migrations.CreateModel( - name='Item', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ], - ), - ] +# Generated by Django 6.0 on 2025-12-31 05:18 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Item', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ], + ), + ] diff --git a/src/apps/dashboard/migrations/0002_item_text.py b/src/apps/dashboard/migrations/0002_item_text.py index 0c2fab1..c9025af 100644 --- a/src/apps/dashboard/migrations/0002_item_text.py +++ b/src/apps/dashboard/migrations/0002_item_text.py @@ -1,18 +1,18 @@ -# Generated by Django 6.0 on 2025-12-31 05:24 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('dashboard', '0001_initial'), - ] - - operations = [ - migrations.AddField( - model_name='item', - name='text', - field=models.TextField(default=''), - ), - ] +# Generated by Django 6.0 on 2025-12-31 05:24 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dashboard', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='item', + name='text', + field=models.TextField(default=''), + ), + ] diff --git a/src/apps/dashboard/migrations/0003_list_item_list.py b/src/apps/dashboard/migrations/0003_list_item_list.py index 33707bb..f377a46 100644 --- a/src/apps/dashboard/migrations/0003_list_item_list.py +++ b/src/apps/dashboard/migrations/0003_list_item_list.py @@ -1,25 +1,25 @@ -# Generated by Django 6.0 on 2026-01-03 03:05 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('dashboard', '0002_item_text'), - ] - - operations = [ - migrations.CreateModel( - name='List', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ], - ), - migrations.AddField( - model_name='item', - name='list', - field=models.ForeignKey(default=None, on_delete=django.db.models.deletion.CASCADE, to='dashboard.list'), - ), - ] +# Generated by Django 6.0 on 2026-01-03 03:05 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dashboard', '0002_item_text'), + ] + + operations = [ + migrations.CreateModel( + name='List', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ], + ), + migrations.AddField( + model_name='item', + name='list', + field=models.ForeignKey(default=None, on_delete=django.db.models.deletion.CASCADE, to='dashboard.list'), + ), + ] diff --git a/src/apps/dashboard/models.py b/src/apps/dashboard/models.py index 1ab7882..9939e99 100644 --- a/src/apps/dashboard/models.py +++ b/src/apps/dashboard/models.py @@ -1,9 +1,9 @@ -from django.db import models - -class List(models.Model): - pass - -class Item(models.Model): - text = models.TextField(default='') - list = models.ForeignKey(List, default=None, on_delete=models.CASCADE) - +from django.db import models + +class List(models.Model): + pass + +class Item(models.Model): + text = models.TextField(default='') + list = models.ForeignKey(List, default=None, on_delete=models.CASCADE) + diff --git a/src/apps/dashboard/tests.py b/src/apps/dashboard/tests.py index aa3ea94..797021c 100644 --- a/src/apps/dashboard/tests.py +++ b/src/apps/dashboard/tests.py @@ -1,111 +1,111 @@ -from django.test import TestCase -from .models import Item, List -import lxml.html - -class HomePageTest(TestCase): - def test_uses_home_template(self): - response = self.client.get('/') - self.assertTemplateUsed(response, 'apps/dashboard/home.html') - - def test_renders_input_form(self): - response = self.client.get('/') - parsed = lxml.html.fromstring(response.content) - [form] = parsed.cssselect('form[method=POST]') - self.assertEqual(form.get('action'), '/apps/dashboard/newlist') - inputs = form.cssselect('input') - self.assertIn('item_text', [input.get('name') for input in inputs]) - -class ListAndItemModelsTest(TestCase): - def test_saving_and_retrieving_items(self): - mylist = List() - mylist.save() - - first_item = Item() - first_item.text = "The first (ever) list item" - first_item.list = mylist - first_item.save() - - second_item = Item() - second_item.text = "A sequel somehow better than the first" - second_item.list = mylist - second_item.save() - - saved_list = List.objects.get() - self.assertEqual(saved_list, mylist) - - saved_items = Item.objects.all() - self.assertEqual(saved_items.count(), 2) - - first_saved_item = saved_items[0] - second_saved_item = saved_items[1] - self.assertEqual(first_saved_item.text, "The first (ever) list item") - self.assertEqual(first_saved_item.list, mylist) - self.assertEqual(second_saved_item.text, "A sequel somehow better than the first") - self.assertEqual(second_saved_item.list, mylist) - -class DashViewTest(TestCase): - def test_uses_list_template(self): - mylist = List.objects.create() - response = self.client.get(f'/apps/dashboard/{mylist.id}/') - self.assertTemplateUsed(response, 'apps/dashboard/list.html') - - def test_renders_input_form(self): - mylist = List.objects.create() - response = self.client.get(f'/apps/dashboard/{mylist.id}/') - parsed = lxml.html.fromstring(response.content) - [form] = parsed.cssselect('form[method=POST]') - self.assertEqual(form.get('action'), f"/apps/dashboard/{mylist.id}/add-item") - inputs = form.cssselect('input') - self.assertIn('item_text', [input.get('name') for input in inputs]) - - def test_displays_only_items_for_that_list(self): - # Given/Arrange - correct_list = List.objects.create() - Item.objects.create(text='itemey 1', list=correct_list) - Item.objects.create(text='itemey 2', list=correct_list) - other_list = List.objects.create() - Item.objects.create(text='other list item', list=other_list) - # When/Act - response = self.client.get(f'/apps/dashboard/{correct_list.id}/') - # Then/Assert - self.assertContains(response, 'itemey 1') - self.assertContains(response, 'itemey 2') - self.assertNotContains(response, 'other list item') - -class NewListTest(TestCase): - def test_can_save_a_POST_request(self): - self. client.post('/apps/dashboard/newlist', data={'item_text': 'A new list item'}) - self.assertEqual(Item.objects.count(), 1) - new_item = Item.objects.get() - self.assertEqual(new_item.text, 'A new list item') - - def test_redirects_after_POST(self): - response = self.client.post('/apps/dashboard/newlist', data={'item_text': 'A new list item'}) - new_list = List.objects.get() - self.assertRedirects(response, f'/apps/dashboard/{new_list.id}/') - -class NewItemTest(TestCase): - def test_can_save_a_POST_request_to_an_existing_list(self): - other_list = List.objects.create() - correct_list = List.objects.create() - - self.client.post( - f'/apps/dashboard/{correct_list.id}/add-item', - data={'item_text': 'A new item for an existing list'}, - ) - - self.assertEqual(Item.objects.count(), 1) - new_item = Item.objects.get() - self.assertEqual(new_item.text, 'A new item for an existing list') - self.assertEqual(new_item.list, correct_list) - - def test_redirects_to_list_view(self): - other_list = List.objects.create() - correct_list = List.objects.create() - - response = self.client.post( - f'/apps/dashboard/{correct_list.id}/add-item', - data={'item_text': 'A new item for an existing list'}, - ) - - self.assertRedirects(response, f'/apps/dashboard/{correct_list.id}/') +from django.test import TestCase +from .models import Item, List +import lxml.html + +class HomePageTest(TestCase): + def test_uses_home_template(self): + response = self.client.get('/') + self.assertTemplateUsed(response, 'apps/dashboard/home.html') + + def test_renders_input_form(self): + response = self.client.get('/') + parsed = lxml.html.fromstring(response.content) + [form] = parsed.cssselect('form[method=POST]') + self.assertEqual(form.get('action'), '/apps/dashboard/newlist') + inputs = form.cssselect('input') + self.assertIn('item_text', [input.get('name') for input in inputs]) + +class ListAndItemModelsTest(TestCase): + def test_saving_and_retrieving_items(self): + mylist = List() + mylist.save() + + first_item = Item() + first_item.text = "The first (ever) list item" + first_item.list = mylist + first_item.save() + + second_item = Item() + second_item.text = "A sequel somehow better than the first" + second_item.list = mylist + second_item.save() + + saved_list = List.objects.get() + self.assertEqual(saved_list, mylist) + + saved_items = Item.objects.all() + self.assertEqual(saved_items.count(), 2) + + first_saved_item = saved_items[0] + second_saved_item = saved_items[1] + self.assertEqual(first_saved_item.text, "The first (ever) list item") + self.assertEqual(first_saved_item.list, mylist) + self.assertEqual(second_saved_item.text, "A sequel somehow better than the first") + self.assertEqual(second_saved_item.list, mylist) + +class DashViewTest(TestCase): + def test_uses_list_template(self): + mylist = List.objects.create() + response = self.client.get(f'/apps/dashboard/{mylist.id}/') + self.assertTemplateUsed(response, 'apps/dashboard/list.html') + + def test_renders_input_form(self): + mylist = List.objects.create() + response = self.client.get(f'/apps/dashboard/{mylist.id}/') + parsed = lxml.html.fromstring(response.content) + [form] = parsed.cssselect('form[method=POST]') + self.assertEqual(form.get('action'), f"/apps/dashboard/{mylist.id}/add-item") + inputs = form.cssselect('input') + self.assertIn('item_text', [input.get('name') for input in inputs]) + + def test_displays_only_items_for_that_list(self): + # Given/Arrange + correct_list = List.objects.create() + Item.objects.create(text='itemey 1', list=correct_list) + Item.objects.create(text='itemey 2', list=correct_list) + other_list = List.objects.create() + Item.objects.create(text='other list item', list=other_list) + # When/Act + response = self.client.get(f'/apps/dashboard/{correct_list.id}/') + # Then/Assert + self.assertContains(response, 'itemey 1') + self.assertContains(response, 'itemey 2') + self.assertNotContains(response, 'other list item') + +class NewListTest(TestCase): + def test_can_save_a_POST_request(self): + self. client.post('/apps/dashboard/newlist', data={'item_text': 'A new list item'}) + self.assertEqual(Item.objects.count(), 1) + new_item = Item.objects.get() + self.assertEqual(new_item.text, 'A new list item') + + def test_redirects_after_POST(self): + response = self.client.post('/apps/dashboard/newlist', data={'item_text': 'A new list item'}) + new_list = List.objects.get() + self.assertRedirects(response, f'/apps/dashboard/{new_list.id}/') + +class NewItemTest(TestCase): + def test_can_save_a_POST_request_to_an_existing_list(self): + other_list = List.objects.create() + correct_list = List.objects.create() + + self.client.post( + f'/apps/dashboard/{correct_list.id}/add-item', + data={'item_text': 'A new item for an existing list'}, + ) + + self.assertEqual(Item.objects.count(), 1) + new_item = Item.objects.get() + self.assertEqual(new_item.text, 'A new item for an existing list') + self.assertEqual(new_item.list, correct_list) + + def test_redirects_to_list_view(self): + other_list = List.objects.create() + correct_list = List.objects.create() + + response = self.client.post( + f'/apps/dashboard/{correct_list.id}/add-item', + data={'item_text': 'A new item for an existing list'}, + ) + + self.assertRedirects(response, f'/apps/dashboard/{correct_list.id}/') diff --git a/src/apps/dashboard/urls.py b/src/apps/dashboard/urls.py index 097dfc8..b1891e9 100644 --- a/src/apps/dashboard/urls.py +++ b/src/apps/dashboard/urls.py @@ -1,8 +1,8 @@ -from django.urls import path -from . import views - -urlpatterns = [ - path('newlist', views.new_list, name='new_list'), - path('/', views.view_list, name='view_list'), - path('/add-item', views.add_item, name='add_item'), -] +from django.urls import path +from . import views + +urlpatterns = [ + path('newlist', views.new_list, name='new_list'), + path('/', views.view_list, name='view_list'), + path('/add-item', views.add_item, name='add_item'), +] diff --git a/src/apps/dashboard/views.py b/src/apps/dashboard/views.py index 5ff91f2..f5be08b 100644 --- a/src/apps/dashboard/views.py +++ b/src/apps/dashboard/views.py @@ -1,20 +1,20 @@ -from django.shortcuts import redirect, render -from .models import Item, List - -def home_page(request): - return render(request, 'apps/dashboard/home.html') - -def view_list(request, list_id): - our_list = List.objects.get(id=list_id) - return render(request, 'apps/dashboard/list.html', {'list': our_list}) - -def new_list(request): - nulist = List.objects.create() - Item.objects.create(text=request.POST['item_text'], list=nulist) - return redirect(f'/apps/dashboard/{nulist.id}/') - -def add_item(request, list_id): - our_list = List.objects.get(id=list_id) - Item.objects.create(text=request.POST['item_text'], list=our_list) - return redirect(f'/apps/dashboard/{our_list.id}/') - +from django.shortcuts import redirect, render +from .models import Item, List + +def home_page(request): + return render(request, 'apps/dashboard/home.html') + +def view_list(request, list_id): + our_list = List.objects.get(id=list_id) + return render(request, 'apps/dashboard/list.html', {'list': our_list}) + +def new_list(request): + nulist = List.objects.create() + Item.objects.create(text=request.POST['item_text'], list=nulist) + return redirect(f'/apps/dashboard/{nulist.id}/') + +def add_item(request, list_id): + our_list = List.objects.get(id=list_id) + Item.objects.create(text=request.POST['item_text'], list=our_list) + return redirect(f'/apps/dashboard/{our_list.id}/') + diff --git a/src/core/asgi.py b/src/core/asgi.py index cf099bf..9b89a2c 100644 --- a/src/core/asgi.py +++ b/src/core/asgi.py @@ -1,16 +1,16 @@ -""" -ASGI config for core 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/6.0/howto/deployment/asgi/ -""" - -import os - -from django.core.asgi import get_asgi_application - -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings') - -application = get_asgi_application() +""" +ASGI config for core 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/6.0/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings') + +application = get_asgi_application() diff --git a/src/core/settings.py b/src/core/settings.py index 8eb31f2..72a49b8 100644 --- a/src/core/settings.py +++ b/src/core/settings.py @@ -1,141 +1,141 @@ -""" -Django settings for core project. - -Generated by 'django-admin startproject' using Django 6.0. - -For more information on this file, see -https://docs.djangoproject.com/en/6.0/topics/settings/ - -For the full list of settings and their values, see -https://docs.djangoproject.com/en/6.0/ref/settings/ -""" - -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/6.0/howto/deployment/checklist/ - - -# SECURITY WARNING: don't run with debug turned on in production! -if 'DJANGO_DEBUG_FALSE' in os.environ: - DEBUG = False - SECRET_KEY = os.environ['DJANGO_SECRET_KEY'] - ALLOWED_HOSTS = [os.environ['DJANGO_ALLOWED_HOST']] - db_path = os.environ['DJANGO_DB_PATH'] -else: - DEBUG = True - # SECURITY WARNING: keep the secret key used in production secret! - SECRET_KEY = 'django-insecure-&9b_h=qpjy=sshhnsyg98&jp7(t6*v78__y%h2l$b#_@6z$-9r' - ALLOWED_HOSTS = [] - db_path = BASE_DIR / 'db.sqlite3' - - -# Application definition - -INSTALLED_APPS = [ - # Django apps - # 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', - # Custom apps - 'apps.dashboard', - # Depend apps -] - -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 = 'core.urls' - -TEMPLATES = [ - { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [BASE_DIR / 'templates'], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', - ], - }, - }, -] - -WSGI_APPLICATION = 'core.wsgi.application' -ASGI_APPLICATION = 'core.asgi.application' - - -# Database -# https://docs.djangoproject.com/en/6.0/ref/settings/#databases - -DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': db_path, - } -} - - -# Password validation -# https://docs.djangoproject.com/en/6.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/6.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/6.0/howto/static-files/ - -STATIC_URL = 'static/' -STATIC_ROOT = BASE_DIR / 'static' - -LOGGING = { - "version": 1, - "disable_existing_loggers": False, - "handlers": { - "console": {"class": "logging.StreamHandler"}, - }, - "loggers": { - "root": {"handlers": ["console"], "level": "INFO"}, - }, -} +""" +Django settings for core project. + +Generated by 'django-admin startproject' using Django 6.0. + +For more information on this file, see +https://docs.djangoproject.com/en/6.0/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/6.0/ref/settings/ +""" + +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/6.0/howto/deployment/checklist/ + + +# SECURITY WARNING: don't run with debug turned on in production! +if 'DJANGO_DEBUG_FALSE' in os.environ: + DEBUG = False + SECRET_KEY = os.environ['DJANGO_SECRET_KEY'] + ALLOWED_HOSTS = [os.environ['DJANGO_ALLOWED_HOST']] + db_path = os.environ['DJANGO_DB_PATH'] +else: + DEBUG = True + # SECURITY WARNING: keep the secret key used in production secret! + SECRET_KEY = 'django-insecure-&9b_h=qpjy=sshhnsyg98&jp7(t6*v78__y%h2l$b#_@6z$-9r' + ALLOWED_HOSTS = [] + db_path = BASE_DIR / 'db.sqlite3' + + +# Application definition + +INSTALLED_APPS = [ + # Django apps + # 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + # Custom apps + 'apps.dashboard', + # Depend apps +] + +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 = 'core.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [BASE_DIR / 'templates'], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'core.wsgi.application' +ASGI_APPLICATION = 'core.asgi.application' + + +# Database +# https://docs.djangoproject.com/en/6.0/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': db_path, + } +} + + +# Password validation +# https://docs.djangoproject.com/en/6.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/6.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/6.0/howto/static-files/ + +STATIC_URL = 'static/' +STATIC_ROOT = BASE_DIR / 'static' + +LOGGING = { + "version": 1, + "disable_existing_loggers": False, + "handlers": { + "console": {"class": "logging.StreamHandler"}, + }, + "loggers": { + "root": {"handlers": ["console"], "level": "INFO"}, + }, +} diff --git a/src/core/urls.py b/src/core/urls.py index dc58d46..846ab05 100644 --- a/src/core/urls.py +++ b/src/core/urls.py @@ -1,9 +1,9 @@ -# from django.contrib import admin -from django.urls import include, path -from apps.dashboard import views as list_views - -urlpatterns = [ - # path('admin/', admin.site.urls), - path('', list_views.home_page, name='home'), - path('apps/dashboard/', include('apps.dashboard.urls')), -] +# from django.contrib import admin +from django.urls import include, path +from apps.dashboard import views as list_views + +urlpatterns = [ + # path('admin/', admin.site.urls), + path('', list_views.home_page, name='home'), + path('apps/dashboard/', include('apps.dashboard.urls')), +] diff --git a/src/core/wsgi.py b/src/core/wsgi.py index 6d36530..835c123 100644 --- a/src/core/wsgi.py +++ b/src/core/wsgi.py @@ -1,16 +1,16 @@ -""" -WSGI config for core 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/6.0/howto/deployment/wsgi/ -""" - -import os - -from django.core.wsgi import get_wsgi_application - -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings') - -application = get_wsgi_application() +""" +WSGI config for core 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/6.0/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings') + +application = get_wsgi_application() diff --git a/src/functional_tests/tests.py b/src/functional_tests/tests.py index 3485829..d2c4a77 100644 --- a/src/functional_tests/tests.py +++ b/src/functional_tests/tests.py @@ -1,107 +1,107 @@ -from django.contrib.staticfiles.testing import StaticLiveServerTestCase -from selenium import webdriver -from selenium.common.exceptions import WebDriverException -from selenium.webdriver.common.by import By -from selenium.webdriver.common.keys import Keys -import os -import time -import unittest - -MAX_WAIT = 5 - -class NewVisitorTest(StaticLiveServerTestCase): - # Helper methods - def setUp(self): - self.browser = webdriver.Firefox() - if test_server := os.environ.get('TEST_SERVER'): - self.live_server_url = 'http://' + test_server - - def tearDown(self): - self.browser.quit() - - def wait_for_row_in_list_table(self, row_text): - start_time = time.time() - while True: - try: - table = self.browser.find_element(By.ID, 'id-list-table') - rows = table.find_elements(By.TAG_NAME, 'tr') - self.assertIn(row_text, [row.text for row in rows]) - return - except (AssertionError, WebDriverException): - if time.time() - start_time > MAX_WAIT: - raise - time.sleep(0.5) - - # Test methods - def test_can_start_a_todo_list(self): - self.browser.get(self.live_server_url) - - self.assertIn('Dashboard | ', self.browser.title) - header_text = self.browser.find_element(By.TAG_NAME, 'h1').text - self.assertIn('Dashboard | ', header_text) - - inputbox = self.browser.find_element(By.ID, 'id-new-item') - self.assertEqual(inputbox.get_attribute('placeholder'), 'Enter a to-do item') - - inputbox.send_keys('Buy peacock feathers') - - inputbox.send_keys(Keys.ENTER) - self.wait_for_row_in_list_table('1. Buy peacock feathers') - - inputbox = self.browser.find_element(By.ID, 'id-new-item') - inputbox.send_keys('Use peacock feathers to make a fly') - inputbox.send_keys(Keys.ENTER) - - self.wait_for_row_in_list_table('2. Use peacock feathers to make a fly') - self.wait_for_row_in_list_table('1. Buy peacock feathers') - - def test_multiple_users_can_start_lists_at_different_urls(self): - self.browser.get(self.live_server_url) - inputbox = self.browser.find_element(By.ID, 'id-new-item') - inputbox.send_keys('Buy peacock feathers') - inputbox.send_keys(Keys.ENTER) - self.wait_for_row_in_list_table('1. Buy peacock feathers') - - edith_dash_url = self.browser.current_url - self.assertRegex(edith_dash_url, '/apps/dashboard/.+') - - self.browser.delete_all_cookies() - - self.browser.get(self.live_server_url) - page_text = self.browser.find_element(By.TAG_NAME, 'body').text - self.assertNotIn('Buy peacock feathers', page_text) - - inputbox = self.browser.find_element(By.ID, 'id-new-item') - inputbox.send_keys('Buy milk') - inputbox.send_keys(Keys.ENTER) - self.wait_for_row_in_list_table('1. Buy milk') - - francis_dash_url = self.browser.current_url - self.assertRegex(francis_dash_url, '/apps/dashboard/.+') - self.assertNotEqual(francis_dash_url, edith_dash_url) - - page_text = self.browser.find_element(By.TAG_NAME, 'body').text - self.assertNotIn('Buy peacock feathers', page_text) - self.assertIn('Buy milk', page_text) - - def test_layout_and_styling(self): - self.browser.get(self.live_server_url) - - self.browser.set_window_size(1024, 768) - - inputbox = self.browser.find_element(By.ID, 'id-new-item') - self.assertAlmostEqual( - inputbox.location['x'] + inputbox.size['width'] / 2, - 512, - delta=10, - ) - - inputbox.send_keys('testing') - inputbox.send_keys(Keys.ENTER) - self.wait_for_row_in_list_table('1. testing') - inputbox = self.browser.find_element(By.ID, 'id-new-item') - self.assertAlmostEqual( - inputbox.location['x'] + inputbox.size['width'] / 2, - 512, - delta=10, - ) +from django.contrib.staticfiles.testing import StaticLiveServerTestCase +from selenium import webdriver +from selenium.common.exceptions import WebDriverException +from selenium.webdriver.common.by import By +from selenium.webdriver.common.keys import Keys +import os +import time +import unittest + +MAX_WAIT = 5 + +class NewVisitorTest(StaticLiveServerTestCase): + # Helper methods + def setUp(self): + self.browser = webdriver.Firefox() + if test_server := os.environ.get('TEST_SERVER'): + self.live_server_url = 'http://' + test_server + + def tearDown(self): + self.browser.quit() + + def wait_for_row_in_list_table(self, row_text): + start_time = time.time() + while True: + try: + table = self.browser.find_element(By.ID, 'id-list-table') + rows = table.find_elements(By.TAG_NAME, 'tr') + self.assertIn(row_text, [row.text for row in rows]) + return + except (AssertionError, WebDriverException): + if time.time() - start_time > MAX_WAIT: + raise + time.sleep(0.5) + + # Test methods + def test_can_start_a_todo_list(self): + self.browser.get(self.live_server_url) + + self.assertIn('Dashboard | ', self.browser.title) + header_text = self.browser.find_element(By.TAG_NAME, 'h1').text + self.assertIn('Dashboard | ', header_text) + + inputbox = self.browser.find_element(By.ID, 'id-new-item') + self.assertEqual(inputbox.get_attribute('placeholder'), 'Enter a to-do item') + + inputbox.send_keys('Buy peacock feathers') + + inputbox.send_keys(Keys.ENTER) + self.wait_for_row_in_list_table('1. Buy peacock feathers') + + inputbox = self.browser.find_element(By.ID, 'id-new-item') + inputbox.send_keys('Use peacock feathers to make a fly') + inputbox.send_keys(Keys.ENTER) + + self.wait_for_row_in_list_table('2. Use peacock feathers to make a fly') + self.wait_for_row_in_list_table('1. Buy peacock feathers') + + def test_multiple_users_can_start_lists_at_different_urls(self): + self.browser.get(self.live_server_url) + inputbox = self.browser.find_element(By.ID, 'id-new-item') + inputbox.send_keys('Buy peacock feathers') + inputbox.send_keys(Keys.ENTER) + self.wait_for_row_in_list_table('1. Buy peacock feathers') + + edith_dash_url = self.browser.current_url + self.assertRegex(edith_dash_url, '/apps/dashboard/.+') + + self.browser.delete_all_cookies() + + self.browser.get(self.live_server_url) + page_text = self.browser.find_element(By.TAG_NAME, 'body').text + self.assertNotIn('Buy peacock feathers', page_text) + + inputbox = self.browser.find_element(By.ID, 'id-new-item') + inputbox.send_keys('Buy milk') + inputbox.send_keys(Keys.ENTER) + self.wait_for_row_in_list_table('1. Buy milk') + + francis_dash_url = self.browser.current_url + self.assertRegex(francis_dash_url, '/apps/dashboard/.+') + self.assertNotEqual(francis_dash_url, edith_dash_url) + + page_text = self.browser.find_element(By.TAG_NAME, 'body').text + self.assertNotIn('Buy peacock feathers', page_text) + self.assertIn('Buy milk', page_text) + + def test_layout_and_styling(self): + self.browser.get(self.live_server_url) + + self.browser.set_window_size(1024, 768) + + inputbox = self.browser.find_element(By.ID, 'id-new-item') + self.assertAlmostEqual( + inputbox.location['x'] + inputbox.size['width'] / 2, + 512, + delta=10, + ) + + inputbox.send_keys('testing') + inputbox.send_keys(Keys.ENTER) + self.wait_for_row_in_list_table('1. testing') + inputbox = self.browser.find_element(By.ID, 'id-new-item') + self.assertAlmostEqual( + inputbox.location['x'] + inputbox.size['width'] / 2, + 512, + delta=10, + ) diff --git a/src/manage.py b/src/manage.py index f2a662c..88737c4 100644 --- a/src/manage.py +++ b/src/manage.py @@ -1,22 +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', 'core.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() +#!/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', 'core.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/src/templates/apps/dashboard/home.html b/src/templates/apps/dashboard/home.html index dec8b4f..312d8c0 100644 --- a/src/templates/apps/dashboard/home.html +++ b/src/templates/apps/dashboard/home.html @@ -1,6 +1,6 @@ -{% extends "core/base.html" %} - -{% block title_text %}Start a new to-do list{% endblock title_text %} -{% block header_text %}Start a new to-do list{% endblock header_text %} - -{% block form_action %}/apps/dashboard/newlist{% endblock form_action %} +{% extends "core/base.html" %} + +{% block title_text %}Start a new to-do list{% endblock title_text %} +{% block header_text %}Start a new to-do list{% endblock header_text %} + +{% block form_action %}/apps/dashboard/newlist{% endblock form_action %} diff --git a/src/templates/apps/dashboard/list.html b/src/templates/apps/dashboard/list.html index bdb481b..4aaadfc 100644 --- a/src/templates/apps/dashboard/list.html +++ b/src/templates/apps/dashboard/list.html @@ -1,15 +1,15 @@ -{% extends "core/base.html" %} - -{% block title_text %}Your to-do list{% endblock title_text %} -{% block header_text %}Your to-do list{% endblock header_text %} - -{% block form_action %}/apps/dashboard/{{ list.id }}/add-item{% endblock form_action %} - -{% block table %} - - {% for item in list.item_set.all %} - - {% endfor %} -
{{ forloop.counter }}. {{ item.text }}
-{% endblock table %} - +{% extends "core/base.html" %} + +{% block title_text %}Your to-do list{% endblock title_text %} +{% block header_text %}Your to-do list{% endblock header_text %} + +{% block form_action %}/apps/dashboard/{{ list.id }}/add-item{% endblock form_action %} + +{% block table %} + + {% for item in list.item_set.all %} + + {% endfor %} +
{{ forloop.counter }}. {{ item.text }}
+{% endblock table %} + diff --git a/src/templates/core/base.html b/src/templates/core/base.html index b90b2c8..7ba65c0 100644 --- a/src/templates/core/base.html +++ b/src/templates/core/base.html @@ -1,45 +1,45 @@ - - - - - - - - - Dashboard | {% block title_text %}{% endblock title_text %} - - - - -
-
-
-

Dashboard | {% block header_text %}{% endblock header_text %}

- -
- - {% csrf_token %} -
-
-
- -
-
- {% block table %} - {% endblock table %} -
-
- -
- - - - + + + + + + + + + Dashboard | {% block title_text %}{% endblock title_text %} + + + + +
+
+
+

Dashboard | {% block header_text %}{% endblock header_text %}

+ +
+ + {% csrf_token %} +
+
+
+ +
+
+ {% block table %} + {% endblock table %} +
+
+ +
+ + + + \ No newline at end of file