From 7dc4925b4c25cbf515f89b43f726cb35502d7f90 Mon Sep 17 00:00:00 2001 From: ivan Date: Tue, 22 Nov 2022 17:12:15 +0100 Subject: [PATCH 1/8] functions_test apllied --- functional_tests.py | 45 +++++++++++++++++++++++++++++ lists/__init__.py | 0 lists/admin.py | 3 ++ lists/apps.py | 6 ++++ lists/migrations/__init__.py | 0 lists/models.py | 3 ++ lists/tests.py | 20 +++++++++++++ lists/views.py | 7 +++++ pydjango_ci_integration/settings.py | 4 +++ pydjango_ci_integration/urls.py | 24 ++++----------- requirements.txt | 20 ++++++------- 11 files changed, 104 insertions(+), 28 deletions(-) create mode 100644 functional_tests.py create mode 100644 lists/__init__.py create mode 100644 lists/admin.py create mode 100644 lists/apps.py create mode 100644 lists/migrations/__init__.py create mode 100644 lists/models.py create mode 100644 lists/tests.py create mode 100644 lists/views.py diff --git a/functional_tests.py b/functional_tests.py new file mode 100644 index 0000000..54396a2 --- /dev/null +++ b/functional_tests.py @@ -0,0 +1,45 @@ +from selenium import webdriver +import unittest + + +class NewVisitorTest(unittest.TestCase): + + def setUp(self): + self.browser = webdriver.Firefox() + + def tearDown(self): + self.browser.quit() + + 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('http://localhost:8000') + + # She notices the page title and header mention to-do lists + self.assertIn('To-Do', self.browser.title) + self.fail('Finish the test!') + + # She is invited to enter a to-do item straight away + + # She types "Buy peacock feathers" into a text box (Edith's hobby + # is tying fly-fishing lures) + + # When she hits enter, the page updates, and now the page lists + # "1: Buy peacock feathers" as an item in a to-do list + + # 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) + + # The page updates again, and now shows both items on her list + + # 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. + + # She visits that URL - her to-do list is still there. + + # Satisfied, she goes back to sleep + + +if __name__ == '__main__': + unittest.main() 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..8c38f3f --- /dev/null +++ b/lists/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. 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/__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..71a8362 --- /dev/null +++ b/lists/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/lists/tests.py b/lists/tests.py new file mode 100644 index 0000000..b0f7447 --- /dev/null +++ b/lists/tests.py @@ -0,0 +1,20 @@ +from django.urls import resolve +from django.test import TestCase +from django.http import HttpRequest + +from lists.views import home_page + + +class HomePageTest(TestCase): + + def test_root_url_resolves_to_home_page_view(self): + found = resolve('home/') + self.assertEqual(found.func, home_page) + + def test_home_page_returns_correct_html(self): + request = HttpRequest() + response = home_page(request) + html = response.content.decode('utf8') + self.assertTrue(html.startswith('')) + self.assertIn('To-Do lists', html) + self.assertTrue(html.endswith('')) diff --git a/lists/views.py b/lists/views.py new file mode 100644 index 0000000..7210248 --- /dev/null +++ b/lists/views.py @@ -0,0 +1,7 @@ +from django.shortcuts import render + +from django.http import HttpResponse + + +def home_page(request): + return HttpResponse('To-Do lists') diff --git a/pydjango_ci_integration/settings.py b/pydjango_ci_integration/settings.py index cb1cd75..e882b10 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__))) diff --git a/pydjango_ci_integration/urls.py b/pydjango_ci_integration/urls.py index 70d3344..cb96956 100644 --- a/pydjango_ci_integration/urls.py +++ b/pydjango_ci_integration/urls.py @@ -1,26 +1,14 @@ -"""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 from django.contrib import admin from tasks import views +from lists import views as list + urlpatterns = [ - url(r'^admin/', admin.site.urls), - url('', include('tasks.urls')) + path('admin/', admin.site.urls), + path('home/', list.home_page, name='home'), + path('', include('tasks.urls')) ] handler404 = views.Custom404.as_view() diff --git a/requirements.txt b/requirements.txt index 750086e..47c576c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,10 +1,10 @@ -Django==2.1.7 -mysqlclient==1.4.2 -coverage==4.5.2 -django-nose==1.4.6 -nose==1.3.7 -pylint==2.6.0 -pylint-django==2.3.0 -pylint-plugin-utils==0.6 -selenium==3.141.0 -python-dotenv==0.10.1 +Django +psycopgy2-binary +coverage +django-nose2 +nose2 +pylint +pylint-django +pylint-plugin-utils +selenium +python-dotenv From 9deb5afa8eff687f39e3c08eb047903ff9127d02 Mon Sep 17 00:00:00 2001 From: ivan Date: Wed, 23 Nov 2022 12:40:39 +0100 Subject: [PATCH 2/8] chapter 4 were readed and aded to the code --- functional_tests.py | 26 +++++++++++++++++++++++--- lists/templates/home.html | 11 +++++++++++ lists/tests.py | 18 +++--------------- lists/views.py | 4 +--- pydjango_ci_integration/settings.py | 1 + pydjango_ci_integration/urls.py | 10 +++++----- tasks/tests/test_browser.py | 2 ++ tasks/tests/test_models.py | 2 ++ tasks/tests/test_views.py | 2 ++ tasks/urls.py | 15 --------------- 10 files changed, 50 insertions(+), 41 deletions(-) create mode 100644 lists/templates/home.html diff --git a/functional_tests.py b/functional_tests.py index 54396a2..f4daeae 100644 --- a/functional_tests.py +++ b/functional_tests.py @@ -1,4 +1,6 @@ from selenium import webdriver +from selenium.webdriver.common.keys import Keys +import time import unittest @@ -17,18 +19,36 @@ def test_can_start_a_list_and_retrieve_it_later(self): # She notices the page title and header mention to-do lists self.assertIn('To-Do', self.browser.title) - self.fail('Finish the test!') + 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 + # "1: Buy peacock feathers" as an item in a to-do list table + inputbox.send_keys(Keys.ENTER) + time.sleep(1) + + table = self.browser.find_element_by_id('id_list_table') + rows = table.find_elements_by_tag_name('tr') + self.assertTrue( + any(row.text == '1: Buy peacock feathers' for row in rows), + "New to-do item did not appear in table" + ) # 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) + # enters "Use peacock feathers to make a fly" (Edith is very + # methodical) + self.fail('Finish the test!') # The page updates again, and now shows both items on her list diff --git a/lists/templates/home.html b/lists/templates/home.html new file mode 100644 index 0000000..a668ef4 --- /dev/null +++ b/lists/templates/home.html @@ -0,0 +1,11 @@ + + + To-Do lists + + +

