diff --git a/.gitignore b/.gitignore index 72364f9..62616a4 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,15 @@ var/ *.egg-info/ .installed.cfg *.egg +envNeuro/ +.DS_Store +bin/ +man/ +pip-selfcheck.json +pyvenv.cfg +share/ + + # PyInstaller # Usually these files are written by a python script from a template diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..90c9361 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,18 @@ +language: python +python: +- '2.7' +- '3.5' +before_install: +- pip install -U pip setuptools +- pip install six +install: +- pip install -r requirements.pip +env: + global: + - secure: O0pOPi+1KGOJMaKqJB+09lRhIsXSNP7ycGn9jsJhvN6XljHBK+bgqWEriSNZ2y01l8GKy+fcnNuiMQLDIc4tQvFkxmYUhoBClkcx12Css1glS9cYpssmbav1YvazKdPL6Y/XdDTtA7na1ggPUaV3Ww4zcIazxOPifnR+Rc5YwGxFBsv0cVRGaAZ8Z7XeqitV7M1DxYa4i2O0hTWQEkw5d9BQH+kZxvervSIE4e4RrPvkopvsXd/Al1djTsJA3ZX0qvCyykuMwANj2ERes9ye2SvP5+etugdmeo267nsXasFN+FO2x+IZYWBNGL8Yl689pOuiGdmvvuP5pG1VYRi+cyk2nMZUC+aN0iuipocfLqJTjaZg7rjdjpRy22M/9eQbMBAUBKHSUkeaLDba+lpvbNiY6ybKQZlwd2pKQZSjHHsAa3lgBG3WduNSCGEKCsKmUgGBRn1c4h5yQFXcHjIRdqr9k21tbF6hCE/LJJoWJEY5XIJIqboitGvNcxWF3qtfau+koTYqH8ZufePOikkKvxiOpoK0bML1HViBdCvSFNB7ioYsLTqXaCN4RhXRUuFrU1O7k4wTZXoLLvyhIYIdnwjpXJX+7HgZq7ieIvAIYkB9rX199xK8cgKb/3WjMGZ7AEF1y4Tay8dRGcAsPu/MUBTSm/BzJvXB+Yy/SrqIXNk= +script: + - cd neuropy + - ./manage.py test + +notifications: + email: false \ No newline at end of file diff --git a/README.md b/README.md index e2699bd..b38de24 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,147 @@ # NeuroPy +[![Build Status](https://travis-ci.org/NeuroPyPlanner/NeuroPy.svg?branch=development)](https://travis-ci.org/NeuroPyPlanner/NeuroPy) + Personalized CBT-based priority planner + +## Creators: + +[Amos Bolder](https://github.com/amosboldor) | [Claire Gatenby](https://github.com/clair3st) | [Patrick Saunders](https://github.com/pasaunders) | [Sera Smith](https://github.com/serashioda) + +### URL: [HERE](http://ec2-52-14-126-118.us-east-2.compute.amazonaws.com/) + +### About the App: +A personalized priority app to support a *Cognitive Behavioral Therapy* (CBT) approach to efficiently organize your day according to a struggle/disorder that is effectively treated by CBT. CBT is a goal-oriented pyschotherapy treatment, taking a hands-on approach to problem solving. Goal of CBT is to change the patterns of thinking or behavior that are behind a person's difficulties, modifying their feelings and therefore their thinking and behavior overtime. + +*Neuropy* Considers what the habit/disorder the user wants to treat with CBT, along with medication, it's half-life and peak periods. Also works around black-out periods on user's schedule by syncing with Google Calender, personal preference or most productive period of the day for user, and finally considers time commitments for each activity. Each "TO-DO" will be ranked considering these aspects and will be worked into periods of the day. + +##### version-0.1 (in active development) +##### Keywords: + +No medical data is stored by NeuroPy + +##Getting Started + +Clone this repository into whatever directory you want to work from. +``` +https://github.com/NeuroPyPlanner/NeuroPy.git +``` +Assuming that you have access to Python 3 at the system level, start up a new virtual environment. +``` +$ cd NeuroPy +$ python3 -m venv ENV +$ source ENV/bin/activate +``` +Once your environment has been activated, make sure to install Django and all of this project's required packages. +``` +(NeuroPy) $ pip install -r requirements.pip +``` +Navigate to the project root, imagersite, and apply the migrations for the app. +``` +(NeuroPy) $ cd imagersite + +(NeuroPy) $ ./manage.py migrate +``` +Finally, run the server in order to server the app on localhost +``` +(NeuroPy) $ ./manage.py runserver +``` +Django will typically serve on port 8000, unless you specify otherwise. You can access the locally-served site at the address http://localhost:8000. + + +##Current Models (outside of Django built-ins): + +This application allow users to store and organize photos. + +**The `Profile` model contains:** + +- The period which the user is up and active +- The time of day when a user is best able to focus and work +- The time of day when a user takes their medication +- A __str__ method which returns the user's username. + +**The `Todo` model contains:** + +- Date +- Ease of accomplishing the task +- Duration of task +- Owner of the task +- Title of the task +- Description +- Priority level +- A __str__ method which returns the task title + +**The `Medication` model contains:** + +- Name +- Medication Type +- Use (on or off label) +- Half life +- Ramp-up time +- Peak Period +- Start and end times for the easy, medium and peak energy periods + represented as a comma seperated set of integers where the first + integer represents the number of hours since the user took the + medicaion, and the second integer representing the number of extra + minutes. +- A __str__ method which returns the medication's name + +##Current URL Routes + +- `/oauth2/` Google calendar request authorization +- `/admin` Superuser admin page. +- `/` Home page. +- `/login` Login page. +- `/logout` Logout route, no view. +- `/accounts/register` Register a user form. +- `/accounts/activate/complete/` Activation complete view. +- `/accounts/register/complete/` Registration complete, email sent. +- `/profile/` Links to the following routes: + - `/` Shows the user their profile data. + - `/edit/` Allows the user to edit their profile +- `/profile/todo/ Links to the following routes: + - `/calendar/` Allows the user to view their schedule + - `/schedule/` Shows the user's to-do list ordered by priority and difficulty + - `/[todo_id]/edit/` Allows the user to edit their to-do items + - `/[todo_id]/` Detail view for an individula to-do item + - `/add/` Allows the user to create a new to-do item + - `/` Shows the user a summary of all their to-do items + + +##Running Tests + +Running tests for the NeuroPy is fairly straightforward. Navigate to the same directory as the manage.py file and type: +``` +(NeuroPy) $ coverage run manage.py test +``` +This will show you which tests have failed, which tests have passed. If you'd like a report of the actual coverage of your tests, type +``` +(NeuroPy) $ coverage report +``` +This will read from the included .coverage file, with configuration set in the .coveragerc file. Currently the configuration will show which lines were missing from the test coverage. + + +### USER STORIES: + +#### User Stories + +- As a user I want the app to use good CBT principles so that I can effectively manage my energy. +- As a user I want the app to take my medications into account so that I can work at my highest-focus time of day. +- As a user I want the app to take in my personal preferences into account so that I don’t work on easy tasks when I have energy and hard tasks when I’m tired. +- As a user I want the app to preserve my preferences so that I can conveniently generate new schedules. +- As a user I want to be able to input my personal preferences and data easily and accurately so that the app can prioritize my day effectively. +- As a user I want my medical data to be secure so that I can avoid fraud and identity theft. +- As a user I want to be able to quickly generate schedules with minimal clicks so that I don’t have to spend all my energy organizing. +- As a user I want my newly generated schedule to be integrated into my google calendar with notifications. +- As a user I want the app to take into account my sleep schedule so that I can work when I have the most energy. + + +#### Developer Stories + +- As a developer I want readable and well documented code to make maintenance easier. +- As a developer I want to practise test driven development in building my app and spotting bugs early on. +- As a developer I want to avoid storing any medical data so that I don’t have to deal with HIPAA violations. +- As a developer I want to store data of users in appropriate models, with logical relationships. +- As a developer I want to build for one disorder before expanding so that I can build on a strong foundation. +- As a developer I want a narrow open source license so that we retain plenty of rights to our work. +- As a developer I want to write an algorithm that prioritises todos based on a user’s profile. +- As a developer I want to integrate the google calendars with my app using an api. diff --git a/neuropy/.coveragerc b/neuropy/.coveragerc new file mode 100644 index 0000000..6f834f5 --- /dev/null +++ b/neuropy/.coveragerc @@ -0,0 +1,21 @@ +[run] +omit = + */migrations/*, + */tests.py + manage.py + neuropy/wsgi.py + neuropy/urls.py + neuropy/views.py + */envNeuro/* + */apps.py + */admin.py + */__init__.py + +source = + neuropy + medication + todo + userprofile + +[report] +show_missing = True \ No newline at end of file diff --git a/neuropy/manage.py b/neuropy/manage.py new file mode 100755 index 0000000..19d8b1d --- /dev/null +++ b/neuropy/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +import os +import sys + +if __name__ == "__main__": + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "neuropy.settings") + try: + from django.core.management import execute_from_command_line + except ImportError: + # The above import may fail for some other reason. Ensure that the + # issue is really that Django is missing to avoid masking other + # exceptions on Python 2. + try: + import django + except ImportError: + 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?" + ) + raise + execute_from_command_line(sys.argv) diff --git a/neuropy/medication/__init__.py b/neuropy/medication/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/neuropy/medication/admin.py b/neuropy/medication/admin.py new file mode 100644 index 0000000..23c2c1f --- /dev/null +++ b/neuropy/medication/admin.py @@ -0,0 +1,4 @@ +from django.contrib import admin +from medication.models import Medication + +admin.site.register(Medication) diff --git a/neuropy/medication/apps.py b/neuropy/medication/apps.py new file mode 100644 index 0000000..e095071 --- /dev/null +++ b/neuropy/medication/apps.py @@ -0,0 +1,7 @@ +from __future__ import unicode_literals + +from django.apps import AppConfig + + +class MedicationConfig(AppConfig): + name = 'medication' diff --git a/neuropy/medication/fixtures/medication.json b/neuropy/medication/fixtures/medication.json new file mode 100644 index 0000000..a68dcf3 --- /dev/null +++ b/neuropy/medication/fixtures/medication.json @@ -0,0 +1,112 @@ +[ + { + "fields":{ + "treating_dis":"ADD/ADHD", + "name":"CONCERTA", + "post_peak_medium_end":"12:00:00", + "post_peak_medium_start":"09:30:00", + "ramp_up":"03:30:00", + "half_life":"03:30:00", + "medium_end":"05:00:00", + "medium_start":"03:30:00", + "peak_period":"09:30:00", + "med_type":"stimulant", + "peak_end":"09:30:00", + "post_peak_easy_start":"12:00:00", + "post_peak_easy_end":"16:00:00", + "peak_start":"00:00:00", + "easy_start":"00:00:00", + "easy_end":"03:30:00" + }, + "model":"medication.medication", + "pk":1 + }, + { + "fields":{ + "treating_dis":"ADD/ADHD", + "name":"ADDERALL", + "post_peak_medium_end":"12:00:00", + "post_peak_medium_start":"09:30:00", + "ramp_up":"07:00:00", + "half_life":"10:00:00", + "medium_end":"05:00:00", + "medium_start":"03:30:00", + "peak_period":"09:30:00", + "med_type":"stimulant", + "peak_end":"09:30:00", + "post_peak_easy_start":"12:00:00", + "post_peak_easy_end":"16:00:00", + "peak_start":"00:00:00", + "easy_start":"00:00:00", + "easy_end":"03:30:00" + }, + "model":"medication.medication", + "pk":2 + }, + { + "fields":{ + "treating_dis":"ADD/ADHD", + "name":"Focalin", + "post_peak_medium_end":"12:00:00", + "post_peak_medium_start":"09:30:00", + "ramp_up":"06:30:00", + "half_life":"07:00:00", + "medium_end":"05:00:00", + "medium_start":"03:30:00", + "peak_period":"09:30:00", + "med_type":"stimulant", + "peak_end":"09:30:00", + "post_peak_easy_start":"12:00:00", + "post_peak_easy_end":"16:00:00", + "peak_start":"00:00:00", + "easy_start":"00:00:00", + "easy_end":"03:30:00" + }, + "model":"medication.medication", + "pk":3 + }, + { + "fields":{ + "treating_dis":"ADD/ADHD", + "name":"Ritalin LA", + "post_peak_medium_end":"12:00:00", + "post_peak_medium_start":"09:30:00", + "ramp_up":"07:00:00", + "half_life":"03:30:00", + "medium_end":"05:00:00", + "medium_start":"03:30:00", + "peak_period":"09:30:00", + "med_type":"stimulant", + "peak_end":"09:30:00", + "post_peak_easy_start":"12:00:00", + "post_peak_easy_end":"16:00:00", + "peak_start":"00:00:00", + "easy_start":"00:00:00", + "easy_end":"03:30:00" + }, + "model":"medication.medication", + "pk":5 + }, + { + "fields":{ + "treating_dis":"ADD/ADHD", + "name":"Vyvanse", + "post_peak_medium_end":"12:00:00", + "post_peak_medium_start":"09:30:00", + "ramp_up":"02:00:00", + "half_life":"11:00:00", + "medium_end":"05:00:00", + "medium_start":"03:30:00", + "peak_period":"09:30:00", + "med_type":"stimulant", + "peak_end":"09:30:00", + "post_peak_easy_start":"12:00:00", + "post_peak_easy_end":"16:00:00", + "peak_start":"00:00:00", + "easy_start":"00:00:00", + "easy_end":"03:30:00" + }, + "model":"medication.medication", + "pk":6 + } +] \ No newline at end of file diff --git a/neuropy/medication/migrations/0001_initial.py b/neuropy/medication/migrations/0001_initial.py new file mode 100644 index 0000000..972c277 --- /dev/null +++ b/neuropy/medication/migrations/0001_initial.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2017-02-17 01:28 +from __future__ import unicode_literals + +import django.core.validators +from django.db import migrations, models +import re + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Medication', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(default='CONCERTA', max_length=20)), + ('med_type', models.CharField(default='stimulant', max_length=20)), + ('treating_dis', models.CharField(default='ADD/ADHD', max_length=25)), + ('half_life', models.DurationField()), + ('ramp_up', models.DurationField()), + ('peak_period', models.DurationField()), + ('easy_start', models.CharField(max_length=50, validators=[django.core.validators.RegexValidator(re.compile('^\\d+(?:\\,\\d+)*\\Z', 32), code='invalid', message='Enter only digits separated by commas.')])), + ('easy_end', models.CharField(max_length=50, validators=[django.core.validators.RegexValidator(re.compile('^\\d+(?:\\,\\d+)*\\Z', 32), code='invalid', message='Enter only digits separated by commas.')])), + ('medium_start', models.CharField(max_length=50, validators=[django.core.validators.RegexValidator(re.compile('^\\d+(?:\\,\\d+)*\\Z', 32), code='invalid', message='Enter only digits separated by commas.')])), + ('medium_end', models.CharField(max_length=50, validators=[django.core.validators.RegexValidator(re.compile('^\\d+(?:\\,\\d+)*\\Z', 32), code='invalid', message='Enter only digits separated by commas.')])), + ('peak_start', models.CharField(max_length=50, validators=[django.core.validators.RegexValidator(re.compile('^\\d+(?:\\,\\d+)*\\Z', 32), code='invalid', message='Enter only digits separated by commas.')])), + ('peak_end', models.CharField(max_length=50, validators=[django.core.validators.RegexValidator(re.compile('^\\d+(?:\\,\\d+)*\\Z', 32), code='invalid', message='Enter only digits separated by commas.')])), + ('post_peak_medium_start', models.CharField(max_length=50, validators=[django.core.validators.RegexValidator(re.compile('^\\d+(?:\\,\\d+)*\\Z', 32), code='invalid', message='Enter only digits separated by commas.')])), + ('post_peak_medium_end', models.CharField(max_length=50, validators=[django.core.validators.RegexValidator(re.compile('^\\d+(?:\\,\\d+)*\\Z', 32), code='invalid', message='Enter only digits separated by commas.')])), + ('post_peak_easy_start', models.CharField(max_length=50, validators=[django.core.validators.RegexValidator(re.compile('^\\d+(?:\\,\\d+)*\\Z', 32), code='invalid', message='Enter only digits separated by commas.')])), + ('post_peak_easy_end', models.CharField(max_length=50, validators=[django.core.validators.RegexValidator(re.compile('^\\d+(?:\\,\\d+)*\\Z', 32), code='invalid', message='Enter only digits separated by commas.')])), + ], + ), + ] diff --git a/neuropy/medication/migrations/0002_auto_20170216_1728.py b/neuropy/medication/migrations/0002_auto_20170216_1728.py new file mode 100644 index 0000000..5af5f74 --- /dev/null +++ b/neuropy/medication/migrations/0002_auto_20170216_1728.py @@ -0,0 +1,25 @@ +""".""" + +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2017-02-14 00:12 +from __future__ import unicode_literals + +from django.db import migrations + + +def load_medications_from_fixture(apps, schema_editor): + """.""" + from django.core.management import call_command + call_command("loaddata", "medication") + + +class Migration(migrations.Migration): + """.""" + + dependencies = [ + ('medication', '0001_initial'), + ] + + operations = [ + migrations.RunPython(load_medications_from_fixture), + ] diff --git a/neuropy/medication/migrations/__init__.py b/neuropy/medication/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/neuropy/medication/models.py b/neuropy/medication/models.py new file mode 100644 index 0000000..a932bbc --- /dev/null +++ b/neuropy/medication/models.py @@ -0,0 +1,35 @@ +"""Medication model- no user relation.""" + +from __future__ import unicode_literals + +from django.db import models +from django.utils.encoding import python_2_unicode_compatible +from django.core.validators import validate_comma_separated_integer_list + + +@python_2_unicode_compatible +class Medication(models.Model): + """Medication instace class.""" + + name = models.CharField( + max_length=20, + default='CONCERTA') + med_type = models.CharField(max_length=20, default='stimulant') + treating_dis = models.CharField(max_length=25, default='ADD/ADHD') + half_life = models.DurationField() + ramp_up = models.DurationField() + peak_period = models.DurationField() + easy_start = models.CharField(validators=[validate_comma_separated_integer_list], max_length=50) + easy_end = models.CharField(validators=[validate_comma_separated_integer_list], max_length=50) + medium_start = models.CharField(validators=[validate_comma_separated_integer_list], max_length=50) + medium_end = models.CharField(validators=[validate_comma_separated_integer_list], max_length=50) + peak_start = models.CharField(validators=[validate_comma_separated_integer_list], max_length=50) + peak_end = models.CharField(validators=[validate_comma_separated_integer_list], max_length=50) + post_peak_medium_start = models.CharField(validators=[validate_comma_separated_integer_list], max_length=50) + post_peak_medium_end = models.CharField(validators=[validate_comma_separated_integer_list], max_length=50) + post_peak_easy_start = models.CharField(validators=[validate_comma_separated_integer_list], max_length=50) + post_peak_easy_end = models.CharField(validators=[validate_comma_separated_integer_list], max_length=50) + + def __str__(self): + """String representation of Medication.""" + return self.name diff --git a/neuropy/medication/tests.py b/neuropy/medication/tests.py new file mode 100644 index 0000000..780d698 --- /dev/null +++ b/neuropy/medication/tests.py @@ -0,0 +1,82 @@ +"""Tests for medication app.""" + +from django.test import TestCase +from medication.models import Medication +import factory +import datetime +import dateutil + +class MedicationTestCase(TestCase): + """Test the Medication model.""" + + def setUp(self): + """Setup for medications.""" + self.medications = [medication for medication in Medication.objects.all()] + + def test_all_meds_are_present(self): + """Test top all ADHD medications are present.""" + self.assertTrue(self.medications[0].name == 'CONCERTA') + self.assertTrue(self.medications[1].name == 'ADDERALL') + self.assertTrue(self.medications[2].name == 'Focalin') + self.assertTrue(self.medications[3].name == 'Ritalin LA') + self.assertTrue(self.medications[4].name == 'Vyvanse') + + def test_medication1_info_is_correct(self): + """Test that a medication instance has a name.""" + med1 = self.medications[0] + self.assertTrue(med1.name == "CONCERTA") + self.assertTrue(med1.med_type == 'stimulant') + self.assertTrue(med1.treating_dis == 'ADD/ADHD') + self.assertTrue(med1.half_life == datetime.timedelta(hours=3, minutes=30)) + self.assertTrue(med1.ramp_up == datetime.timedelta(hours=3, minutes=30)) + + def test_treating_disorder_are_all_same(self): + """Test that a medication instance has a name.""" + for medication in self.medications: + self.assertTrue(medication.treating_dis == "ADD/ADHD") + + def test_med_type_are_all_same(self): + """Test medicatcion instance type.""" + for medication in self.medications: + self.assertTrue(medication.med_type == "stimulant") + + def test_no_dupelicate_medication(self): + """Test that there are no duplicates of a medication.""" + seen_meds = [] + for medication in self.medications: + if medication in seen_meds: + raise ValueError("Medication already exists") + else: + seen_meds.append(medication) + return seen_meds + + def test_peak_start_is_before_peak_end(self): + """Test that peak start time is before peak end.""" + for medication in self.medications: + self.assertTrue(medication.peak_start < medication.peak_end) + + def test_medication_duration(self): + """Test medication peak duration is equal to diff between peak-end and start.""" + for medication in self.medications: + peak_end = dateutil.parser.parse(medication.peak_end) + peak_start = dateutil.parser.parse(medication.peak_start) + peak_delta = peak_end - peak_start + peak_period = medication.peak_period + self.assertTrue(peak_delta == peak_period) + + def test_change_data(self): + """Test Editing medication data.""" + medication = self.medications[1] + medication = Medication.objects.get(id=medication.id) + medication.peak_period = datetime.timedelta(hours=10) + medication.save() + medication = Medication.objects.get(id=medication.id) + self.assertTrue(medication.peak_period == datetime.timedelta(hours=10)) + + def test_delete_med(self): + """Test fetching medication after delete throws an exception.""" + medication = self.medications[1] + medication = Medication.objects.get(id=medication.id) + medication.delete() + with self.assertRaises(Medication.DoesNotExist): + Medication.objects.get(id=medication.id) diff --git a/neuropy/medication/views.py b/neuropy/medication/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/neuropy/medication/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/neuropy/neuropy/__init__.py b/neuropy/neuropy/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/neuropy/neuropy/oauth.py b/neuropy/neuropy/oauth.py new file mode 100644 index 0000000..b4d8bf0 --- /dev/null +++ b/neuropy/neuropy/oauth.py @@ -0,0 +1,29 @@ +"""Code for return auth.""" +from oauth2client.contrib.django_util.storage import DjangoORMStorage +from django.contrib.auth.decorators import login_required +from oauth2client.client import OAuth2WebServerFlow +from userprofile.models import CredentialsModel +from django.http import HttpResponseBadRequest +from django.http import HttpResponseRedirect +from oauth2client.contrib import xsrfutil +from neuropy import settings + +FLOW = OAuth2WebServerFlow( + client_id=settings.GOOGLE_OAUTH2_CLIENT_ID, + client_secret=settings.GOOGLE_OAUTH2_CLIENT_SECRET, + scope='https://www.googleapis.com/auth/calendar', + redirect_uri='http://localhost:8000/oauth2callback', + prompt='consent' +) + + +@login_required +def auth_return(request): + """Oauth return view.""" + if not xsrfutil.validate_token(settings.SECRET_KEY, request.GET['state'], + request.user): + return HttpResponseBadRequest() + credential = FLOW.step2_exchange(request.GET['code']) + storage = DjangoORMStorage(CredentialsModel, 'user_id', request.user, 'credential') + storage.put(credential) + return HttpResponseRedirect("/") diff --git a/neuropy/neuropy/settings.py b/neuropy/neuropy/settings.py new file mode 100644 index 0000000..55b9f74 --- /dev/null +++ b/neuropy/neuropy/settings.py @@ -0,0 +1,148 @@ +""" +Django settings for neuropy project. + +Generated by 'django-admin startproject' using Django 1.10.5. + +For more information on this file, see +https://docs.djangoproject.com/en/1.10/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/1.10/ref/settings/ +""" + +import os + +# Build paths inside the project like this: os.path.join(BASE_DIR, ...) +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.10/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = 'y6o$5t_po!&22rrkc@-dewa4(us%g5k)fyrk-2ln)e!rlb#n^j' + +# 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', + 'medication', + 'neuropy', + 'userprofile', + 'todo', + 'bootstrap3', + 'oauth2client.contrib.django_util' +] + +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 = 'neuropy.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 = 'neuropy.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/1.10/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.postgresql', + 'NAME': 'neuropy', + 'USER': os.environ.get("USER_NAME", ''), + 'PASSWORD': os.environ.get('DATABASE_PASSWORD', ''), + 'HOST': '127.0.0.1', + 'PORT': '5432', + 'TEST': { + 'NAME': 'test_neuropy' + } + } +} + + +# Password validation +# https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + +GOOGLE_OAUTH2_CLIENT_ID = os.environ.get('GOOGLE_CLIENT_ID', '') + +GOOGLE_OAUTH2_CLIENT_SECRET = os.environ.get('GOOGLE_CLIENT_SECRET', '') + +GOOGLE_OAUTH2_SCOPES = ('https://www.googleapis.com/auth/calendar',) + +GOOGLE_OAUTH2_STORAGE_MODEL = { + 'model': 'userprofile.models.CredentialsModel', + 'user_property': 'user_id', + 'credentials_property': 'credential', +} + + +# Internationalization +# https://docs.djangoproject.com/en/1.10/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'America/Los_Angeles' + +USE_I18N = True + +USE_L10N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/1.10/howto/static-files/ + +STATIC_URL = '/static/' +ACCOUNT_ACTIVATION_DAYS = 7 +EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' +LOGIN_REDIRECT_URL = '/' diff --git a/neuropy/neuropy/static/neuropy/Amos.jpg b/neuropy/neuropy/static/neuropy/Amos.jpg new file mode 100644 index 0000000..f47f131 Binary files /dev/null and b/neuropy/neuropy/static/neuropy/Amos.jpg differ diff --git a/neuropy/neuropy/static/neuropy/Claire.jpg b/neuropy/neuropy/static/neuropy/Claire.jpg new file mode 100644 index 0000000..130331f Binary files /dev/null and b/neuropy/neuropy/static/neuropy/Claire.jpg differ diff --git a/neuropy/neuropy/static/neuropy/cover.css b/neuropy/neuropy/static/neuropy/cover.css new file mode 100755 index 0000000..d2df406 --- /dev/null +++ b/neuropy/neuropy/static/neuropy/cover.css @@ -0,0 +1,179 @@ +/* + * Globals + */ + +@import url('https://fonts.googleapis.com/css?family=Open+Sans|Pacifico|Sansita'); +/*font-family: 'Pacifico', cursive; +font-family: 'Open Sans Condensed', sans-serif; +font-family: 'Open Sans', sans-serif;*/ + +/* Links */ +a, +a:focus, +a:hover { + color: #fff; +} + +/* Custom default button */ +.btn-default, +.btn-default:hover, +.btn-default:focus { + color: #333; + text-shadow: none; /* Prevent inheritance from `body` */ + background-color: #fff; + border: 1px solid #fff; +} + + +/* + * Base structure + */ + +html, +body { + height: 100%; +} +body { + color: #fff; + text-align: center; + text-shadow: 0 1px 3px rgba(0,0,0,.5); + font-family: 'Open Sans', bold, sans-serif; + position: relative; + background-image: + linear-gradient(rgba(0, 0, 0, 0.3), + rgba(0, 0, 0, 0.3)), url('pexels-photo-109019.jpeg'); + background-repeat: no-repeat; + background-attachment: fixed; + background-size: cover; +} + +/* Extra markup and styles for table-esque vertical and horizontal centering */ +.site-wrapper { + display: table; + width: 100%; + height: 100%; /* For at least Firefox */ + min-height: 100%; + -webkit-box-shadow: inset 0 0 100px rgba(0,0,0,.5); + box-shadow: inset 0 0 100px rgba(0,0,0,.5); +} +.site-wrapper-inner { + display: table-cell; + vertical-align: top; +} +.cover-container { + margin-right: auto; + margin-left: auto; +} + +/* Padding for spacing */ +.inner { + padding: 30px; +} + + +/* + * Header + */ +.masthead-brand { + margin-top: 10px; + margin-bottom: 10px; +} + +.masthead-nav > li { + display: inline-block; +} +.masthead-nav > li + li { + margin-left: 20px; +} +.masthead-nav > li > a { + padding-right: 0; + padding-left: 0; + font-size: 16px; + font-weight: bold; + color: #fff; /* IE8 proofing */ + color: rgba(255,255,255,.75); + border-bottom: 2px solid transparent; +} +.masthead-nav > li > a:hover, +.masthead-nav > li > a:focus { + background-color: transparent; + border-bottom-color: #a9a9a9; + border-bottom-color: rgba(255,255,255,.25); +} +.masthead-nav > .active > a, +.masthead-nav > .active > a:hover, +.masthead-nav > .active > a:focus { + color: #fff; + border-bottom-color: #fff; +} + +@media (min-width: 768px) { + .masthead-brand { + float: left; + } + .masthead-nav { + float: right; + } +} + + +/* + * Cover + */ + +.cover { + padding: 0 20px; +} +.cover .btn-lg { + padding: 10px 20px; + font-weight: bold; +} + +.cover-heading { + font-family: 'Pacifico', cursive; +} + + +/* + * Footer + */ + +.mastfoot { + color: #999; /* IE8 proofing */ + color: rgba(255,255,255,.5); +} + + +/* + * Affix and center + */ + +@media (min-width: 768px) { + /* Pull out the header and footer */ + .masthead { + position: fixed; + top: 0; + } + .mastfoot { + position: fixed; + bottom: 0; + } + /* Start the vertical centering */ + .site-wrapper-inner { + vertical-align: middle; + } + /* Handle the widths */ + .masthead, + .mastfoot, + .cover-container { + width: 100%; /* Must be percentage or pixels for horizontal alignment */ + } +} + +@media (min-width: 992px) { + .masthead, + .mastfoot, + .cover-container { + width: 700px; + } +} diff --git a/neuropy/neuropy/static/neuropy/icon.png b/neuropy/neuropy/static/neuropy/icon.png new file mode 100644 index 0000000..61024c3 Binary files /dev/null and b/neuropy/neuropy/static/neuropy/icon.png differ diff --git a/neuropy/neuropy/static/neuropy/patrick.jpg b/neuropy/neuropy/static/neuropy/patrick.jpg new file mode 100644 index 0000000..5d10718 Binary files /dev/null and b/neuropy/neuropy/static/neuropy/patrick.jpg differ diff --git a/neuropy/neuropy/static/neuropy/pexels-photo-109019.jpeg b/neuropy/neuropy/static/neuropy/pexels-photo-109019.jpeg new file mode 100644 index 0000000..3e2e8cd Binary files /dev/null and b/neuropy/neuropy/static/neuropy/pexels-photo-109019.jpeg differ diff --git a/neuropy/neuropy/static/neuropy/puzzle.png b/neuropy/neuropy/static/neuropy/puzzle.png new file mode 100644 index 0000000..acdd7da Binary files /dev/null and b/neuropy/neuropy/static/neuropy/puzzle.png differ diff --git a/neuropy/neuropy/static/neuropy/sera.jpg b/neuropy/neuropy/static/neuropy/sera.jpg new file mode 100644 index 0000000..d8c2737 Binary files /dev/null and b/neuropy/neuropy/static/neuropy/sera.jpg differ diff --git a/neuropy/neuropy/static/neuropy/sticky-footer-navbar.css b/neuropy/neuropy/static/neuropy/sticky-footer-navbar.css new file mode 100755 index 0000000..ca27d75 --- /dev/null +++ b/neuropy/neuropy/static/neuropy/sticky-footer-navbar.css @@ -0,0 +1,142 @@ +/* Sticky footer styles +-------------------------------------------------- */ +@import url('https://fonts.googleapis.com/css?family=Open+Sans|Pacifico|Sansita'); + +html { + position: relative; + min-height: 100%; + +} +body { + /* Margin bottom by footer height */ + margin-bottom: 60px; + font-family: 'Open Sans', bold, sans-serif; +} +.footer { + position: absolute; + bottom: 0; + width: 100%; + color: #fff; + /* Set the fixed height of the footer here */ + height: 60px; + background-image: + linear-gradient(rgba(0, 0, 0, 0.3), + rgba(0, 0, 0, 0.3)), url('pexels-photo-109019.jpeg'); + background-repeat: no-repeat; + background-size: cover; +} +nav { + background-image: + linear-gradient(rgba(0, 0, 0, 0.3), + rgba(0, 0, 0, 0.3)), url('pexels-photo-109019.jpeg'); + background-repeat: no-repeat; + background-size: cover; + color: #fff; +} +.navbar-default .navbar-nav>li>a, +.navbar-default .navbar-brand { + color: #fff; +} + +.neuropy { + font-family: 'Pacifico', cursive; + font-size: 2em; +} + +.name { + font-family: 'Pacifico', cursive; + +} + +.form-section { + margin-bottom: 25px; +} + +h1 { + font-family: 'Sansita', sans-serif; +} + +.uppercase { + text-transform: uppercase; +} + +/* Custom page CSS +-------------------------------------------------- */ +/* Not required for template or sticky footer method. */ + +body > .container { + padding: 60px 15px 0; +} +.container .text-muted { + margin: 20px 0; +} + +.footer > .container { + padding-right: 15px; + padding-left: 15px; + margin-top: 30px; +} + +code { + font-size: 80%; +} + +.terms { + width: 60%; + height: 300px; + border: 1px solid #ccc; + background: #f2f2f2; + padding: 6px; + overflow: auto; + margin-bottom: 30px; +} + +.unstyled { + list-style: none; +} + +.color1 { + background-color: rgba(164, 189, 252, 0.5); +} + +.color2 { + background-color: rgba(122, 231, 191, 0.5); +} + +.color3 { + background-color: rgba(219, 173, 255, 0.5); +} + +.color4 { + background-color: rgba(255, 136, 124, 0.5); +} + +.color5 { + background-color: rgba(251, 215, 91, 0.5); +} + +.color6 { + background-color: rgba(255, 184, 120, 0.5); +} + +.color7 { + background-color: rgba(70, 214, 219, 0.5); +} + +.color8 { + background-color: rgba(225, 225, 225, 0.5); +} + +.color9 { + background-color: rgba(84, 132, 237, 0.5); +} + +.color10 { + background-color: rgba(81, 183, 73, 0.5); +} + +.color11 { + background-color: rgba(220, 33, 39, 0.5); +} + + diff --git a/neuropy/neuropy/static/neuropy/white-icon.png b/neuropy/neuropy/static/neuropy/white-icon.png new file mode 100644 index 0000000..fe3df4f Binary files /dev/null and b/neuropy/neuropy/static/neuropy/white-icon.png differ diff --git a/neuropy/neuropy/templates/neuropy/about.html b/neuropy/neuropy/templates/neuropy/about.html new file mode 100644 index 0000000..18761a5 --- /dev/null +++ b/neuropy/neuropy/templates/neuropy/about.html @@ -0,0 +1,64 @@ +{% extends 'neuropy/layout.html' %} +{% load static %} + + +{% block content %} + +
+ +
+ +

