diff --git a/.gitignore b/.gitignore index 7de88e3..a5e9648 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,8 @@ __pycache__/ *.db3 *.sqlite *.sqlite3 +<<<<<<< HEAD +======= +*.log +local_settings.py +>>>>>>> 1688b0ad1e792c142f9f753754c029e1225d374f diff --git a/Procfile b/Procfile new file mode 100644 index 0000000..0785be5 --- /dev/null +++ b/Procfile @@ -0,0 +1 @@ +web: gunicorn squawker.wsgi --log-file - diff --git a/README.md b/README.md index 1635648..69d8797 100644 --- a/README.md +++ b/README.md @@ -1,106 +1,12 @@ -# Squawker +# BuddyBear -In this assignment, you will build simplified Twitter clone. We'll refer to the individual messages as "squawks". +## Narrative +1 in 68 children suffer from autism spectrum disorder, a disorder that prevents them from comprehending basic human emotion. We are developing Buddy Bear, a companion that utilizes computer vision to recognize emotions in others and conveys this emotion to the child in a simple, easy to understand manner. -## Requirements +## Product -* The homepage (`/`) contains: - * A single form, to post a new squawk (**5%**) - * All past squawks (**20%**) - * Sorted from newest to oldest (**20%**) -* Submitting the form: - 1. Creates a new squawk (**30%**) - 1. Shows / takes the user back to the homepage (**5%**) - * In other words, they should see the updated homepage with the new squawk. -* Squawks are limited to 140 characters - * Client-side (**5%**) - * Uses HTML5 form validation - * Server-side (**10%**) - * Responds with a status code of 400 if the form is submitted with invalid data. -* Passes Code Climate checks (**5%**) -* The site works without JavaScript -* Built using [Flask](http://flask.pocoo.org/) and [`sqlite3`](https://docs.python.org/3/library/sqlite3.html) +Buddy Bear - physical device -Visual styling is not considered as part of the score, though feel free to get creative! In other words, feel free to make your site pretty, but not a problem if it isn't. +Dashboard - metric tracking -### Extra credit -* Pagination (**20%**) - * The squawks are shown 20 at a time - * There's a `Next` link to see older squawks, if there are any - -## Setup - -1. Update your [VM](https://github.com/startup-systems/vm), if you didn't do so for the [time](https://docs.google.com/document/d/15VzRMLHLGm_l9dzUObQlsOoY12J_jH3U0b9Bu2yi6EI/edit#heading=h.lyptz0o698my) assignment already. From your host machine: - - ```shell - cd path/to/vm/ - git pull -s recursive -X ours https://github.com/startup-systems/vm.git master - vagrant reload - ``` - -1. [Set up the database.](#set-up-the-database) - -## Development workflow - -1. Start the server. From your VM: - - ```shell - cd /vagrant/squawker - pip3 install -r requirements.txt - FLASK_APP=squawker/server.py FLASK_DEBUG=1 flask run --host=0.0.0.0 - ``` - -1. Open http://localhost:5000 from your host machine. -1. Modify [`squawker/server.py`](squawker/server.py). -1. Refresh. - -## Set up the database - -Note that this will delete any existing content. - -1. Modify the [`squawker/schema.sql`](squawker/schema.sql) file. -1. Run - - ```shell - FLASK_APP=squawker/server.py flask initdb - ``` -Repeat these steps when you need to update the schema. - -## Running tests locally - -Run the following from this directory: - -```shell -# run the pytests -pytest --tb short -# run the pep8 checks -pep8 -# check the extra credit -pytest --tb short --runxfail -``` - -## Things you will need - -* [Flask template(s)](http://flask.pocoo.org/docs/0.11/quickstart/#rendering-templates) - * [Jinja2 syntax](http://jinja.pocoo.org/docs/dev/templates/) -* An [HTML form](https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/Forms) -* Change/addition of routes in Flask -* Some basic SQL understanding -* SQLite3 CLI - * Useful for inspecting your database - * Install in your VM with - - ```shell - sudo apt-get update - sudo apt-get install sqlite3 - ``` - -### Code Climate checks - -If you want to try running these locally: - -1. [Install Docker](https://docs.docker.com/engine/installation/linux/ubuntulinux/) (follow the "Ubuntu Xenial 16.04 (LTS)" instructions) -1. Run the [Code Climate CLI](https://github.com/codeclimate/codeclimate#readme). - -Note that **this is advanced**, so don't worry if you have trouble getting it running. diff --git a/main/__init__.py b/main/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/main/admin.py b/main/admin.py new file mode 100644 index 0000000..dbefa63 --- /dev/null +++ b/main/admin.py @@ -0,0 +1,5 @@ +from django.contrib import admin +from .models import Squawk + +# Register your models here. +admin.site.register(Squawk) diff --git a/main/migrations/0001_initial.py b/main/migrations/0001_initial.py new file mode 100644 index 0000000..06062f0 --- /dev/null +++ b/main/migrations/0001_initial.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Squawk', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('message', models.CharField(max_length=140)), + ('created', models.DateTimeField(auto_now_add=True)), + ], + ), + ] diff --git a/main/migrations/__init__.py b/main/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/main/models.py b/main/models.py new file mode 100644 index 0000000..b0533ea --- /dev/null +++ b/main/models.py @@ -0,0 +1,7 @@ +from django.db import models + +# Create your models here. +class Squawk(models.Model): + message = models.CharField(max_length=140) + created = models.DateTimeField(auto_now_add=True) + diff --git a/main/templates/index.html b/main/templates/index.html new file mode 100644 index 0000000..8322708 --- /dev/null +++ b/main/templates/index.html @@ -0,0 +1,38 @@ + + + + + + + + + +
+
+

