Compare commits

...

10 Commits

Author SHA1 Message Date
Disco DeDisco
4e1feddb45 new name @property in List model, replete w. proper testing in tests.test_models 2026-02-08 22:50:03 -05:00
Disco DeDisco
94a161fe09 List objects now container owner values, saved upon creation, linked to user fk; apps.dashboard.views updated accordingly; 36 UTs passing (2 new) 2026-02-08 22:33:15 -05:00
Disco DeDisco
6c0e9bb6ec new_list() FBV tries to assign List owner, but List() model has no such attr 2026-02-08 22:23:43 -05:00
Disco DeDisco
c176fe6cb3 new UT ensures correct list ownership from apps.dashboard.views 2026-02-08 22:18:41 -05:00
Disco DeDisco
fad8657c97 placeholder view & code for my_lists.html implemented 2026-02-08 21:52:04 -05:00
Disco DeDisco
306b4c8e5e new template _partials for apps.dashboard, incl. _form.html & _scripts.html; respective form, table & content code offloaded from base.html, home.html & list.html; functional_tests.test_my_list TODO resumed (only FT not currently passing, but this is as expected) 2026-02-08 21:43:58 -05:00
Disco DeDisco
8190317c21 nginx compatibility added to serve static files on server; whitenoise installed to catch static file serving in local docker container, also added to core.settings middleware; console logs & print statements removed from dashboard.js & functional_tests.container_commands; ansible playbook and nginx config file support nginx w.in deployment workflow 2026-02-08 17:55:09 -05:00
Disco DeDisco
07a76cb32d added new template apps/dashboard/my_lists.html; all FTs passing green locally, tho half og .test_my_lists TODO'd out; test_login uses mock-patch architecture to avoid Mailgun and DigitalOcean magic login link testing restraints 2026-02-07 22:47:04 -05:00
Disco DeDisco
0c413a9cc2 pre–db-reset commit; attempting to update User model w. uuid pk enumeration 2026-02-07 20:15:27 -05:00
Disco DeDisco
58b526f434 add_list_item() helper added to functional_tests.base; further propagated into .test_layout_and_styling, .test_list_item_validation, .test_my_lists & .test_simple_list_creation; all FTs passing locally (tho js-dependent FT still require nginx to serve scripts properly in docker or on server) 2026-02-07 19:44:47 -05:00
36 changed files with 325 additions and 220 deletions

Binary file not shown.

View File

@@ -1,4 +1,4 @@
- hosts: staging
- hosts: all
tasks:
- name: Debug django_allowed_host
@@ -12,6 +12,34 @@
update_cache: true
become: true
- name: Install nginx
ansible.builtin.apt:
name: nginx
state: latest
become: true
- name: Deploy nginx config
ansible.builtin.template:
src: nginx.conf.j2
dest: /etc/nginx/sites-available/gamearray
become: true
notify: Restart nginx
- name: Enable nginx site
ansible.builtin.file:
src: /etc/nginx/sites-available/gamearray
dest: /etc/nginx/sites-enabled/gamearray
state: link
become: true
notify: Restart nginx
- name: Remove default nginx site
ansible.builtin.file:
path: /etc/nginx/sites-enabled/default
state: absent
become: true
notify: Restart nginx
- name: Add our user to the docker group, so we don't need sudo/become
ansible.builtin.user:
name: '{{ ansible_user }}'
@@ -88,9 +116,29 @@
EMAIL_HOST_PASSWORD: "{{ email_host_password }}"
MAILGUN_API_KEY: "{{ mailgun_api_key }}"
ports:
80:8888 # container port 80 (standard http port) maps to server port 8888 (arbitrary internal port)
127.0.0.1:8888:8888
- name: Create static files directory
ansible.builtin.file:
path: /var/www/gamearray/static
state: directory
owner: www-data
group: www-data
become: true
- name: Copy static files from container to host
ansible.builtin.command:
cmd: docker cp gamearray:/src/static/. /var/www/gamearray/static/
become: true
- name: Run migration inside container
community.docker.docker_container_exec:
container: gamearray
command: ./manage.py migrate
handlers:
- name: Restart nginx
ansible.builtin.service:
name: nginx
state: restarted
become: true

16
infra/nginx.conf.j2 Normal file
View File

