diff --git a/functional_tests/functional_tests.py b/functional_tests/functional_tests.py
new file mode 100644
index 0000000..445d475
--- /dev/null
+++ b/functional_tests/functional_tests.py
@@ -0,0 +1,150 @@
+from selenium import webdriver
+from selenium.webdriver.common.by import By
+from selenium.webdriver.common.keys import Keys
+from selenium.common.exceptions import WebDriverException
+from selenium.webdriver.support.ui import WebDriverWait
+from selenium.webdriver.support import expected_conditions
+
+import time
+
+from django.contrib.staticfiles.testing import StaticLiveServerTestCase
+
+
+MAX_WAIT = 10
+
+
+class NewVisitorTest(StaticLiveServerTestCase):
+
+ def setUp(self):
+ self.browser = webdriver.Firefox()
+
+ 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) as e:
+ if time.time() - start_time > MAX_WAIT:
+ raise e
+ time.sleep(0.5)
+
+ def test_can_start_a_list_and_retrieve_it_later(self):
+ # Edith has heard about a cool new online to-do app. She goes
+ # to check out its homepage
+ self.browser.get(self.live_server_url)
+
+ # She notices the page title and header mention to-do lists
+ self.assertIn('To-Do', self.browser.title)
+ header_text = self.browser.find_element_by_tag_name('h1').text
+ self.assertIn('To-Do', header_text)
+
+ # She is invited to enter a to-do item straight away
+ inputbox = self.browser.find_element_by_id('id_new_item')
+ self.assertEqual(
+ inputbox.get_attribute('placeholder'),
+ 'Enter a to-do item'
+ )
+
+ # She types "Buy peacock feathers" into a text box (Edith's hobby
+ # is tying fly-fishing lures)
+ inputbox.send_keys('Buy peacock feathers')
+
+ # When she hits enter, the page updates, and now the page lists
+ # "1: Buy peacock feathers" as an item in a to-do list table
+ inputbox.send_keys(Keys.ENTER)
+ WebDriverWait(self.browser, 10).until(
+ expected_conditions.text_to_be_present_in_element(
+ (By.ID, 'id_list_table'), 'Buy peacock feathers'))
+
+ self.wait_for_row_in_list_table('1: Buy peacock feathers')
+
+ # There is still a text box inviting her to add another item. She
+ # enters "Use peacock feathers to make a fly" (Edith is very
+ # methodical)
+ 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)
+
+ # The page updates again, and now shows both items on her list
+ 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')
+
+ # Edith wonders whether the site will remember her list. Then she sees
+ # that the site has generated a unique URL for her -- there is some
+ # explanatory text to that effect.
+
+ def test_multiple_users_can_start_lists_at_different_urls(self):
+ # Edith start a new todo list
+ 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')
+
+ # She notices that her list has a unique URL
+ edith_list_url = self.browser.current_url
+ self.assertRegex(edith_list_url, '/lists/.+')
+
+ # Now a new user, Francis, comes along to the site.
+
+ # We use a new browser session to make sure that no information
+ # of Edith's is coming through from cookies etc
+ self.browser.quit()
+ self.browser = webdriver.Firefox()
+
+ # Francis visits the home page. There is no sign of Edith's
+ # list
+ 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)
+ self.assertNotIn('make a fly', page_text)
+
+ # Francis starts a new list by entering a new item. He
+ # is less interesting than Edith...
+ 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 gets his own unique URL
+ francis_list_url = self.browser.current_url
+ self.assertRegex(francis_list_url, '/lists/.+')
+ self.assertNotEqual(francis_list_url, edith_list_url)
+
+ # Again, there is no trace of Edith's list
+ 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):
+ # Edith goes to the home page
+ self.browser.get(self.live_server_url)
+ self.browser.set_window_size(1024, 768)
+
+ # She notices the input box is nicely centered
+ inputbox = self.browser.find_element_by_id('id_new_item')
+ self.assertAlmostEqual(
+ inputbox.location['x'] + inputbox.size['width'] / 2,
+ 512,
+ delta=5
+ )
+
+ # She starts a new list and sees the input is nicely
+ # centered there too
+ 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=5
+ )
diff --git a/lists/__init__.py b/lists/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/lists/admin.py b/lists/admin.py
new file mode 100644
index 0000000..d76d4e1
--- /dev/null
+++ b/lists/admin.py
@@ -0,0 +1,5 @@
+from django.contrib import admin
+from lists.models import Item, List
+
+admin.site.register(Item)
+admin.site.register(List)
\ No newline at end of file
diff --git a/lists/apps.py b/lists/apps.py
new file mode 100644
index 0000000..595ab53
--- /dev/null
+++ b/lists/apps.py
@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class ListsConfig(AppConfig):
+ default_auto_field = "django.db.models.BigAutoField"
+ name = "lists"
diff --git a/lists/migrations/0001_initial.py b/lists/migrations/0001_initial.py
new file mode 100644
index 0000000..2846d8b
--- /dev/null
+++ b/lists/migrations/0001_initial.py
@@ -0,0 +1,27 @@
+# Generated by Django 4.1.3 on 2022-11-23 14:29
+
+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/lists/migrations/0002_item_text.py b/lists/migrations/0002_item_text.py
new file mode 100644
index 0000000..0b91247
--- /dev/null
+++ b/lists/migrations/0002_item_text.py
@@ -0,0 +1,19 @@
+# Generated by Django 4.1.3 on 2022-11-23 14:38
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("lists", "0001_initial"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="item",
+ name="text",
+ field=models.TextField(default="test"),
+ preserve_default=False,
+ ),
+ ]
diff --git a/lists/migrations/0003_alter_item_text.py b/lists/migrations/0003_alter_item_text.py
new file mode 100644
index 0000000..1373553
--- /dev/null
+++ b/lists/migrations/0003_alter_item_text.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.1.3 on 2022-11-23 14:38
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("lists", "0002_item_text"),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name="item",
+ name="text",
+ field=models.TextField(blank=True),
+ ),
+ ]
diff --git a/lists/migrations/0004_list_alter_item_text_item_list.py b/lists/migrations/0004_list_alter_item_text_item_list.py
new file mode 100644
index 0000000..fe21178
--- /dev/null
+++ b/lists/migrations/0004_list_alter_item_text_item_list.py
@@ -0,0 +1,42 @@
+# Generated by Django 4.1.3 on 2022-11-24 14:41
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("lists", "0003_alter_item_text"),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name="List",
+ fields=[
+ (
+ "id",
+ models.BigAutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ ],
+ ),
+ migrations.AlterField(
+ model_name="item",
+ name="text",
+ field=models.TextField(default=""),
+ ),
+ migrations.AddField(
+ model_name="item",
+ name="list",
+ field=models.ForeignKey(
+ default=None,
+ on_delete=django.db.models.deletion.CASCADE,
+ to="lists.list",
+ ),
+ ),
+ ]
diff --git a/lists/migrations/__init__.py b/lists/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/lists/models.py b/lists/models.py
new file mode 100644
index 0000000..54ef7ac
--- /dev/null
+++ b/lists/models.py
@@ -0,0 +1,10 @@
+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/lists/templates/base.html b/lists/templates/base.html
new file mode 100644
index 0000000..7ae037d
--- /dev/null
+++ b/lists/templates/base.html
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+ To-Do lists
+
+
+
+
+
+
+
{% block header_text %}{% endblock %}
+
+
+
+
+
+
+
+ {% block table %}
+ {% endblock %}
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/lists/templates/home.html b/lists/templates/home.html
new file mode 100644
index 0000000..34ad36d
--- /dev/null
+++ b/lists/templates/home.html
@@ -0,0 +1,5 @@
+{% extends 'base.html' %}
+
+{% block header_text %}Start a new To-Do list{% endblock %}
+
+{% block form_action %}/lists/new{% endblock %}
\ No newline at end of file
diff --git a/lists/templates/list.html b/lists/templates/list.html
new file mode 100644
index 0000000..5cd4522
--- /dev/null
+++ b/lists/templates/list.html
@@ -0,0 +1,13 @@
+{% extends 'base.html' %}
+
+{% block header_text %}Your To-Do list{% endblock %}
+
+{% block form_action %}/lists/{{ list.id }}/add_item{% endblock %}
+
+{% block table %}
+
+ {% for item in list.item_set.all %}
+ {{ forloop.counter }}: {{ item.text }} |
+ {% endfor %}
+
+{% endblock %}
\ No newline at end of file
diff --git a/lists/tests.py b/lists/tests.py
new file mode 100644
index 0000000..9575abb
--- /dev/null
+++ b/lists/tests.py
@@ -0,0 +1,109 @@
+from django.test import TestCase
+
+from lists.models import Item, List
+
+
+class HomePageTest(TestCase):
+
+ def test_uses_home_template(self):
+ response = self.client.get('/')
+ self.assertTemplateUsed(response, 'home.html')
+
+
+class NewListTest(TestCase):
+
+ def test_can_save_a_POST_request(self):
+ self.client.post('/lists/new', data={'item_text': 'A new list item'})
+
+ self.assertEqual(Item.objects.count(), 1)
+ new_item = Item.objects.first()
+ self.assertEqual(new_item.text, 'A new list item')
+
+ def test_redirects_after_POST(self):
+ response = self.client.post('/lists/new', data={'item_text': 'A new list item'})
+ new_list = List.objects.first()
+ self.assertRedirects(response, '/lists/%d/' % (new_list.id,))
+
+
+class NewItemTest(TestCase):
+
+ def test_can_save_a_POST_request_to_an_existing_list(self):
+ correct_list = List.objects.create()
+
+ self.client.post(
+ '/lists/%d/add_item' % (correct_list.id,),
+ data={'item_text': 'A new item for an existing list'}
+ )
+
+ self.assertEqual(Item.objects.count(), 1)
+ new_item = Item.objects.first()
+ 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):
+ correct_list = List.objects.create()
+
+ response = self.client.post(
+ '/lists/%d/add_item' % (correct_list.id,),
+ data={'item_text': 'A new item for an existing list'}
+ )
+
+ self.assertRedirects(response, '/lists/%d/' % (correct_list.id,))
+
+
+class ListViewTest(TestCase):
+
+ def test_uses_list_template(self):
+ list_ = List.objects.create()
+ response = self.client.get('/lists/%d/' % (list_.id,))
+ self.assertTemplateUsed(response, 'list.html')
+
+ def test_passes_correct_list_to_template(self):
+ correct_list = List.objects.create()
+ response = self.client.get('/lists/%d/' % (correct_list.id,))
+ self.assertEqual(response.context['list'], correct_list)
+
+ def test_displays_only_items_for_that_list(self):
+ 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 1', list=other_list)
+ Item.objects.create(text='other list item 2', list=other_list)
+
+ response = self.client.get('/lists/%d/' % (correct_list.id,))
+
+ self.assertContains(response, 'itemey 1')
+ self.assertContains(response, 'itemey 2')
+ self.assertNotContains(response, 'other list item 1')
+ self.assertNotContains(response, 'other list item 2')
+
+
+class ListAndItemModelsTest(TestCase):
+
+ def test_saving_and_retrieving_items(self):
+ list_ = List()
+ list_.save()
+
+ first_item = Item()
+ first_item.text = 'The first (ever) list item'
+ first_item.list = list_
+ first_item.save()
+
+ second_item = Item()
+ second_item.text = 'Item the second'
+ second_item.list = list_
+ second_item.save()
+
+ saved_list = List.objects.first()
+ self.assertEqual(saved_list, list_)
+
+ 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, list_)
+ self.assertEqual(second_saved_item.text, 'Item the second')
+ self.assertEqual(second_saved_item.list, list_)
diff --git a/lists/urls.py b/lists/urls.py
new file mode 100644
index 0000000..6ce0dbf
--- /dev/null
+++ b/lists/urls.py
@@ -0,0 +1,9 @@
+from django.urls import re_path
+from lists import views
+
+
+urlpatterns = [
+ re_path(r'^new$', views.new_list, name='new_list'),
+ re_path(r'^(\d+)/$', views.view_list, name='view_list'),
+ re_path(r'^(\d+)/add_item$', views.add_item, name='add_item'),
+]
diff --git a/lists/views.py b/lists/views.py
new file mode 100644
index 0000000..fd3a003
--- /dev/null
+++ b/lists/views.py
@@ -0,0 +1,23 @@
+from django.shortcuts import redirect, render
+from lists.models import Item, List
+
+
+def home_page(request):
+ return render(request, 'home.html')
+
+
+def new_list(request):
+ list_ = List.objects.create()
+ Item.objects.create(text=request.POST['item_text'], list=list_)
+ return redirect('/lists/%d/' % (list_.id,))
+
+
+def add_item(request, list_id):
+ list_ = List.objects.get(id=list_id)
+ Item.objects.create(text=request.POST['item_text'], list=list_)
+ return redirect('/lists/%d/' % (list_.id,))
+
+
+def view_list(request, list_id):
+ list_ = List.objects.get(id=list_id)
+ return render(request, 'list.html', {'list': list_})
diff --git a/pydjango_ci_integration/settings.py b/pydjango_ci_integration/settings.py
index cb1cd75..5cabea6 100644
--- a/pydjango_ci_integration/settings.py
+++ b/pydjango_ci_integration/settings.py
@@ -12,6 +12,10 @@
import os
+from dotenv import load_dotenv
+
+load_dotenv()
+
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
@@ -23,7 +27,7 @@
SECRET_KEY = os.getenv('SECRET_KEY', 'g!^gs#bib&6sn5ow5i&ho0bj4dlz(y%v9!h-fnmh#6h=u_&ip=')
# SECURITY WARNING: don't run with debug turned on in production!
-DEBUG = False
+DEBUG = True
ALLOWED_HOSTS = ['*']
@@ -43,6 +47,7 @@
# Custom apps
'tasks.apps.TasksConfig',
+ 'lists',
]
SITE_ID = 1
@@ -84,7 +89,7 @@
DATABASES = {
'default': {
- 'ENGINE': os.getenv('DB_ENGINE', 'django.db.backends.mysql'),
+ 'ENGINE': os.getenv('DB_ENGINE', 'django.db.backends.postgresql_psycopg2'),
'NAME': os.getenv('DB_NAME', 'pydjango'),
'USER': os.getenv('DB_USER', 'root'),
'PASSWORD': os.getenv('DB_PASSWORD', ''),
diff --git a/pydjango_ci_integration/urls.py b/pydjango_ci_integration/urls.py
index 70d3344..bfd1867 100644
--- a/pydjango_ci_integration/urls.py
+++ b/pydjango_ci_integration/urls.py
@@ -1,27 +1,16 @@
-"""pydjango_ci_integration URL Configuration
-
-The `urlpatterns` list routes URLs to views. For more information please see:
- https://docs.djangoproject.com/en/1.11/topics/http/urls/
-Examples:
-Function views
- 1. Add an import: from my_app import views
- 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home')
-Class-based views
- 1. Add an import: from other_app.views import Home
- 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home')
-Including another URLconf
- 1. Import the include() function: from django.conf.urls import url, include
- 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls'))
-"""
-from django.conf.urls import url, include
+from django.urls import include, path, re_path
from django.contrib import admin
-from tasks import views
+from tasks import views as task
+from lists.views import home_page
+
urlpatterns = [
- url(r'^admin/', admin.site.urls),
- url('', include('tasks.urls'))
+ path('admin/', admin.site.urls),
+ re_path(r'^$', home_page, name='home'),
+ re_path(r'^lists/', include('lists.urls')),
+ #path('task/', include('tasks.urls'))
]
-handler404 = views.Custom404.as_view()
-handler500 = views.Custom500.as_view()
+#handler404 = task.Custom404.as_view()
+#handler500 = task.Custom500.as_view()
diff --git a/requirements.txt b/requirements.txt
index 750086e..7e95b2e 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,10 +1,19 @@
-Django==2.1.7
-mysqlclient==1.4.2
+asgiref==3.5.2
+astroid==2.5
coverage==4.5.2
-django-nose==1.4.6
+Django==4.1.3
+django-nose==1.4.7
+isort==5.10.1
+lazy-object-proxy==1.8.0
+mccabe==0.6.1
nose==1.3.7
+psycopg2-binary==2.8.6
pylint==2.6.0
pylint-django==2.3.0
pylint-plugin-utils==0.6
-selenium==3.141.0
python-dotenv==0.10.1
+selenium==3.141.0
+sqlparse==0.4.3
+toml==0.10.2
+urllib3==1.26.12
+wrapt==1.12.1
diff --git a/tasks/tests/test_browser.py b/tasks/tests/test_browser.py
index 37007d6..62621e4 100644
--- a/tasks/tests/test_browser.py
+++ b/tasks/tests/test_browser.py
@@ -1,6 +1,7 @@
"""
Unit Test file for views
"""
+'''
from django.test import TestCase
from selenium import webdriver
@@ -18,3 +19,4 @@ def test_chrome_site_homepage(self):
browser.get(SITE_URL)
self.assertIn('Semaphore', browser.title)
browser.close()
+'''
\ No newline at end of file
diff --git a/tasks/tests/test_models.py b/tasks/tests/test_models.py
index 0e7f8e5..d110431 100644
--- a/tasks/tests/test_models.py
+++ b/tasks/tests/test_models.py
@@ -1,6 +1,7 @@
"""
Unit test file for models
"""
+'''
from django.test import TestCase
from tasks.models import Task
@@ -41,3 +42,4 @@ def test_get_absolute_url(self):
"""
task = Task.objects.get(id=1)
self.assertEqual(task.get_absolute_url(), '/edit/1')
+'''
\ No newline at end of file
diff --git a/tasks/tests/test_views.py b/tasks/tests/test_views.py
index 0ec1f8b..848054c 100644
--- a/tasks/tests/test_views.py
+++ b/tasks/tests/test_views.py
@@ -1,6 +1,7 @@
"""
Unit Test file for views
"""
+'''
from django.test import TestCase
from django.urls import reverse
@@ -44,3 +45,4 @@ def test_view_template(self):
response = self.client.get(reverse('tasks:tasks_list'), follow=True)
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'tasks/task_list.html')
+'''
\ No newline at end of file
diff --git a/tasks/urls.py b/tasks/urls.py
index 6e863d4..606c85c 100644
--- a/tasks/urls.py
+++ b/tasks/urls.py
@@ -1,18 +1,3 @@
-"""tasks URL Configuration
-
-The `urlpatterns` list routes URLs to views. For more information please see:
- https://docs.djangoproject.com/en/1.11/topics/http/urls/
-Examples:
-Function views
- 1. Add an import: from my_app import views
- 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home')
-Class-based views
- 1. Add an import: from other_app.views import Home
- 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home')
-Including another URLconf
- 1. Import the include() function: from django.conf.urls import url, include
- 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls'))
-"""
from django.urls import path
from . import views