A personalized priority app to support a Cognitive Behavioral Therapy (CBT) approach to efficiently organize your day according to a struggle/disorder that is effectively treated by CBT. CBT is a goal-oriented pyschotherapy treatment, taking a hands-on approach to problem solving. Goal of CBT is to change the patterns of thinking or behavior that are behind a person's difficulties, modifying their feelings and therefore their thinking and behavior overtime.

+ +

Neuropy Considers what the habit/disorder the user wants to treat with CBT, along with medication, its efficacy, and peak periods. Also works around black-out periods on user's schedule by syncing with Google Calender, personal preference or most productive period of the day for user, and finally considers time commitments for each activity. Each "TO-DO" will be ranked considering these aspects and will be worked into periods of the day.

+ +Join Today! + + + +
+
+
+
+

Sera Smith

+
Image
+
Hi, I am Sera. I was born and raised in Tokyo, Japan until Age 11. I then moved to Seattle where I stayed until graduating from University of Washington with a degree in Criminal Psychology and Classical Japanese. I am bilingual and have 2 Shiba Inu's. I was also a Shiba Inu breeder while living in NV after UW. I am a Python developer.
+
+
+
+
+

Amos Bolder

+
Image
+
Hi, I am Amos I love to code. Python rules everything else drools. I am Python developer.
+
+
+
+
+