Squawker > Twitter

+
+
+ + + +

Squawks

+
+{% csrf_token %} +
+ + +
+ + + +
+ +{% for squawk in squawks %} +
+

{{ squawk.message }}

+

{{ squawk.created }}

+
+{% endfor %} +
+ + diff --git a/main/tests.py b/main/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/main/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/main/views.py b/main/views.py new file mode 100644 index 0000000..030e1bd --- /dev/null +++ b/main/views.py @@ -0,0 +1,17 @@ +from django.shortcuts import render +from django.http import HttpResponseBadRequest +from django.http import HttpResponse +from .models import Squawk + + +def root(request): + if (request.method == "POST"): + inputMessage = request.POST['message'] + if (len(inputMessage) <= 140): + squawk = Squawk(message=inputMessage) + squawk.save() + else: + return HttpResponseBadRequest("Error: Message Exceeded Max Length of 140") + + squawkers = Squawk.objects.order_by('-created') + return render(request, "index.html", {"squawks":squawkers}) diff --git a/manage.py b/manage.py new file mode 100755 index 0000000..ec6bfb9 --- /dev/null +++ b/manage.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python +import os +import sys + +if __name__ == "__main__": + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "squawker.settings") + + from django.core.management import execute_from_command_line + + execute_from_command_line(sys.argv) diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..4fea6b6 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,2 @@ +[pytest] +DJANGO_SETTINGS_MODULE=squawker.settings diff --git a/runtime.txt b/runtime.txt new file mode 100644 index 0000000..c0354ee --- /dev/null +++ b/runtime.txt @@ -0,0 +1 @@ +python-3.5.2 diff --git a/squawker/schema.sql b/squawker/schema.sql index 5e67ffb..0604241 100644 --- a/squawker/schema.sql +++ b/squawker/schema.sql @@ -1,3 +1,7 @@ -- TODO change this -DROP TABLE IF EXISTS mytable; -CREATE TABLE mytable (id integer); +DROP TABLE IF EXISTS squawks; +CREATE TABLE squawks( + id INTEGER PRIMARY KEY AUTOINCREMENT, + message VARCHAR(140) NOT NULL, + timestamp INTEGER DEFAULT CURRENT_TIMESTAMP +); diff --git a/squawker/server.py b/squawker/server.py index 6ff24ba..3cb0474 100644 --- a/squawker/server.py +++ b/squawker/server.py @@ -1,4 +1,4 @@ -from flask import Flask, g +from flask import Flask, g, render_template, abort, request import sqlite3 @@ -37,11 +37,21 @@ def close_connection(exception): # ------------------------------ -@app.route('/') +@app.route('/', methods=["GET", "POST"]) def root(): conn = get_db() - # TODO change this - return "Hello World!" + if (request.method == "POST"): + message = request.form['message'] + if len(message) <= 140: + cc_object = conn.execute('INSERT INTO squawks (message) VALUES (?)', [message]) + conn.commit() + else: + abort(400) + + cc_object = conn.execute('SELECT * FROM squawks ORDER BY timestamp desc') + squawkers = cc_object.fetchall() + + return render_template('index.html', squawks=squawkers) if __name__ == '__main__': diff --git a/squawker/settings.py b/squawker/settings.py new file mode 100644 index 0000000..38428d5 --- /dev/null +++ b/squawker/settings.py @@ -0,0 +1,103 @@ +""" +Django settings for squawker project. + +Generated by 'django-admin startproject' using Django 1.8.7. + +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/ +""" +# add this near the top +import dj_database_url + +# 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 = '&1!5zbuw_rhff)pn85p25akvr9r5$f)5(a$2rw7ko#kl07c@=z' + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = ['an536-squawker.herokuapp.com'] + + +# Application definition + +INSTALLED_APPS = ( + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'main' +) + +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 = 'squawker.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 = 'squawker.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/1.8/ref/settings/#databases +# replace the DATABASES config +DATABASES = { + "default": dj_database_url.config(default='sqlite:///db.sqlite3'), +} + +# 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/' +PROJECT_DIR = os.path.dirname(os.path.abspath(__file__)) +STATIC_ROOT = os.path.join(PROJECT_DIR, 'static') diff --git a/squawker/static/.keep b/squawker/static/.keep new file mode 100644 index 0000000..e69de29 diff --git a/squawker/templates/index.html b/squawker/templates/index.html new file mode 100644 index 0000000..0ca8477 --- /dev/null +++ b/squawker/templates/index.html @@ -0,0 +1,162 @@ + + + + + + + + + + + +
+
+