@@ -0,0 +1,16 @@
server {
listen 80;
server_name {{ django_allowed_host }};
location /static/ {
alias /var/www/gamearray/static/;
}
location / {
proxy_pass http://127.0.0.1:8888;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}

View File

@@ -27,4 +27,5 @@ typing_extensions==4.15.0
tzdata==2025.3
urllib3==2.6.2
websocket-client==1.9.0
whitenoise==6.11.0
wsproto==1.3.2

View File

@@ -3,3 +3,4 @@ django-stubs==5.2.8
django-stubs-ext==5.2.8
gunicorn==23.0.0
requests==2.31.0
whitenoise==6.11.0

View File

@@ -1,5 +1,6 @@
# Generated by Django 6.0 on 2025-12-31 05:18
# Generated by Django 6.0 on 2026-02-08 01:19
import django.db.models.deletion
from django.db import migrations, models
@@ -12,9 +13,21 @@ class Migration(migrations.Migration):
operations = [
migrations.CreateModel(
name='Item',
name='List',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
],
),
migrations.CreateModel(
name='Item',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('text', models.TextField(default='')),
('list', models.ForeignKey(default=None, on_delete=django.db.models.deletion.CASCADE, to='dashboard.list')),
],
options={
'ordering': ('id',),
'unique_together': {('list', 'text')},
},
),
]

View File

@@ -1,18 +0,0 @@
# 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=''),
),
]

View File

@@ -0,0 +1,21 @@
# Generated by Django 6.0 on 2026-02-09 03:29
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dashboard', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name='list',
name='owner',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='lists', to=settings.AUTH_USER_MODEL),
),
]

View File

@@ -1,25 +0,0 @@
# 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'),
),
]

View File

@@ -1,17 +0,0 @@
# Generated by Django 6.0 on 2026-01-24 02:45
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('dashboard', '0003_list_item_list'),
]
operations = [
migrations.AlterUniqueTogether(
name='item',
unique_together={('list', 'text')},
),
]

View File

@@ -1,17 +0,0 @@
# Generated by Django 6.0 on 2026-01-24 17:54
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('dashboard', '0004_alter_item_unique_together'),
]
operations = [
migrations.AlterModelOptions(
name='item',
options={'ordering': ('id',)},
),
]

View File

@@ -2,6 +2,18 @@ from django.db import models
from django.urls import reverse
class List(models.Model):
owner = models.ForeignKey(
"lyric.User",
related_name="lists",
blank=True,
null=True,
on_delete=models.CASCADE,
)
@property
def name(self):
return self.item_set.first().text
def get_absolute_url(self):
return reverse("view_list", args=[self.id])

View File

@@ -1,9 +1,9 @@
console.log("apps/scripts/dashboard.js loading");
// console.log("apps/scripts/dashboard.js loading");
const initialize = (inputSelector) => {
console.log("initialize called!");
// console.log("initialize called!");
const textInput = document.querySelector(inputSelector);
textInput.oninput = () => {
console.log("oninput triggered");
// console.log("oninput triggered");
textInput.classList.remove("is-invalid");
};
};

View File

@@ -2,6 +2,7 @@ from django.core.exceptions import ValidationError
from django.db.utils import IntegrityError
from django.test import TestCase
from ..models import Item, List
from apps.lyric.models import User
class ItemModelTest(TestCase):
def test_default_text(self):
@@ -59,3 +60,17 @@ class ListModelTest(TestCase):
list(list1.item_set.all()),
[item1, item2, item3],
)
def test_lists_can_have_owners(self):
user = User.objects.create(email="a@b.cde")
mylist = List.objects.create(owner=user)
self.assertIn(mylist, user.lists.all())
def test_list_owner_is_optional(self):
List.objects.create()
def test_list_name_is_first_item_text(self):
list_ = List.objects.create()
Item.objects.create(list=list_, text="first item")
Item.objects.create(list=list_, text="second item")
self.assertEqual(list_.name, "first item")

View File

