Initial commit

This commit is contained in:
Stephan Porada 2021-03-23 21:25:45 +01:00
commit 46bb3be3ca
23 changed files with 516 additions and 0 deletions

20
.gitignore vendored Normal file
View File

@ -0,0 +1,20 @@
### Files
*.pyc
__pycache
.DS_Store
*_initial.py
**/migrations/*_*.py
.idea
live.env
!**/.gitkeep
### Folder
staticfiles
postgres_data
static_volume
venv

10
README.md Normal file
View File

@ -0,0 +1,10 @@
# Start the app
Start the django application with `docker-compose --env-file live.env up`.
# Run the functional_tests.py
1. Set up a python venv with Selenium
2. Run the tests with `python functional_tests.py`
# Run the django tests
1. Access the running web container with `docker exec -it palindrome-web /bin/bash`
2. Run the tests with `python manage.py test`

45
docker-compose.yml Normal file
View File

@ -0,0 +1,45 @@
version: '3.7'
services:
web:
build: ./web
container_name: palindrome-web
restart: unless-stopped
volumes:
- ./web/app:/usr/src/palindrome-app/app
- ./web/palindromes:/usr/src/palindrome-app/palindromes
- ./web/functional_tests.py:/usr/src/palindrome-app/functional_tests.py
- ./web/manage.py:/usr/src/palindrome-app/manage.py
- ./static_volume:/usr/src/palindrome-app/staticfiles
expose:
- 8000
env_file: live.env
depends_on:
- db
command: >
bash -c "./wait-for-it/wait-for-it.sh db:5432 --strict --timeout=0
&& gunicorn --bind 0.0.0.0:8000 --workers=4 app.wsgi:application"
db:
container_name: palindrome-db
env_file: live.env
restart: unless-stopped
environment:
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_DB_NAME: ${POSTGRES_DB_NAME}
expose:
- 5432
image: postgres:13
volumes:
- ./postgres_data:/var/lib/postgresql/data/
nginx:
container_name: palindrome-nginx
build: ./nginx
restart: unless-stopped
volumes:
- ./static_volume:/usr/src/app/staticfiles
ports:
- 8000:80
depends_on:
- web

11
live.env.tpl Normal file
View File

@ -0,0 +1,11 @@
### These are examples do not use in production! ###
### Django settings ###
SECRET_KEY=secretkey
DEBUG=False
### Postgress DB ###
POSTGRES_PASSWORD=password
# USER and DB name have to be the same
POSTGRES_USER=dbname
POSTGRES_DB_NAME=dbname

4
nginx/Dockerfile Normal file
View File

@ -0,0 +1,4 @@
FROM nginx:1.19.8
RUN rm /etc/nginx/conf.d/default.conf
COPY nginx.conf /etc/nginx/conf.d

20
nginx/nginx.conf Normal file
View File

@ -0,0 +1,20 @@
upstream app {
server web:8000;
}
server {
listen 80;
location / {
proxy_pass http://app;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_redirect off;
}
location /static/ {
alias /usr/src/app/staticfiles/;
}
}

26
web/Dockerfile Normal file
View File

@ -0,0 +1,26 @@
# pull official base image
FROM python:3.9.2
# set environment varibles
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
# set work directory
WORKDIR /usr/src/palindrome-app
# install dependencies
RUN pip install --upgrade pip
COPY ./requirements.txt /usr/src/palindrome-app/requirements.txt
RUN pip install -r requirements.txt
# copy project
COPY . /usr/src/palindrome-app/
# add wait for it
RUN git clone https://github.com/vishnubob/wait-for-it.git
RUN chmod a+x wait-for-it/wait-for-it.sh
# add entry point
COPY ./entrypoint.sh entrypoint.sh
ENTRYPOINT ["./entrypoint.sh"]

0
web/app/__init__.py Normal file
View File

16
web/app/asgi.py Normal file
View File

@ -0,0 +1,16 @@
"""
ASGI config for app project.
It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/3.1/howto/deployment/asgi/
"""
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'app.settings')
application = get_asgi_application()

128
web/app/settings.py Normal file
View File

