diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index b0274513300b5926dd7205a546c3911aa7c0a08f..56f6aab06777e903315ebc7d6074bd129edba46f 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,6 +1,7 @@ stages: - - test + - unit-test - deploy + - functional-test before_script: - apt-get install -f @@ -27,19 +28,12 @@ before_script: - python manage.py runserver 8000 & UnitTest: - stage: test + stage: unit-test when: on_success script: - coverage run --include='tutorial_*/*' manage.py test -p "unit_test*.py" - coverage report -m -FunctionalTest: - stage: test - when: on_success - script: - - coverage run --include='tutorial_*/*' manage.py test -p "functional_test*.py" - - coverage report -m - DeployToHeroku: image: ruby:2.4 stage: deploy @@ -53,3 +47,10 @@ DeployToHeroku: environment: name: production url: $HEROKU_APP_HOST + +FunctionalTest: + stage: functional-test + when: on_success + script: + - coverage run --include='tutorial_*/*' manage.py test -p "functional_test*.py" + - coverage report -m diff --git a/requirements.txt b/requirements.txt index edd331552a3c9012967e4769320066c0be9b7a20..efa3c33df46f42de747a63fcc0836a6a03dc2677 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ +pytz appdirs==1.4.0 Django==1.11.17 django-webpack-loader==0.4.1 diff --git a/tutorial/static/css/tutorial_2.366cb34e0003.css b/tutorial/static/css/tutorial_2.366cb34e0003.css new file mode 100644 index 0000000000000000000000000000000000000000..abc49e145e42f2ae80560eaa29eb6369736a02a0 --- /dev/null +++ b/tutorial/static/css/tutorial_2.366cb34e0003.css @@ -0,0 +1,11 @@ +body { + min-height: 2000px; +} + +.navbar-static-top { + margin-bottom: 19px; +} +/*Textarea not resizeable*/ +textarea { + resize:none +} diff --git a/tutorial/static/css/tutorial_2.366cb34e0003.css.gz b/tutorial/static/css/tutorial_2.366cb34e0003.css.gz new file mode 100644 index 0000000000000000000000000000000000000000..7f63373e8538dac1edc79e9a31eb2f4891ee80cc Binary files /dev/null and b/tutorial/static/css/tutorial_2.366cb34e0003.css.gz differ diff --git a/tutorial/static/css/tutorial_2.css b/tutorial/static/css/tutorial_2.css new file mode 100644 index 0000000000000000000000000000000000000000..abc49e145e42f2ae80560eaa29eb6369736a02a0 --- /dev/null +++ b/tutorial/static/css/tutorial_2.css @@ -0,0 +1,11 @@ +body { + min-height: 2000px; +} + +.navbar-static-top { + margin-bottom: 19px; +} +/*Textarea not resizeable*/ +textarea { + resize:none +} diff --git a/tutorial/static/css/tutorial_2.css.gz b/tutorial/static/css/tutorial_2.css.gz new file mode 100644 index 0000000000000000000000000000000000000000..7f63373e8538dac1edc79e9a31eb2f4891ee80cc Binary files /dev/null and b/tutorial/static/css/tutorial_2.css.gz differ diff --git a/tutorial/static/staticfiles.json b/tutorial/static/staticfiles.json index 5e58c0caf874b4499c9a27ab807736108c083b34..c423aa219b1f36db2c2734799eda7ea0c736049e 100644 --- a/tutorial/static/staticfiles.json +++ b/tutorial/static/staticfiles.json @@ -1 +1 @@ -{"paths": {"admin/js/vendor/jquery/LICENSE-JQUERY.txt": "admin/js/vendor/jquery/LICENSE-JQUERY.a158210a2737.txt", "admin/js/vendor/jquery/jquery.min.js": "admin/js/vendor/jquery/jquery.min.33cabfa15c10.js", "admin/js/vendor/jquery/jquery.js": "admin/js/vendor/jquery/jquery.aacc43d6f308.js", "admin/js/vendor/xregexp/xregexp.min.js": "admin/js/vendor/xregexp/xregexp.min.c95393b8ca4d.js", "admin/js/vendor/xregexp/xregexp.js": "admin/js/vendor/xregexp/xregexp.1865b1cf5085.js", "admin/js/vendor/xregexp/LICENSE-XREGEXP.txt": "admin/js/vendor/xregexp/LICENSE-XREGEXP.d64cecf4f157.txt", "admin/js/admin/RelatedObjectLookups.js": "admin/js/admin/RelatedObjectLookups.d2bdb1018963.js", "admin/js/admin/DateTimeShortcuts.js": "admin/js/admin/DateTimeShortcuts.1536fb6bea1d.js", "admin/img/gis/move_vertex_on.svg": "admin/img/gis/move_vertex_on.0047eba25b67.svg", "admin/img/gis/move_vertex_off.svg": "admin/img/gis/move_vertex_off.7a23bf31ef8a.svg", "admin/css/widgets.css": "admin/css/widgets.5e372b41c483.css", "admin/css/login.css": "admin/css/login.a846c0e2ef65.css", "admin/css/dashboard.css": "admin/css/dashboard.7ac78187c567.css", "admin/css/forms.css": "admin/css/forms.2003a066ae02.css", "admin/css/fonts.css": "admin/css/fonts.494e4ec545c9.css", "admin/css/rtl.css": "admin/css/rtl.4c867197b256.css", "admin/css/base.css": "admin/css/base.6b517d0d5813.css", "admin/css/changelists.css": "admin/css/changelists.f6dc691f8d62.css", "admin/js/urlify.js": "admin/js/urlify.411bc3bb651c.js", "admin/js/inlines.min.js": "admin/js/inlines.min.d75b9ed03975.js", "admin/js/core.js": "admin/js/core.fd861d43f0f5.js", "admin/js/collapse.js": "admin/js/collapse.17d715df2104.js", "admin/js/actions.js": "admin/js/actions.32861b6cbdf9.js", "admin/js/prepopulate.js": "admin/js/prepopulate.ff9208865444.js", "admin/js/cancel.js": "admin/js/cancel.1d69cba4b4bf.js", "admin/js/inlines.js": "admin/js/inlines.eda404ee376d.js", "admin/js/change_form.js": "admin/js/change_form.9e85003a1a38.js", "admin/js/SelectFilter2.js": "admin/js/SelectFilter2.17009b6d4428.js", "admin/js/jquery.init.js": "admin/js/jquery.init.95b62fa19378.js", "admin/js/popup_response.js": "admin/js/popup_response.6ce3197f8fc8.js", "admin/js/SelectBox.js": "admin/js/SelectBox.b49f008d186b.js", "admin/js/actions.min.js": "admin/js/actions.min.f51f04edab28.js", "admin/js/calendar.js": "admin/js/calendar.9ac94d055fbd.js", "admin/js/timeparse.js": "admin/js/timeparse.51258861a46a.js", "admin/js/prepopulate.min.js": "admin/js/prepopulate.min.f4057ebb9b62.js", "admin/js/collapse.min.js": "admin/js/collapse.min.dc930adb2821.js", "admin/js/prepopulate_init.js": "admin/js/prepopulate_init.0d3b53c37074.js", "admin/img/search.svg": "admin/img/search.7cf54ff789c6.svg", "admin/img/icon-calendar.svg": "admin/img/icon-calendar.ac7aea671bea.svg", "admin/img/icon-clock.svg": "admin/img/icon-clock.e1d4dfac3f2b.svg", "admin/img/icon-no.svg": "admin/img/icon-no.439e821418cd.svg", "admin/img/tooltag-add.svg": "admin/img/tooltag-add.e59d620a9742.svg", "admin/img/inline-delete.svg": "admin/img/inline-delete.fec1b761f254.svg", "admin/img/LICENSE": "admin/img/LICENSE.2c54f4e1ca1c", "admin/img/icon-changelink.svg": "admin/img/icon-changelink.18d2fd706348.svg", "admin/img/icon-unknown.svg": "admin/img/icon-unknown.a18cb4398978.svg", "admin/img/sorting-icons.svg": "admin/img/sorting-icons.3a097b59f104.svg", "admin/img/icon-yes.svg": "admin/img/icon-yes.d2f9f035226a.svg", "admin/img/icon-addlink.svg": "admin/img/icon-addlink.d519b3bab011.svg", "admin/img/icon-unknown-alt.svg": "admin/img/icon-unknown-alt.81536e128bb6.svg", "admin/img/icon-deletelink.svg": "admin/img/icon-deletelink.564ef9dc3854.svg", "admin/img/README.txt": "admin/img/README.837277fa1908.txt", "admin/img/selector-icons.svg": "admin/img/selector-icons.b4555096cea2.svg", "admin/img/calendar-icons.svg": "admin/img/calendar-icons.39b290681a8b.svg", "admin/img/tooltag-arrowright.svg": "admin/img/tooltag-arrowright.bbfb788a849e.svg", "admin/img/icon-alert.svg": "admin/img/icon-alert.034cc7d8a67f.svg", "admin/fonts/Roboto-Light-webfont.woff": "admin/fonts/Roboto-Light-webfont.b446c2399bb6.woff", "admin/fonts/Roboto-Bold-webfont.woff": "admin/fonts/Roboto-Bold-webfont.2ad99072841e.woff", "admin/fonts/Roboto-Regular-webfont.woff": "admin/fonts/Roboto-Regular-webfont.ec39515ae8c6.woff", "admin/fonts/README.txt": "admin/fonts/README.2c3d0bcdede2.txt", "admin/fonts/LICENSE.txt": "admin/fonts/LICENSE.d273d63619c9.txt"}, "version": "1.0"} \ No newline at end of file +{"paths": {"admin/js/vendor/jquery/LICENSE-JQUERY.txt": "admin/js/vendor/jquery/LICENSE-JQUERY.a158210a2737.txt", "admin/js/vendor/jquery/jquery.min.js": "admin/js/vendor/jquery/jquery.min.33cabfa15c10.js", "admin/js/vendor/jquery/jquery.js": "admin/js/vendor/jquery/jquery.aacc43d6f308.js", "admin/js/vendor/xregexp/xregexp.min.js": "admin/js/vendor/xregexp/xregexp.min.c95393b8ca4d.js", "admin/js/vendor/xregexp/xregexp.js": "admin/js/vendor/xregexp/xregexp.1865b1cf5085.js", "admin/js/vendor/xregexp/LICENSE-XREGEXP.txt": "admin/js/vendor/xregexp/LICENSE-XREGEXP.d64cecf4f157.txt", "admin/js/admin/RelatedObjectLookups.js": "admin/js/admin/RelatedObjectLookups.d2bdb1018963.js", "admin/js/admin/DateTimeShortcuts.js": "admin/js/admin/DateTimeShortcuts.1536fb6bea1d.js", "admin/img/gis/move_vertex_on.svg": "admin/img/gis/move_vertex_on.0047eba25b67.svg", "admin/img/gis/move_vertex_off.svg": "admin/img/gis/move_vertex_off.7a23bf31ef8a.svg", "admin/css/widgets.css": "admin/css/widgets.5e372b41c483.css", "admin/css/login.css": "admin/css/login.a846c0e2ef65.css", "admin/css/dashboard.css": "admin/css/dashboard.7ac78187c567.css", "admin/css/forms.css": "admin/css/forms.2003a066ae02.css", "admin/css/fonts.css": "admin/css/fonts.494e4ec545c9.css", "admin/css/rtl.css": "admin/css/rtl.4c867197b256.css", "admin/css/base.css": "admin/css/base.6b517d0d5813.css", "admin/css/changelists.css": "admin/css/changelists.f6dc691f8d62.css", "admin/js/urlify.js": "admin/js/urlify.411bc3bb651c.js", "admin/js/inlines.min.js": "admin/js/inlines.min.d75b9ed03975.js", "admin/js/core.js": "admin/js/core.fd861d43f0f5.js", "admin/js/collapse.js": "admin/js/collapse.17d715df2104.js", "admin/js/actions.js": "admin/js/actions.32861b6cbdf9.js", "admin/js/prepopulate.js": "admin/js/prepopulate.ff9208865444.js", "admin/js/cancel.js": "admin/js/cancel.1d69cba4b4bf.js", "admin/js/inlines.js": "admin/js/inlines.eda404ee376d.js", "admin/js/change_form.js": "admin/js/change_form.9e85003a1a38.js", "admin/js/SelectFilter2.js": "admin/js/SelectFilter2.17009b6d4428.js", "admin/js/jquery.init.js": "admin/js/jquery.init.95b62fa19378.js", "admin/js/popup_response.js": "admin/js/popup_response.6ce3197f8fc8.js", "admin/js/SelectBox.js": "admin/js/SelectBox.b49f008d186b.js", "admin/js/actions.min.js": "admin/js/actions.min.f51f04edab28.js", "admin/js/calendar.js": "admin/js/calendar.9ac94d055fbd.js", "admin/js/timeparse.js": "admin/js/timeparse.51258861a46a.js", "admin/js/prepopulate.min.js": "admin/js/prepopulate.min.f4057ebb9b62.js", "admin/js/collapse.min.js": "admin/js/collapse.min.dc930adb2821.js", "admin/js/prepopulate_init.js": "admin/js/prepopulate_init.0d3b53c37074.js", "admin/img/search.svg": "admin/img/search.7cf54ff789c6.svg", "admin/img/icon-calendar.svg": "admin/img/icon-calendar.ac7aea671bea.svg", "admin/img/icon-clock.svg": "admin/img/icon-clock.e1d4dfac3f2b.svg", "admin/img/icon-no.svg": "admin/img/icon-no.439e821418cd.svg", "admin/img/tooltag-add.svg": "admin/img/tooltag-add.e59d620a9742.svg", "admin/img/inline-delete.svg": "admin/img/inline-delete.fec1b761f254.svg", "admin/img/LICENSE": "admin/img/LICENSE.2c54f4e1ca1c", "admin/img/icon-changelink.svg": "admin/img/icon-changelink.18d2fd706348.svg", "admin/img/icon-unknown.svg": "admin/img/icon-unknown.a18cb4398978.svg", "admin/img/sorting-icons.svg": "admin/img/sorting-icons.3a097b59f104.svg", "admin/img/icon-yes.svg": "admin/img/icon-yes.d2f9f035226a.svg", "admin/img/icon-addlink.svg": "admin/img/icon-addlink.d519b3bab011.svg", "admin/img/icon-unknown-alt.svg": "admin/img/icon-unknown-alt.81536e128bb6.svg", "admin/img/icon-deletelink.svg": "admin/img/icon-deletelink.564ef9dc3854.svg", "admin/img/README.txt": "admin/img/README.837277fa1908.txt", "admin/img/selector-icons.svg": "admin/img/selector-icons.b4555096cea2.svg", "admin/img/calendar-icons.svg": "admin/img/calendar-icons.39b290681a8b.svg", "admin/img/tooltag-arrowright.svg": "admin/img/tooltag-arrowright.bbfb788a849e.svg", "admin/img/icon-alert.svg": "admin/img/icon-alert.034cc7d8a67f.svg", "admin/fonts/Roboto-Light-webfont.woff": "admin/fonts/Roboto-Light-webfont.b446c2399bb6.woff", "admin/fonts/Roboto-Bold-webfont.woff": "admin/fonts/Roboto-Bold-webfont.2ad99072841e.woff", "admin/fonts/Roboto-Regular-webfont.woff": "admin/fonts/Roboto-Regular-webfont.ec39515ae8c6.woff", "admin/fonts/README.txt": "admin/fonts/README.2c3d0bcdede2.txt", "admin/fonts/LICENSE.txt": "admin/fonts/LICENSE.d273d63619c9.txt", "css/tutorial_2.css": "css/tutorial_2.366cb34e0003.css"}, "version": "1.0"} \ No newline at end of file diff --git a/tutorial/urls.py b/tutorial/urls.py index 5d9b2be7b9b094fddcfb2ab86880b239d58a6448..e39dabaa310c3fb0d7b05a1b19bc160f0d0fb6aa 100644 --- a/tutorial/urls.py +++ b/tutorial/urls.py @@ -17,8 +17,10 @@ from django.conf.urls import url, include from django.contrib import admin from django.views.generic.base import RedirectView import tutorial_1.urls as tutorial_1 +import tutorial_2.urls as tutorial_2 urlpatterns = [ url(r'^admin/', admin.site.urls), url(r'^tutorial-1/', include(tutorial_1, namespace='tutorial-1')), + url(r'^tutorial-2/', include(tutorial_2, namespace='tutorial-2')), ] diff --git a/tutorial_2/functional_tests.py b/tutorial_2/functional_tests.py new file mode 100644 index 0000000000000000000000000000000000000000..c75061f29258c7bb8a1e42e00f0bc1e2d98dcdc0 --- /dev/null +++ b/tutorial_2/functional_tests.py @@ -0,0 +1,100 @@ +import os +from django.test import TestCase + +from selenium import webdriver +from selenium.webdriver.chrome.options import Options +from selenium.webdriver.common.keys import Keys + +GITLAB_ENV = os.environ.get("HEROKU_APP_HOST") != None + + +class Tutorial2FunctionalTest(TestCase): + + def setUp(self): + chrome_options = Options() + chrome_options.add_argument('--dns-prefatch-disable') + chrome_options.add_argument('--no-sandbox') + chrome_options.add_argument('--headless') + chrome_options.add_argument('--disable-gpu') + self.selenium = webdriver.Chrome('./chromedriver', chrome_options=chrome_options) + + self.localhost = "http://127.0.0.1:8000/tutorial-2/" + + self.cases = [ + (5, "oh, tidak"), + (2, "sibuk tapi santai"), + (0, "yey, waktunya berlibur"), + ] + print("GITLAB ENV:", GITLAB_ENV) + if GITLAB_ENV: + self.localhost = "https://pmpl-izzan.herokuapp.com/tutorial-2/" + + super(Tutorial2FunctionalTest, self).setUp() + + def tearDown(self): + self.selenium.quit() + super(Tutorial2FunctionalTest, self).tearDown() + + def test_find_name(self): + selenium = self.selenium + selenium.get(self.localhost) + self.assertIn('Izzan Fakhril Islam', selenium.page_source) + + def test_input_todo_item(self): + selenium = self.selenium + selenium.get(self.localhost) + + todo_date = selenium.find_element_by_id('todo_date') + todo_date.send_keys('23052019') + todo_date.send_keys(Keys.TAB) + todo_date.send_keys('1245') + + todo_text = selenium.find_element_by_id('activity') + todo_text.send_keys('Dummy') + + selenium.find_element_by_id('todo_submit').click() + self.assertIn('Dummy', selenium.page_source) + + def test_input_todo_commentary_item(self): + print("LOCALHOST:", self.localhost) + selenium = self.selenium + selenium.get(self.localhost) + + todo_date = selenium.find_element_by_id('comment_date') + todo_date.send_keys('23122019') + + todo_comment = selenium.find_element_by_id('comment') + todo_comment.send_keys('Dummy comment') + + selenium.find_element_by_id('comment_submit').click() + self.assertIn('Dummy comment', selenium.page_source) + + def test_input_todo_on_the_same_date_and_generate_comment(self): + selenium = self.selenium + selenium.get(self.localhost) + date = 24 + month_and_year = '112019' + + for n, comment in self.cases: + time = 1045 + for i in range(n): + todo_date = selenium.find_element_by_id('todo_date') + todo_date.send_keys(str(date) + month_and_year) + todo_date.send_keys(Keys.TAB) + todo_date.send_keys(str(time)) + + todo_text = selenium.find_element_by_id('activity') + todo_text.send_keys('Dummy Activity' + str(n)) + + selenium.find_element_by_id('todo_submit').click() + time += 1 + + todo_date = selenium.find_element_by_id('comment_date') + todo_date.send_keys(str(date) + month_and_year) + + todo_comment = selenium.find_element_by_id('comment') + todo_comment.send_keys(comment) + selenium.find_element_by_id('comment_submit').click() + + self.assertIn(comment, selenium.page_source) + date += 1 diff --git a/tutorial_2/migrations/0001_initial.py b/tutorial_2/migrations/0001_initial.py new file mode 100644 index 0000000000000000000000000000000000000000..c34415b8d724781da70853433e231ef228459d5e --- /dev/null +++ b/tutorial_2/migrations/0001_initial.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.17 on 2019-09-23 12:42 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='TodoList', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('date', models.DateTimeField()), + ('todo_list', models.TextField(max_length=100)), + ], + ), + migrations.CreateModel( + name='TodoListCommentary', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('date', models.DateTimeField()), + ('comment', models.TextField(max_length=100)), + ], + ), + ] diff --git a/tutorial_2/migrations/0002_auto_20190924_1309.py b/tutorial_2/migrations/0002_auto_20190924_1309.py new file mode 100644 index 0000000000000000000000000000000000000000..d2b0bf813f77fff1e8e9255ebc4bd75244e9b7fb --- /dev/null +++ b/tutorial_2/migrations/0002_auto_20190924_1309.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.17 on 2019-09-24 06:09 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tutorial_2', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='todolistcommentary', + name='date', + field=models.DateField(), + ), + ] diff --git a/tutorial_2/models.py b/tutorial_2/models.py index 71a836239075aa6e6e4ecb700e9c42c95c022d91..1110cb7f93828cf4eb037f7360ee940030a5c088 100644 --- a/tutorial_2/models.py +++ b/tutorial_2/models.py @@ -1,3 +1,18 @@ from django.db import models + # Create your models here. +class TodoList(models.Model): + date = models.DateTimeField() + todo_list = models.TextField(max_length=100) + + def __str__(self): + return "Todo at %s: %s" % (self.date.timestamp(), self.todo_list) + + +class TodoListCommentary(models.Model): + date = models.DateField() + comment = models.TextField(max_length=100) + + def __str__(self): + return "Commentary at %s: %s" % (self.date.__str__, self.comment) diff --git a/tutorial_2/static/css/tutorial_2.css b/tutorial_2/static/css/tutorial_2.css new file mode 100644 index 0000000000000000000000000000000000000000..abc49e145e42f2ae80560eaa29eb6369736a02a0 --- /dev/null +++ b/tutorial_2/static/css/tutorial_2.css @@ -0,0 +1,11 @@ +body { + min-height: 2000px; +} + +.navbar-static-top { + margin-bottom: 19px; +} +/*Textarea not resizeable*/ +textarea { + resize:none +} diff --git a/tutorial_2/templates/layout/base.html b/tutorial_2/templates/layout/base.html new file mode 100644 index 0000000000000000000000000000000000000000..5c3539c1d846fcda55c243b6c7f26aa570eaf88d --- /dev/null +++ b/tutorial_2/templates/layout/base.html @@ -0,0 +1,46 @@ +{% load staticfiles %} +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="utf-8"> + <meta name="description" content="Tutorial 2 PMPL"> + <meta name="author" content="{{ author }}"> + <!-- bootstrap csss --> + <link href="//netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap.min.css" rel="stylesheet"> + + <title> + {% block title %} Tutorial 2 PMPL by {{ author }} - {{ npm }}{% endblock %} + </title> +</head> +<style> + @import url(https://fonts.googleapis.com/css?family=Roboto|Roboto+Slab); + .container, .footer-down, .header-up{ + font-family: "Roboto", "sans-serif"; + font-weight: normal; + } + +</style> +<body style="background-color: darkseagreen"> + <header class="header-up"> + {% include "tutorial_2/../partials/header.html" %} + </header> + <content> + <div class="container"> + {% block content %} + + {% endblock %} + </div> + + </content> + <footer class="footer-down"> + <!-- TODO Block Footer dan include footer.html --> + {% block footer %} + {% include "tutorial_2/../partials/footer.html" %} + {% endblock %} + </footer> + + <!-- Jquery n Bootstrap Script --> + <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script> + <script type="application/javascript" src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script> +</body> +</html> diff --git a/tutorial_2/templates/partials/footer.html b/tutorial_2/templates/partials/footer.html new file mode 100644 index 0000000000000000000000000000000000000000..7b444475c27afc22cce7bca2e845897c6f714a18 --- /dev/null +++ b/tutorial_2/templates/partials/footer.html @@ -0,0 +1 @@ +<p align="center">© {{ author }}</p> diff --git a/tutorial_2/templates/partials/header.html b/tutorial_2/templates/partials/header.html new file mode 100644 index 0000000000000000000000000000000000000000..094848a450432daeb4b8902f13474a458b16fa07 --- /dev/null +++ b/tutorial_2/templates/partials/header.html @@ -0,0 +1,23 @@ +<!-- Fixed navbar --> +<style type = text/css> + .navbar-static-top{ + background-color: darkgray; + font-size: 17px; + } + .navbar-brand{ + font-size: 17px; + } +</style> +<nav class="navbar navbar-default navbar-static-top"> + <div class="container"> + <div class="navbar-header"> + <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar"> + <span class="sr-only">Toggle navigation</span> + <span class="icon-bar"></span> + <span class="icon-bar"></span> + <span class="icon-bar"></span> + </button> + <a class="navbar-brand" href="{% url 'tutorial-2:index' %}" style="color: black">Tutorial 2 PMPL</a> + </div> + </div> +</nav> diff --git a/tutorial_2/templates/tutorial_2.html b/tutorial_2/templates/tutorial_2.html new file mode 100644 index 0000000000000000000000000000000000000000..693c35bfb1b6c8afb5cdf2e93799ddbc02bdbdba --- /dev/null +++ b/tutorial_2/templates/tutorial_2.html @@ -0,0 +1,111 @@ +{% extends "tutorial_2/../layout/base.html" %} + +{% block content %} + <style> + .form-control { + @import url(https://fonts.googleapis.com/css?family=Droid+Sans+Mono); + margin-top: 3px; + margin-bottom: 3px; + font-family: "Droid Sans Mono"; + } + table { + text-align: left; + padding: 8px; + width: 100%; + } + </style> + <h1 align="center" style="font-weight: bold">{{ author }}'s Todo List</h1> + <br><br> + <div class="row"> + <div class="col-md-6 col-xs-6"> + <h4>Todo List Table</h4> + <br> + <table> + <tr> + <th><h5><b>Date/Time</b></h5></th> + <th><h5><b>Todo</b></h5></th> + </tr> + {% for todos in todos_dict %} + <tr> + <td>{{ todos.date }}</td> + <td>{{ todos.todo_list }}</td> + </tr> + {% endfor %} + </table> + <br> + <h4>Daily Todo List Comments</h4> + <br> + <table> + <tr> + <th><h5><b>Date</b></h5></th> + <th><h5><b>Comment</b></h5></th> + </tr> + {% for commentary in todos_commentary_dict %} + <tr> + <td>{{ commentary.date }}</td> + <td>{{ commentary.comment }}</td> + </tr> + {% endfor %} + </table> + <br> + </div> + <div class="col-md-6 col-xs-6"> + <h4><b>Add Todo</b></h4> + <form method="POST" action="{% url 'tutorial-2:add_todo' %}"> + {% csrf_token %} + <table> + <tr> + <td> + Date/Time + </td> + <td> + <input type="datetime-local" name="date" id="todo_date" required="required" size="27"/> + </td> + </tr> + <tr> + <td> + Todo + </td> + <td> + <input type="text" name="activity" id="activity" required="required" size="27"> + </td> + </tr> + </table> + <br> + <button type="submit" id="todo_submit" class="button btn-success">Submit</button> + </form> + <br> + <div class="errornote"> + <p style="color: black; font-family: 'Roboto Slab'">{{ error_msg }}</p> + </div> + <h4><b>Add Daily Todo List Comment</b></h4> + <form method="POST" action="{% url 'tutorial-2:add_todo_commentary' %}"> + {% csrf_token %} + <table> + <tr> + <td> + Date + </td> + <td> + <input type="date" name="date" id="comment_date" required="required" size="27"/> + </td> + </tr> + <tr> + <td> + Todo List Comment + </td> + <td> + <input type="text" name="comment" id="comment" required="required" size="27"> + </td> + </tr> + </table> + <br> + <button type="submit" id="comment_submit" class="button btn-success">Submit</button> + </form> + <br> + <div class="errornote"> + <p style="color: black; font-family: 'Roboto Slab'">{{ commentary_error_msg }}</p> + </div> + </div> + </div> +{% endblock %} diff --git a/tutorial_2/unit_tests.py b/tutorial_2/unit_tests.py index a4de0ad62de23661a74d2e2bf0314417b83bf9e7..eaab15fd044a3c50abe80f6c2c56c5fcc51cbeae 100644 --- a/tutorial_2/unit_tests.py +++ b/tutorial_2/unit_tests.py @@ -1,9 +1,81 @@ from django.test import TestCase - +from django.test import Client +from .views import index +from .models import TodoListCommentary, TodoList +from django.http import HttpRequest +from django.utils import timezone # Create your tests here. +DUMMY_TODO_ITEM = "Dummy todo item" +DUMMY_TODO_COMMENTARY_ITEM = "Dummy todo commentary item" + class Tutorial2UnitTest(TestCase): - def test_true(self): - self.assertTrue(1 + 1, 2) + def test_page_rendered_successfully(self): + response = Client().get('/tutorial-2/') + self.assertEqual(response.status_code, 200) + + def test_name_in_page(self): + request = HttpRequest() + response = index(request) + html_response = response.content.decode('utf8') + self.assertIn('Izzan Fakhril Islam', html_response) + + def test_todo_table_rendered_successfully(self): + request = HttpRequest() + response = index(request) + html_response = response.content.decode('utf8') + self.assertIn('<th><h5><b>Todo</b></h5></th>', html_response) + + def test_todo_commentary_table_rendered_successfully(self): + request = HttpRequest() + response = index(request) + html_response = response.content.decode('utf8') + self.assertIn('<th><h5><b>Comment</b></h5></th>', html_response) + + def test_add_todo_item_via_model(self): + TodoList.objects.create( + date=timezone.now(), + todo_list=DUMMY_TODO_ITEM + ) + todo_count = TodoList.objects.all().count() + self.assertEqual(todo_count, 1) + + def test_add_todo_commentary_item_via_model(self): + TodoListCommentary.objects.create( + date=timezone.now(), + comment=DUMMY_TODO_COMMENTARY_ITEM + ) + todo_commentary_count = TodoListCommentary.objects.all().count() + self.assertEqual(todo_commentary_count, 1) + + def test_POST_request_create_todo(self): + self.client.post( + '/tutorial-2/add_todo/', + data={ + 'date': '2019-09-12T15:15', + 'activity': DUMMY_TODO_ITEM + } + ) + todo_count = TodoList.objects.all().count() + page_response = self.client.get('/tutorial-2/') + html_response = page_response.content.decode('utf8') + + self.assertEqual(todo_count, 1) + self.assertIn(DUMMY_TODO_ITEM, html_response) + + def test_POST_request_create_todo_commentary(self): + self.client.post( + '/tutorial-2/add_todo_commentary/', + data={ + 'date': '2019-09-12', + 'comment': DUMMY_TODO_COMMENTARY_ITEM + } + ) + todo_comment_count = TodoListCommentary.objects.all().count() + page_response = self.client.get('/tutorial-2/') + html_response = page_response.content.decode('utf8') + + self.assertEqual(todo_comment_count, 1) + self.assertIn(DUMMY_TODO_COMMENTARY_ITEM, html_response) diff --git a/tutorial_2/urls.py b/tutorial_2/urls.py new file mode 100644 index 0000000000000000000000000000000000000000..a3be594985d7f0702a418c79e4c3662680820498 --- /dev/null +++ b/tutorial_2/urls.py @@ -0,0 +1,8 @@ +from django.conf.urls import url +from .views import index, add_todo, add_todo_commentary + +urlpatterns = [ + url(r'^$', index, name='index'), + url(r'add_todo/$', add_todo, name='add_todo'), + url(r'add_todo_commentary/$', add_todo_commentary, name='add_todo_commentary') +] diff --git a/tutorial_2/views.py b/tutorial_2/views.py index 91ea44a218fbd2f408430959283f0419c921093e..423522ff52a8525d894b7b9aa0ceacf223f507fe 100644 --- a/tutorial_2/views.py +++ b/tutorial_2/views.py @@ -1,3 +1,66 @@ -from django.shortcuts import render +from django.shortcuts import render, redirect +from .models import TodoList, TodoListCommentary +from datetime import datetime +from django.http import HttpResponseRedirect +from django.urls import reverse +import pytz # Create your views here. +todo = {} +todo_commentary = {} +HTML_FILE = "tutorial_2.html" +URL = "/tutorial-2/" +response = { + 'author': 'Izzan Fakhril Islam', + 'npm': '1606875806', +} + + +def index(request): + todo_dict = TodoList.objects.all().values() + todo_commentary_dict = TodoListCommentary.objects.all().values() + response['todos_dict'] = convert_queryset_into_json(todo_dict) + response['todos_commentary_dict'] = convert_queryset_into_json(todo_commentary_dict) + + return render(request, HTML_FILE, response) + + +def add_todo(request): + if request.method == 'POST': + try: + date = datetime.strptime(request.POST['date'], '%Y-%m-%dT%H:%M') + TodoList.objects.create( + todo_list=request.POST['activity'], + date=date + ) + return HttpResponseRedirect(reverse('tutorial-2:index')) + except ValueError: + todo = TodoList.objects.all().values() + response['error_msg'] = 'ERROR: Failed to add Todo List' + response['todos_dict'] = todo + + return render(request, HTML_FILE, response) + + +def add_todo_commentary(request): + if request.method == 'POST': + try: + date = datetime.strptime(request.POST['date'], '%Y-%m-%d') + TodoListCommentary.objects.create( + comment=request.POST['comment'], + date=date + ) + return HttpResponseRedirect(reverse('tutorial-2:index')) + except ValueError: + todo_commentary = TodoListCommentary.objects.all().values() + response['commentary_error_msg'] = 'ERROR: Failed to add Todo List Commentary' + response['todos_commentary_dict'] = todo_commentary + + return render(request, HTML_FILE, response) + + +def convert_queryset_into_json(queryset_dict): + res = [] + for data in queryset_dict: + res.append(data) + return res