Your To-Do list

+ + +
+ + \ No newline at end of file diff --git a/lists/tests.py b/lists/tests.py index b0f7447..f719315 100644 --- a/lists/tests.py +++ b/lists/tests.py @@ -1,20 +1,8 @@ -from django.urls import resolve from django.test import TestCase -from django.http import HttpRequest - -from lists.views import home_page class HomePageTest(TestCase): - def test_root_url_resolves_to_home_page_view(self): - found = resolve('home/') - self.assertEqual(found.func, home_page) - - def test_home_page_returns_correct_html(self): - request = HttpRequest() - response = home_page(request) - html = response.content.decode('utf8') - self.assertTrue(html.startswith('')) - self.assertIn('To-Do lists', html) - self.assertTrue(html.endswith('')) + def test_uses_home_template(self): + response = self.client.get('/') + self.assertTemplateUsed(response, 'home.html') diff --git a/lists/views.py b/lists/views.py index 7210248..fe0fc9a 100644 --- a/lists/views.py +++ b/lists/views.py @@ -1,7 +1,5 @@ from django.shortcuts import render -from django.http import HttpResponse - def home_page(request): - return HttpResponse('To-Do lists') + return render(request, 'home.html') diff --git a/pydjango_ci_integration/settings.py b/pydjango_ci_integration/settings.py index e882b10..4fbf739 100644 --- a/pydjango_ci_integration/settings.py +++ b/pydjango_ci_integration/settings.py @@ -47,6 +47,7 @@ # Custom apps 'tasks.apps.TasksConfig', + 'lists', ] SITE_ID = 1 diff --git a/pydjango_ci_integration/urls.py b/pydjango_ci_integration/urls.py index cb96956..3c3f62f 100644 --- a/pydjango_ci_integration/urls.py +++ b/pydjango_ci_integration/urls.py @@ -1,15 +1,15 @@ from django.urls import include, path from django.contrib import admin -from tasks import views +from tasks import views as task from lists import views as list urlpatterns = [ path('admin/', admin.site.urls), - path('home/', list.home_page, name='home'), - path('', include('tasks.urls')) + path('', list.home_page, name='home'), + 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/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 From eb59efb217d1dae28a6087d8a4f306305f9528d1 Mon Sep 17 00:00:00 2001 From: ivan Date: Wed, 23 Nov 2022 15:37:22 +0100 Subject: [PATCH 3/8] Model test started --- functional_tests.py | 22 +++++++++++++++------- lists/migrations/0001_initial.py | 27 +++++++++++++++++++++++++++ lists/models.py | 4 +++- lists/templates/home.html | 6 +++++- lists/tests.py | 25 +++++++++++++++++++++++++ lists/views.py | 4 +++- 6 files changed, 78 insertions(+), 10 deletions(-) create mode 100644 lists/migrations/0001_initial.py diff --git a/functional_tests.py b/functional_tests.py index f4daeae..d4164aa 100644 --- a/functional_tests.py +++ b/functional_tests.py @@ -12,6 +12,13 @@ def setUp(self): def tearDown(self): self.browser.quit() + + def check_for_row_in_list_table(self, row_text): + 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]) + + 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 @@ -38,23 +45,24 @@ def test_can_start_a_list_and_retrieve_it_later(self): inputbox.send_keys(Keys.ENTER) time.sleep(1) - table = self.browser.find_element_by_id('id_list_table') - rows = table.find_elements_by_tag_name('tr') - self.assertTrue( - any(row.text == '1: Buy peacock feathers' for row in rows), - "New to-do item did not appear in table" - ) + self.check_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) - self.fail('Finish the test!') + 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) + time.sleep(1) # The page updates again, and now shows both items on her list + self.check_for_row_in_list_table('2: Use peacock feathers to make a fly') + self.check_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. + self.fail('Finish the test!') # She visits that URL - her to-do list is still there. 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/models.py b/lists/models.py index 71a8362..6f1ea2e 100644 --- a/lists/models.py +++ b/lists/models.py @@ -1,3 +1,5 @@ from django.db import models -# Create your models here. + +class Item(models.Model): + text = models.TextField() diff --git a/lists/templates/home.html b/lists/templates/home.html index a668ef4..160fb7e 100644 --- a/lists/templates/home.html +++ b/lists/templates/home.html @@ -4,8 +4,12 @@