Claire Gatenby

+
Image
+
I am a born and bred Australian who made the wild decision to quit my job and move to Seattle where I am learning how to code. I dream of using my past life as a scientist and my new skills as a developer to move into bioinformatics. I love cats, staying active, travelling, the outdoors and taking photos. I am Python developer.
+
+
+
+
+

Patrick Saunders

+
Image
+
Hi, I am Patrick. I am a new, starry-eyed software developer. I am happiest when his tests are passing or at least when they’re failing in a predictable way. I'm excited about python but I'm also looking forward to expanding my set of languages.
+
+
+
+
+
+
+
+
+
+ +

+ + +{%endblock%} \ No newline at end of file diff --git a/neuropy/neuropy/templates/neuropy/base.html b/neuropy/neuropy/templates/neuropy/base.html new file mode 100644 index 0000000..0fc2092 --- /dev/null +++ b/neuropy/neuropy/templates/neuropy/base.html @@ -0,0 +1,20 @@ +{% load static %} + + + + NeuroPy + {% load bootstrap3 %} + + + {% bootstrap_javascript %} + {% bootstrap_css %} + {% block stylesheets %} + {% endblock stylesheets %} + + + + {% block body %} + {% endblock body %} + + + diff --git a/neuropy/neuropy/templates/neuropy/home.html b/neuropy/neuropy/templates/neuropy/home.html new file mode 100644 index 0000000..c4520a1 --- /dev/null +++ b/neuropy/neuropy/templates/neuropy/home.html @@ -0,0 +1,60 @@ +{% extends 'neuropy/base.html' %} +{% load static %} + +{% block body %} + + +{% block stylesheets %} + +{% endblock stylesheets %} + + + +
+ +
+ +
+ +
+
+ + +
+
+ +
+

NeruoPy

+

Integrate with your calendar for priority planning

+

+ {% if not user.is_authenticated %} + Join Today! + {% endif %} +

+
+ +
+
+ +
+
+ +
+ +
+ +
+ + {% endblock body %} \ No newline at end of file diff --git a/neuropy/neuropy/templates/neuropy/layout.html b/neuropy/neuropy/templates/neuropy/layout.html new file mode 100644 index 0000000..cfea2e1 --- /dev/null +++ b/neuropy/neuropy/templates/neuropy/layout.html @@ -0,0 +1,53 @@ +{% extends 'neuropy/base.html' %} +{% load static %} + +{% block stylesheets %} + +{% endblock stylesheets %} + +{% block body %} + + + + +
+ {% block content %} + {% endblock content %} +
+ + +{% endblock body %} \ No newline at end of file diff --git a/neuropy/neuropy/templates/registration/activate.html b/neuropy/neuropy/templates/registration/activate.html new file mode 100644 index 0000000..b2436ef --- /dev/null +++ b/neuropy/neuropy/templates/registration/activate.html @@ -0,0 +1,8 @@ +{% extends 'neuropy/layout.html' %} +{% block content %} +{% activation_key %} +

Congratulations!

+

You have successfully completed registration to NeuroPy

+ Home + +{% endblock content %} \ No newline at end of file diff --git a/neuropy/neuropy/templates/registration/activation_complete.html b/neuropy/neuropy/templates/registration/activation_complete.html new file mode 100644 index 0000000..24f95b9 --- /dev/null +++ b/neuropy/neuropy/templates/registration/activation_complete.html @@ -0,0 +1,9 @@ +{% extends 'neuropy/layout.html' %} +{% block content %} +

Congratulations!

+

You have successfully completed registration to IMAGE-Nation

+

To get started, why not edit your profile, so we can get to know you.

+ + + Home Edit Profile +{% endblock content %} \ No newline at end of file diff --git a/neuropy/neuropy/templates/registration/activation_email.txt b/neuropy/neuropy/templates/registration/activation_email.txt new file mode 100644 index 0000000..162f70d --- /dev/null +++ b/neuropy/neuropy/templates/registration/activation_email.txt @@ -0,0 +1,7 @@ +Thanks for joining NeuroPy! + +To activate your account, please visit the following page: + +http://{{ site }}{% url 'registration_activate' activation_key %} + +This page will expire in {{ expiration_days }} days. \ No newline at end of file diff --git a/neuropy/neuropy/templates/registration/activation_email_subject.txt b/neuropy/neuropy/templates/registration/activation_email_subject.txt new file mode 100644 index 0000000..633047a --- /dev/null +++ b/neuropy/neuropy/templates/registration/activation_email_subject.txt @@ -0,0 +1 @@ +NeuroPy: Account Activation \ No newline at end of file diff --git a/neuropy/neuropy/templates/registration/login.html b/neuropy/neuropy/templates/registration/login.html new file mode 100644 index 0000000..9eb0a00 --- /dev/null +++ b/neuropy/neuropy/templates/registration/login.html @@ -0,0 +1,14 @@ +{% extends 'neuropy/layout.html' %} +{% block content %} + +{% if form.errors %} +