@ -0,0 +1,128 @@
"""
Django settings for app project.
Generated by 'django-admin startproject' using Django 3.1.7.
For more information on this file, see
https://docs.djangoproject.com/en/3.1/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/3.1/ref/settings/
"""
import os
from pathlib import Path
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/3.1/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = os.environ.get('SECRET_KEY') or 'hard to guess string'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = os.environ.get('DEBUG') or False
ALLOWED_HOSTS = ["127.0.0.1", "localhost", "0.0.0.0"]
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'app.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'app.wsgi.application'
# Database
# https://docs.djangoproject.com/en/3.1/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': os.environ.get("POSTGRES_DB_NAME"),
'USER': os.environ.get("POSTGRES_USER"),
'PASSWORD': os.environ.get("POSTGRES_PASSWORD"),
'HOST': 'db',
'PORT': '5432',
'TEST': {
'NAME': 'mytestdatabase',
},
}
}
# Password validation
# https://docs.djangoproject.com/en/3.1/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', # noqa
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', # noqa
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', # noqa
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', # noqa
},
]
# Internationalization
# https://docs.djangoproject.com/en/3.1/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_L10N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/3.1/howto/static-files/
STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')

23
web/app/urls.py Normal file
View File

@ -0,0 +1,23 @@
"""app URL Configuration
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/3.1/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path
from palindromes import views
urlpatterns = [
path('admin/', admin.site.urls),
path('', views.caculate_palindromes, name='palindromes')
]

16
web/app/wsgi.py Normal file
View File

@ -0,0 +1,16 @@
"""
WSGI config for app project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/3.1/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'app.settings')
application = get_wsgi_application()

7
web/entrypoint.sh Executable file
View File

@ -0,0 +1,7 @@
#!/bin/bash
python manage.py collectstatic --noinput
python manage.py makemigrations --noinput
python manage.py migrate --noinput
exec "$@"

30
web/functional_tests.py Normal file
View File

@ -0,0 +1,30 @@
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_input_integer(self):
# Stephan has heard about a cool new online palindromes app. He goes
# to check out its homepage
self.browser.get('http://localhost:8000')
# He notices the page title and header mention the name Plaindromes!
self.assertIn('Palindromes', self.browser.title)
self.fail('Finish the test!')
# He is prompted to type in an integer for which a palindrome will be calculated # noqa
# He types in an integer and hits enter to get it calculated
# The server sends back the calculated palindrome or another message
# Stephan is done for now and exits the pag
if __name__ == '__main__':
unittest.main(warnings='ignore')

100
web/geckodriver.log Normal file
View File

