From 622fd61eaa0c22a394e4866c3b80deae3aa7ba70 Mon Sep 17 00:00:00 2001 From: Tony Bajan Date: Thu, 27 Oct 2016 15:46:55 +0100 Subject: [PATCH] Code test --- .gitignore | 1 + README.md | 31 ++++++ Vagrantfile | 19 ++++ fabfile.py | 21 ++++ requirements.txt | 8 ++ scripts/server-setup-user.sh | 34 ++++++ scripts/server-setup.sh | 23 ++++ testsite/.gitattributes | 1 + testsite/episodes/__init__.py | 0 testsite/episodes/admin.py | 5 + testsite/episodes/migrations/0001_initial.py | 23 ++++ testsite/episodes/migrations/__init__.py | 0 testsite/episodes/models.py | 8 ++ testsite/episodes/tests.py | 3 + testsite/episodes/views.py | 3 + testsite/manage.py | 10 ++ testsite/reactions/__init__.py | 0 testsite/reactions/admin.py | 6 + testsite/reactions/migrations/0001_initial.py | 39 +++++++ testsite/reactions/migrations/__init__.py | 0 testsite/reactions/models.py | 18 +++ testsite/reactions/tests.py | 3 + testsite/reactions/views.py | 3 + testsite/testsite/__init__.py | 0 testsite/testsite/settings.py | 104 ++++++++++++++++++ testsite/testsite/urls.py | 20 ++++ testsite/testsite/wsgi.py | 16 +++ 27 files changed, 399 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 Vagrantfile create mode 100644 fabfile.py create mode 100644 requirements.txt create mode 100755 scripts/server-setup-user.sh create mode 100755 scripts/server-setup.sh create mode 100644 testsite/.gitattributes create mode 100644 testsite/episodes/__init__.py create mode 100644 testsite/episodes/admin.py create mode 100644 testsite/episodes/migrations/0001_initial.py create mode 100644 testsite/episodes/migrations/__init__.py create mode 100644 testsite/episodes/models.py create mode 100644 testsite/episodes/tests.py create mode 100644 testsite/episodes/views.py create mode 100755 testsite/manage.py create mode 100644 testsite/reactions/__init__.py create mode 100644 testsite/reactions/admin.py create mode 100644 testsite/reactions/migrations/0001_initial.py create mode 100644 testsite/reactions/migrations/__init__.py create mode 100644 testsite/reactions/models.py create mode 100644 testsite/reactions/tests.py create mode 100644 testsite/reactions/views.py create mode 100644 testsite/testsite/__init__.py create mode 100644 testsite/testsite/settings.py create mode 100644 testsite/testsite/urls.py create mode 100644 testsite/testsite/wsgi.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8000dd9 --- /dev/null +++ b/.gitignore @@ -0,0 +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 new file mode 100644 index 0000000..cc0ac61 --- /dev/null +++ b/Vagrantfile @@ -0,0 +1,19 @@ +# -*- mode: ruby -*- +# vi: set ft=ruby : + +# Vagrantfile API/syntax version. Don't touch unless you know what you're doing! +VAGRANTFILE_API_VERSION = "2" + +Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| + config.vm.box = "ubuntu/xenial64" + + # Django runserver networking + config.vm.network "forwarded_port", guest: 8888, host: 8888 + config.vm.network "forwarded_port", guest: 80, host: 8989 + + config.vm.provision "shell", path:"./scripts/server-setup.sh" + + config.vm.provider :virtualbox do |vb, override| + vb.memory = 512 + end +end diff --git a/fabfile.py b/fabfile.py new file mode 100644 index 0000000..d25ef81 --- /dev/null +++ b/fabfile.py @@ -0,0 +1,21 @@ +from fabric.api import local + + +def run_manage(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/ubuntu/.virtualenvs/code-test/bin/pip install -r requirements.txt ') diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..1c417ee --- /dev/null +++ b/requirements.txt @@ -0,0 +1,8 @@ +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 new file mode 100755 index 0000000..1ce8d1f --- /dev/null +++ b/scripts/server-setup-user.sh @@ -0,0 +1,34 @@ +#!/bin/bash +# VirtualEnv and Django setup + +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 > Main User: $USER\033[0m" + +# Set up virtualenv directory for the user if required +if [ ! -d /home/$USER/.virtualenvs ]; then + echo -e "\033[0;31m > Creating .virtualenvs folder" + mkdir /home/$USER/.virtualenvs +fi + +# write all the profile stuff for the user if required +grep -q virtualenvs /home/$USER/.bashrc +if [ $? -ne 0 ]; then + echo -e "\033[0;31m > Updating profile file\033[0m" + 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 +export PIP_VIRTUALENV_BASE=/home/$USER/.virtualenvs +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 new file mode 100755 index 0000000..2b8d72e --- /dev/null +++ b/scripts/server-setup.sh @@ -0,0 +1,23 @@ +#!/bin/bash +USER=ubuntu + +echo -e "\033[0;34m > Provisioning Vagrant server, with the following parameters:\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 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 +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 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 -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/episodes/__init__.py b/testsite/episodes/__init__.py new file mode 100644 index 0000000..e69de29 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/episodes/migrations/__init__.py b/testsite/episodes/migrations/__init__.py new file mode 100644 index 0000000..e69de29 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/episodes/tests.py b/testsite/episodes/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/testsite/episodes/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/testsite/episodes/views.py b/testsite/episodes/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/testsite/episodes/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/testsite/manage.py b/testsite/manage.py new file mode 100755 index 0000000..4671d39 --- /dev/null +++ b/testsite/manage.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python +import os +import sys + +if __name__ == "__main__": + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "testsite.settings") + + from django.core.management import execute_from_command_line + + execute_from_command_line(sys.argv) diff --git a/testsite/reactions/__init__.py b/testsite/reactions/__init__.py new file mode 100644 index 0000000..e69de29 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/reactions/migrations/0001_initial.py b/testsite/reactions/migrations/0001_initial.py new file mode 100644 index 0000000..948ac0d --- /dev/null +++ b/testsite/reactions/migrations/0001_initial.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models +from django.conf import settings + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='ImageReaction', + fields=[ + ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True, serialize=False)), + ('created_at', models.DateTimeField()), + ('image', models.ImageField(upload_to='')), + ('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='TweetReaction', + fields=[ + ('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)), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/testsite/reactions/migrations/__init__.py b/testsite/reactions/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/testsite/reactions/models.py b/testsite/reactions/models.py new file mode 100644 index 0000000..764f391 --- /dev/null +++ b/testsite/reactions/models.py @@ -0,0 +1,18 @@ +from django.db import models +from django.contrib.auth.models import User + + +class AbstractReaction(models.Model): + user = models.ForeignKey(User) + created_at = models.DateTimeField() + + class Meta: + abstract = True + + +class ImageReaction(AbstractReaction): + image = models.ImageField() + + +class TweetReaction(AbstractReaction): + text = models.CharField(max_length=150) diff --git a/testsite/reactions/tests.py b/testsite/reactions/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/testsite/reactions/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/testsite/reactions/views.py b/testsite/reactions/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/testsite/reactions/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/testsite/testsite/__init__.py b/testsite/testsite/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/testsite/testsite/settings.py b/testsite/testsite/settings.py new file mode 100644 index 0000000..f79f0d5 --- /dev/null +++ b/testsite/testsite/settings.py @@ -0,0 +1,104 @@ +""" +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.8/topics/settings/ + +For the full list of settings and their values, see +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(os.path.abspath(__file__))) + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/1.8/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +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 + +ALLOWED_HOSTS = [] + + +# Application definition + +INSTALLED_APPS = ( + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'episodes', + 'reactions', +) + +MIDDLEWARE_CLASSES = ( + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + '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.8/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.postgresql_psycopg2', + 'NAME': 'codetest', + } +} + + +# Internationalization +# https://docs.djangoproject.com/en/1.8/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/1.8/howto/static-files/ + +STATIC_URL = '/static/' diff --git a/testsite/testsite/urls.py b/testsite/testsite/urls.py new file mode 100644 index 0000000..c065cad --- /dev/null +++ b/testsite/testsite/urls.py @@ -0,0 +1,20 @@ +"""testsite URL Configuration + +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 new file mode 100644 index 0000000..f266fc3 --- /dev/null +++ b/testsite/testsite/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for testsite 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/1.8/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "testsite.settings") + +application = get_wsgi_application()