@@ -1,3 +1,4 @@
import lxml.html
from django.test import TestCase
from django.utils import html
from unittest import skip
@@ -6,7 +7,7 @@ from ..forms import (
EMPTY_ITEM_ERROR,
)
from ..models import Item, List
import lxml.html
from apps.lyric.models import User
class HomePageTest(TestCase):
def test_uses_home_template(self):
@@ -144,3 +145,22 @@ class ListViewTest(TestCase):
self.assertContains(response, expected_error)
self.assertTemplateUsed(response, "apps/dashboard/list.html")
self.assertEqual(Item.objects.all().count(), 1)
class MyListsTest(TestCase):
def test_my_lists_url_renders_my_lists_template(self):
user = User.objects.create(email="a@b.cde")
response = self.client.get(f"/apps/dashboard/users/{user.id}/")
self.assertTemplateUsed(response, "apps/dashboard/my_lists.html")
def test_passes_correct_owner_to_template(self):
User.objects.create(email="wrong@owner.com")
correct_user = User.objects.create(email="a@b.cde")
response = self.client.get(f"/apps/dashboard/users/{correct_user.id}/")
self.assertEqual(response.context["owner"], correct_user)
def test_list_owner_is_saved_if_user_is_authenticated(self):
user = User.objects.create(email="a@b.cde")
self.client.force_login(user)
self.client.post("/apps/dashboard/new_list", data={"text": "new item"})
new_list = List.objects.get()
self.assertEqual(new_list.owner, user)

View File

@@ -4,4 +4,5 @@ from . import views
urlpatterns = [
path('new_list', views.new_list, name='new_list'),
path('<int:list_id>/', views.view_list, name='view_list'),
path('users/<uuid:user_id>/', views.my_lists, name='my_lists'),
]

View File

@@ -1,6 +1,7 @@
from django.shortcuts import redirect, render
from .forms import ExistingListItemForm, ItemForm
from .models import Item, List
from apps.lyric.models import User
def home_page(request):
return render(request, "apps/dashboard/home.html", {"form": ItemForm()})
@@ -9,6 +10,9 @@ def new_list(request):
form = ItemForm(data=request.POST)
if form.is_valid():
nulist = List.objects.create()
if request.user.is_authenticated:
nulist.owner = request.user
nulist.save()
form.save(for_list=nulist)
return redirect(nulist)
else:
@@ -25,3 +29,6 @@ def view_list(request, list_id):
return redirect(our_list)
return render(request, "apps/dashboard/list.html", {"list": our_list, "form": form})
def my_lists(request, user_id):
owner = User.objects.get(id=user_id)
return render(request, "apps/dashboard/my_lists.html", {"owner": owner})

View File

@@ -1,5 +1,6 @@
# Generated by Django 6.0 on 2026-01-30 20:02
# Generated by Django 6.0 on 2026-02-08 01:19
import uuid
from django.db import migrations, models
@@ -14,8 +15,15 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='User',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('email', models.EmailField(max_length=254, unique=True)),
],
),
migrations.CreateModel(
name='Token',
fields=[
('email', models.EmailField(max_length=254)),
('uid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
],
),
]

View File

@@ -1,21 +0,0 @@
# Generated by Django 6.0 on 2026-01-30 20:10
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('lyric', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='Token',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('email', models.EmailField(max_length=254)),
('uid', models.UUIDField()),
],
),
]

View File