@ -0,0 +1,100 @@
1616509351779 geckodriver INFO Listening on 127.0.0.1:42261
1616509351781 mozrunner::runner INFO Running command: "/usr/bin/firefox" "--marionette" "-foreground" "-no-remote" "-profile" "/tmp/rust_mozprofileelUWrk"
console.warn: SearchSettings: "get: No settings file exists, new profile?" (new Error("", "(unknown module)"))
1616509353329 Marionette INFO Listening on port 41753
1616509353395 Marionette WARN TLS certificate errors will be ignored for this session
1616509365607 Marionette INFO Stopped listening on port 41753
1616509723831 geckodriver INFO Listening on 127.0.0.1:56649
1616509723833 mozrunner::runner INFO Running command: "/usr/bin/firefox" "--marionette" "-foreground" "-no-remote" "-profile" "/tmp/rust_mozprofilegw3SOG"
console.warn: SearchSettings: "get: No settings file exists, new profile?" (new Error("", "(unknown module)"))
1616509725247 Marionette INFO Listening on port 42047
1616509725351 Marionette WARN TLS certificate errors will be ignored for this session
1616509727851 Marionette INFO Stopped listening on port 42047
1616510191802 geckodriver INFO Listening on 127.0.0.1:52183
1616510191803 mozrunner::runner INFO Running command: "/usr/bin/firefox" "--marionette" "-foreground" "-no-remote" "-profile" "/tmp/rust_mozprofileGDLkvw"
console.warn: SearchSettings: "get: No settings file exists, new profile?" (new Error("", "(unknown module)"))
1616510193279 Marionette INFO Listening on port 35715
1616510193327 Marionette WARN TLS certificate errors will be ignored for this session
1616510201579 Marionette INFO Stopped listening on port 35715
1616510878061 geckodriver INFO Listening on 127.0.0.1:38391
1616510878064 mozrunner::runner INFO Running command: "/usr/bin/firefox" "--marionette" "-foreground" "-no-remote" "-profile" "/tmp/rust_mozprofilewsA2j7"
console.warn: SearchSettings: "get: No settings file exists, new profile?" (new Error("", "(unknown module)"))
1616510879662 Marionette INFO Listening on port 39019
1616510879678 Marionette WARN TLS certificate errors will be ignored for this session
1616510886787 Marionette INFO Stopped listening on port 39019
1616510894854 geckodriver INFO Listening on 127.0.0.1:37057
1616510894856 mozrunner::runner INFO Running command: "/usr/bin/firefox" "--marionette" "-foreground" "-no-remote" "-profile" "/tmp/rust_mozprofilebrrnOV"
console.warn: SearchSettings: "get: No settings file exists, new profile?" (new Error("", "(unknown module)"))
1616510896409 Marionette INFO Listening on port 43297
1616510896475 Marionette WARN TLS certificate errors will be ignored for this session
1616510903397 Marionette INFO Stopped listening on port 43297
1616511204761 geckodriver INFO Listening on 127.0.0.1:56245
1616511204763 mozrunner::runner INFO Running command: "/usr/bin/firefox" "--marionette" "-foreground" "-no-remote" "-profile" "/tmp/rust_mozprofileYtNXIF"
console.warn: SearchSettings: "get: No settings file exists, new profile?" (new Error("", "(unknown module)"))
1616511206280 Marionette INFO Listening on port 41989
1616511206381 Marionette WARN TLS certificate errors will be ignored for this session
1616511326187 geckodriver INFO Listening on 127.0.0.1:45449
1616511326190 mozrunner::runner INFO Running command: "/usr/bin/firefox" "--marionette" "-foreground" "-no-remote" "-profile" "/tmp/rust_mozprofileIkAIW4"
console.warn: SearchSettings: "get: No settings file exists, new profile?" (new Error("", "(unknown module)"))
1616511327714 Marionette INFO Listening on port 40899
1616511327810 Marionette WARN TLS certificate errors will be ignored for this session
1616511332398 Marionette INFO Stopped listening on port 40899
1616512002527 geckodriver INFO Listening on 127.0.0.1:37541
1616512002530 mozrunner::runner INFO Running command: "/usr/bin/firefox" "--marionette" "-foreground" "-no-remote" "-profile" "/tmp/rust_mozprofileqarZY3"
console.warn: SearchSettings: "get: No settings file exists, new profile?" (new Error("", "(unknown module)"))
1616512004140 Marionette INFO Listening on port 46251
1616512004271 Marionette WARN TLS certificate errors will be ignored for this session
1616512007556 Marionette INFO Stopped listening on port 46251
1616512373159 geckodriver INFO Listening on 127.0.0.1:34869
1616512373160 mozrunner::runner INFO Running command: "/usr/bin/firefox" "--marionette" "-foreground" "-no-remote" "-profile" "/tmp/rust_mozprofileU86nfh"
console.warn: SearchSettings: "get: No settings file exists, new profile?" (new Error("", "(unknown module)"))
1616512374654 Marionette INFO Listening on port 46413
1616512374691 Marionette WARN TLS certificate errors will be ignored for this session
1616512378579 Marionette INFO Stopped listening on port 46413
1616512603567 geckodriver INFO Listening on 127.0.0.1:53947
1616512603569 mozrunner::runner INFO Running command: "/usr/bin/firefox" "--marionette" "-foreground" "-no-remote" "-profile" "/tmp/rust_mozprofileT5g6T9"
console.warn: SearchSettings: "get: No settings file exists, new profile?" (new Error("", "(unknown module)"))
1616512605069 Marionette INFO Listening on port 40321
1616512605082 Marionette WARN TLS certificate errors will be ignored for this session
1616512605165 Marionette INFO Stopped listening on port 40321
JavaScript error: resource://activity-stream/lib/ASRouter.jsm, line 988: NS_ERROR_ILLEGAL_VALUE: Component returned failure code: 0x80070057 (NS_ERROR_ILLEGAL_VALUE) [nsIObserverService.removeObserver]
console.error: Region.jsm: "Error fetching region" (new TypeError("NetworkError when attempting to fetch resource.", ""))
console.error: Region.jsm: "Failed to fetch region" (new Error("NO_RESULT", "resource://gre/modules/Region.jsm", 422))
console.error: "Could not load engine chambers-en-GB@search.mozilla.org: Error: Attempt to use XPI database when it is not initialized"
console.error: "Could not load engine wikipedia@search.mozilla.org: TypeError: AddonManagerInternal._getProviderByName(...) is undefined"
console.error: "Could not load engine bing@search.mozilla.org: TypeError: AddonManagerInternal._getProviderByName(...) is undefined"
console.error: "Could not load engine amazon@search.mozilla.org: TypeError: AddonManagerInternal._getProviderByName(...) is undefined"
console.error: "Could not load engine ddg@search.mozilla.org: TypeError: AddonManagerInternal._getProviderByName(...) is undefined"
console.error: "Could not load engine ebay@search.mozilla.org: TypeError: AddonManagerInternal._getProviderByName(...) is undefined"
console.warn: SearchService: "_init: abandoning init due to shutting down"
JavaScript error: resource://gre/modules/DeferredTask.jsm, line 215: Error: Unable to arm timer, the object has been finalized.
JavaScript error: resource://gre/modules/AsyncShutdown.jsm, line 573: uncaught exception: 2147500036
JavaScript error: resource://gre/modules/AsyncShutdown.jsm, line 573: uncaught exception: 2147500036
JavaScript error: resource://gre/modules/AsyncShutdown.jsm, line 573: uncaught exception: 2147500036
JavaScript error: resource://gre/modules/AsyncShutdown.jsm, line 573: uncaught exception: 2147500036
JavaScript error: resource://gre/modules/AsyncShutdown.jsm, line 573: uncaught exception: 2147500036
JavaScript error: resource://gre/modules/AsyncShutdown.jsm, line 573: uncaught exception: 2147500036
JavaScript error: resource://gre/modules/AsyncShutdown.jsm, line 573: uncaught exception: 2147500036
JavaScript error: resource://gre/modules/AsyncShutdown.jsm, line 573: uncaught exception: 2147500036
1616514655590 Marionette INFO Stopped listening on port 41989
1616530706680 geckodriver INFO Listening on 127.0.0.1:43501
1616530706681 mozrunner::runner INFO Running command: "/usr/bin/firefox" "--marionette" "-foreground" "-no-remote" "-profile" "/tmp/rust_mozprofileII5pAd"
console.warn: SearchSettings: "get: No settings file exists, new profile?" (new Error("", "(unknown module)"))
1616530708189 Marionette INFO Listening on port 39913
1616530708297 Marionette WARN TLS certificate errors will be ignored for this session
1616530708361 Marionette INFO Stopped listening on port 39913
JavaScript error: resource://activity-stream/lib/ASRouter.jsm, line 988: NS_ERROR_ILLEGAL_VALUE: Component returned failure code: 0x80070057 (NS_ERROR_ILLEGAL_VALUE) [nsIObserverService.removeObserver]
console.warn: services.settings: main/message-groups sync interrupted by shutdown
console.warn: SearchService: "_init: abandoning init due to shutting down"
JavaScript error: , line 0: uncaught exception: 2147500036
JavaScript error: , line 0: uncaught exception: 2147500036
JavaScript error: , line 0: uncaught exception: 2147500036
JavaScript error: , line 0: uncaught exception: 2147500036
JavaScript error: , line 0: uncaught exception: 2147500036
JavaScript error: , line 0: uncaught exception: 2147500036
JavaScript error: , line 0: uncaught exception: 2147500036
JavaScript error: , line 0: uncaught exception: 2147500036
console.error: Region.jsm: "Error fetching region" (new TypeError("NetworkError when attempting to fetch resource.", ""))
console.error: Region.jsm: "Failed to fetch region" (new Error("NO_RESULT", "resource://gre/modules/Region.jsm", 422))
console.error: services.settings:
main/fxmonitor-breaches Signature failed Error: Shutdown, aborting read-only worker requests.