Your Username and password didn't match. Please try again.

+{% endif %} +
+ {% csrf_token %} + {{ form.as_p }} + +
+{% endblock content %} diff --git a/neuropy/neuropy/templates/registration/registration_complete.html b/neuropy/neuropy/templates/registration/registration_complete.html new file mode 100644 index 0000000..2bd2516 --- /dev/null +++ b/neuropy/neuropy/templates/registration/registration_complete.html @@ -0,0 +1,7 @@ +{% extends 'neuropy/layout.html' %} +{% block content %} +

Almost Done!

+

We've sent you a confirmation email. Please check your email and click to confirm, in order to complete registration.

+

This link will expire in 7 days.

+ Home +{% endblock content %} \ No newline at end of file diff --git a/neuropy/neuropy/templates/registration/registration_form.html b/neuropy/neuropy/templates/registration/registration_form.html new file mode 100644 index 0000000..30550f0 --- /dev/null +++ b/neuropy/neuropy/templates/registration/registration_form.html @@ -0,0 +1,71 @@ +{% extends 'neuropy/layout.html' %} +{% block content %} +
+

Register

+
+ {% csrf_token %} + + {% if form.non_field_errors %} + + {% endif %} + +
+ {{ field.help_text }} +
+ {{ field.label_tag }} {{ field }} +
+
+ {{ field.errors }} +
+
+ +

{{ form.username.label_tag }} + {{ form.username }} {{ form.username.help_text}}

+

{{ form.username.errors }}

+

{{ form.email.label_tag }} + {{ form.email }}

+

{{ form.email.errors }}

+ +

{{ form.password1.label_tag }} + {{ form.password1 }}

+

{{ form.password1.errors }}

+ +

{{ form.password2.label_tag }} + {{ form.password2 }}

+

{{ form.password2.errors }}

+ +

Terms of Service

+
+ 1. Use of service +

The developers authorized to edit NeuroPy (NeuroPy devs) may alter augment or remove Neuropy’s source code and/or functionality at any time and at their sole discretion. This service is intended for personal use under the supervision of a qualified psychological or psychiatric professional. NeuroPy is intended for personal convenience only, and is not a substitute for qualified professional judgment. In cases where the user doubts the usefulness of NeuroPy’s scheduling advice, the user should exercise their own judgment or consult with a qualified professional.

+ +2. Disclaimer of Warranties +

The user acknowledges that NeuroPy is provided on an as-is basis. Except for warranties which may not be disclaimed as a matter of law, the NeuroPy devs make no representation or warranties whatsoever. The user understands and acknowledges that their sole and exclusive remedy for any defect or dissatisfaction with NeuroPy is to cease the use of the application. + The user accepts all responsibility and liability resulting from any change to or redistribution of NeuroPy’s source code.

+ +3. Disclaimer of Content +

NeuroPy incorporates data gathered from medical studies. These studies may be updated or superseded. The user understands and acknowledges that the NeuroPy devs cannot guarantee the accuracy of the data, and therefore cannot guarantee the accuracy of any recommendations based on the data. The user also understands and acknowledges that said medical studies are based on a general population which may not be accurate to any specific user. + NeuroPy integrates with the user’s Google calendar. The user understands and acknowledges that the NeuroPy devs are unable to control the content of any particular user’s calendar. The user further understands that the NeuroPy devs are unable to control or anticipate any changes that Google may make to their service. As a result, the NeuroPy devs disclaim all responsibility for any content derived from the user’s Google account.

+ +4. Limitation of Liability +

+ The user expressly understands and agrees that the NeuroPy devs shall not be liable for any direct, indirect, incidental, special, consequential, exemplary or other damages resulting from the use or misuse of NeuroPy, even if the NeuroPy devs have been advised of the possibility of damages. The user further understands and agrees that the NeuroPy devs shall not be liable for the statements or conduct of any third party provided or received as a result of the use or misues of NeuroPy or link provided in connection with NeuroPy. The user hereby assumes full responsibility for insuring the appropriatenes of using and relying on the scheduling suggestions provided by NeuroPy.

+ + +5. Severability +

In the event that any portion of this agreement is declared unenforceable or invalid, the remainder will continue to be valid and enforceable.

+ +{% if form.tos.errors %} +

{{ form.tos.errors.as_text }}

+{% endif %} +{{ form.tos }} +

+
+ +
+
+{% endblock content %} diff --git a/neuropy/neuropy/urls.py b/neuropy/neuropy/urls.py new file mode 100644 index 0000000..4987ab6 --- /dev/null +++ b/neuropy/neuropy/urls.py @@ -0,0 +1,40 @@ +"""Neuropy URL Configuration. + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/1.10/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. Import the include() function: from django.conf.urls import url, include + 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) +""" +from django.conf.urls import url, include +from django.contrib import admin +from django.contrib.auth import views as auth_views +from django.views.generic import TemplateView +from registration.backends.hmac.views import RegistrationView +from registration.forms import RegistrationFormTermsOfService +from neuropy.oauth import auth_return + + +urlpatterns = [ + url(r'^oauth2callback', auth_return), + url(r'^admin/', admin.site.urls), + url(r'^$', TemplateView.as_view(template_name="neuropy/home.html"), + name='home'), + url(r'^about/', TemplateView.as_view(template_name="neuropy/about.html"), + name='about'), + url(r'^accounts/register/$', + RegistrationView.as_view(form_class=RegistrationFormTermsOfService), + name='registration_register'), + url(r'^accounts/', include('registration.backends.hmac.urls')), + url(r'^login/', auth_views.login, name='login'), + url(r'^logout/', auth_views.logout, {'next_page': '/'}, name='logout'), + url(r'^profile/', include('userprofile.urls')), + url(r'^profile/todo/', include('todo.urls')) +] diff --git a/neuropy/neuropy/wsgi.py b/neuropy/neuropy/wsgi.py new file mode 100644 index 0000000..4d1a4fa --- /dev/null +++ b/neuropy/neuropy/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for neuropy 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.10/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "neuropy.settings") + +application = get_wsgi_application() diff --git a/neuropy/todo/__init__.py b/neuropy/todo/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/neuropy/todo/admin.py b/neuropy/todo/admin.py new file mode 100644 index 0000000..a32e927 --- /dev/null +++ b/neuropy/todo/admin.py @@ -0,0 +1,13 @@ +"""Todo model for admin.""" + +from django.contrib import admin +from todo.models import Todo + + +# Register your models here. +class TodoAdmin(admin.ModelAdmin): + """Display list for admin.""" + + list_display = ("title", "description", "date") + +admin.site.register(Todo, TodoAdmin) diff --git a/neuropy/todo/apps.py b/neuropy/todo/apps.py new file mode 100644 index 0000000..6c85b8d --- /dev/null +++ b/neuropy/todo/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class TodoConfig(AppConfig): + name = 'todo' diff --git a/neuropy/todo/forms.py b/neuropy/todo/forms.py new file mode 100644 index 0000000..8cc3428 --- /dev/null +++ b/neuropy/todo/forms.py @@ -0,0 +1,37 @@ +"""Form module.""" + +from django import forms +from todo.models import Todo + + +class TodoForm(forms.ModelForm): + """Update form for users profile.""" + + def __init__(self, *args, **kwargs): + """Setup the form fields to include User properties.""" + super(TodoForm, self).__init__(*args, **kwargs) + + PRIORITY_CHOICES = ( + (4, 'Now'), + (3, 'Urgent'), + (2, 'Semi Urgent'), + (1, 'Non Urgent'), + ) + + EASE_CHOICES = ( + (3, 'Difficult'), + (2, 'Medium'), + (1, 'Easy'), + ) + self.fields['title'] = forms.CharField(initial=self.instance.title) + self.fields['description'] = forms.CharField(widget=forms.Textarea, initial=self.instance.description) + self.fields['date'] = forms.DateField(widget=forms.SelectDateWidget(), initial=self.instance.date) + self.fields['duration'] = forms.IntegerField(min_value=1, initial=self.instance.duration) + self.fields['ease'] = forms.ChoiceField(choices=EASE_CHOICES) + self.fields['priority'] = forms.ChoiceField(choices=PRIORITY_CHOICES) + + class Meta: + """Model for form and fields to exclude.""" + + model = Todo + exclude = ['owner', 'start_time', 'end_time'] diff --git a/neuropy/todo/migrations/0001_initial.py b/neuropy/todo/migrations/0001_initial.py new file mode 100644 index 0000000..1d8e978 --- /dev/null +++ b/neuropy/todo/migrations/0001_initial.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2017-02-13 21:56 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('userprofile', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='Todo', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(blank=True, max_length=255)), + ('description', models.TextField(blank=True)), + ('date', models.DateTimeField(blank=True, null=True)), + ('duration', models.PositiveIntegerField(default=1)), + ('ease', models.PositiveIntegerField(choices=[(1, 'Easy'), (2, 'Medium'), (3, 'Difficult')], default=1)), + ('priority', models.PositiveIntegerField(choices=[(1, 'Now'), (2, 'Urgent'), (3, 'Semi Urgent'), (4, 'Non Urgent')], default=4)), + ('owner', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='todo', to='userprofile.Profile')), + ], + ), + ] diff --git a/neuropy/todo/migrations/0002_auto_20170215_1745.py b/neuropy/todo/migrations/0002_auto_20170215_1745.py new file mode 100644 index 0000000..f29ca21 --- /dev/null +++ b/neuropy/todo/migrations/0002_auto_20170215_1745.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2017-02-16 01:45 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('todo', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='todo', + name='priority', + field=models.PositiveIntegerField(choices=[(4, 'Non Urgent'), (3, 'Urgent'), (2, 'Semi-Urgent'), (1, 'Now')], default=4), + ), + ] diff --git a/neuropy/todo/migrations/0002_auto_20170215_2124.py b/neuropy/todo/migrations/0002_auto_20170215_2124.py new file mode 100644 index 0000000..b509ffd --- /dev/null +++ b/neuropy/todo/migrations/0002_auto_20170215_2124.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2017-02-16 05:24 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('todo', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='todo', + name='date', + field=models.DateField(blank=True, null=True), + ), + ] diff --git a/neuropy/todo/migrations/0003_merge_20170216_0940.py b/neuropy/todo/migrations/0003_merge_20170216_0940.py new file mode 100644 index 0000000..f313538 --- /dev/null +++ b/neuropy/todo/migrations/0003_merge_20170216_0940.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2017-02-16 17:40 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('todo', '0002_auto_20170215_2124'), + ('todo', '0002_auto_20170215_1745'), + ] + + operations = [ + ] diff --git a/neuropy/todo/migrations/__init__.py b/neuropy/todo/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/neuropy/todo/models.py b/neuropy/todo/models.py new file mode 100644 index 0000000..f3bbbe6 --- /dev/null +++ b/neuropy/todo/models.py @@ -0,0 +1,41 @@ +"""Model for users todos.""" + +from django.db import models +from django.utils.encoding import python_2_unicode_compatible +from userprofile.models import Profile + + +@python_2_unicode_compatible +class Todo(models.Model): + """Model for an individual Todo.""" + + PRIORITY_CHOICES = ( + (4, 'Now'), + (3, 'Urgent'), + (2, 'Semi Urgent'), + (1, 'Non Urgent'), + ) + + EASE_CHOICES = ( + (3, 'Difficult'), + (2, 'Medium'), + (1, 'Easy'), + ) + + title = models.CharField(max_length=255, blank=True) + description = models.TextField(blank=True) + date = models.DateField(blank=True, null=True) + duration = models.PositiveIntegerField(default=1) + start_time = None + end_time = None + ease = models.PositiveIntegerField(choices=EASE_CHOICES, default=1) + priority = models.PositiveIntegerField(choices=PRIORITY_CHOICES, default=4) + owner = models.ForeignKey(Profile, + related_name='todo', + blank=True, + null=True + ) + + def __str__(self): + """String representation of Todo.""" + return self.title diff --git a/neuropy/todo/templates/todo/add_todo.html b/neuropy/todo/templates/todo/add_todo.html new file mode 100644 index 0000000..12e89bd --- /dev/null +++ b/neuropy/todo/templates/todo/add_todo.html @@ -0,0 +1,19 @@ +{% extends 'neuropy/layout.html' %} +{% block content %} +
+ +
+
+
+ {% csrf_token %} + {{ form.as_p }} + +
+
+{% endblock content%} \ No newline at end of file diff --git a/neuropy/todo/templates/todo/create_schedule.html b/neuropy/todo/templates/todo/create_schedule.html new file mode 100644 index 0000000..24ab76c --- /dev/null +++ b/neuropy/todo/templates/todo/create_schedule.html @@ -0,0 +1,25 @@ +{% extends 'neuropy/layout.html' %} +{% block content %} + +
+ + + {% for todo in todos %} +

{{todo.title}}

+

Start: {{ todo.start }}

+

End: {{ todo.end }}

+

Description: {{todo.description}}

+ {% endfor %} +
+ + + +{% endblock %} diff --git a/neuropy/todo/templates/todo/detail_todo.html b/neuropy/todo/templates/todo/detail_todo.html new file mode 100644 index 0000000..2ccd920 --- /dev/null +++ b/neuropy/todo/templates/todo/detail_todo.html @@ -0,0 +1,22 @@ +{% extends 'neuropy/layout.html' %} +{% block content %} + +
+ +
+
+

Date: {{todo.date}}

+

Priority: {{todo.priority}}