Buddy Bear

+
+
+ + +
+
+ +
+ + + + +

Charts

+ +
+ + +
+ + + +
+ +{% for message in squawks %} +
+

{{ message[2] }}

+

{{ message[1] }}

+
+{% endfor %} +
+ + + diff --git a/squawker/urls.py b/squawker/urls.py new file mode 100644 index 0000000..37359d3 --- /dev/null +++ b/squawker/urls.py @@ -0,0 +1,23 @@ +"""squawker 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 an import: from blog import urls as blog_urls + 2. Add a URL to urlpatterns: url(r'^blog/', include(blog_urls)) +""" +from django.conf.urls import include, url +from django.contrib import admin +from main.views import root + +urlpatterns = [ + url(r'^admin/', include(admin.site.urls)), + url(r'^$', root) +] diff --git a/squawker/wsgi.py b/squawker/wsgi.py new file mode 100644 index 0000000..3d14a35 --- /dev/null +++ b/squawker/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for squawker 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", "squawker.settings") + +application = get_wsgi_application() diff --git a/src/pip-delete-this-directory.txt b/src/pip-delete-this-directory.txt new file mode 100644 index 0000000..c8883ea --- /dev/null +++ b/src/pip-delete-this-directory.txt @@ -0,0 +1,5 @@ +This file is placed here by pip to indicate the source was put +here by pip. + +Once this package is successfully installed this source code will be +deleted (unless you remove this file). diff --git a/src/splinter b/src/splinter new file mode 160000 index 0000000..ba3afc8 --- /dev/null +++ b/src/splinter @@ -0,0 +1 @@ +Subproject commit ba3afc8a0750dcfea096f8f89adb58f8f8d78276 diff --git a/tests/test_squawker.py b/tests/test_squawker.py deleted file mode 100644 index 5deda48..0000000 --- a/tests/test_squawker.py +++ /dev/null @@ -1,184 +0,0 @@ -import os -import pytest -import random -import re -from splinter import Browser -from squawker import server -import string -import tempfile -import time - - -URL = '/' -PAGE_SIZE = 20 -# match case-insensitively -# http://stackoverflow.com/a/1625859/358804 -NEXT_XPATH = "//a[contains(translate(text(),'abcdefghijklmnopqrstuvwxyz','ABCDEFGHIJKLMNOPQRSTUVWXYZ'), 'NEXT')]" - - -@pytest.fixture() -def db_fd(): - server.app.config['TESTING'] = True - db_fd, server.app.config['DATABASE'] = tempfile.mkstemp() - return db_fd - - -@pytest.fixture() -def test_app(db_fd, request): - client = server.app.test_client() - with server.app.app_context(): - server.init_db() - - def teardown(): - os.close(db_fd) - os.unlink(server.app.config['DATABASE']) - request.addfinalizer(teardown) - - return client - - -@pytest.fixture() -def browser(test_app): - return Browser('flask', app=server.app) - - -def random_string(minlength=5, maxlength=40): - charset = string.ascii_letters + string.digits - length = random.randint(minlength, maxlength) - return ''.join(random.choice(charset) for _ in range(length)) - - -def find_body_field(browser): - field = browser.find_by_css('input[type="text"],textarea').first - assert field is not None - return field - - -def create_squawk(browser, body): - browser.visit(URL) - - input_el = find_body_field(browser) - input_el.fill(body) - - button = browser.find_by_css('input[type="submit"],button[type="submit"]').first - assert button is not None - button.click() - - -def create_squawks(browser, count, delay=0): - bodies = ["Post {}".format(i) for i in range(count)] - for body in bodies: - create_squawk(browser, body) - if delay > 0: - time.sleep(delay) - - return bodies - - -def test_response_code(test_app): - response = test_app.get(URL) - assert response.status_code == 200 - - -@pytest.mark.score(5) -def test_form_present(browser): - browser.visit(URL) - assert browser.is_element_present_by_tag('form') - - -@pytest.mark.score(20) -def test_all_squawks_present(browser): - num_squawks = random.randint(3, 9) - bodies = create_squawks(browser, num_squawks) - - # in case they didn't return to the homepage - browser.visit(URL) - for body in bodies: - assert browser.is_text_present(body) - - -@pytest.mark.score(20) -def test_reverse_chronological_order(browser): - # the SQLite3 `datetime` type is down to the second by default, so wait between creating each squawk - bodies = create_squawks(browser, 3, delay=1) - - bodies.reverse() - pattern = '.*'.join(bodies) - - browser.visit(URL) - - assert re.search(pattern, browser.html, re.DOTALL + re.MULTILINE) is not None - - -@pytest.mark.score(30) -def test_create_squawk(browser): - TEXT = random_string() - create_squawk(browser, TEXT) - browser.visit(URL) - assert browser.is_text_present(TEXT) - - -@pytest.mark.score(5) -def test_returns_to_homepage(browser): - TEXT = random_string() - create_squawk(browser, TEXT) - # the latter checks are needed because there seems to be a splinter(?) bug where it doesn't handle (certain?) redirects properly - assert browser.is_element_present_by_tag('form') or (browser.status_code.code == 405 and browser.url == 'http://localhost/') - - -@pytest.mark.score(5) -def test_client_side_validation(browser): - browser.visit(URL) - assert browser.is_element_present_by_css('[maxlength="140"],[pattern]') - - -@pytest.mark.score(10) -def test_server_side_validation(browser): - TEXT = random_string(minlength=141, maxlength=200) - create_squawk(browser, TEXT) - # TODO ignore if it's in the `value` of the `` - assert browser.is_text_not_present(TEXT) - - -@pytest.mark.score(5) -@pytest.mark.xfail -def test_page_size_limit(browser): - bodies = create_squawks(browser, PAGE_SIZE + 1) - - browser.visit(URL) - assert browser.is_text_not_present(bodies[0]) - - -@pytest.mark.score(5) -@pytest.mark.xfail -def test_next_only_present_for_pagination(browser): - browser.visit(URL) - assert browser.is_element_not_present_by_xpath(NEXT_XPATH), "`Next` link should not be present." - - create_squawks(browser, PAGE_SIZE + 1) - - browser.visit(URL) - assert browser.is_element_present_by_xpath(NEXT_XPATH), "`Next` link should be present." - - -@pytest.mark.score(5) -@pytest.mark.xfail -def test_next_not_present_on_last_page(browser): - bodies = create_squawks(browser, PAGE_SIZE + 1) - - browser.visit(URL) - browser.find_by_xpath(NEXT_XPATH).first.click() - - assert browser.is_element_not_present_by_xpath(NEXT_XPATH), "`Next` link should not be present." - - -@pytest.mark.score(5) -@pytest.mark.xfail -def test_pagination(browser): - bodies = create_squawks(browser, PAGE_SIZE + 1) - - browser.visit(URL) - browser.find_by_xpath(NEXT_XPATH).first.click() - - assert browser.is_text_present(bodies[0]) - assert browser.is_text_not_present(bodies[-1])