@@ -1,28 +0,0 @@
# Generated by Django 6.0 on 2026-01-31 01:03
import uuid
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('lyric', '0002_token'),
]
operations = [
migrations.RemoveField(
model_name='token',
name='id',
),
migrations.AlterField(
model_name='token',
name='uid',
field=models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='user',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
]

View File

@@ -6,7 +6,7 @@ class Token(models.Model):
uid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
class User(models.Model):
id = models.BigAutoField(primary_key=True)
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
email = models.EmailField(unique=True)
REQUIRED_FIELDS = []

View File

@@ -57,6 +57,7 @@ INSTALLED_APPS = [
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'whitenoise.middleware.WhiteNoiseMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',

View File

@@ -5,6 +5,7 @@ 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
from .container_commands import reset_database
@@ -49,6 +50,13 @@ class FunctionalTest(StaticLiveServerTestCase):
rows = self.browser.find_elements(By.CSS_SELECTOR, "#id_list_table tr")
self.assertIn(row_text, [row.text for row in rows])
def add_list_item(self, item_text):
num_rows = len(self.browser.find_elements(By.CSS_SELECTOR, "#id_list_table tr"))
self.get_item_input_box().send_keys(item_text)
self.get_item_input_box().send_keys(Keys.ENTER)
item_number = num_rows + 1
self.wait_for_row_in_list_table(f"{item_number}. {item_text}")
@wait
def wait_to_be_logged_in(self, email):
self.browser.find_element(By.CSS_SELECTOR, "#id_logout"),

View File

@@ -20,7 +20,7 @@ def _exec_in_container(host, commands):
return _exec_in_container_on_server(host, commands)
def _exec_in_container_locally(commands):
print(f"Running {commands} on inside local docker container")
# print(f"Running {commands} on inside local docker container")
return _run_commands(["docker", "exec", _get_container_id()] + commands)
def _exec_in_container_on_server(host, commands):
@@ -52,5 +52,5 @@ def _run_commands(commands):
result = process.stdout.decode()
if process.returncode != 0:
raise Exception(result)
print(f"Result: {result!r}")
# print(f"Result: {result!r}")
return result.strip()

View File

@@ -17,7 +17,7 @@ class Command(BaseCommand):
def create_pre_authenticated_session(email):
user = User.objects.create(email=email)
session = SessionStore()
session[SESSION_KEY] = user.pk
session[SESSION_KEY] = str(user.pk) # Convert UUID to string for JSON serialization
session[BACKEND_SESSION_KEY] = settings.AUTHENTICATION_BACKENDS[0]
session.save()
return session.session_key

View File

@@ -17,9 +17,7 @@ class LayoutAndStylingTest(FunctionalTest):
delta=10,
)
inputbox.send_keys('testing')
inputbox.send_keys(Keys.ENTER)
self.wait_for_row_in_list_table('1. testing')
self.add_list_item("testing")
inputbox = self.get_item_input_box()
self.assertAlmostEqual(
inputbox.location['x'] + inputbox.size['width'] / 2,

View File

@@ -44,9 +44,7 @@ class ItemValidationTest(FunctionalTest):
def test_cannot_add_duplicate_items(self):
self.browser.get(self.live_server_url)
self.get_item_input_box().send_keys("Witness divinity")
self.get_item_input_box().send_keys(Keys.ENTER)
self.wait_for_row_in_list_table("1. Witness divinity")
self.add_list_item("Witness divinity")
self.get_item_input_box().send_keys("Witness divinity")
self.get_item_input_box().send_keys(Keys.ENTER)
@@ -60,10 +58,8 @@ class ItemValidationTest(FunctionalTest):
def test_error_messages_are_cleared_on_input(self):
self.browser.get(self.live_server_url)
self.get_item_input_box().send_keys("Banter too thicc")
self.get_item_input_box().send_keys(Keys.ENTER)
self.wait_for_row_in_list_table("1. Banter too thicc")
self.get_item_input_box().send_keys("Banter too thicc")
self.add_list_item("Gobbledygook")
self.get_item_input_box().send_keys("Gobbledygook")
self.get_item_input_box().send_keys(Keys.ENTER)
self.wait_for(
lambda: self.assertTrue(self.get_error_element().is_displayed())

View File

@@ -1,5 +1,5 @@
import re
from django.core import mail
from unittest.mock import patch
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from .base import FunctionalTest
@@ -8,7 +8,11 @@ TEST_EMAIL = "discoman@example.com"
SUBJECT = "A magic login link to your Dashboard"
class LoginTest(FunctionalTest):
def test_login_using_magic_link(self):
@patch('apps.lyric.views.requests.post')
def test_login_using_magic_link(self, mock_post):
# Mock successful Mailgun API response
mock_post.return_value.status_code = 200
self.browser.get(self.live_server_url)
self.browser.find_element(By.CSS_SELECTOR, "input[name=email]").send_keys(
TEST_EMAIL, Keys.ENTER
@@ -24,14 +28,20 @@ class LoginTest(FunctionalTest):
if self.test_server:
return
email = mail.outbox.pop()
self.assertIn(TEST_EMAIL, email.to)
self.assertEqual(email.subject, SUBJECT)
# Verify Mailgun API was called
self.assertEqual(mock_post.call_count, 1)
call_kwargs = mock_post.call_args.kwargs
self.assertIn("Use this magic link to login to your Dashboard", email.body)
url_search = re.search(r"http://.+/.+$", email.body)
# Check email data
self.assertEqual(call_kwargs['data']['to'], TEST_EMAIL)
self.assertEqual(call_kwargs['data']['subject'], SUBJECT)
# Extract magic link URL from email body
email_body = call_kwargs['data']['text']
self.assertIn("Use this magic link to login to your Dashboard", email_body)
url_search = re.search(r"http://.+/.+$", email_body)
if not url_search:
self.fail(f"Could not find url in email body:\n{email.body}")
self.fail(f"Could not find url in email body:\n{email_body}")
url = url_search.group(0)
self.assertIn(self.live_server_url, url)

View File

@@ -1,12 +1,9 @@
from django.conf import settings
from django.contrib.auth import BACKEND_SESSION_KEY, SESSION_KEY, get_user_model
from django.contrib.sessions.backends.db import SessionStore
from selenium.webdriver.common.by import By
from .base import FunctionalTest
from .container_commands import create_session_on_server
from .management.commands.create_session import create_pre_authenticated_session
User = get_user_model()
class MyListsTest(FunctionalTest):
def create_pre_authenticated_session(self, email):
@@ -25,11 +22,48 @@ class MyListsTest(FunctionalTest):
)
)
def test_logged_in_users_are_saved_as_my_lists(self):
email = "discoman@example.com"
self.browser.get(self.live_server_url)
self.wait_to_be_logged_out(email)
def test_logged_in_users_lists_are_saved_as_my_lists(self):
self.create_pre_authenticated_session("discoman@example.com")
self.create_pre_authenticated_session(email)
self.browser.get(self.live_server_url)
self.wait_to_be_logged_in(email)
self.add_list_item("Reticulate splines")
self.add_list_item("Regurgitate spines")
first_list_url = self.browser.current_url
self.browser.find_element(By.LINK_TEXT, "My lists").click()
self.wait_for(
lambda: self.assertIn(
"discoman@example.com",
self.browser.find_element(By.CSS_SELECTOR, "h2").text,
)
)
self.wait_for(
lambda: self.browser.find_element(By.LINK_TEXT, "Reticulate splines")
)
self.browser.find_element(By.LINK_TEXT, "Reticulate splines").click()
self.wait_for(
lambda: self.assertEqual(self.browser.current_url, first_list_url)
)
self.browser.get(self.live_server_url)
self.add_list_item("Ribbon of death")
second_list_url = self.browser.current_url
self.browser.find_element(By.LINK_TEXT, "My lists").click()
self.wait_for(
lambda: self.browser.find_element(By.LINK_TEXT, "Ribbon of death")
)
self.browser.find_element(By.LINK_TEXT, "Ribbon of death").click()
self.wait_for(
lambda: self.assertEqual(self.browser.current_url, second_list_url)
)
self.browser.find_element(By.CSS_SELECTOR, "#id_logout").click()
self.wait_for(
lambda: self.assertEqual(
self.browser.find_elements(By.LINK_TEXT, "My lists"),
[],
)
)

View File

@@ -22,19 +22,14 @@ class NewVisitorTest(FunctionalTest):
inputbox.send_keys(Keys.ENTER)
self.wait_for_row_in_list_table('1. Buy peacock feathers')
inputbox = self.get_item_input_box()
inputbox.send_keys('Use peacock feathers to make a fly')
inputbox.send_keys(Keys.ENTER)
self.add_list_item("Use peacock feathers to make a fly")
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.get_item_input_box()
inputbox.send_keys('Buy peacock feathers')
inputbox.send_keys(Keys.ENTER)
self.wait_for_row_in_list_table('1. Buy peacock feathers')
self.add_list_item("Buy peacock feathers")
edith_dash_url = self.browser.current_url
self.assertRegex(edith_dash_url, '/apps/dashboard/.+')
@@ -45,10 +40,7 @@ class NewVisitorTest(FunctionalTest):
page_text = self.browser.find_element(By.TAG_NAME, 'body').text
self.assertNotIn('Buy peacock feathers', page_text)
inputbox = self.get_item_input_box()
inputbox.send_keys('Buy milk')
inputbox.send_keys(Keys.ENTER)
self.wait_for_row_in_list_table('1. Buy milk')
self.add_list_item("Buy milk")
francis_dash_url = self.browser.current_url
self.assertRegex(francis_dash_url, '/apps/dashboard/.+')

View File

@@ -0,0 +1,17 @@
<form method="POST" action="{{ form_action }}">
{% csrf_token %}
<input
id="id_text"
name="text"
class="form-control form-control-lg{% if form.errors %} is-invalid{% endif %}"
placeholder="Enter a to-do item"
value="{{ form.text.value | default:'' }}"
aria-describedby="id_text_feedback"
required
/>
{% if form.errors %}
<div id="id_text_feedback" class="invalid-feedback">
{{ form.errors.text.0 }}
</div>
{% endif %}
</form>

View File

@@ -0,0 +1,6 @@
<script src="/static/apps/scripts/dashboard.js"></script>
<script>
window.onload = () => {
initialize("#id_text");
};
</script>

View File

@@ -3,4 +3,11 @@
{% 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 %}{% url "new_list" %}{% endblock form_action %}
{% block extra_header %}
{% url "new_list" as form_action %}
{% include "apps/dashboard/_partials/_form.html" with form=form form_action=form_action %}
{% endblock extra_header %}
{% block scripts %}
{% include "apps/dashboard/_partials/_scripts.html" %}
{% endblock scripts %}

View File

@@ -3,13 +3,24 @@
{% block title_text %}Your to-do list{% endblock title_text %}
{% block header_text %}Your to-do list{% endblock header_text %}
{% block form_action %}{% url "view_list" list.id %}{% endblock form_action %}
{% block table %}
{% block extra_header %}
{% url "view_list" list.id as form_action %}
{% include "apps/dashboard/_partials/_form.html" with form=form form_action=form_action %}
{% endblock extra_header %}
{% block content %}
<div class="row justify-content-center">
<div class="col-lg-6">
<table id="id_list_table" class="table">
{% for item in list.item_set.all %}
<tr><td>{{ forloop.counter }}. {{ item.text }}</td></tr>
{% endfor %}
</table>
{% endblock table %}
</div>
</div>
{% endblock content %}
{% block scripts %}
{% include "apps/dashboard/_partials/_scripts.html" %}
{% endblock scripts %}

View File

@@ -0,0 +1,12 @@
{% extends "core/base.html" %}
{% block header_text %}{{ user.email }}'s lists{% endblock header_text %}
{% block content %}
<h3>{{ owner.email }}'s lists</h3>
<ul>
{% for list in owner.lists.all %}
<li><a href="{{ list.get_absolute_url }}">{{ list.name }}</a></li>
{% endfor %}
</ul>
{% endblock content %}

View File

@@ -18,6 +18,7 @@
<h1>Welcome, Earthman</h1>
</a>
{% if user.email %}
<a class="navbar-link" href="{% url 'my_lists' user.id %}">My lists</a>
<span class="navbar-text">Logged in as {{ user.email }}</span>
<form method="POST" action="{% url "logout" %}">
{% csrf_token %}
@@ -61,33 +62,13 @@
<div class="row justify-content-center p-5 bg-body-tertiary rounded-3">
<div class="col-lg-6 text-center">
<h2 class="display-1 mb-4">{% block header_text %}{% endblock header_text %}</h2>
<form method="POST" action="{% block form_action %}{% endblock form_action %}">
{% csrf_token %}
<input
id="id_text"
name="text"
class="form-control form-control-lg{% if form.errors %} is-invalid{% endif %}"
placeholder="Enter a to-do item"
value="{{ form.text.value | default:'' }}"
aria-describedby="id_text_feedback"
required
/>
{% if form.errors %}
<div id="id_text_feedback" class="invalid-feedback">
{{ form.errors.text.0 }}
</div>
{% endif %}
</form>
{% block extra_header %}
{% endblock extra_header %}
</div>
</div>
<div class="row justify-content-center">
<div class="col-lg-6">
{% block table %}
{% endblock table %}
</div>
</div>
{% block content %}
{% endblock content %}
</div>
</body>
@@ -95,12 +76,7 @@
<script
src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.8/js/bootstrap.min.js"
></script>
<script src="/static/apps/scripts/dashboard.js"></script>
<script>
window.onload = () => {
initialize("#id_text");
};
</script>
{% block scripts %}
{% endblock scripts %}
</html>