+

Ease: {{todo.ease}}

+

Duration: {{todo.duration}}

+

Description: {{todo.description}}

+
+{% endblock %} diff --git a/neuropy/todo/templates/todo/edit_todo.html b/neuropy/todo/templates/todo/edit_todo.html new file mode 100644 index 0000000..b686b5c --- /dev/null +++ b/neuropy/todo/templates/todo/edit_todo.html @@ -0,0 +1,19 @@ +{% extends 'neuropy/layout.html' %} +{% block content %} +
+ +
+
+
+ {% csrf_token %} + {{ form.as_p }} + +
+
+{% endblock content%} diff --git a/neuropy/todo/templates/todo/list_todo.html b/neuropy/todo/templates/todo/list_todo.html new file mode 100644 index 0000000..5225b93 --- /dev/null +++ b/neuropy/todo/templates/todo/list_todo.html @@ -0,0 +1,23 @@ +{% extends 'neuropy/layout.html' %} +{% block content %} +
+ +
+
+ {% for todo in todos %} +
+

{{todo.title}}

+

Date: {{todo.date}}

+

Priority: {{todo.priority}}

+ Edit +
+ {% endfor %} +
+{% endblock %} diff --git a/neuropy/todo/templates/todo/schedule_view.html b/neuropy/todo/templates/todo/schedule_view.html new file mode 100644 index 0000000..cbd0144 --- /dev/null +++ b/neuropy/todo/templates/todo/schedule_view.html @@ -0,0 +1,24 @@ +{% extends 'neuropy/layout.html' %} +{% block content %} + +
+

Schedule {% now "jS F Y" %}

+
+ {% for event in events %} + + {% if event.colorId %} + + {% else %} + + {% endif %} +

{{ event.summary }}

+
Start: {{ event.start.dateTime }}
+
End: {{ event.end.dateTime }}
+ Description: +

{{ event.description }}

+
+ + {% endfor %} +
+
+{% endblock %} diff --git a/neuropy/todo/tests.py b/neuropy/todo/tests.py new file mode 100644 index 0000000..83ffb7c --- /dev/null +++ b/neuropy/todo/tests.py @@ -0,0 +1,614 @@ +"""Test for todo app.""" + +from django.contrib.auth.models import User, Group +from django.test import TestCase, Client, RequestFactory +from django.urls import reverse_lazy + +from todo.views import create_event_list +from todo.models import Todo +from userprofile.models import Profile +import factory +from bs4 import BeautifulSoup +import datetime + + +class TodoFactory(factory.django.DjangoModelFactory): + """Create test instance of todos.""" + + class Meta: + """Invoke Todo instance using Todo model class.""" + + model = Todo + + description = factory.Sequence(lambda n: "Todo {}".format(n)) + title = factory.Sequence(lambda n: "Some Todo {}".format(n)) + + +class UserFactory(factory.django.DjangoModelFactory): + """Create test instance of User Class.""" + + class Meta: + """Invoke user instance using User model class.""" + + model = User + + username = factory.Sequence(lambda n: "User {}".format(n)) + email = factory.LazyAttribute( + lambda x: "{}@gmail.com".format(x.username.replace(" ", "")) + ) + + +def add_user_group(): + """ + Add a simulated default user group. + + This will require edits as we add features and permissions. + Commented lines are example lines for adding permissions to the group. + """ + new_group, created = Group.objects.get_or_create(name='user') + # permission = Permission.objects.get(name='permission_name') + # new_group.permissions.add(permission) + new_group.save() + + +class TodoTestCase(TestCase): + """The Todo Model test class.""" + + def setUp(self): + """The setup and buildout for users, todos.""" + add_user_group() + self.users = [UserFactory.create() for i in range(20)] + self.todos = [TodoFactory.create() for i in range(20)] + + def test_todo_exists(self): + """Test existance of a todo.""" + this_todo = self.todos[0] + this_todo.save() + self.assertTrue(self.todos[0]) + + def test_unique_todo_created(self): + """Test that the number of todos in the database equla the nuber of todos created.""" + self.assertTrue(Todo.objects.count() == 20) + + def test_todo_has_attributes(self): + """Test todo is created with attributes.""" + todo1 = self.todos[0] + attributes = ['title', + 'description', + 'date', + 'ease', + 'duration', + 'priority', + 'owner'] + for attribute in attributes: + self.assertTrue(hasattr(todo1, attribute)) + + def test_owner_is_a_profile(self): + """Test owner is an instance of a Profile.""" + todo1 = self.todos[0] + todo1.owner = self.users[0].profile + self.assertIsInstance(todo1.owner, Profile) + + def test_profile_has_a_todo(self): + """Test profile has associated attribute todo.""" + todo1 = self.todos[0] + todo1.owner = self.users[0].profile + self.assertTrue(hasattr(self.users[0].profile, 'todo')) + + def test_todo_str_method_returns_username(self): + """Test str method on todo returns the title.""" + todo = Todo.objects.first() + self.assertTrue(str(todo) == todo.title) + + def test_assign_multiple_todos_to_single_user(self): + """Test the many-to-one relationship between todo and user works.""" + todo1 = self.todos[4] + todo2 = self.todos[5] + owner = self.users[6].profile + todo1.owner, todo2.owner = owner, owner + todo1.save() + todo2.save() + self.assertTrue(owner.todo.count() == 2) + + +class TodoFrontEndTestCase(TestCase): + """The Todo route and view test class.""" + + def setUp(self): + """The setup and buildout for users, todos.""" + self.client = Client() + self.request = RequestFactory() + add_user_group() + self.users = [UserFactory.create() for i in range(20)] + self.todos = [TodoFactory.create() for i in range(20)] + + def generate_todos(self): + """Generate todos.""" + user = self.users[5] + todo1 = self.todos[0] + todo2 = self.todos[2] + todo3 = self.todos[1] + todo1.owner, todo2.owner, todo3.owner = user.profile, user.profile, user.profile + + todo1.date = datetime.date.today() + todo2.date = datetime.date.today() + todo3.date = datetime.date.today() + + todo1.ease = 1 + todo2.ease = 2 + todo3.ease = 3 + + todo1.priority = 2 + todo2.priority = 3 + todo3.priority = 4 + + todo1.duration = 1 + todo2.duration = 2 + todo3.duration = 3 + + user.save() + todo1.save() + todo2.save() + todo3.save() + return user.profile, [todo1, todo2, todo3] + + def make_user_and_login(self): + """Make user and login.""" + add_user_group() + user_register = UserFactory.create() + user_register.is_active = True + user_register.username = "bobdole" + user_register.first_name = 'Bob' + user_register.last_name = 'Dole' + user_register.email = 'awesome@gmail.com' + user_register.set_password("rutabega") + user_register.save() + self.client.post("/login/", { + "username": user_register.username, + "password": "rutabega" + + }) + return self.client, user_register + + def test_todo_list_route_is_status_ok(self): + """Funcional test for todo list.""" + self.client.force_login(self.users[0]) + response = self.client.get(reverse_lazy("list_todo")) + self.assertTrue(response.status_code == 200) + + """Test todo list returns the right templates.""" + self.client.force_login(self.users[0]) + response = self.client.get(reverse_lazy("list_todo")) + self.assertTemplateUsed(response, "neuropy/layout.html") + self.assertTemplateUsed(response, "todo/list_todo.html") + + def test_todo_detail_route_is_status_ok(self): + """Funcional test for todo list.""" + self.client.force_login(self.users[0]) + todo = self.todos[0] + todo.owner = self.users[0].profile + todo.save() + response = self.client.get('/profile/todo/' + str(todo.pk)) + self.assertTrue(response.status_code == 200) + + def test_todo_detail_route_uses_right_templates(self): + """Test todo list returns the right templates.""" + self.client.force_login(self.users[0]) + todo = self.todos[0] + todo.owner = self.users[0].profile + todo.save() + response = self.client.get('/profile/todo/' + str(todo.pk)) + self.assertTemplateUsed(response, "neuropy/layout.html") + self.assertTemplateUsed(response, "todo/detail_todo.html") + + def test_edit_todo_default_values(self): + """Test that the response when calling the edit todo views includes default values.""" + todo = self.todos[0] + self.client.force_login(self.users[0]) + response = self.client.get(reverse_lazy( + 'edit_todo', kwargs={'pk': todo.id}) + ) + self.assertTrue('Edit a To-Do item' in response.content.decode()) + + def test_todo_list_route_displays_correctly(self): + """Test todo list route displays correctly.""" + self.client.force_login(self.users[0]) + user = self.users[0] + todo = self.todos[3] + todo.owner = user.profile + todo.save() + response = self.client.get(reverse_lazy("list_todo")) + parsed_html = BeautifulSoup(response.content, 'html5lib') + self.assertTrue(len(parsed_html.find_all('article')) == 1) + + def test_edit_todo_will_change_template(self): + """Test edit todo will redirect to profile.""" + client, user = self.make_user_and_login() + todo = self.todos[9] + todo.owner = user.profile + todo.save() + + response = client.post('/profile/todo/' + str(todo.id) + '/edit/', { + "title": "Todo 1", + "description": "Some Text", + "duration": "1", + "priority": "1", + "ease": "1", + "date_month": "1", + "date_year": "2017", + "date_day": "2", + }, follow=True) + self.assertTrue(b'Todo 1' in response.content) + + def test_edit_todo_will_redirect_to_todo_list(self): + """Test edit todo will redirect to todo list.""" + client, user = self.make_user_and_login() + todo = self.todos[9] + todo.owner = user.profile + todo.save() + + response = client.post('/profile/todo/' + str(todo.id) + '/edit/', { + "title": "Todo 1", + "description": "Some Text", + "duration": "1", + "priority": "1", + "ease": "1", + "date_month": "1", + "date_year": "2017", + "date_day": "2", + }) + self.assertRedirects(response, '/profile/todo/') + + def test_add_todo_will_redirect_to_todo_list(self): + """Test add todo will redirect to todo list.""" + client, user = self.make_user_and_login() + + response = client.post('/profile/todo/add/', { + "title": "Todo 1", + "description": "Some Text", + "duration": "1", + "priority": "1", + "ease": "1", + "date_month": "1", + "date_year": "2017", + "date_day": "2", + }) + self.assertRedirects(response, '/profile/todo/') + + def test_add_todo_will_redirect_with_new_content(self): + """Test add todo will redirect to todo list with new content.""" + client, user = self.make_user_and_login() + + response = client.post('/profile/todo/add/', { + "title": "Todo 1", + "description": "Some Text", + "duration": "1", + "priority": "1", + "ease": "1", + "date_month": "1", + "date_year": "2017", + "date_day": "2", + }, follow=True) + self.assertTrue(b'Todo 1' in response.content) + + def test_add_with_missing_fields_will_not_redirect(self): + """Test add todo will not redirect if fields missing.""" + client, user = self.make_user_and_login() + + response = client.post('/profile/todo/add/', { + "title": "Todo 1", + "description": "Some Text", + "duration": "1", + "priority": "1", + "ease": "1", + }) + self.assertFalse(response.status_code == 302) + + def test_edit_with_missing_fields_will_not_redirect(self): + """Test add todo will not redirect if fields missing.""" + client, user = self.make_user_and_login() + todo = self.todos[9] + todo.owner = user.profile + todo.save() + + response = client.post('/profile/todo/' + str(todo.id) + '/edit/', { + "title": "Todo 1", + "description": "Some Text", + "duration": "1", + "priority": "1", + "ease": "1", + }) + self.assertFalse(response.status_code == 302) + + def test_add_todo_status_code_302(self): + """Test add todo status code 302.""" + user = self.users[4] + self.client.force_login(user) + html = self.client.get('/profile/todo/add/').content + html = BeautifulSoup(html, "html5lib") + csrf = html.find("input", {"name": 'csrfmiddlewaretoken'})['value'] + response = self.client.post('/profile/todo/add/', { + "csrfmiddlewaretoken": csrf, + "title": "Buy Google", + "description": "Then Buy Amazon", + "date_month": "1", + "date_day": "6", + "date_year": "2017", + "duration": "4", + "ease": "2", + "priority": "1", + }) + self.assertTrue(response.status_code == 302) + + def test_add_todo_saves_db_and_shows_to_list(self): + """Test add todo saves db and shows to list.""" + user = self.users[4] + self.client.force_login(user) + html = self.client.get('/profile/todo/add/').content + html = BeautifulSoup(html, "html5lib") + csrf = html.find("input", {"name": 'csrfmiddlewaretoken'})['value'] + self.client.post('/profile/todo/add/', { + "csrfmiddlewaretoken": csrf, + "title": "Buy Google", + "description": "Then Buy Amazon", + "date_month": "1", + "date_day": "6", + "date_year": "2017", + "duration": "4", + "ease": "2", + "priority": "1", + }) + html = self.client.get('/profile/todo/').content + html = str(html) + self.assertTrue('Buy Google' in html) + self.assertTrue('Priority: 1' in html) + + def test_add_todo_saves_db_and_shows_to_detail_todo_view(self): + """Test add todo saves db and shows to detail todo view.""" + user = self.users[4] + self.client.force_login(user) + html = self.client.get('/profile/todo/add/').content + html = BeautifulSoup(html, "html5lib") + csrf = html.find("input", {"name": 'csrfmiddlewaretoken'})['value'] + self.client.post('/profile/todo/add/', { + "csrfmiddlewaretoken": csrf, + "title": "Buy Google", + "description": "Then Buy Amazon", + "date_month": "1", + "date_day": "6", + "date_year": "2017", + "duration": "4", + "ease": "2", + "priority": "1", + }) + pk = Todo.objects.get(title='Buy Google').id + html = self.client.get('/profile/todo/' + str(pk)).content + html = str(html) + self.assertTrue('Buy Google' in html) + self.assertTrue('

Priority: 1

' in html) + self.assertTrue('

