diff --git a/.gitignore b/.gitignore index 740bb18..8000dd9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1 @@ - .vagrant - diff --git a/README.md b/README.md new file mode 100644 index 0000000..1d563d8 --- /dev/null +++ b/README.md @@ -0,0 +1,31 @@ +# Ostmodern Python Code Test + +The goal of this exercise is to test that you know your way around the command line and Django tools. Whilst there is some simple HTML to write, you won't be expected to style the page. + +The idea is to do some prototype work on a site that allows users to add their comments & reactions to an episode of a television program. They'll be able to upload an image, or write a tweet, which will be stored locally in the application. Each episode will have a stream of reactions, showing a list of items with their details. + +A Django project has been created and some initial data models have been created. + +## Getting started +* You'll need to install [VirtualBox](https://www.virtualbox.org/) and [Vagrant](https://www.vagrantup.com/) on your development machine +* Get the code from `https://github.com/ostmodern/python-code-test` +* Do all your work in your own `develop` branch +* Once you have downloaded the code the following commands will get the site up and running + +```shell +vagrant up +vagrant ssh +fab migrate +fab web +``` +* The default Django "It worked!" page should now be available at http://localhost:8888/ + +## Tasks + +* Update the models so that `Episode`s can have `PhotoReaction`s or `TweetReaction`s +* Add some sample data using the Django admin interface and save it as fixtures +* Make it possible for an admin to moderate the site by deleting photos/tweets (and un­deleting them) using the Django admin interace +* Make a page that displays the reactions for a particular episode in chronological order at `/episodes/:id/` +* The stream should only display items that are not deleted +* Make a fabric command that clears the database and loads your sample data +* Create a release branch in your repo and send us the link diff --git a/Vagrantfile b/Vagrantfile index 9f95721..cc0ac61 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -5,25 +5,15 @@ VAGRANTFILE_API_VERSION = "2" Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| - config.vm.box = "ubuntu/trusty64" - - # local box - config.vm.define :local, primary: true do |local| - local.vm.box_url = "https://vagrantcloud.com/ubuntu/trusty64" - local.vm.hostname = "local" + config.vm.box = "ubuntu/xenial64" # Django runserver networking - local.vm.network "forwarded_port", guest: 8888, host: 8888 - local.vm.network "forwarded_port", guest: 80, host: 8989 - local.vm.network "forwarded_port", guest: 4000, host: 4000 + config.vm.network "forwarded_port", guest: 8888, host: 8888 + config.vm.network "forwarded_port", guest: 80, host: 8989 - local.vm.provision "shell", - path:"./scripts/server-setup.sh", args: ["dev", "vagrant"] + config.vm.provision "shell", path:"./scripts/server-setup.sh" - # VirtualBox Provider config - local.vm.provider "virtualbox" do |vb| - vb.customize ["modifyvm", :id, "--memory", "512"] + config.vm.provider :virtualbox do |vb, override| + vb.memory = 512 end - end - end diff --git a/config/dev.sh b/config/dev.sh deleted file mode 100644 index 1e55fbc..0000000 --- a/config/dev.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/bash -# Dev environment-specific settings - -DB_NAME="lecodetest" -DB_USER="devuser" -DB_PASS="devpass" diff --git a/fabfile.py b/fabfile.py index 18a53bb..d25ef81 100644 --- a/fabfile.py +++ b/fabfile.py @@ -2,17 +2,20 @@ def run_manage(command): - local('/home/vagrant/.virtualenvs/le-code-test/bin/python /vagrant/testsite/manage.py %s' % command) + local('/home/ubuntu/.virtualenvs/code-test/bin/python /vagrant/testsite/manage.py %s' % command) def web(): run_manage('runserver 0.0.0.0:8888') + def migrate(): run_manage('migrate') + def make_migrations(): run_manage('makemigrations') + def requirements(): - local('/home/vagrant/.virtualenvs/le-code-test/bin/pip install -r requirements.txt ') \ No newline at end of file + local('/home/ubuntu/.virtualenvs/code-test/bin/pip install -r requirements.txt ') diff --git a/requirements.txt b/requirements.txt index 88e7096..1c417ee 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,8 @@ -Django==1.7.2 -ecdsa==0.11 -Fabric==1.10.1 -paramiko==1.15.2 -Pillow==2.7.0 -psycopg2==2.5.4 +Django==1.8.15 +ecdsa==0.13 +Fabric3==1.12.post1 +paramiko==1.17.2 +Pillow==3.4.1 +psycopg2==2.6.2 pycrypto==2.6.1 +six==1.10.0 diff --git a/scripts/server-setup-user.sh b/scripts/server-setup-user.sh index b1e5862..1ce8d1f 100755 --- a/scripts/server-setup-user.sh +++ b/scripts/server-setup-user.sh @@ -1,18 +1,10 @@ #!/bin/bash # VirtualEnv and Django setup -# -# This is a distinct file as it's meant to be run as the primary user we SSH in as - -# Grab the environment var, default to 'dev' -ENV=${1-dev} -# ... and pick up related vars -source /vagrant/config/$ENV.sh -# Grab the user var, default to 'vagrant' -USER=${2-vagrant} +USER=ubuntu +# This is a distinct file as it's meant to be run as the primary user we SSH in as echo -e "\033[0;34m > Running main-user setup script, with the following parameters:\033[0m" -echo -e "\033[0;34m > Environment: $ENV\033[0m" echo -e "\033[0;34m > Main User: $USER\033[0m" # Set up virtualenv directory for the user if required @@ -22,19 +14,21 @@ if [ ! -d /home/$USER/.virtualenvs ]; then fi # write all the profile stuff for the user if required -grep -q WORKON /home/$USER/.bashrc +grep -q virtualenvs /home/$USER/.bashrc if [ $? -ne 0 ]; then echo -e "\033[0;31m > Updating profile file\033[0m" - echo "export WORKON_HOME=~/.virtualenvs" >> /home/$USER/.bashrc - echo "source /usr/local/bin/virtualenvwrapper.sh" >> /home/$USER/.bashrc - echo "export PIP_VIRTUALENV_BASE=~/.virtualenvs" >> /home/$USER/.bashrc - echo "workon le-code-test" >> /home/$USER/.bashrc + echo "source ~/.virtualenvs/code-test/bin/activate" >> /home/$USER/.bashrc echo "cd /vagrant/" >> /home/$USER/.bashrc fi echo -e "\033[0;34m > Setting up virtualenv\033[0m" export WORKON_HOME=/home/$USER/.virtualenvs -source /usr/local/bin/virtualenvwrapper.sh export PIP_VIRTUALENV_BASE=/home/$USER/.virtualenvs -mkvirtualenv le-code-test -workon le-code-test +python3 -m venv $PIP_VIRTUALENV_BASE/code-test +source $PIP_VIRTUALENV_BASE/code-test/bin/activate + +# install requirements +echo -e "\033[0;34m > Installing the pip requirements.\033[0m" +$PIP_VIRTUALENV_BASE/code-test/bin/pip install -U pip +$PIP_VIRTUALENV_BASE/code-test/bin/pip install wheel==0.29.0 +$PIP_VIRTUALENV_BASE/code-test/bin/pip install -r /vagrant/requirements.txt diff --git a/scripts/server-setup.sh b/scripts/server-setup.sh index 64bb430..2b8d72e 100755 --- a/scripts/server-setup.sh +++ b/scripts/server-setup.sh @@ -1,49 +1,23 @@ #!/bin/bash -# Server Setup -# -# Script to install all the requirements for the server-side part of the Infinity Health project - -# Note that we may want to tighten it up a little for production - e.g. better DB user privs. - -# Grab the environment var, default to 'dev' -ENV=${1-dev} -# ... and pick up related vars -source /vagrant/config/$ENV.sh - -# Grab the user var, default to 'vagrant' -USER=${2-vagrant} +USER=ubuntu echo -e "\033[0;34m > Provisioning Vagrant server, with the following parameters:\033[0m" -echo -e "\033[0;34m > Environment: $ENV\033[0m" echo -e "\033[0;34m > Main User: $USER\033[0m" # Housekeeping +echo -e "\033[0;34m > Installing system packages.\033[0m" apt-get update -apt-get install -y git vim - -# Python environment and tools -apt-get install -y python-setuptools python2.7 build-essential python-dev libncurses5-dev fabric -easy_install pip -pip install virtualenv virtualenvwrapper +apt-get install -y git vim build-essential python3.5-dev python3-venv \ + libncurses5-dev fabric postgresql-9.5 postgresql-server-dev-9.5 \ + libjpeg62-dev zlib1g-dev libfreetype6-dev # Postgres DB setup -apt-get install -y postgresql-9.3 postgresql-client-9.3 postgresql-server-dev-9.3 echo -e "\033[0;34m > Setting up DB. If it already exists this will generate warnings, but no harm will be done.\033[0m" -sudo -u postgres psql -c "CREATE DATABASE $DB_NAME ENCODING='UTF8' TEMPLATE=template0;" -sudo -u postgres psql -c "CREATE USER $DB_USER WITH PASSWORD '$DB_PASS';" -sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE $DB_NAME TO $DB_USER;" -if [ $ENV == 'dev' -o $ENV == 'test' ] - then - sudo -u postgres psql -c "ALTER USER $DB_USER CREATEDB;" -fi - -echo -e "\033[0;34m > Installing all the image support libs for pillow.\033[0m" -sudo apt-get install -y libjpeg62-dev zlib1g-dev libfreetype6-dev liblcms1-dev +sudo -u postgres psql -c "CREATE DATABASE codetest ENCODING='UTF8' TEMPLATE=template0;" +sudo -u postgres psql -c "CREATE USER ubuntu;" +sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE codetest TO ubuntu;" +sudo -u postgres psql -c "ALTER USER ubuntu CREATEDB;" # do the rest as the user we'll be logging in as through SSH chmod +x /vagrant/scripts/server-setup-user.sh -sudo -u $USER /vagrant/scripts/server-setup-user.sh $ENV $USER - -# install requirements -echo -e "\033[0;34m > Installing the pip requirements.\033[0m" -sudo -H -u vagrant /home/vagrant/.virtualenvs/le-code-test/bin/pip install -r /vagrant/requirements.txt +sudo -H -u $USER /vagrant/scripts/server-setup-user.sh diff --git a/testsite/.gitattributes b/testsite/.gitattributes new file mode 100644 index 0000000..fcadb2c --- /dev/null +++ b/testsite/.gitattributes @@ -0,0 +1 @@ +* text eol=lf diff --git a/testsite/items/__init__.py b/testsite/episodes/__init__.py similarity index 100% rename from testsite/items/__init__.py rename to testsite/episodes/__init__.py diff --git a/testsite/episodes/admin.py b/testsite/episodes/admin.py new file mode 100644 index 0000000..2b13f3c --- /dev/null +++ b/testsite/episodes/admin.py @@ -0,0 +1,5 @@ +from django.contrib import admin + +from .models import Episode + +admin.site.register(Episode) diff --git a/testsite/episodes/migrations/0001_initial.py b/testsite/episodes/migrations/0001_initial.py new file mode 100644 index 0000000..04fb20e --- /dev/null +++ b/testsite/episodes/migrations/0001_initial.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Episode', + fields=[ + ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True, serialize=False)), + ('title', models.TextField()), + ('episode_number', models.IntegerField()), + ('created_at', models.DateTimeField()), + ('hero_image', models.ImageField(upload_to='')), + ], + ), + ] diff --git a/testsite/items/migrations/__init__.py b/testsite/episodes/migrations/__init__.py similarity index 100% rename from testsite/items/migrations/__init__.py rename to testsite/episodes/migrations/__init__.py diff --git a/testsite/episodes/models.py b/testsite/episodes/models.py new file mode 100644 index 0000000..7660888 --- /dev/null +++ b/testsite/episodes/models.py @@ -0,0 +1,8 @@ +from django.db import models + + +class Episode(models.Model): + title = models.TextField() + episode_number = models.IntegerField() + created_at = models.DateTimeField() + hero_image = models.ImageField() diff --git a/testsite/items/tests.py b/testsite/episodes/tests.py similarity index 100% rename from testsite/items/tests.py rename to testsite/episodes/tests.py diff --git a/testsite/items/views.py b/testsite/episodes/views.py similarity index 100% rename from testsite/items/views.py rename to testsite/episodes/views.py diff --git a/testsite/items/admin.py b/testsite/items/admin.py deleted file mode 100644 index debc267..0000000 --- a/testsite/items/admin.py +++ /dev/null @@ -1,6 +0,0 @@ -from django.contrib import admin - -from models import PhotoItem, TweetItem - -admin.site.register(PhotoItem) -admin.site.register(TweetItem) diff --git a/testsite/stream/__init__.py b/testsite/reactions/__init__.py similarity index 100% rename from testsite/stream/__init__.py rename to testsite/reactions/__init__.py diff --git a/testsite/reactions/admin.py b/testsite/reactions/admin.py new file mode 100644 index 0000000..5c2d93c --- /dev/null +++ b/testsite/reactions/admin.py @@ -0,0 +1,6 @@ +from django.contrib import admin + +from .models import ImageReaction, TweetReaction + +admin.site.register(ImageReaction) +admin.site.register(TweetReaction) diff --git a/testsite/items/migrations/0001_initial.py b/testsite/reactions/migrations/0001_initial.py similarity index 65% rename from testsite/items/migrations/0001_initial.py rename to testsite/reactions/migrations/0001_initial.py index 9392339..948ac0d 100644 --- a/testsite/items/migrations/0001_initial.py +++ b/testsite/reactions/migrations/0001_initial.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import models, migrations +from django.db import migrations, models from django.conf import settings @@ -13,22 +13,21 @@ class Migration(migrations.Migration): operations = [ migrations.CreateModel( - name='PhotoItem', + name='ImageReaction', fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True, serialize=False)), ('created_at', models.DateTimeField()), - ('image', models.ImageField(upload_to=b'')), + ('image', models.ImageField(upload_to='')), ('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)), ], options={ 'abstract': False, }, - bases=(models.Model,), ), migrations.CreateModel( - name='TweetItem', + name='TweetReaction', fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True, serialize=False)), ('created_at', models.DateTimeField()), ('text', models.CharField(max_length=150)), ('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)), @@ -36,6 +35,5 @@ class Migration(migrations.Migration): options={ 'abstract': False, }, - bases=(models.Model,), ), ] diff --git a/testsite/stream/migrations/__init__.py b/testsite/reactions/migrations/__init__.py similarity index 100% rename from testsite/stream/migrations/__init__.py rename to testsite/reactions/migrations/__init__.py diff --git a/testsite/items/models.py b/testsite/reactions/models.py similarity index 70% rename from testsite/items/models.py rename to testsite/reactions/models.py index 7d7ebe0..764f391 100644 --- a/testsite/items/models.py +++ b/testsite/reactions/models.py @@ -2,7 +2,7 @@ from django.contrib.auth.models import User -class ItemAbstract(models.Model): +class AbstractReaction(models.Model): user = models.ForeignKey(User) created_at = models.DateTimeField() @@ -10,9 +10,9 @@ class Meta: abstract = True -class PhotoItem(ItemAbstract): +class ImageReaction(AbstractReaction): image = models.ImageField() -class TweetItem(ItemAbstract): +class TweetReaction(AbstractReaction): text = models.CharField(max_length=150) diff --git a/testsite/stream/tests.py b/testsite/reactions/tests.py similarity index 100% rename from testsite/stream/tests.py rename to testsite/reactions/tests.py diff --git a/testsite/stream/views.py b/testsite/reactions/views.py similarity index 100% rename from testsite/stream/views.py rename to testsite/reactions/views.py diff --git a/testsite/stream/admin.py b/testsite/stream/admin.py deleted file mode 100644 index 54a2562..0000000 --- a/testsite/stream/admin.py +++ /dev/null @@ -1,5 +0,0 @@ -from django.contrib import admin - -from models import Stream - -admin.site.register(Stream) diff --git a/testsite/stream/migrations/0001_initial.py b/testsite/stream/migrations/0001_initial.py deleted file mode 100644 index 30d7667..0000000 --- a/testsite/stream/migrations/0001_initial.py +++ /dev/null @@ -1,26 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from django.db import models, migrations -from django.conf import settings - - -class Migration(migrations.Migration): - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name='Stream', - fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('created_at', models.DateTimeField()), - ('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)), - ], - options={ - }, - bases=(models.Model,), - ), - ] diff --git a/testsite/stream/models.py b/testsite/stream/models.py deleted file mode 100644 index e10fd24..0000000 --- a/testsite/stream/models.py +++ /dev/null @@ -1,7 +0,0 @@ -from django.db import models -from django.contrib.auth.models import User - - -class Stream(models.Model): - user = models.ForeignKey(User) - created_at = models.DateTimeField() \ No newline at end of file diff --git a/testsite/testsite/settings.py b/testsite/testsite/settings.py index 2821860..f79f0d5 100644 --- a/testsite/testsite/settings.py +++ b/testsite/testsite/settings.py @@ -1,31 +1,35 @@ """ Django settings for testsite project. +Generated by 'django-admin startproject' using Django 1.8.15. + For more information on this file, see -https://docs.djangoproject.com/en/1.7/topics/settings/ +https://docs.djangoproject.com/en/1.8/topics/settings/ For the full list of settings and their values, see -https://docs.djangoproject.com/en/1.7/ref/settings/ +https://docs.djangoproject.com/en/1.8/ref/settings/ """ # Build paths inside the project like this: os.path.join(BASE_DIR, ...) import os -BASE_DIR = os.path.dirname(os.path.dirname(__file__)) + +BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # Quick-start development settings - unsuitable for production -# See https://docs.djangoproject.com/en/1.7/howto/deployment/checklist/ +# See https://docs.djangoproject.com/en/1.8/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = '!4oojc^tmd%+*c_^(n)7^p^6-0f8td(_o&(2bj*e7demr&jtvy' +SECRET_KEY = '*mr(2&=ytx3u!n@(v!mvzq)y@1#!gbx)#q12d3v^&j0iu0!dtu' # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True -TEMPLATE_DEBUG = True - ALLOWED_HOSTS = [] + +# Application definition + INSTALLED_APPS = ( 'django.contrib.admin', 'django.contrib.auth', @@ -33,8 +37,8 @@ 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', - 'stream', - 'items', + 'episodes', + 'reactions', ) MIDDLEWARE_CLASSES = ( @@ -45,29 +49,43 @@ 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', + 'django.middleware.security.SecurityMiddleware', ) ROOT_URLCONF = 'testsite.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 = 'testsite.wsgi.application' # Database -# https://docs.djangoproject.com/en/1.7/ref/settings/#databases +# https://docs.djangoproject.com/en/1.8/ref/settings/#databases DATABASES = { 'default': { 'ENGINE': 'django.db.backends.postgresql_psycopg2', - 'NAME': 'lecodetest', - 'USER': 'devuser', - 'PASSWORD': 'devpass', - 'HOST': '127.0.0.1', - 'PORT': '', + 'NAME': 'codetest', } } + # Internationalization -# https://docs.djangoproject.com/en/1.7/topics/i18n/ +# https://docs.djangoproject.com/en/1.8/topics/i18n/ LANGUAGE_CODE = 'en-us' @@ -81,6 +99,6 @@ # Static files (CSS, JavaScript, Images) -# https://docs.djangoproject.com/en/1.7/howto/static-files/ +# https://docs.djangoproject.com/en/1.8/howto/static-files/ STATIC_URL = '/static/' diff --git a/testsite/testsite/urls.py b/testsite/testsite/urls.py index bcd73c7..c065cad 100644 --- a/testsite/testsite/urls.py +++ b/testsite/testsite/urls.py @@ -1,10 +1,20 @@ -from django.conf.urls import patterns, include, url -from django.contrib import admin +"""testsite URL Configuration -urlpatterns = patterns('', - # Examples: - # url(r'^$', 'testsite.views.home', name='home'), - # url(r'^blog/', include('blog.urls')), +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/1.8/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. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) +""" +from django.conf.urls import include, url +from django.contrib import admin +urlpatterns = [ url(r'^admin/', include(admin.site.urls)), -) +] diff --git a/testsite/testsite/wsgi.py b/testsite/testsite/wsgi.py index 2091485..f266fc3 100644 --- a/testsite/testsite/wsgi.py +++ b/testsite/testsite/wsgi.py @@ -4,11 +4,13 @@ It exposes the WSGI callable as a module-level variable named ``application``. For more information on this file, see -https://docs.djangoproject.com/en/1.7/howto/deployment/wsgi/ +https://docs.djangoproject.com/en/1.8/howto/deployment/wsgi/ """ import os -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "testsite.settings") from django.core.wsgi import get_wsgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "testsite.settings") + application = get_wsgi_application()