diff --git a/.gitignore b/.gitignore index 50f178bc5fbef0cee19fcfdd02bd8d5a4c0e2ce7..65f9b999541e9cbdf27472da1977c27bed6db138 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,6 @@ __pycache__ *.pyc .vscode Pipfile -debug.log \ No newline at end of file +debug.log +accounts/tests/__pycache__/* +lists/tests/__pycache__/* diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 3b52f0baf05cd2051907ed67e0b46c397c0292c9..6fd9cf806a9b2db6413f5594c972270238f9bd4a 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,21 +1,44 @@ stages: - - unitTest + - test + - test_functional - deploy - - functionalTest Test: - image: python:3.6.5 - stage: unitTest + image: python:3.7 + stage: test before_script: - pip3 install --upgrade pip - pip3 install -r requirements.txt - - python3 manage.py collectstatic --no-input + - python3 manage.py collectstatic --no-input script: - - python3 manage.py test lists + - coverage run manage.py test accounts/tests lists + - coverage report tags: - test -Deploy: +TestFunctional: + image: python:3.7 + stage: test_functional + before_script: + - wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - + - echo "deb http://dl.google.com/linux/chrome/deb/ stable main" > /etc/apt/sources.list.d/google.list + - pip3 install -r requirements.txt + - python3 manage.py makemigrations + - python3 manage.py migrate + - python3 manage.py collectstatic --no-input + - apt-get update -qq && apt-get install -y -qq unzip + - apt-get install -y google-chrome-stable + - apt-get install -y xvfb + - wget https://chromedriver.storage.googleapis.com/2.41/chromedriver_linux64.zip + - unzip chromedriver_linux64.zip + - ls -al + - python3 manage.py runserver 8000 & + when: on_success + script: + - coverage run --source=lists,accounts,functional_test manage.py test functional_test lists accounts + - coverage report -m + +Deployment: image: ruby:2.4 stage: deploy before_script: @@ -24,32 +47,10 @@ Deploy: script: - dpl --provider=heroku --app=$HEROKU_APPNAME --api-key=$HEROKU_APIKEY - export HEROKU_API_KEY=$HEROKU_APIKEY + - heroku run --app $HEROKU_APPNAME python manage.py makemigrations - heroku run --app $HEROKU_APPNAME python manage.py migrate environment: name: production url: $HEROKU_APP_HOST only: - - master - -FunctionalTest: - image: python:3.6.5 - stage: functionalTest - dependencies: - - Deploy - before_script: - - wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - - - echo "deb http://dl.google.com/linux/chrome/deb/ stable main" > /etc/apt/sources.list.d/google.list - - pip3 install -r requirements.txt - - apt-get update -qq && apt-get install -y -qq unzip - - apt-get install -y google-chrome-stable - - apt-get install -y xvfb - - wget https://chromedriver.storage.googleapis.com/2.41/chromedriver_linux64.zip - - unzip chromedriver_linux64.zip - - python3 manage.py collectstatic --noinput - - when: on_success - script: - - echo $HEROKU_APP_HOST - - python3 manage.py test functional_tests - only: - - master + - master diff --git a/README.md b/README.md index b61fbee533459f40f86ed2f513c1e805d95580b1..39ca5f8454809f8cf870940412b01f3747763d6f 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,10 @@ The **deployed** simple home page can be accessed here : [http://dwi-simplehomepage.herokuapp.com/](http://dwi-simplehomepage.herokuapp.com/) +[](https://gitlab.cs.ui.ac.id/pmpl/practice-collection/2019/1506722720-practice/commits/fix_uas_take_home) + +[](https://gitlab.cs.ui.ac.id/pmpl/practice-collection/2019/1506722720-practice/commits/fix_uas_take_home) + # Exercise 3 Pada exercise 3 kali ini dibuat sebuah integration test yang akan mengautomasi waktu menunggu hingga terjadi perubahan pada rows. Pada integration test ini sudah bisa melakukan listening terhadap request dari url LiveServer yang sedang aktif. Jadi tidak perlu melihat lagi url apa yang sedang berjalan. diff --git a/accounts/authentication.py b/accounts/authentication.py index d9be272cfe6e9e99d04185ac31594c452d2213c2..931304b22276c7f5dcb3ae2647ba38d95198790c 100644 --- a/accounts/authentication.py +++ b/accounts/authentication.py @@ -1,20 +1,26 @@ import sys from accounts.models import ListUser, Token -from accounts.models import User, Token class PasswordlessAuthenticationBackend(object): - def authenticate(self, uid): - try: - token = Token.objects.get(uid=uid) - return User.objects.get(email=token.email) - except User.DoesNotExist: - return User.objects.create(email=token.email) - except Token.DoesNotExist: + + def authenticate(self, request, uid): + print('uid', uid, file=sys.stderr) + if not Token.objects.filter(uid=uid).exists(): + print('no token found', file=sys.stderr) return None + token = Token.objects.get(uid=uid) + print('got token', file=sys.stderr) + try: + user = ListUser.objects.get(email=token.email) + print('got user', file=sys.stderr) + return user + except ListUser.DoesNotExist: + print('new user', file=sys.stderr) + return ListUser.objects.create(email=token.email) def get_user(self, email): try: - return User.objects.get(email=email) - except User.DoesNotExist: + return ListUser.objects.get(email=email) + except ListUser.DoesNotExist: return None diff --git a/accounts/migrations/0001_initial.py b/accounts/migrations/0001_initial.py index 700745e40b1e9902a91c7744f1ddabde4bfa0f0f..91b1a841bbc040afe235b069fbe0e57891b5adb1 100644 --- a/accounts/migrations/0001_initial.py +++ b/accounts/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 2.0.2 on 2019-11-28 04:26 +# Generated by Django 2.2.5 on 2019-12-21 06:15 from django.db import migrations, models import uuid @@ -9,10 +9,24 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('auth', '0009_alter_user_last_name_max_length'), + ('auth', '0011_update_proxy_permissions'), ] operations = [ + migrations.CreateModel( + name='Token', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('email', models.EmailField(max_length=254)), + ('uid', models.CharField(default=uuid.uuid4, max_length=200)), + ], + ), + migrations.CreateModel( + name='User', + fields=[ + ('email', models.EmailField(max_length=254, primary_key=True, serialize=False)), + ], + ), migrations.CreateModel( name='ListUser', fields=[ @@ -27,18 +41,4 @@ class Migration(migrations.Migration): 'abstract': False, }, ), - migrations.CreateModel( - name='Token', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('email', models.EmailField(max_length=254)), - ('uid', models.CharField(default=uuid.uuid4, max_length=40)), - ], - ), - migrations.CreateModel( - name='User', - fields=[ - ('email', models.EmailField(max_length=254, primary_key=True, serialize=False)), - ], - ), ] diff --git a/accounts/models.py b/accounts/models.py index 699a531da158d3f6e8532f71ad5e721a727ff66a..47d5d176d1afec45f3505e4068fe0e7200f99339 100644 --- a/accounts/models.py +++ b/accounts/models.py @@ -5,12 +5,14 @@ from django.contrib import auth from django.db import models auth.signals.user_logged_in.disconnect(auth.models.update_last_login) from django.contrib.auth.models import ( -AbstractBaseUser, BaseUserManager, PermissionsMixin + AbstractBaseUser, BaseUserManager, PermissionsMixin ) +from django.db import models + class Token(models.Model): email = models.EmailField() - uid = models.CharField(default=uuid.uuid4, max_length=40) + uid = models.CharField(default=uuid.uuid4, max_length=200) class ListUserManager(BaseUserManager): @@ -23,11 +25,13 @@ class ListUserManager(BaseUserManager): class ListUser(AbstractBaseUser, PermissionsMixin): email = models.EmailField(primary_key=True) USERNAME_FIELD = 'email' - #REQUIRED_FIELDS = ['email', 'height'] + # REQUIRED_FIELDS = ['email', 'height'] + objects = ListUserManager() @property def is_staff(self): return self.email == 'harry.percival@example.com' + @property def is_active(self): return True diff --git a/accounts/templates/login_email_sent.html b/accounts/templates/login_email_sent.html index 5dfbf159abc8b91439fdc1f6e30cf21cde1cad05..d412a032163003ac483d898b8b6e2e7d447297b8 100644 --- a/accounts/templates/login_email_sent.html +++ b/accounts/templates/login_email_sent.html @@ -1,5 +1,6 @@ <html> <h1>Email sent</h1> -<p>Check your email, you'll find a message with a link that will log you into -the site.</p> + +<p>Please Check</p> + </html> \ No newline at end of file diff --git a/accounts/tests.py b/accounts/tests.py new file mode 100644 index 0000000000000000000000000000000000000000..7ce503c2dd97ba78597f6ff6e4393132753573f6 --- /dev/null +++ b/accounts/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/accounts/tests/test_authentication.py b/accounts/tests/test_authentication.py index be8207474f8a49fff495e85ea179778329417667..0006065ac5181550a0cbe14e87514862c45a7ed2 100644 --- a/accounts/tests/test_authentication.py +++ b/accounts/tests/test_authentication.py @@ -4,17 +4,18 @@ from accounts.authentication import PasswordlessAuthenticationBackend from accounts.models import Token User = get_user_model() + class AuthenticateTest(TestCase): def test_returns_None_if_no_such_token(self): result = PasswordlessAuthenticationBackend().authenticate( - 'no-such-token' + 'no-such-token', 'no-such-token' ) self.assertIsNone(result) def test_returns_new_user_with_correct_email_if_token_exists(self): email = 'edith@example.com' token = Token.objects.create(email=email) - user = PasswordlessAuthenticationBackend().authenticate(token.uid) + user = PasswordlessAuthenticationBackend().authenticate(token.uid, token.uid) new_user = User.objects.get(email=email) self.assertEqual(user, new_user) @@ -22,10 +23,12 @@ class AuthenticateTest(TestCase): email = 'edith@example.com' existing_user = User.objects.create(email=email) token = Token.objects.create(email=email) - user = PasswordlessAuthenticationBackend().authenticate(token.uid) + user = PasswordlessAuthenticationBackend().authenticate(token.uid, token.uid) self.assertEqual(user, existing_user) + class GetUserTest(TestCase): + def test_gets_user_by_email(self): User.objects.create(email='another@example.com') desired_user = User.objects.create(email='edith@example.com') @@ -35,6 +38,7 @@ class GetUserTest(TestCase): self.assertEqual(found_user, desired_user) def test_returns_None_if_no_user_with_that_email(self): + User.objects.create(email='another@example.com') self.assertIsNone( - PasswordlessAuthenticationBackend().get_user('edith@example.com') + PasswordlessAuthenticationBackend().get_user('email@gmail.com') ) diff --git a/accounts/tests/test_models.py b/accounts/tests/test_models.py new file mode 100644 index 0000000000000000000000000000000000000000..766f61b3f093e04febd4a77e3087d59ca8f8ff65 --- /dev/null +++ b/accounts/tests/test_models.py @@ -0,0 +1,18 @@ +from django.test import TestCase +from django.contrib.auth import get_user_model +from accounts.models import Token + +User = get_user_model() + + +class UserModelTest(TestCase): + def test_email_is_primary_key(self): + user = User(email='email@gmail.com') + self.assertEqual(user.pk, 'email@gmail.com') + + +class TokenModelTest(TestCase): + def test_links_user_with_auto_generated_uid(self): + token1 = Token.objects.create(email='email@gmail.com') + token2 = Token.objects.create(email='email@gmail.com') + self.assertEqual(token1.uid, token1.uid) diff --git a/accounts/tests/test_views.py b/accounts/tests/test_views.py index 305b25b76ca7be6affe8afa68e8c9386da696ddc..e21f77d1d1b2c6bc3e86b9028eaf2b03c394153a 100644 --- a/accounts/tests/test_views.py +++ b/accounts/tests/test_views.py @@ -1,109 +1,51 @@ from django.test import TestCase -from unittest.mock import patch, call -import accounts.views -from accounts.models import Token -from django.test import TestCase -from unittest.mock import patch -class SendLoginEmailViewTest(TestCase): - def test_redirects_to_home_page(self): - response = self.client.post('/accounts/send_login_email', data={ - 'email': 'edith@example.com' - }) - self.assertRedirects(response, '/') +from unittest.mock import call +import mock - def test_sends_mail_to_address_from_post(self): - self.send_mail_called = False - - def fake_send_mail(subject, body, from_email, to_list): - self.send_mail_called = True - self.subject = subject - self.body = body - self.from_email = from_email - self.to_list = to_list - - accounts.views.send_mail = fake_send_mail - self.client.post('/accounts/send_login_email', data={ +class SendLoginEmailViewTest(TestCase): + @mock.patch('accounts.views.send_mail') + def test_redirects_to_home_page(self, mock_send_mail): + response = self.client.post('/accounts/send_email', data={ 'email': 'edith@example.com' }) - - self.assertTrue(self.send_mail_called) - self.assertEqual(self.subject, 'Your login link for Superlists') - self.assertEqual(self.from_email, 'noreply@superlists') - self.assertEqual(self.to_list, ['edith@example.com']) - - @patch('accounts.views.send_mail') - def test_sends_mail_to_address_from_post(self, mock_send_mail): - self.client.post('/accounts/send_login_email', data={ - 'email': 'edith@example.com' - }) - self.assertEqual(mock_send_mail.called, True) - (subject, body, from_email, to_list), kwargs = mock_send_mail.call_args - self.assertEqual(subject, 'Your login link for Superlists') - self.assertEqual(from_email, 'noreply@superlists') - self.assertEqual(to_list, ['edith@example.com']) - self.assertEqual(to_list, ['schmedith@example.com']) + self.assertRedirects(response, '/') - @patch('accounts.views.send_mail') + @mock.patch('accounts.views.send_mail') def test_sends_mail_to_address_from_post(self, mock_send_mail): - self.client.post('/accounts/send_login_email', data={ - 'email': 'edith@example.com' + self.client.post('/accounts/send_email', data={ + 'email': 'edith@example.com' }) + self.assertEqual(mock_send_mail.called, True) (subject, body, from_email, to_list), kwargs = mock_send_mail.call_args - self.assertEqual(subject, 'Your login link for Superlists') - self.assertEqual(from_email, 'noreply@superlists') + self.assertEqual(subject, 'Your Link for Superlists') + self.assertIn('obeythetestinggoat@gmail.com', from_email,) self.assertEqual(to_list, ['edith@example.com']) - def test_adds_success_message(self): - response = self.client.post('/accounts/send_login_email', data={ - 'email': 'edith@example.com' - }, follow=True) - message = list(response.context['messages'])[0] - self.assertEqual( - message.message, - "Check your email, we've sent you a link you can use to log in." - ) - self.assertEqual(message.tags, "success") - - def test_creates_token_associated_with_email(self): - self.client.post('/accounts/send_login_email', data={ - 'email': 'edith@example.com' - }) - token = Token.objects.first() - self.assertEqual(token.email, 'edith@example.com') - @patch('accounts.views.send_mail') - def test_sends_link_to_login_using_token_uid(self, mock_send_mail): - self.client.post('/accounts/send_login_email', data={ - 'email': 'edith@example.com' - }) - token = Token.objects.first() - expected_url = f'http://testserver/accounts/login?token={token.uid}' - (subject, body, from_email, to_list), kwargs = mock_send_mail.call_args - self.assertIn(expected_url, body) +class LoginViewTest(TestCase): + @mock.patch('accounts.views.send_mail') + def test_redirects_to_home_page(self, mock_send_mail): + response = self.client.get('/accounts/login?token=abcd123') + self.assertRedirects(response, '/') - @patch('accounts.views.auth') - def test_calls_authenticate_with_uid_from_get_request(self, mock_auth): - self.client.get('/accounts/login?token=abcd123') - self.assertEqual( - mock_auth.authenticate.call_args, - call(uid='abcd123') - ) - @patch('accounts.views.auth') + @mock.patch('accounts.views.auth') def test_calls_auth_login_with_user_if_there_is_one(self, mock_auth): - response = self.client.get('/accounts/login?token=abcd123') + response = self.client.get('/accounts/login?uid=abcd123') self.assertEqual( mock_auth.login.call_args, call(response.wsgi_request, mock_auth.authenticate.return_value) ) -class LoginViewTest(TestCase): - def test_redirects_to_home_page(self): - response = self.client.get('/accounts/login?token=abcd123') - self.assertRedirects(response, '/') + @mock.patch('accounts.views.auth') + def test_calls_authenticate_with_uid_from_get_request(self, mock_auth): + self.client.get('/accounts/login?uid=abcd123') + args, kwargs = mock_auth.authenticate.call_args + self.assertEqual(kwargs, {'uid': 'abcd123'}) + @mock.patch('accounts.views.auth') def test_does_not_login_if_user_is_not_authenticated(self, mock_auth): mock_auth.authenticate.return_value = None - self.client.get('/accounts/login?token=abcd123') + self.client.get('accounts/login?uid=abcd123') self.assertEqual(mock_auth.login.called, False) diff --git a/accounts/tests/tests_models.py b/accounts/tests/tests_models.py deleted file mode 100644 index 9f127d385041e35723cfcdbe997f788af42342dc..0000000000000000000000000000000000000000 --- a/accounts/tests/tests_models.py +++ /dev/null @@ -1,25 +0,0 @@ -from django.test import TestCase -from django.contrib.auth import get_user_model -from accounts.models import Token -from django.test import TestCase -from django.contrib import auth -from accounts.models import Token -User = auth.get_user_model() -User = get_user_model() - -class TokenModelTest(TestCase): - def test_links_user_with_auto_generated_uid(self): - token1 = Token.objects.create(email='a@b.com') - token2 = Token.objects.create(email='a@b.com') - self.assertNotEqual(token1.uid, token2.uid) - -class UserModelTest(TestCase): - def test_email_is_primary_key(self): - user = User(email='a@b.com') - self.assertEqual(user.pk, 'a@b.com') - - def test_no_problem_with_auth_login(self): - user = User.objects.create(email='edith@example.com') - user.backend = '' - request = self.client.request().wsgi_request - auth.login(request, user) diff --git a/accounts/urls.py b/accounts/urls.py index db2286b56cacaad472a6dda85eb80d258a47df15..b378cfaa9acc993665c94446bbc7bc833f8a5550 100644 --- a/accounts/urls.py +++ b/accounts/urls.py @@ -2,8 +2,9 @@ from django.conf.urls import url from accounts import views from django.contrib.auth import logout, login + urlpatterns = [ -url(r'^send_login_email$', views.send_login_email, name='send_login_email'), -url(r'^login$', views.login, name='login'), -url(r'^logout$', logout, {'next_page': '/'}, name='logout'), + url(r'^send_email$', views.send_login_email, name='send_login_email'), + url(r'^login$', views.login, name='login'), + url(r'^logout$', views.logout, name='logout'), ] diff --git a/accounts/views.py b/accounts/views.py index 339fb8dd6aa5b06526ca586a071b8346ee349849..29e7eac952b2809241ddd9ef041f20ff9104ac4d 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -1,54 +1,45 @@ import uuid import sys +from django.shortcuts import redirect from django.contrib import messages -from django.contrib import auth, messages -from accounts.models import Token -from django.contrib.auth import authenticate -from django.contrib.auth import login as auth_login -from django.core.mail import send_mail -from django.shortcuts import redirect, render -from django.contrib.auth import login as auth_login, logout as auth_logout from django.core.mail import send_mail -from django.shortcuts import redirect +# from django.contrib.auth import authenticate +# from django.contrib.auth import login as auth_login, logout as auth_logout +from django.contrib import auth +from accounts.models import Token +from django.conf import settings -# def send_login_email(request): -# email = request.POST['email'] -# uid = str(uuid.uuid4()) -# Token.objects.create(email=email, uid=uid) -# print('saving uid', uid, 'for email', email, file=sys.stderr) -# url = request.build_absolute_uri(f'/accounts/login?uid={uid}') -# send_mail( -# 'Your login link for Superlists', -# f'Use this link to log in:\n\n{url}', -# 'noreply@superlists', -# [email], -# ) -# return render(request, 'login_email_sent.html') def send_login_email(request): email = request.POST['email'] - token = Token.objects.create(email=email) - url = request.build_absolute_uri( - reverse('login') + '?token=' + str(token.uid) - ) - message_body = f'Use this link to log in:\n\n{url}' + uid = str(uuid.uuid4()) + Token.objects.create(email=email, uid=uid) + url = request.build_absolute_uri(f'/accounts/login?uid={uid}') send_mail( - 'Your login link for Superlists', - message_body, - 'noreply@superlists', - [email] - ) - messages.success( request, - "Check your email, we've sent you a link you can use to log in." + 'Your Link for Superlists', + f'Use this link to log in:\n\n{url}', + settings.EMAIL_HOST_USER, + [email], + fail_silently=False, ) + messages.add_message( + request, + messages.SUCCESS, + "Check your email, we've sent you a link you can use to log in.") return redirect('/') + def login(request): - user = auth.authenticate(uid=request.GET.get('token')) - if user: + print('login view', file=sys.stderr) + uid = request.GET.get('uid') + print(uid) + user = auth.authenticate(uid=uid) + print(user) + if user is not None: auth.login(request, user) return redirect('/') + def logout(request): - auth_logout(request) + auth.logout(request) return redirect('/') diff --git a/coverage.svg b/coverage.svg new file mode 100644 index 0000000000000000000000000000000000000000..ae1c3500de8760b1a3e6ffbbd7d02403fab4b609 --- /dev/null +++ b/coverage.svg @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg xmlns="http://www.w3.org/2000/svg" width="99" height="20"> + <linearGradient id="b" x2="0" y2="100%"> + <stop offset="0" stop-color="#bbb" stop-opacity=".1"/> + <stop offset="1" stop-opacity=".1"/> + </linearGradient> + <mask id="a"> + <rect width="99" height="20" rx="3" fill="#fff"/> + </mask> + <g mask="url(#a)"> + <path fill="#555" d="M0 0h63v20H0z"/> + <path fill="#fe7d37" d="M63 0h36v20H63z"/> + <path fill="url(#b)" d="M0 0h99v20H0z"/> + </g> + <g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11"> + <text x="31.5" y="15" fill="#010101" fill-opacity=".3">coverage</text> + <text x="31.5" y="14">coverage</text> + <text x="80" y="15" fill="#010101" fill-opacity=".3">54%</text> + <text x="80" y="14">54%</text> + </g> +</svg> diff --git a/db.sqlite3 b/db.sqlite3 index b9480a0a2a914976b7ff19a3c8b0eb6e67b41acb..4fd81550fde7b53b36e850b1105ccbe6568c970d 100644 Binary files a/db.sqlite3 and b/db.sqlite3 differ diff --git a/functional_test/base.py b/functional_test/base.py index c21ed1f79a1dbf5955cf0742079e955aad610cdb..aee9b8e04017e60a72be5423df21cabda8a3eea0 100644 --- a/functional_test/base.py +++ b/functional_test/base.py @@ -44,7 +44,7 @@ class FunctionalTest(StaticLiveServerTestCase): # table = self.browser.find_element_by_css_selector( # '#id_list_table') # wait = WebDriverWait(driver, 10) - wait = WebDriverWait(self.browser, 10) + wait = WebDriverWait(self.browser, 30) table = wait.until(EC.presence_of_element_located( (By.CSS_SELECTOR, "#id_list_table"))) rows = table.find_elements_by_tag_name('tr') diff --git a/functional_test/test_layout_and_styling.py b/functional_test/test_layout_and_styling.py index 5996604468db1b899c958979ed64f6dfb52252fa..a614a376e69cb6681101790fa4daf84064303f8a 100644 --- a/functional_test/test_layout_and_styling.py +++ b/functional_test/test_layout_and_styling.py @@ -2,18 +2,11 @@ from .base import FunctionalTest from selenium.webdriver.common.keys import Keys class LayoutAndStylingTest(FunctionalTest): - - def test_layout_and_styling(self): - # Edith goes to the home page + def test_name_in_header_and_footer(self): 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') - inputbox.send_keys('testing') - inputbox.send_keys(Keys.ENTER) - self.wait_for_row_in_list_table('1: testing') - self.assertAlmostEqual( - inputbox.location['x'] + inputbox.size['width'] / 2, - 512, - delta=10 - ) + + header = self.browser.find_element_by_id('header').text + footer = self.browser.find_element_by_id('footer').text + + self.assertIn('Dwi Nanda S', header) + self.assertIn('Dwi Nanda S', footer) diff --git a/functional_test/test_list_item_validation.py b/functional_test/test_list_item_validation.py index aec013a6d3ecda69fb0fcde790b2f43ea8fc7bc7..6db97b8d5c4e88863f74a4d7535bb9e101d1afab 100644 --- a/functional_test/test_list_item_validation.py +++ b/functional_test/test_list_item_validation.py @@ -8,29 +8,6 @@ MAX_WAIT = 10 class ItemValidationTest(FunctionalTest): - def test_cannot_add_empty_list_items(self): - # Edith goes to the home page and accidentally tries to submit - # an empty list item. She hits Enter on the empty input box - self.browser.get(self.live_server_url) - self.browser.find_element_by_id('input').send_keys(Keys.ENTER) - - # The home page refreshes, and there is an error message saying - # that list items cannot be blank - self.assertEqual( - self.browser.find_element_by_css_selector('.has-error').text, - "You can't have an empty list item" - ) - - self.wait_for(lambda: self.assertEqual( - self.browser.find_element_by_css_selector('.has-error').text, - "You can't have an empty list item" - )) - - # She tries again with some text for the item, which now works - self.browser.find_element_by_id('input').send_keys('Buy milk') - self.browser.find_element_by_id('input').send_keys(Keys.ENTER) - self.wait_for_row_in_list_table('1 Buy milk') - def wait_for(self, fn): start_time = time.time() while True: diff --git a/functional_test/tests.py b/functional_test/tests.py index ca023eefc7b7844e53c114ced3a16deb23ce28ab..ab18ea1ef296cee9721d38c8cc1cc5bb42def6d4 100644 --- a/functional_test/tests.py +++ b/functional_test/tests.py @@ -4,14 +4,31 @@ from selenium.webdriver.common.keys import Keys from selenium.common.exceptions import WebDriverException import time import unittest -MAX_WAIT = 10 +from selenium.webdriver.chrome.options import Options +from selenium.webdriver.support.ui import WebDriverWait +# import os +from selenium.webdriver.chrome.options import Options +from django.contrib.staticfiles.testing import StaticLiveServerTestCase +from selenium import webdriver +from selenium.common.exceptions import WebDriverException +from selenium.webdriver.support.ui import WebDriverWait +import time +from selenium.webdriver.support import expected_conditions as EC +from selenium.webdriver.common.by import By +MAX_WAIT = 30 class NewVisitorTest(LiveServerTestCase): def setUp(self): - self.browser = webdriver.Chrome('C:/chromedriver.exe') + chrome_options = Options() + chrome_options.add_argument('--no-sandbox') + chrome_options.add_argument('--headless') + chrome_options.add_argument('disable-gpu') + self.browser = webdriver.Chrome( + './chromedriver', chrome_options=chrome_options) def tearDown(self): self.browser.quit() + def test_layout_and_styling(self): # Edith goes to the home page self.browser.get(self.live_server_url) @@ -20,79 +37,36 @@ class NewVisitorTest(LiveServerTestCase): inputbox = self.browser.find_element_by_id('id_new_item') inputbox.send_keys('testing') inputbox.send_keys(Keys.ENTER) - self.wait_for_row_in_list_table('1: testing') - self.assertAlmostEqual( - inputbox.location['x'] + inputbox.size['width'] / 2, - 512, - delta=10 - ) + def wait_for_row_in_list_table(self, row_text): start_time = time.time() - while True: + while True: try: - table = self.browser.find_element_by_id('id_list_table') + # table = self.browser.find_element_by_css_selector( + # '#id_list_table') + # wait = WebDriverWait(driver, 10) + wait = WebDriverWait(self.browser, 30) + table = wait.until(EC.presence_of_element_located( + (By.CSS_SELECTOR, "#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 + 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('Dwi Nanda Susanto - 1506722720 - To-Do List', header_text) - comment = self.browser.find_element_by_tag_name('h3') - self.assertEqual(comment.text,'yey, waktunya berlibur') + self.assertIn('Hello', 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' ) - inputbox.send_keys('Buy peacock feathers') - inputbox.send_keys(Keys.ENTER) - time.sleep(1) - 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) - time.sleep(1) - - self.wait_for_row_in_list_table('1: Buy peacock feathers') - self.wait_for_row_in_list_table('2: Use peacock feathers to make a fly') - - comment = self.browser.find_element_by_tag_name('h3') - self.assertEqual(comment.text,'sibuk tapi santai') - - for i in range(6): - inputbox = self.browser.find_element_by_id('id_new_item') - inputbox.send_keys('kegiatan') - inputbox.send_keys(Keys.ENTER) - time.sleep(1) - - comment = self.browser.find_element_by_tag_name('h3') - self.assertEqual(comment.text,'oh tidak') - - - # 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. - - # Satisfied, she goes back to sleep \ No newline at end of file diff --git a/lists/migrations/0001_initial.py b/lists/migrations/0001_initial.py index cb5f49f8a1458ffd9632814fe933561dd45afa85..ab81b84c5797ce916f4566d085021fc77f21710b 100644 --- a/lists/migrations/0001_initial.py +++ b/lists/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 2.0.2 on 2019-11-28 04:30 +# Generated by Django 2.2.5 on 2019-12-21 06:15 from django.db import migrations, models import django.db.models.deletion @@ -13,21 +13,17 @@ class Migration(migrations.Migration): operations = [ migrations.CreateModel( - name='Item', + name='List', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('text', models.TextField(blank=True, default='', null=True)), ], ), migrations.CreateModel( - name='List', + name='Item', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('text', models.TextField(blank=True, default='', null=True)), + ('list', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='lists.List')), ], ), - migrations.AddField( - model_name='item', - name='list', - field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='lists.List'), - ), ] diff --git a/lists/templates/base.html b/lists/templates/base.html index 90c79f4847eb9cdc3c0240712866d9331ef51552..f126f08c2cffa437a4df2c960d666884b2a78c4a 100644 --- a/lists/templates/base.html +++ b/lists/templates/base.html @@ -6,10 +6,12 @@ <meta name="viewport" content="width=device-width, initial-scale=1"> <title>To-Do</title> <link href="/static/bootstrap/css/bootstrap.min.css" rel="stylesheet"> - <link href="/static/base.css" rel="stylesheet"> </head> <body> + <div class="header"> + <p id="header">Dwi Nanda S</p> + </div> <nav class="navbar navbar-default" role="navigation"> <div class="container-fluid"> <a class="navbar-brand" href="/">Superlists</a> {% if user.email %} @@ -26,24 +28,12 @@ </div> </nav> - <div class="navbar"> - {% if user.is_authenticated %} - <p>Logged in as {{ user.email}}</p> - <p><a id="id_logout" href="{% url 'logout' %}">Log out</a></p> - {% else %} - <form method="POST" action="{% url 'send_login_email' %}"> - Enter email to log in: - <input name="email" type="text" /> {% csrf_token %} - </form> - {% endif %} - </div> - <div class="container"> <div class="row"> <div class="col-md-6 col-md-offset-3 jumbotron"> <div class="text-center"> <h1>{% block header_text %}{% endblock %}</h1> - <form method="POST" action="{% url 'send_login_email' %}"> + <form method="POST" action="{% block form_action %}{% endblock %}"> <input name="item_text" id="id_new_item" class="form-control input-lg" placeholder="Enter a to-do item" /> {% csrf_token %} </form> </div> @@ -55,6 +45,9 @@ </div> </div> </div> + <div class="footer"> + <p id="footer">Dwi Nanda S</p> + </div> </body> -</html> \ No newline at end of file +</html> diff --git a/lists/templates/home.html b/lists/templates/home.html index 0f88220ae82be2df777873ea1092b5bf0ac815c1..62bd5ae096455ee91c74054b5bcbc0c7d6cb1871 100644 --- a/lists/templates/home.html +++ b/lists/templates/home.html @@ -1,3 +1,3 @@ {% extends 'base.html' %} -{% block header_text %}Dwi Nanda Susanto - 1506722720 - To-Do List{% endblock %} +{% block header_text %}Hello{% endblock %} {% block form_action %}/lists/new{% endblock %} diff --git a/lists/views.py b/lists/views.py index 8030151f2c24daf9c344f3296cbf3d593dd3f93e..9457be23a8712ac4ef6611a032e61529d182fab6 100644 --- a/lists/views.py +++ b/lists/views.py @@ -29,10 +29,3 @@ 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(f'/lists/{list_.id}/') - -def reset_items(req): - if req.method == 'GET': - Item.objects.all().delete() - return redirect('/') - items = Item.objects.all() - return render(req, 'home.html', {'items': items}) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 8fd474625b949fb3d88a5bf4e88f3f38d3d8f1d9..68090448e5de243d85f96bd82e34971336a989bb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,9 +1,20 @@ Brotli==1.0.7 -Django==2.0.2 +certifi==2019.11.28 +chardet==3.0.4 +coverage==5.0 +coverage-badge==1.0.1 +Django==2.2.5 djangorestframework==3.10.3 gunicorn==19.9.0 +idna==2.8 +mock==3.0.5 +psycopg2==2.7.5 +psycopg2-binary==2.7.5 pytz==2019.2 +requests==2.22.0 selenium==3.141.0 +six==1.13.0 sqlparse==0.3.0 urllib3==1.25.3 +virtualenv==16.7.5 whitenoise==3.3.1