Ease: 2

' in html) + self.assertTrue('

Duration: 4

' in html) + self.assertTrue('

Description: Then Buy Amazon

' in html) + + def test_edit_todo_status_code_302(self): + """Test edit todo status code 302.""" + user = self.users[4] + self.client.force_login(user) + html = self.client.get('/profile/todo/add/').content + html = BeautifulSoup(html, "html5lib") + csrf = html.find("input", {"name": 'csrfmiddlewaretoken'})['value'] + self.client.post('/profile/todo/add/', { + "csrfmiddlewaretoken": csrf, + "title": "Buy Google", + "description": "Then Buy Amazon", + "date_month": "1", + "date_day": "6", + "date_year": "2017", + "duration": "4", + "ease": "2", + "priority": "1", + }) + pk = Todo.objects.get(title='Buy Google').id + html = self.client.get('/profile/todo/' + str(pk) + '/edit/').content + html = BeautifulSoup(html, "html5lib") + csrf = html.find("input", {"name": 'csrfmiddlewaretoken'})['value'] + response = self.client.post('/profile/todo/' + str(pk) + '/edit/', { + "csrfmiddlewaretoken": csrf, + "title": "Buy Earth", + "description": "Then Buy 7/11", + "date_month": "2", + "date_day": "7", + "date_year": "2018", + "duration": "3", + "ease": "1", + "priority": "1", + }) + self.assertTrue(response.status_code == 302) + + def test_edit_todo_saves_db_and_shows_to_detail_todo_view(self): + """Test edit todo saves db and shows to detail todo view.""" + user = self.users[4] + self.client.force_login(user) + html = self.client.get('/profile/todo/add/').content + html = BeautifulSoup(html, "html5lib") + csrf = html.find("input", {"name": 'csrfmiddlewaretoken'})['value'] + self.client.post('/profile/todo/add/', { + "csrfmiddlewaretoken": csrf, + "title": "Buy Google", + "description": "Then Buy Amazon", + "date_month": "1", + "date_day": "6", + "date_year": "2017", + "duration": "4", + "ease": "2", + "priority": "1", + }) + pk = Todo.objects.get(title='Buy Google').id + html = self.client.get('/profile/todo/' + str(pk) + '/edit/').content + html = BeautifulSoup(html, "html5lib") + csrf = html.find("input", {"name": 'csrfmiddlewaretoken'})['value'] + self.client.post('/profile/todo/' + str(pk) + '/edit/', { + "csrfmiddlewaretoken": csrf, + "title": "Buy Earth", + "description": "Then Buy 7/11", + "date_month": "2", + "date_day": "7", + "date_year": "2018", + "duration": "3", + "ease": "1", + "priority": "1", + }) + html = self.client.get('/profile/todo/' + str(pk)).content + html = str(html) + self.assertTrue('Buy Earth' in html) + self.assertTrue('

Priority: 1

' in html) + self.assertTrue('

Ease: 1

' in html) + self.assertTrue('

Duration: 3

' in html) + self.assertTrue('

Description: Then Buy 7/11

' in html) + +# --------------- Algorithm Unittests ------------------ + + def test_todo_ease_level_is_correct(self): + """Test todo ease level is correct.""" + profile, todos = self.generate_todos() + todos = create_event_list("CONCERTA", profile) + self.assertTrue(len(todos) == 4) + + def test_todo_is_in_order(self): + """Test todo is arranged in right order.""" + profile, todo_lst = self.generate_todos() + todos = create_event_list("CONCERTA", profile) + self.assertTrue(todos[2]['title'] == todo_lst[1].title) + self.assertTrue(todos[0]['title'] == todo_lst[2].title) + + def test_todos_are_correct_ease_level(self): + """Test todo is assigned to correct ease levels.""" + profile, todo_lst = self.generate_todos() + todos = create_event_list("CONCERTA", profile) + self.assertTrue(todos[0]['ease'] == 'hard') + self.assertTrue(todos[1]['ease'] == 'hard') + + def test_todo_created_on_profile_on_wrong_date(self): + """Test todo is created for current date.""" + user = self.users[5] + todo1 = self.todos[0] + todo2 = self.todos[2] + todo1.owner, todo2.owner = user.profile, user.profile + + todo1.date = datetime.date(1, 2, 3) + todo2.date = datetime.date(1, 2, 3) + + todo1.ease = 1 + todo2.ease = 2 + + todo1.priority = 2 + todo2.priority = 3 + + todo1.duration = 1 + todo2.duration = 2 + + user.save() + todo1.save() + todo2.save() + + events = create_event_list('CONCERTA', user.profile) + self.assertFalse(events) + + def test_todo_duration(self): + """Test todo duration is equal to diff between end and start time of todo.""" + profile, todo_lst = self.generate_todos() + todos = create_event_list("CONCERTA", profile) + diff_todos_1 = todos[1]['end'] - todos[1]['start'] + self.assertTrue(datetime.timedelta(hours=todo_lst[2].duration) == diff_todos_1) + + def test_priority_now_does_not_duplicate(self): + """Test that a priority to-do does not show as duplicate events.""" + from todo.views import create_event_list + profile, todo_lst = self.generate_todos() + todos = create_event_list("CONCERTA", profile) + seen_tasks = [] + for todo in todos: + if todo in seen_tasks: + raise ValueError("Todo already exists") + else: + seen_tasks.append(todo) + self.assertTrue(todos[0]['description'] == 'Todo 381' and todos[1]['description'] == 'Todo 381') + return seen_tasks + +# --------------------- End Algorithm Unittests --------------------- + + def test_logged_out_todo_fails(self): + """Test that a logged out user cannot create a todo.""" + response = self.client.get(reverse_lazy('add_todo')) + parsed_html = BeautifulSoup(response.content, "html5lib") + self.assertFalse(parsed_html.find_all('div')) + + def test_logged_out_schedule_fails(self): + """Test that a logged out user cannot see a schedule.""" + response = self.client.get(reverse_lazy('schedule')) + parsed_html = BeautifulSoup(response.content, "html5lib") + self.assertFalse(parsed_html.find_all('div')) + + def test_logged_out_edit_todo_fails(self): + """Test that a logged out user cannot edit a todo.""" + response = self.client.get(reverse_lazy('edit_todo', kwargs={'pk': self.todos[0].id})) + parsed_html = BeautifulSoup(response.content, "html5lib") + self.assertFalse(parsed_html.find_all('div')) + + def test_logged_out_detail_todo_fails(self): + """Test that a logged out user cannot see todo details.""" + with self.assertRaises(AttributeError,): + self.client.get(reverse_lazy('show_todo', kwargs={'pk': self.todos[0].id})) + + def test_logged_out_build_schedule_fails(self): + """Test that a logged out user cannot build a schedule.""" + response = self.client.get(reverse_lazy('create_sched')) + parsed_html = BeautifulSoup(response.content, "html5lib") + self.assertFalse(parsed_html.find_all('div')) + + def test_edit_todo_without_csrf_fails(self): + """Test edit todo will fail without a csrf token.""" + self.client.force_login(self.users[0]) + html = self.client.get(reverse_lazy('edit_todo', kwargs={'pk': self.todos[0].id})).content + html = BeautifulSoup(html, "html5lib") + self.client.post(reverse_lazy('edit_todo', kwargs={'pk': self.todos[0].id}), { + "csrfmiddlewaretoken": "", + "title": "sam spade", + "description": "Some Text", + "duration": "1", + "priority": "1", + "ease": "1", + "date_month": "1", + "date_year": "2017", + "date_day": "2", + }) + self.assertFalse(self.todos[0].title == 'sam spade') + + def test_add_todo_without_csrf_fails(self): + """Test add todo will fail without a csrf token.""" + self.client.force_login(self.users[0]) + html = self.client.get(reverse_lazy('add_todo')).content + html = BeautifulSoup(html, "html5lib") + self.client.post(reverse_lazy('add_todo'), { + "csrfmiddlewaretoken": "", + "title": "sam spade", + "description": "Some Text", + "duration": "1", + "priority": "1", + "ease": "1", + "date_month": "1", + "date_year": "2017", + "date_day": "2", + }) + with self.assertRaises(AttributeError): + self.todos[0].todo + + def test_schedule_view_returns(self): + """Test that schedule view returns the right page.""" + self.client.force_login(self.users[0]) + session = self.client.session + session['some_list'] = [{}, {}, {}] + session.save() + html = self.client.get(reverse_lazy('create_sched')).content + parsed_html = BeautifulSoup(html, "html5lib") + self.assertTrue(len(parsed_html.find_all('div')) == 10) diff --git a/neuropy/todo/urls.py b/neuropy/todo/urls.py new file mode 100644 index 0000000..63e50c8 --- /dev/null +++ b/neuropy/todo/urls.py @@ -0,0 +1,16 @@ +"""Todo url paths.""" + +from django.conf.urls import url +from todo.views import( + AddTodo, EditTodo, ListTodo, DetailTodo, ScheduleView, CreateScheduleView +) +from oauth2client.contrib.django_util import decorators + +urlpatterns = [ + url(r'calendar/$', decorators.oauth_enabled(ScheduleView.as_view()), name='schedule'), + url(r'^(?P\d+)/edit/$', EditTodo.as_view(), name='edit_todo'), + url(r'^(?P\d+)', DetailTodo.as_view(), name='show_todo'), + url(r'^add/$', AddTodo.as_view(), name='add_todo'), + url(r'^$', ListTodo.as_view(), name='list_todo'), + url(r'^schedule/$', CreateScheduleView.as_view(), name="create_sched"), +] diff --git a/neuropy/todo/views.py b/neuropy/todo/views.py new file mode 100644 index 0000000..1462ca6 --- /dev/null +++ b/neuropy/todo/views.py @@ -0,0 +1,226 @@ +"""Views allowing the user to interact with their own todo items.""" + +from django.http import HttpResponseRedirect +from django.urls import reverse_lazy +from django.views.generic import CreateView, UpdateView, ListView, DetailView, TemplateView +from django.contrib.auth.mixins import (LoginRequiredMixin, + UserPassesTestMixin, + ) +from todo.models import Todo +from medication.models import Medication +from userprofile.models import Profile +from todo.forms import TodoForm +from django.shortcuts import get_object_or_404 +from apiclient import discovery +import datetime +import dateutil.parser +from userprofile.models import CredentialsModel +from oauth2client.contrib.django_util.storage import DjangoORMStorage +from oauth2client.contrib import xsrfutil +import httplib2 +from neuropy import settings +from oauth2client.client import OAuth2WebServerFlow + + +class AddTodo(LoginRequiredMixin, CreateView): + """Add todo.""" + + login_url = reverse_lazy('login') + login_required = True + + model = Todo + template_name = "todo/add_todo.html" + + form_class = TodoForm + + success_url = reverse_lazy('list_todo') + + def form_valid(self, form): + """Form should update the photographer to the user.""" + self.object = form.save(commit=False) + self.object.owner = Profile.objects.get(user=self.request.user) + self.object.save() + return HttpResponseRedirect(self.get_success_url()) + + +class EditTodo(LoginRequiredMixin, UpdateView): + """Edit todo.""" + + login_required = True + + model = Todo + template_name = "todo/edit_todo.html" + + form_class = TodoForm + + success_url = reverse_lazy('list_todo') + + +class ListTodo(LoginRequiredMixin, ListView): + """Lists the todo items attached to a specific user.""" + + login_url = reverse_lazy('login') + login_required = True + template_name = "todo/list_todo.html" + model = Todo + + def get_context_data(self): + """Return a dict of all todos, filtering out todos.""" + the_user = self.request.user + owned_todos = Todo.objects.filter(owner=the_user.profile) + return {'todos': owned_todos} + + +class DetailTodo(UserPassesTestMixin, DetailView): + """Allow user to view details on a specific todo list item.""" + + login_url = reverse_lazy('login') + login_required = True + model = Todo + template_name = "todo/detail_todo.html" + + def test_func(self): + """Override the userpassestest test_func.""" + todo = get_object_or_404(Todo, id=self.kwargs['pk']) + return todo.owner.user == self.request.user + + +def calendar_get(http, date): + """Get and return users calendar.""" + year, month, day, = date.split('-') + start = datetime.date( + year=int(year), month=int(month), day=int(day) + ).isoformat() + 'T00:00:01-08:00' + end = datetime.date( + year=int(year), month=int(month), day=int(day) + ).isoformat() + 'T23:59:59-08:00' + service = discovery.build('calendar', 'v3', http=http) + events_result = service.events().list( + calendarId='primary', + timeMin=start, + timeMax=end, + singleEvents=True, + orderBy='startTime', + timeZone='PST' + ).execute() + return events_result.get('items', []) + + +def calender_insert(http, event, email): + """Insert entries and calender.""" + service = discovery.build('calendar', 'v3', http=http) + event = service.events().insert(calendarId=email, body=event).execute() + return event + + +def calender_update(http, event): + """Insert entries and calender.""" + service = discovery.build('calendar', 'v3', http=http) + event = service.events().update(calendarId='prmary', body=event).execute() + return event + +FLOW = OAuth2WebServerFlow( + client_id=settings.GOOGLE_OAUTH2_CLIENT_ID, + client_secret=settings.GOOGLE_OAUTH2_CLIENT_SECRET, + scope='https://www.googleapis.com/auth/calendar', + redirect_uri='http://localhost:8000/oauth2callback', + prompt='consent' +) + + +def create_event_list(drug_name, profile): + """Create dictionary objects to be inserted into google cal.""" + today = datetime.date.today() + + easy = Todo.objects.filter(owner=profile, date=today, ease=1) + medium = Todo.objects.filter(owner=profile, date=today, ease=2) + hard = Todo.objects.filter(owner=profile, date=today, ease=3) + priority_now = Todo.objects.filter(owner=profile, date=today, priority=4).order_by('ease') + bucket_list = [priority_now, hard, medium, easy] + + # today = datetime.date.today() + start_time = datetime.datetime(today.year, today.month, today.day, 9) + + def td(time): + pt = datetime.datetime.strptime(time, '%H:%M:%S') + return pt.second + pt.minute + pt.hour * 3600 + + drug = Medication.objects.get(name=drug_name) + peak_end = start_time + datetime.timedelta(hours=td(drug.peak_end)) + easy_start = start_time + datetime.timedelta(hours=td(drug.post_peak_easy_start)) + + events_list = [] + for idx, bucket in enumerate(bucket_list): + priority_dict = {} + + for event in bucket: + priority_dict['description'] = event.description + priority_dict['title'] = event.title + priority_dict['start'] = start_time + end_time = start_time + datetime.timedelta(hours=event.duration) + priority_dict['end'] = end_time + + if idx == 0 or idx == 1: + priority_dict['ease'] = 'hard' + + if idx == 2 and priority_dict['start'] < peak_end: + priority_dict['ease'] = 'hard' + + elif idx == 3 and priority_dict['start'] < easy_start: + priority_dict['ease'] = 'medium' + elif idx == 3: + priority_dict['ease'] = 'easy' + + priority_dict['start'] = start_time + priority_dict['end'] = end_time + events_list.append(dict(priority_dict)) + + start_time = start_time + datetime.timedelta(hours=event.duration) + return events_list + + +class ScheduleView(LoginRequiredMixin, TemplateView): + """Schedule View.""" + + template_name = "todo/schedule_view.html" + + def get(self, request, *args, **kwargs): + """Get the data and render.""" + context = self.get_context_data(**kwargs) + storage = DjangoORMStorage(CredentialsModel, 'user_id', request.user, 'credential') + credential = storage.get() + if credential is None or credential.invalid: + FLOW.params['state'] = xsrfutil.generate_token(settings.SECRET_KEY, + request.user) + authorize_url = FLOW.step1_get_authorize_url() + return HttpResponseRedirect(authorize_url) + else: + http = httplib2.Http() + http = credential.authorize(http) + now = str(datetime.datetime.now()).split()[0] + events = calendar_get(request.oauth.http, now) + context["events"] = events + for event in events: + try: + event["start"]["dateTime"] = dateutil.parser.parse(event["start"]["dateTime"]) + except KeyError: + event["start"]["dateTime"] = 'No Time Specified' + try: + event["end"]["dateTime"] = dateutil.parser.parse(event["end"]["dateTime"]) + except KeyError: + event["end"]["dateTime"] = 'No Time Specified' + return self.render_to_response(context) + + +class CreateScheduleView(LoginRequiredMixin, TemplateView): + """Prioritized Schedule View.""" + + template_name = "todo/create_schedule.html" + + def get(self, request, *args, **kwargs): + """.""" + priority_list = self.request.session['some_list'] + context = self.get_context_data(**kwargs) + context['todos'] = priority_list + + return self.render_to_response(context) diff --git a/neuropy/userprofile/__init__.py b/neuropy/userprofile/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/neuropy/userprofile/admin.py b/neuropy/userprofile/admin.py new file mode 100644 index 0000000..75b0254 --- /dev/null +++ b/neuropy/userprofile/admin.py @@ -0,0 +1,6 @@ +from django.contrib import admin +from userprofile.models import CredentialsModel +from userprofile.models import Profile + +admin.site.register(CredentialsModel) +admin.site.register(Profile) diff --git a/neuropy/userprofile/apps.py b/neuropy/userprofile/apps.py new file mode 100644 index 0000000..0252f18 --- /dev/null +++ b/neuropy/userprofile/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class UserprofileConfig(AppConfig): + name = 'userprofile' diff --git a/neuropy/userprofile/forms.py b/neuropy/userprofile/forms.py new file mode 100644 index 0000000..29d9b54 --- /dev/null +++ b/neuropy/userprofile/forms.py @@ -0,0 +1,36 @@ +"""Form module.""" + +from django import forms +from userprofile.models import Profile +from medication.models import Medication + + +class ProfileForm(forms.ModelForm): + """Update form for users profile.""" + + def __init__(self, *args, **kwargs): + """Setup the form fields to include User properties.""" + super(ProfileForm, self).__init__(*args, **kwargs) + self.fields["First Name"] = forms.CharField( + initial=self.instance.user.first_name) + self.fields["Last Name"] = forms.CharField( + initial=self.instance.user.last_name) + self.fields["Email"] = forms.EmailField( + initial=self.instance.user.email) + del self.fields["user"] + + class Meta: + """Model for form and fields to exclude.""" + + model = Profile + exclude = [] + + +class MedicationForm(forms.Form): + """Create a form allowing the user to base the schedule on a medication.""" + + medication = forms.ModelChoiceField( + widget=forms.RadioSelect, + queryset=Medication.objects.all(), + empty_label=None + ) diff --git a/neuropy/userprofile/migrations/0001_initial.py b/neuropy/userprofile/migrations/0001_initial.py new file mode 100644 index 0000000..24c91eb --- /dev/null +++ b/neuropy/userprofile/migrations/0001_initial.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2017-02-13 21:56 +from __future__ import unicode_literals + +import datetime +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Profile', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('active_period_start', models.TimeField(default=datetime.time(8, 0))), + ('active_period_end', models.TimeField(default=datetime.time(22, 0))), + ('peak_period', models.CharField(choices=[('early_bird', 'Early Bird'), ('morning', 'Morning'), ('midday', 'Midday'), ('afternoon', 'Afternoon'), ('evening', 'Evening'), ('night_owl', 'Night Owl')], default='Morning', max_length=15)), + ('dose_time', models.TimeField(default=datetime.time(8, 0))), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='profile', to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/neuropy/userprofile/migrations/0002_credentialsmodel.py b/neuropy/userprofile/migrations/0002_credentialsmodel.py new file mode 100644 index 0000000..76a6c6f --- /dev/null +++ b/neuropy/userprofile/migrations/0002_credentialsmodel.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2017-02-14 19:14 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import oauth2client.contrib.django_util.models + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('userprofile', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='CredentialsModel', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('credential', oauth2client.contrib.django_util.models.CredentialsField(null=True)), + ('user_id', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/neuropy/userprofile/migrations/0003_auto_20170215_1912.py b/neuropy/userprofile/migrations/0003_auto_20170215_1912.py new file mode 100644 index 0000000..44b1b5b --- /dev/null +++ b/neuropy/userprofile/migrations/0003_auto_20170215_1912.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2017-02-16 03:12 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('userprofile', '0002_credentialsmodel'), + ] + + operations = [ + migrations.AlterField( + model_name='profile', + name='peak_period', + field=models.CharField(choices=[('Early Bird', 'Early Bird'), ('morning', 'Morning'), ('midday', 'Midday'), ('afternoon', 'Afternoon'), ('evening', 'Evening'), ('Night Owl', 'Night Owl')], default='Morning', max_length=15), + ), + ] diff --git a/neuropy/userprofile/migrations/__init__.py b/neuropy/userprofile/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/neuropy/userprofile/models.py b/neuropy/userprofile/models.py new file mode 100644 index 0000000..6f74240 --- /dev/null +++ b/neuropy/userprofile/models.py @@ -0,0 +1,55 @@ +from django.db import models +from django.contrib.auth.models import User, Group +from django.utils.encoding import python_2_unicode_compatible +from django.db.models.signals import post_save +from django.dispatch import receiver +from datetime import time +from oauth2client.contrib.django_util.models import CredentialsField + + +class CredentialsModel(models.Model): + """Google Credential Model.""" + + user_id = models.OneToOneField(User) + credential = CredentialsField() + + +@python_2_unicode_compatible +class Profile(models.Model): + """The user's profile in the database.""" + + user = models.OneToOneField( + User, + related_name="profile", + on_delete=models.CASCADE + ) + PEAK_PERIOD_CHOICES = [ + ('Early Bird', 'Early Bird'), + ('morning', 'Morning'), + ('midday', 'Midday'), + ('afternoon', 'Afternoon'), + ('evening', 'Evening'), + ('Night Owl', 'Night Owl'), + ] + active_period_start = models.TimeField(default=time(hour=8)) + active_period_end = models.TimeField(default=time(hour=22)) + peak_period = models.CharField( + max_length=15, + choices=PEAK_PERIOD_CHOICES, + default='Morning' + ) + dose_time = models.TimeField(default=time(hour=8)) + + def __str__(self): + """String representation of Todo.""" + return self.user.username + + +@receiver(post_save, sender=User) +def build_profile(sender, instance, **kwargs): + """Attaches a profile to a user whenever a user is made.""" + if kwargs["created"]: + group = Group.objects.get(name='user') + instance.groups.add(group) + new_profile = Profile(user=instance) + new_profile.save() diff --git a/neuropy/userprofile/templates/userprofile/edit_profile.html b/neuropy/userprofile/templates/userprofile/edit_profile.html new file mode 100644 index 0000000..8bbcc5e --- /dev/null +++ b/neuropy/userprofile/templates/userprofile/edit_profile.html @@ -0,0 +1,21 @@ +{% extends 'neuropy/layout.html' %} +{% block content %} + +
+ +
+
+
+ {% csrf_token %} + {{ form.as_p }} + +
+
+{% endblock %} diff --git a/neuropy/userprofile/templates/userprofile/profile.html b/neuropy/userprofile/templates/userprofile/profile.html new file mode 100644 index 0000000..dce1e77 --- /dev/null +++ b/neuropy/userprofile/templates/userprofile/profile.html @@ -0,0 +1,34 @@ +{% extends 'neuropy/layout.html' %} +{% block content %} + +
+ +
+
+

