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: tasks:
- name: Debug django_allowed_host - name: Debug django_allowed_host
@@ -12,6 +12,34 @@
update_cache: true update_cache: true
become: 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 - name: Add our user to the docker group, so we don't need sudo/become
ansible.builtin.user: ansible.builtin.user:
name: '{{ ansible_user }}' name: '{{ ansible_user }}'
@@ -88,9 +116,29 @@
EMAIL_HOST_PASSWORD: "{{ email_host_password }}" EMAIL_HOST_PASSWORD: "{{ email_host_password }}"
MAILGUN_API_KEY: "{{ mailgun_api_key }}" MAILGUN_API_KEY: "{{ mailgun_api_key }}"
ports: 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 - name: Run migration inside container
community.docker.docker_container_exec: community.docker.docker_container_exec:
container: gamearray container: gamearray
command: ./manage.py migrate 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 tzdata==2025.3
urllib3==2.6.2 urllib3==2.6.2
websocket-client==1.9.0 websocket-client==1.9.0
whitenoise==6.11.0
wsproto==1.3.2 wsproto==1.3.2

View File

@@ -3,3 +3,4 @@ django-stubs==5.2.8
django-stubs-ext==5.2.8 django-stubs-ext==5.2.8
gunicorn==23.0.0 gunicorn==23.0.0
requests==2.31.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 from django.db import migrations, models
@@ -12,9 +13,21 @@ class Migration(migrations.Migration):
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='Item', name='List',
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('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 from django.urls import reverse
class List(models.Model): 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): def get_absolute_url(self):
return reverse("view_list", args=[self.id]) 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) => { const initialize = (inputSelector) => {
console.log("initialize called!"); // console.log("initialize called!");
const textInput = document.querySelector(inputSelector); const textInput = document.querySelector(inputSelector);
textInput.oninput = () => { textInput.oninput = () => {
console.log("oninput triggered"); // console.log("oninput triggered");
textInput.classList.remove("is-invalid"); 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.db.utils import IntegrityError
from django.test import TestCase from django.test import TestCase
from ..models import Item, List from ..models import Item, List
from apps.lyric.models import User
class ItemModelTest(TestCase): class ItemModelTest(TestCase):
def test_default_text(self): def test_default_text(self):
@@ -59,3 +60,17 @@ class ListModelTest(TestCase):
list(list1.item_set.all()), list(list1.item_set.all()),
[item1, item2, item3], [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.test import TestCase
from django.utils import html from django.utils import html
from unittest import skip from unittest import skip
@@ -6,7 +7,7 @@ from ..forms import (
EMPTY_ITEM_ERROR, EMPTY_ITEM_ERROR,
) )
from ..models import Item, List from ..models import Item, List
import lxml.html from apps.lyric.models import User
class HomePageTest(TestCase): class HomePageTest(TestCase):
def test_uses_home_template(self): def test_uses_home_template(self):
@@ -144,3 +145,22 @@ class ListViewTest(TestCase):
self.assertContains(response, expected_error) self.assertContains(response, expected_error)
self.assertTemplateUsed(response, "apps/dashboard/list.html") self.assertTemplateUsed(response, "apps/dashboard/list.html")
self.assertEqual(Item.objects.all().count(), 1) 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 = [ urlpatterns = [
path('new_list', views.new_list, name='new_list'), path('new_list', views.new_list, name='new_list'),
path('<int:list_id>/', views.view_list, name='view_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 django.shortcuts import redirect, render
from .forms import ExistingListItemForm, ItemForm from .forms import ExistingListItemForm, ItemForm
from .models import Item, List from .models import Item, List
from apps.lyric.models import User
def home_page(request): def home_page(request):
return render(request, "apps/dashboard/home.html", {"form": ItemForm()}) return render(request, "apps/dashboard/home.html", {"form": ItemForm()})
@@ -9,6 +10,9 @@ def new_list(request):
form = ItemForm(data=request.POST) form = ItemForm(data=request.POST)
if form.is_valid(): if form.is_valid():
nulist = List.objects.create() nulist = List.objects.create()
if request.user.is_authenticated:
nulist.owner = request.user
nulist.save()
form.save(for_list=nulist) form.save(for_list=nulist)
return redirect(nulist) return redirect(nulist)
else: else:
@@ -25,3 +29,6 @@ def view_list(request, list_id):
return redirect(our_list) return redirect(our_list)
return render(request, "apps/dashboard/list.html", {"list": our_list, "form": form}) 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 from django.db import migrations, models
@@ -14,8 +15,15 @@ class Migration(migrations.Migration):
migrations.CreateModel( migrations.CreateModel(
name='User', name='User',
fields=[ 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)), ('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) uid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
class User(models.Model): 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) email = models.EmailField(unique=True)
REQUIRED_FIELDS = [] REQUIRED_FIELDS = []

View File

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

View File

@@ -5,6 +5,7 @@ from django.contrib.staticfiles.testing import StaticLiveServerTestCase
from selenium import webdriver from selenium import webdriver
from selenium.common.exceptions import WebDriverException from selenium.common.exceptions import WebDriverException
from selenium.webdriver.common.by import By from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from .container_commands import reset_database 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") rows = self.browser.find_elements(By.CSS_SELECTOR, "#id_list_table tr")
self.assertIn(row_text, [row.text for row in rows]) 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 @wait
def wait_to_be_logged_in(self, email): def wait_to_be_logged_in(self, email):
self.browser.find_element(By.CSS_SELECTOR, "#id_logout"), 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) return _exec_in_container_on_server(host, commands)
def _exec_in_container_locally(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) return _run_commands(["docker", "exec", _get_container_id()] + commands)
def _exec_in_container_on_server(host, commands): def _exec_in_container_on_server(host, commands):
@@ -52,5 +52,5 @@ def _run_commands(commands):
result = process.stdout.decode() result = process.stdout.decode()
if process.returncode != 0: if process.returncode != 0:
raise Exception(result) raise Exception(result)
print(f"Result: {result!r}") # print(f"Result: {result!r}")
return result.strip() return result.strip()

View File

@@ -17,7 +17,7 @@ class Command(BaseCommand):
def create_pre_authenticated_session(email): def create_pre_authenticated_session(email):
user = User.objects.create(email=email) user = User.objects.create(email=email)
session = SessionStore() 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[BACKEND_SESSION_KEY] = settings.AUTHENTICATION_BACKENDS[0]
session.save() session.save()
return session.session_key return session.session_key

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
import re import re
from django.core import mail from unittest.mock import patch
from selenium.webdriver.common.by import By from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys from selenium.webdriver.common.keys import Keys
from .base import FunctionalTest from .base import FunctionalTest
@@ -8,7 +8,11 @@ TEST_EMAIL = "discoman@example.com"
SUBJECT = "A magic login link to your Dashboard" SUBJECT = "A magic login link to your Dashboard"
class LoginTest(FunctionalTest): 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.get(self.live_server_url)
self.browser.find_element(By.CSS_SELECTOR, "input[name=email]").send_keys( self.browser.find_element(By.CSS_SELECTOR, "input[name=email]").send_keys(
TEST_EMAIL, Keys.ENTER TEST_EMAIL, Keys.ENTER
@@ -24,14 +28,20 @@ class LoginTest(FunctionalTest):
if self.test_server: if self.test_server:
return return
email = mail.outbox.pop() # Verify Mailgun API was called
self.assertIn(TEST_EMAIL, email.to) self.assertEqual(mock_post.call_count, 1)
self.assertEqual(email.subject, SUBJECT) call_kwargs = mock_post.call_args.kwargs
self.assertIn("Use this magic link to login to your Dashboard", email.body) # Check email data
url_search = re.search(r"http://.+/.+$", email.body) 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: 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) url = url_search.group(0)
self.assertIn(self.live_server_url, url) self.assertIn(self.live_server_url, url)

View File

@@ -1,12 +1,9 @@
from django.conf import settings from django.conf import settings
from django.contrib.auth import BACKEND_SESSION_KEY, SESSION_KEY, get_user_model from selenium.webdriver.common.by import By
from django.contrib.sessions.backends.db import SessionStore
from .base import FunctionalTest from .base import FunctionalTest
from .container_commands import create_session_on_server from .container_commands import create_session_on_server
from .management.commands.create_session import create_pre_authenticated_session from .management.commands.create_session import create_pre_authenticated_session
User = get_user_model()
class MyListsTest(FunctionalTest): class MyListsTest(FunctionalTest):
def create_pre_authenticated_session(self, email): 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): def test_logged_in_users_lists_are_saved_as_my_lists(self):
email = "discoman@example.com" self.create_pre_authenticated_session("discoman@example.com")
self.browser.get(self.live_server_url)
self.wait_to_be_logged_out(email)
self.create_pre_authenticated_session(email)
self.browser.get(self.live_server_url) 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) inputbox.send_keys(Keys.ENTER)
self.wait_for_row_in_list_table('1. Buy peacock feathers') self.wait_for_row_in_list_table('1. Buy peacock feathers')
inputbox = self.get_item_input_box() self.add_list_item("Use peacock feathers to make a fly")
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('2. Use peacock feathers to make a fly')
self.wait_for_row_in_list_table('1. Buy peacock feathers') self.wait_for_row_in_list_table('1. Buy peacock feathers')
def test_multiple_users_can_start_lists_at_different_urls(self): def test_multiple_users_can_start_lists_at_different_urls(self):
self.browser.get(self.live_server_url) self.browser.get(self.live_server_url)
inputbox = self.get_item_input_box() self.add_list_item("Buy peacock feathers")
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 edith_dash_url = self.browser.current_url
self.assertRegex(edith_dash_url, '/apps/dashboard/.+') 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 page_text = self.browser.find_element(By.TAG_NAME, 'body').text
self.assertNotIn('Buy peacock feathers', page_text) self.assertNotIn('Buy peacock feathers', page_text)
inputbox = self.get_item_input_box() self.add_list_item("Buy milk")
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 francis_dash_url = self.browser.current_url
self.assertRegex(francis_dash_url, '/apps/dashboard/.+') 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 title_text %}Start a new to-do list{% endblock title_text %}
{% block header_text %}Start a new to-do list{% endblock header_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 title_text %}Your to-do list{% endblock title_text %}
{% block header_text %}Your to-do list{% endblock header_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"> <table id="id_list_table" class="table">
{% for item in list.item_set.all %} {% for item in list.item_set.all %}
<tr><td>{{ forloop.counter }}. {{ item.text }}</td></tr> <tr><td>{{ forloop.counter }}. {{ item.text }}</td></tr>
{% endfor %} {% endfor %}
</table> </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> <h1>Welcome, Earthman</h1>
</a> </a>
{% if user.email %} {% 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> <span class="navbar-text">Logged in as {{ user.email }}</span>
<form method="POST" action="{% url "logout" %}"> <form method="POST" action="{% url "logout" %}">
{% csrf_token %} {% csrf_token %}
@@ -61,33 +62,13 @@
<div class="row justify-content-center p-5 bg-body-tertiary rounded-3"> <div class="row justify-content-center p-5 bg-body-tertiary rounded-3">
<div class="col-lg-6 text-center"> <div class="col-lg-6 text-center">
<h2 class="display-1 mb-4">{% block header_text %}{% endblock header_text %}</h2> <h2 class="display-1 mb-4">{% block header_text %}{% endblock header_text %}</h2>
{% block extra_header %}
<form method="POST" action="{% block form_action %}{% endblock form_action %}"> {% endblock extra_header %}
{% 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>
</div> </div>
</div> </div>
<div class="row justify-content-center"> {% block content %}
<div class="col-lg-6"> {% endblock content %}
{% block table %}
{% endblock table %}
</div>
</div>
</div> </div>
</body> </body>
@@ -95,12 +76,7 @@
<script <script
src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.8/js/bootstrap.min.js" src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.8/js/bootstrap.min.js"
></script> ></script>
{% block scripts %}
<script src="/static/apps/scripts/dashboard.js"></script> {% endblock scripts %}
<script>
window.onload = () => {
initialize("#id_text");
};
</script>
</html> </html>