22
web/manage.py Executable file
View File

@ -0,0 +1,22 @@
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
def main():
"""Run administrative tasks."""
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'app.settings')
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
if __name__ == '__main__':
main()

View File

3
web/palindromes/admin.py Normal file
View File

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

5
web/palindromes/apps.py Normal file
View File

@ -0,0 +1,5 @@
from django.apps import AppConfig
class PalindromesConfig(AppConfig):
name = 'palindromes'

View File

@ -0,0 +1,3 @@
from django.db import models
# Create your models here.

19
web/palindromes/tests.py Normal file
View File

@ -0,0 +1,19 @@
from django.urls import resolve
from django.test import TestCase
from django.http import HttpRequest
from palindromes.views import caculate_palindromes
class HomePageTest(TestCase):
def test_root_url_resolves_to_calculate_palindrome_view(self):
found = resolve('/')
self.assertEqual(found.func, caculate_palindromes)
def test_home_page_returns_correct_html(self):
request = HttpRequest()
response = caculate_palindromes(request)
html = response.content.decode('utf8')
self.assertTrue(html.startswith('<html>'))
self.assertIn('<title>Palindromes!</title>', html)
self.assertTrue(html.endswith('</html>'))

5
web/palindromes/views.py Normal file
View File

@ -0,0 +1,5 @@
from django.http import HttpResponse
def caculate_palindromes(request):
return HttpResponse('<html><title>Palindromes!</title></html>')

3
web/requirements.txt Normal file
View File

@ -0,0 +1,3 @@
Django==3.1.7
psycopg2==2.8.6
gunicorn==20.0.4