Your To-Do list

- +
+ + {% csrf_token %} +
+
{{ new_item_text }}
\ No newline at end of file diff --git a/lists/tests.py b/lists/tests.py index f719315..ec39567 100644 --- a/lists/tests.py +++ b/lists/tests.py @@ -1,8 +1,33 @@ from django.test import TestCase +from lists.models import Item + class HomePageTest(TestCase): def test_uses_home_template(self): response = self.client.get('/') self.assertTemplateUsed(response, 'home.html') + + def test_can_save_a_POST_request(self): + response = self.client.post('/', data={'item_text': 'A new list item'}) + self.assertIn('A new list item', response.content.decode()) + + +class ItemModelTest(TestCase): + + def test_saving_and_retrieving_items(self): + first_item = Item() + first_item.text = 'The first (ever) list item' + first_item.save() + + second_item = Item() + second_item.text = 'Item the second' + second_item.save() + + 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(second_saved_item.text, 'Item the second') diff --git a/lists/views.py b/lists/views.py index fe0fc9a..e56c030 100644 --- a/lists/views.py +++ b/lists/views.py @@ -2,4 +2,6 @@ def home_page(request): - return render(request, 'home.html') + return render(request, 'home.html', { + 'new_item_text': request.POST.get('item_text', ''), + }) From aefd19579ba287364026a45b7245bd5b19493ba4 Mon Sep 17 00:00:00 2001 From: ivan Date: Thu, 24 Nov 2022 11:54:19 +0100 Subject: [PATCH 4/8] 5 chapter was ended --- .../functional_tests.py | 0 lists/migrations/0002_item_text.py | 19 ++++++++++++ lists/migrations/0003_alter_item_text.py | 18 +++++++++++ lists/models.py | 2 +- lists/templates/home.html | 4 ++- lists/tests.py | 30 +++++++++++++++---- lists/views.py | 12 +++++--- 7 files changed, 74 insertions(+), 11 deletions(-) rename functional_tests.py => functional_tests/functional_tests.py (100%) create mode 100644 lists/migrations/0002_item_text.py create mode 100644 lists/migrations/0003_alter_item_text.py diff --git a/functional_tests.py b/functional_tests/functional_tests.py similarity index 100% rename from functional_tests.py rename to functional_tests/functional_tests.py 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/models.py b/lists/models.py index 6f1ea2e..6317e2f 100644 --- a/lists/models.py +++ b/lists/models.py @@ -2,4 +2,4 @@ class Item(models.Model): - text = models.TextField() + text = models.TextField(blank=True) diff --git a/lists/templates/home.html b/lists/templates/home.html index 160fb7e..302d2f8 100644 --- a/lists/templates/home.html +++ b/lists/templates/home.html @@ -9,7 +9,9 @@