Name: {{user.first_name|title}} {{user.last_name|title}}

+

Email: {{user.email}}

+

Active Period Start: {{ user.profile.active_period_start }}

+

Active Period End: {{ user.profile.active_period_end }}

+

Peak Period: {{ user.profile.peak_period }}

+

Dose Time: {{ user.profile.dose_time }}

+

Todos Created: {{ user.profile.todo.count }}

+
+{% if user.profile.todo %} +
+
+

After creating Todo items, select your medication and generate your schedule

+ {% csrf_token %} + {{ form.as_p }} + +
+{% endif %} +{% endblock %} \ No newline at end of file diff --git a/neuropy/userprofile/tests.py b/neuropy/userprofile/tests.py new file mode 100644 index 0000000..92208e7 --- /dev/null +++ b/neuropy/userprofile/tests.py @@ -0,0 +1,310 @@ +"""Tests for the userprofile app.""" + +from django.test import TestCase, Client, RequestFactory +from django.contrib.auth.models import User, Group +from userprofile.models import Profile, CredentialsModel +import factory +from django.core.urlresolvers import reverse_lazy + +from bs4 import BeautifulSoup + + +def add_user_group(): + """ + Add a simulated default user group. + + This will require edits as we add features and permissions. + Commented lines are example lines for adding permissions to the group. + """ + new_group, created = Group.objects.get_or_create(name='user') + # permission = Permission.objects.get(name='permission_name') + # new_group.permissions.add(permission) + new_group.save() + + +class UserFactory(factory.django.DjangoModelFactory): + """Build users.""" + + class Meta: + """Meta.""" + + model = User + + username = factory.Sequence(lambda n: 'user_number_{}'.format(n)) + email = factory.LazyAttribute( + lambda x: '{}@cbt.com'.format(x.username.replace(" ", "")) + ) + + +class ProfileTestCase(TestCase): + """Test the profile model.""" + + def setUp(self): + """Set up to test user profiles.""" + add_user_group() + self.users = [UserFactory.create() for i in range(20)] + + def test_profile_is_made_when_user_is_saved(self): + """Test that a profile is made for each user save event.""" + self.assertTrue(Profile.objects.count() == 20) + + def test_profile_associated_with_users(self): + """Test that the profile is linked with a user.""" + profile = Profile.objects.first() + self.assertTrue(hasattr(profile, 'user')) + self.assertIsInstance(profile.user, User) + + def test_user_sees_profile(self): + """Test that the user model is related to a profile model.""" + user = self.users[0] + self.assertIsInstance(user.profile, Profile) + self.assertTrue(hasattr(user, 'profile')) + + def test_user_in_group(self): + """Test that created users are added to user group.""" + user = self.users[0] + group = user.groups.first() + self.assertTrue(group.name == 'user') + + def test_profile_has_attributes(self): + """Test that the profile has attributes and assigns defaults.""" + users = self.users + attributes = [ + 'active_period_start', 'active_period_end', 'peak_period', 'dose_time' + ] + for user in users: + for attribute in attributes: + self.assertTrue(hasattr(user.profile, attribute)) + + def test_credentials_model_has_user_id_attribute(self): + """Test Credentials Model has user_id attribute.""" + self.assertTrue(hasattr(CredentialsModel, 'user_id')) + + def test_credentials_model_has_credential_attribute(self): + """Test Credentials Model has credential attribute.""" + self.assertTrue(hasattr(CredentialsModel, 'credential')) + + +class FrontendTestCases(TestCase): + """Test the frontend of the imager_profile site.""" + + def setUp(self): + """Set up client and request factory.""" + add_user_group() + self.client = Client() + self.request = RequestFactory() + self.users = [UserFactory.create() for i in range(20)] + + def make_user_and_login(self): + """Make user and login.""" + add_user_group() + user_register = UserFactory.create() + user_register.is_active = True + user_register.username = "bobdole" + user_register.first_name = 'Bob' + user_register.last_name = 'Dole' + user_register.email = 'awesome@gmail.com' + user_register.set_password("rutabega") + user_register.save() + self.client.post("/login/", { + "username": user_register.username, + "password": "rutabega" + + }) + return self.client, user_register + + def test_home_route_templates(self): + """Test the home route templates are correct.""" + response = self.client.get(reverse_lazy('home')) + self.assertTemplateUsed(response, "neuropy/base.html") + self.assertTemplateUsed(response, "neuropy/home.html") + + def test_home_route_status(self): + """Test home route status is 200.""" + response = self.client.get(reverse_lazy('home')) + self.assertTrue(response.status_code == 200) + + def test_about_route_status(self): + """Test about route status is 200.""" + response = self.client.get(reverse_lazy('about')) + self.assertTrue(response.status_code == 200) + + def test_about_route_templates(self): + """Test about route templates are correct.""" + response = self.client.get(reverse_lazy('about')) + self.assertTemplateUsed(response, "neuropy/base.html") + self.assertTemplateUsed(response, "neuropy/layout.html") + self.assertTemplateUsed(response, "neuropy/about.html") + + def test_login_redirect_code(self): + """Test built-in login route redirects properly.""" + add_user_group() + user_register = UserFactory.create() + user_register.username = "username" + user_register.set_password("rutabega") + user_register.save() + response = self.client.post(reverse_lazy("login"), { + "username": user_register.username, + "password": "rutabega" + }) + self.assertTrue(response.status_code == 302) + self.assertTrue(response.url == '/') + + def test_login_has_input_fields(self): + """Test login has input fields.""" + response = self.client.get(reverse_lazy('login')) + parsed_html = BeautifulSoup(response.content, "html5lib") + self.assertTrue(len(parsed_html.find_all('input')) == 3) + + def test_registeration_has_input_fields(self): + """Test registeration has input fields.""" + response = self.client.get(reverse_lazy('registration_register')) + parsed_html = BeautifulSoup(response.content, "html5lib") + self.assertTrue(len(parsed_html.find_all('input')) == 6) + + def test_registration_has_tos(self): + """Test registration form has TOS.""" + response = self.client.get(reverse_lazy('registration_register')) + parsed_html = BeautifulSoup(response.content, 'html5lib') + self.assertTrue(parsed_html.find('h3').text == ' Terms of Service ') + + def test_registration_has_tos_tick_box(self): + """Test registration form has input for tos.""" + response = self.client.get(reverse_lazy('registration_register')) + parsed_html = BeautifulSoup(response.content, 'html5lib') + tos = parsed_html.findAll('input', attrs={'name': 'tos'}) + self.assertTrue(len(tos) == 1) + + def test_profile_route_has_all_info(self): + """Test profile route has all info.""" + client, user = self.make_user_and_login() + html = client.get('/profile/').content + html = str(html) + self.assertTrue('Bob Dole' in html) + self.assertTrue('bobdole' in html) + self.assertTrue('awesome@gmail.com' in html) + self.assertTrue('8 a.m.' in html) + self.assertTrue('10 p.m.' in html) + self.assertTrue('Morning' in html) + + def test_edit_route_shows_info(self): + """Test_edit_route_shows_info.""" + client, user = self.make_user_and_login() + html = client.get('/profile/edit/').content + parsed_html = BeautifulSoup(html, "html5lib") + + def return_value(id): + return parsed_html.find("input", {"id": id})['value'] + + self.assertTrue(return_value('id_First Name') == 'Bob') + self.assertTrue(return_value('id_Last Name') == 'Dole') + self.assertTrue(return_value('id_Email') == 'awesome@gmail.com') + self.assertTrue(return_value('id_active_period_start') == '08:00:00') + self.assertTrue(return_value('id_active_period_end') == '22:00:00') + self.assertTrue(len(parsed_html.find_all('option')) == 6) + self.assertTrue(return_value('id_dose_time') == '08:00:00') + + def test_that_edit_will_edit_the_model(self): + """Test that edit will edit the model.""" + client, user = self.make_user_and_login() + html = client.get('/profile/edit/').content + html = BeautifulSoup(html, "html5lib") + csrf = html.find("input", {"name": 'csrfmiddlewaretoken'})['value'] + client.post('/profile/edit/', { + "csrfmiddlewaretoken": csrf, + "First Name": "Sam", + "Last Name": "Glad", + "Email": "samglad@gmail.com", + "active_period_start": "09:00:00", + "active_period_end": "23:00:00", + "peak_period": "afternoon", + "dose_time": "09:00:00" + }) + html = client.get('/profile/').content + html = str(html) + self.assertTrue('Sam Glad' in html) + self.assertTrue('bobdole' in html) + self.assertTrue('samglad@gmail.com' in html) + self.assertTrue('9 a.m.' in html) + self.assertTrue('11 p.m.' in html) + self.assertTrue('afternoon' in html) + + def test_edit_will_redirect_to_profile(self): + """Test edit will redirect to profile.""" + client, user = self.make_user_and_login() + html = client.get('/profile/edit/').content + html = BeautifulSoup(html, "html5lib") + csrf = html.find("input", {"name": 'csrfmiddlewaretoken'})['value'] + response = client.post('/profile/edit/', { + "csrfmiddlewaretoken": csrf, + "First Name": "Sam", + "Last Name": "Glad", + "Email": "samglad@gmail.com", + "active_period_start": "09:00:00", + "active_period_end": "23:00:00", + "peak_period": "afternoon", + "dose_time": "09:00:00" + }) + self.assertRedirects(response, '/profile/') + + def test_profile_templates(self): + """Test the profile templates are correct.""" + self.client.force_login(self.users[0]) + response = self.client.get(reverse_lazy('profile')) + self.assertTemplateUsed(response, "neuropy/base.html") + self.assertTemplateUsed(response, "neuropy/layout.html") + self.assertTemplateUsed(response, "userprofile/profile.html") + + def test_edit_profile_templates(self): + """Test the edit profile templates are correct.""" + self.client.force_login(self.users[0]) + response = self.client.get(reverse_lazy('edit-profile')) + self.assertTemplateUsed(response, "neuropy/base.html") + self.assertTemplateUsed(response, "neuropy/layout.html") + self.assertTemplateUsed(response, "userprofile/edit_profile.html") + + def test_profile_has_form(self): + """Test that the user profile page includes a medication form.""" + self.client.force_login(self.users[0]) + response = self.client.get(reverse_lazy('profile')) + parsed_html = BeautifulSoup(response.content, "html5lib") + self.assertTrue(len(parsed_html.find_all('form')) == 1) + + def test_profile_form_has_medication_field(self): + """Test that the user profile page includes a medication form.""" + self.client.force_login(self.users[0]) + response = self.client.get(reverse_lazy('profile')) + parsed_html = BeautifulSoup(response.content, "html5lib") + self.assertTrue(len(parsed_html.find_all('input')) == 7) + + def test_access_profile_without_login_fails(self): + """Test that a user can't get into a profile without authenticating.""" + response = self.client.get(reverse_lazy('profile')) + parsed_html = BeautifulSoup(response.content, "html5lib") + self.assertFalse(parsed_html.find_all('div')) + + def test_access_edit_profile_without_login_fails(self): + """Test that a user can't edit a profile without authenticating.""" + response = self.client.get(reverse_lazy('edit-profile')) + parsed_html = BeautifulSoup(response.content, "html5lib") + self.assertFalse(parsed_html.find_all('div')) + + def test_profile_redirect_login(self): + """Test that without a login, profile redirects to login.""" + response = self.client.get(reverse_lazy('profile')) + self.assertRedirects(response, '/accounts/login/?next=/profile/') + + def test_edit_profile_redirect_login(self): + """Test that without a login, profile redirects to login.""" + response = self.client.get(reverse_lazy('edit-profile')) + self.assertRedirects(response, '/accounts/login/?next=/profile/edit/') + + def test_edit_profile_without_csrf_fails(self): + """Test a logged in user can't edit their profile without a token.""" + self.client.force_login(self.users[0]) + html = self.client.get('/profile/edit/').content + html = BeautifulSoup(html, "html5lib") + self.client.post('/profile/edit/', {"csrfmiddlewaretoken": "", "First Name": "Bob"}) + html = self.client.get('/profile/').content + html = str(html) + self.assertFalse('Bob Glad' in html) diff --git a/neuropy/userprofile/urls.py b/neuropy/userprofile/urls.py new file mode 100644 index 0000000..f139b54 --- /dev/null +++ b/neuropy/userprofile/urls.py @@ -0,0 +1,10 @@ +"""Profile urls.""" +from django.conf.urls import url +from django.views.decorators.http import require_POST +from .views import ProfileView, EditProfile, ProfileFormView + +urlpatterns = [ + url(r'^edit/$', EditProfile.as_view(), name='edit-profile'), + url(r'^$', ProfileView.as_view(), name='profile'), + url(r'submission_form/$', require_POST(ProfileFormView.as_view()), name='profile_form') +] diff --git a/neuropy/userprofile/views.py b/neuropy/userprofile/views.py new file mode 100644 index 0000000..f4fab46 --- /dev/null +++ b/neuropy/userprofile/views.py @@ -0,0 +1,117 @@ +"""Views for profile.""" + +import httplib2 +from django.contrib.auth.mixins import LoginRequiredMixin +from django.http import HttpResponseRedirect +from django.urls import reverse_lazy +from django.views.generic import DetailView, UpdateView, FormView +from userprofile.forms import ProfileForm, MedicationForm +from userprofile.models import Profile +from todo.views import create_event_list, calender_insert +from userprofile.models import CredentialsModel +from oauth2client.contrib.django_util.storage import DjangoORMStorage +from oauth2client.contrib import xsrfutil +from neuropy import settings +from oauth2client.client import OAuth2WebServerFlow + +FLOW = OAuth2WebServerFlow( + client_id=settings.GOOGLE_OAUTH2_CLIENT_ID, + client_secret=settings.GOOGLE_OAUTH2_CLIENT_SECRET, + scope='https://www.googleapis.com/auth/calendar', + redirect_uri='http://localhost:8000/oauth2callback', + prompt='consent' +) + + +class ProfileView(LoginRequiredMixin, DetailView): + """View for profile.""" + + login_required = True + model = Profile + template_name = 'userprofile/profile.html' + slug_field = 'id' + + def get_context_data(self, **kwargs): + """Attach form to detail view page.""" + context = super(ProfileView, self).get_context_data(**kwargs) + context['form'] = MedicationForm + return context + + def get_object(self): + """Return logged in user.""" + return self.request.user + + +class ProfileFormView(LoginRequiredMixin, FormView): + """Form view for reference by profile view so we can include a form.""" + + form_class = MedicationForm + success_url = reverse_lazy('create_sched') + + def form_valid(self, form): + """Return HttpResponse when valid data is posted.""" + medication = form.cleaned_data['medication'] + priority_list = create_event_list(medication.name, self.request.user.profile) + + storage = DjangoORMStorage(CredentialsModel, 'user_id', self.request.user, 'credential') + credential = storage.get() + if credential is None or credential.invalid: + FLOW.params['state'] = xsrfutil.generate_token(settings.SECRET_KEY, + self.request.user) + authorize_url = FLOW.step1_get_authorize_url() + return HttpResponseRedirect(authorize_url) + else: + http = httplib2.Http() + http = credential.authorize(http) + + for event in priority_list: + google_event = {} + if event['ease'] == 'easy': + google_event['colorId'] = 3 + elif event['ease'] == 'medium': + google_event['colorId'] = 2 + else: + google_event['colorId'] = 11 + + google_event['description'] = event['description'] + google_event['summary'] = event['title'] + google_event['start'] = {'dateTime': event['start'].isoformat() + '-08:00'} + google_event['end'] = {'dateTime': event['end'].isoformat() + '-08:00'} + google_event['reminders'] = { + 'useDefault': False, + 'overrides': [ + {'method': 'email', 'minutes': 24 * 60}, + {'method': 'popup', 'minutes': 10}, + ] + } + calender_insert(http, google_event, self.request.user.email) + event['start'] = event['start'].strftime("%H:%M") + event['end'] = event['end'].strftime("%H:%M") + return HttpResponseRedirect(reverse_lazy('schedule')) + + self.request.session['some_list'] = priority_list + return HttpResponseRedirect(self.get_success_url()) + + +class EditProfile(LoginRequiredMixin, UpdateView): + """Edit users profile.""" + + login_required = True + template_name = 'userprofile/edit_profile.html' + model = Profile + form_class = ProfileForm + success_url = reverse_lazy('profile') + + def get_object(self): + """Return logged in users profile.""" + return self.request.user.profile + + def form_valid(self, form): + """Save object after post.""" + self.object = form.save() + self.object.user.first_name = form.cleaned_data['First Name'] + self.object.user.last_name = form.cleaned_data['Last Name'] + self.object.user.email = form.cleaned_data['Email'] + self.object.user.save() + self.object.save() + return HttpResponseRedirect(self.get_success_url()) diff --git a/requirements.pip b/requirements.pip new file mode 100644 index 0000000..b6bb034 --- /dev/null +++ b/requirements.pip @@ -0,0 +1,34 @@ +appdirs==1.4.0 +appnope==0.1.0 +beautifulsoup4==4.5.3 +decorator==4.0.11 +Django==1.10.5 +django-bootstrap3==8.1.0 +django-registration==2.2 +factory-boy==2.8.1 +Faker==0.7.7 +google-api-python-client==1.6.2 +html5lib==0.999999999 +httplib2==0.10.3 +ipython==5.2.2 +ipython-genutils==0.1.0 +jsonpickle==0.9.4 +oauth2client==4.0.0 +packaging==16.8 +pexpect==4.2.1 +pickleshare==0.7.4 +prompt-toolkit==1.0.13 +psycopg2==2.6.2 +ptyprocess==0.5.1 +pyasn1==0.2.2 +pyasn1-modules==0.0.8 +Pygments==2.2.0 +pyparsing==2.1.10 +python-dateutil==2.6.0 +rsa==3.4.2 +simplegeneric==0.8.1 +six==1.10.0 +traitlets==4.3.1 +uritemplate==3.0.0 +wcwidth==0.1.7 +webencodings==0.5