Your To-Do list

{% csrf_token %} - + {% for item in items %} + + {% endfor %}
{{ new_item_text }}
{{ forloop.counter }} {{ item.text }}
\ No newline at end of file diff --git a/lists/tests.py b/lists/tests.py index ec39567..ecc8255 100644 --- a/lists/tests.py +++ b/lists/tests.py @@ -8,23 +8,43 @@ class HomePageTest(TestCase): def test_uses_home_template(self): response = self.client.get('/') self.assertTemplateUsed(response, 'home.html') - + def test_can_save_a_POST_request(self): + self.client.post('/', 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('/', data={'item_text': 'A new list item'}) - self.assertIn('A new list item', response.content.decode()) + self.assertEqual(response.status_code, 302) + self.assertEqual(response['location'], '/') + + def test_only_saves_items_when_necessary(self): + self.client.get('/') + self.assertEqual(Item.objects.count(), 0) + + def test_display_all_lists_items(self): + Item.objects.create(text='itemey 1') + Item.objects.create(text='itemey 2') + + response = self.client.get('/') + + self.assertIn('itemey 1', response.content.decode()) + self.assertIn('itemey 2', response.content.decode()) class ItemModelTest(TestCase): - + def test_saving_and_retrieving_items(self): first_item = Item() first_item.text = 'The first (ever) list item' first_item.save() - + second_item = Item() second_item.text = 'Item the second' second_item.save() - + saved_items = Item.objects.all() self.assertEqual(saved_items.count(), 2) first_saved_item = saved_items[0] diff --git a/lists/views.py b/lists/views.py index e56c030..90fcc1b 100644 --- a/lists/views.py +++ b/lists/views.py @@ -1,7 +1,11 @@ -from django.shortcuts import render +from django.shortcuts import render, redirect + +from lists.models import Item def home_page(request): - return render(request, 'home.html', { - 'new_item_text': request.POST.get('item_text', ''), - }) + if request.method == 'POST': + Item.objects.create(text=request.POST['item_text']) + return redirect('/') + items = Item.objects.all() + return render(request, 'home.html', {'items': items}) From 37ebc2aa67637f416ce86d503647caa425de5300 Mon Sep 17 00:00:00 2001 From: ivan Date: Thu, 24 Nov 2022 18:00:17 +0100 Subject: [PATCH 5/8] 7 chapter were applied --- functional_tests/functional_tests.py | 115 +++++++++++++++--- lists/admin.py | 4 +- .../0004_list_alter_item_text_item_list.py | 42 +++++++ lists/models.py | 7 +- lists/templates/base.html | 40 ++++++ lists/templates/home.html | 22 +--- lists/templates/list.html | 13 ++ lists/tests.py | 86 ++++++++++--- lists/urls.py | 9 ++ lists/views.py | 28 +++-- pydjango_ci_integration/settings.py | 2 +- pydjango_ci_integration/urls.py | 13 +- 12 files changed, 313 insertions(+), 68 deletions(-) create mode 100644 lists/migrations/0004_list_alter_item_text_item_list.py create mode 100644 lists/templates/base.html create mode 100644 lists/templates/list.html create mode 100644 lists/urls.py diff --git a/functional_tests/functional_tests.py b/functional_tests/functional_tests.py index d4164aa..445d475 100644 --- a/functional_tests/functional_tests.py +++ b/functional_tests/functional_tests.py @@ -1,10 +1,19 @@ 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 -import unittest + +from django.contrib.staticfiles.testing import StaticLiveServerTestCase + + +MAX_WAIT = 10 -class NewVisitorTest(unittest.TestCase): +class NewVisitorTest(StaticLiveServerTestCase): def setUp(self): self.browser = webdriver.Firefox() @@ -12,17 +21,23 @@ def setUp(self): def tearDown(self): self.browser.quit() - - def check_for_row_in_list_table(self, row_text): - 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]) - + 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('http://localhost:8000') + 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) @@ -43,9 +58,11 @@ def test_can_start_a_list_and_retrieve_it_later(self): # 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) - time.sleep(1) + WebDriverWait(self.browser, 10).until( + expected_conditions.text_to_be_present_in_element( + (By.ID, 'id_list_table'), 'Buy peacock feathers')) - self.check_for_row_in_list_table('1: 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 @@ -53,21 +70,81 @@ def test_can_start_a_list_and_retrieve_it_later(self): 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) - time.sleep(1) # The page updates again, and now shows both items on her list - self.check_for_row_in_list_table('2: Use peacock feathers to make a fly') - self.check_for_row_in_list_table('1: Buy peacock feathers') + 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. - self.fail('Finish the test!') - # She visits that URL - her to-do list is still there. + 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/.+') - # Satisfied, she goes back to sleep + # 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) -if __name__ == '__main__': - unittest.main() + # 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/admin.py b/lists/admin.py index 8c38f3f..d76d4e1 100644 --- a/lists/admin.py +++ b/lists/admin.py @@ -1,3 +1,5 @@ from django.contrib import admin +from lists.models import Item, List -# Register your models here. +admin.site.register(Item) +admin.site.register(List) \ No newline at end of file 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/models.py b/lists/models.py index 6317e2f..54ef7ac 100644 --- a/lists/models.py +++ b/lists/models.py @@ -1,5 +1,10 @@ from django.db import models +class List(models.Model): + pass + + class Item(models.Model): - text = models.TextField(blank=True) + 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 %}

+
+ + {% csrf_token %} +
+
+
+
+ +
+
+ {% block table %} + {% endblock %} +
+
+ +
+ + + + + + + \ No newline at end of file diff --git a/lists/templates/home.html b/lists/templates/home.html index 302d2f8..34ad36d 100644 --- a/lists/templates/home.html +++ b/lists/templates/home.html @@ -1,17 +1,5 @@ - - - To-Do lists - - -

Your To-Do list

-
- - {% csrf_token %} -
- - {% for item in items %} - - {% endfor %} -
{{ forloop.counter }} {{ item.text }}
- - \ No newline at end of file +{% 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 %} + + {% endfor %} +
{{ forloop.counter }}: {{ item.text }}
+{% endblock %} \ No newline at end of file diff --git a/lists/tests.py b/lists/tests.py index ecc8255..9575abb 100644 --- a/lists/tests.py +++ b/lists/tests.py @@ -1,6 +1,6 @@ from django.test import TestCase -from lists.models import Item +from lists.models import Item, List class HomePageTest(TestCase): @@ -9,45 +9,101 @@ 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('/', data={'item_text': 'A new list item'}) + 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('/', data={'item_text': 'A new list item'}) - self.assertEqual(response.status_code, 302) - self.assertEqual(response['location'], '/') + 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,)) - def test_only_saves_items_when_necessary(self): - self.client.get('/') - self.assertEqual(Item.objects.count(), 0) - def test_display_all_lists_items(self): - Item.objects.create(text='itemey 1') - Item.objects.create(text='itemey 2') +class NewItemTest(TestCase): - response = self.client.get('/') + 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.assertIn('itemey 1', response.content.decode()) - self.assertIn('itemey 2', response.content.decode()) + self.assertRedirects(response, '/lists/%d/' % (correct_list.id,)) -class ItemModelTest(TestCase): +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 index 90fcc1b..fd3a003 100644 --- a/lists/views.py +++ b/lists/views.py @@ -1,11 +1,23 @@ -from django.shortcuts import render, redirect - -from lists.models import Item +from django.shortcuts import redirect, render +from lists.models import Item, List def home_page(request): - if request.method == 'POST': - Item.objects.create(text=request.POST['item_text']) - return redirect('/') - items = Item.objects.all() - return render(request, 'home.html', {'items': items}) + 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 4fbf739..2295f88 100644 --- a/pydjango_ci_integration/settings.py +++ b/pydjango_ci_integration/settings.py @@ -27,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 = ['*'] diff --git a/pydjango_ci_integration/urls.py b/pydjango_ci_integration/urls.py index 3c3f62f..bfd1867 100644 --- a/pydjango_ci_integration/urls.py +++ b/pydjango_ci_integration/urls.py @@ -1,15 +1,16 @@ -from django.urls import include, path +from django.urls import include, path, re_path from django.contrib import admin from tasks import views as task -from lists import views as list +from lists.views import home_page urlpatterns = [ path('admin/', admin.site.urls), - path('', list.home_page, name='home'), - path('task/', include('tasks.urls')) + re_path(r'^$', home_page, name='home'), + re_path(r'^lists/', include('lists.urls')), + #path('task/', include('tasks.urls')) ] -handler404 = task.Custom404.as_view() -handler500 = task.Custom500.as_view() +#handler404 = task.Custom404.as_view() +#handler500 = task.Custom500.as_view() From accdd3d6699802197daf795711c9349ca75c414d Mon Sep 17 00:00:00 2001 From: ivan Date: Thu, 24 Nov 2022 19:29:35 +0100 Subject: [PATCH 6/8] jenkins --- requirements.txt | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/requirements.txt b/requirements.txt index 47c576c..499a151 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,10 +1,19 @@ -Django -psycopgy2-binary -coverage -django-nose2 -nose2 -pylint -pylint-django -pylint-plugin-utils -selenium -python-dotenv +asgiref==3.5.2 +astroid==2.5 +coverage==4.5.2 +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.9.5 +pylint==2.6.0 +pylint-django==2.3.0 +pylint-plugin-utils==0.6 +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 From 9b16461085c8ca7773033132934f4e920a02c20f Mon Sep 17 00:00:00 2001 From: ivan Date: Thu, 24 Nov 2022 20:01:11 +0100 Subject: [PATCH 7/8] jenkins --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 499a151..7e95b2e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,7 +7,7 @@ isort==5.10.1 lazy-object-proxy==1.8.0 mccabe==0.6.1 nose==1.3.7 -psycopg2-binary==2.9.5 +psycopg2-binary==2.8.6 pylint==2.6.0 pylint-django==2.3.0 pylint-plugin-utils==0.6 From 5266749f7f8869c3d9d90c272c4a0f7b8bfcfc5e Mon Sep 17 00:00:00 2001 From: ivan Date: Thu, 24 Nov 2022 20:04:43 +0100 Subject: [PATCH 8/8] jenkins v1 --- pydjango_ci_integration/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pydjango_ci_integration/settings.py b/pydjango_ci_integration/settings.py index 2295f88..5cabea6 100644 --- a/pydjango_ci_integration/settings.py +++ b/pydjango_ci_integration/settings.py @@ -89,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', ''),