From e98d92fb0b4f604c9998cd9e94d6c924741d43c9 Mon Sep 17 00:00:00 2001 From: pasaunders Date: Mon, 16 Jan 2017 16:53:44 -0800 Subject: [PATCH 01/50] added site --- imagersite/imagersite/__init__.py | 0 imagersite/imagersite/settings.py | 120 ++++++++++++++++++++++++++++++ imagersite/imagersite/urls.py | 21 ++++++ imagersite/imagersite/wsgi.py | 16 ++++ imagersite/manage.py | 22 ++++++ 5 files changed, 179 insertions(+) create mode 100644 imagersite/imagersite/__init__.py create mode 100644 imagersite/imagersite/settings.py create mode 100644 imagersite/imagersite/urls.py create mode 100644 imagersite/imagersite/wsgi.py create mode 100755 imagersite/manage.py diff --git a/imagersite/imagersite/__init__.py b/imagersite/imagersite/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/imagersite/imagersite/settings.py b/imagersite/imagersite/settings.py new file mode 100644 index 0000000..c1f24be --- /dev/null +++ b/imagersite/imagersite/settings.py @@ -0,0 +1,120 @@ +""" +Django settings for imagersite 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 = '5m(o9rkvq-2$u452_313+-m9&gn*8%mqj+&^yum=r!%h%@(%!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', +] + +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 = 'imagersite.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 = 'imagersite.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/1.10/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.postgresql', + 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), + } +} + + +# 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', + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/1.10/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_L10N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/1.10/howto/static-files/ + +STATIC_URL = '/static/' diff --git a/imagersite/imagersite/urls.py b/imagersite/imagersite/urls.py new file mode 100644 index 0000000..384e743 --- /dev/null +++ b/imagersite/imagersite/urls.py @@ -0,0 +1,21 @@ +"""imagersite 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 +from django.contrib import admin + +urlpatterns = [ + url(r'^admin/', admin.site.urls), +] diff --git a/imagersite/imagersite/wsgi.py b/imagersite/imagersite/wsgi.py new file mode 100644 index 0000000..8ef8781 --- /dev/null +++ b/imagersite/imagersite/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for imagersite 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", "imagersite.settings") + +application = get_wsgi_application() diff --git a/imagersite/manage.py b/imagersite/manage.py new file mode 100755 index 0000000..b28a0f2 --- /dev/null +++ b/imagersite/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +import os +import sys + +if __name__ == "__main__": + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "imagersite.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) From 1bb7e551154eea9b34a4fe7917defaa2efd4dbc1 Mon Sep 17 00:00:00 2001 From: pasaunders Date: Mon, 16 Jan 2017 18:00:23 -0800 Subject: [PATCH 02/50] user profile fields built --- imagersite/imager_profile/__init__.py | 0 imagersite/imager_profile/admin.py | 3 ++ imagersite/imager_profile/apps.py | 5 ++++ .../imager_profile/migrations/__init__.py | 0 imagersite/imager_profile/models.py | 28 +++++++++++++++++++ imagersite/imager_profile/tests.py | 3 ++ imagersite/imager_profile/views.py | 3 ++ imagersite/imagersite/settings.py | 11 ++++++-- 8 files changed, 51 insertions(+), 2 deletions(-) create mode 100644 imagersite/imager_profile/__init__.py create mode 100644 imagersite/imager_profile/admin.py create mode 100644 imagersite/imager_profile/apps.py create mode 100644 imagersite/imager_profile/migrations/__init__.py create mode 100644 imagersite/imager_profile/models.py create mode 100644 imagersite/imager_profile/tests.py create mode 100644 imagersite/imager_profile/views.py diff --git a/imagersite/imager_profile/__init__.py b/imagersite/imager_profile/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/imagersite/imager_profile/admin.py b/imagersite/imager_profile/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/imagersite/imager_profile/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/imagersite/imager_profile/apps.py b/imagersite/imager_profile/apps.py new file mode 100644 index 0000000..8900e6d --- /dev/null +++ b/imagersite/imager_profile/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class ImagerProfileConfig(AppConfig): + name = 'imager_profile' diff --git a/imagersite/imager_profile/migrations/__init__.py b/imagersite/imager_profile/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/imagersite/imager_profile/models.py b/imagersite/imager_profile/models.py new file mode 100644 index 0000000..c623f98 --- /dev/null +++ b/imagersite/imager_profile/models.py @@ -0,0 +1,28 @@ +from django.db import models +from django.contrib.auth.models import User +import uuid + +from django.db.models.signals import post_save +from django.dispatch import receiver + +# Create your models here. + + +class UserProfile(models.Model): + """The imager user and all their attributes.""" + + user = models.OneToOneField( + User, + related_name="profile", + on_delete=models.CASCADE + ) + CAMERA_CHOICES = ('Nikon', 'iPhone', 'Canon'), + ACTIVE = False + camera_type = models.CharField(max_length=10, choices=CAMERA_CHOICES, null=True, blank=True), + address = models.CharField(max_length=40, null=True, blank=True), + bio = models.TextField(), + personal_website = models.URLField(), + for_hire = models.BooleanField(default=False), + travel_distance = models.IntegerField(null=True, blank=True) + phone_number = models.CharField(max_length=15, null=True, blank=True) + photography_type = models.CharField(max_length=20, null=True, blank=True) diff --git a/imagersite/imager_profile/tests.py b/imagersite/imager_profile/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/imagersite/imager_profile/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/imagersite/imager_profile/views.py b/imagersite/imager_profile/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/imagersite/imager_profile/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/imagersite/imagersite/settings.py b/imagersite/imagersite/settings.py index c1f24be..70a2211 100644 --- a/imagersite/imagersite/settings.py +++ b/imagersite/imagersite/settings.py @@ -75,8 +75,15 @@ DATABASES = { 'default': { - 'ENGINE': 'django.db.backends.postgresql', - 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), + 'ENGINE': 'django.db.backends.postgresql_psycopg2', + 'NAME': os.environ['IMAGER_DATABASE'], + # 'USER': os.environ['DATABASE_USER'], + # 'PASSWORD': os.environ['DATABASE_PASSWORD'], + # 'HOST': '127.0.0.1', + # 'PORT': '5432', + 'TEST': { + 'NAME': os.environ['TEST_IMAGER'] + } } } From bc7867f262f94a927c4cad297ae0e769c04a630f Mon Sep 17 00:00:00 2001 From: pasaunders Date: Mon, 16 Jan 2017 18:17:35 -0800 Subject: [PATCH 03/50] pre-manager statusquo --- .gitignore | 1 + imagersite/imager_profile/models.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 8312747..d6499d1 100644 --- a/.gitignore +++ b/.gitignore @@ -27,6 +27,7 @@ bin/ lib64 pyvenv.cfg share/ +pip-selfcheck.json # PyInstaller # Usually these files are written by a python script from a template diff --git a/imagersite/imager_profile/models.py b/imagersite/imager_profile/models.py index c623f98..48eba4d 100644 --- a/imagersite/imager_profile/models.py +++ b/imagersite/imager_profile/models.py @@ -8,7 +8,7 @@ # Create your models here. -class UserProfile(models.Model): +class ImagerProfile(models.Model): """The imager user and all their attributes.""" user = models.OneToOneField( From 50cd84c5fdf0603aba464c88421db706970cb7be Mon Sep 17 00:00:00 2001 From: Amos Boldor Date: Mon, 16 Jan 2017 19:18:57 -0800 Subject: [PATCH 04/50] add make profile when user is made and tests --- .../imager_profile/migrations/0001_initial.py | 29 ++++++++++++ imagersite/imager_profile/models.py | 8 +++- imagersite/imager_profile/tests.py | 47 +++++++++++++++++++ imagersite/imagersite/settings.py | 1 + 4 files changed, 84 insertions(+), 1 deletion(-) create mode 100644 imagersite/imager_profile/migrations/0001_initial.py diff --git a/imagersite/imager_profile/migrations/0001_initial.py b/imagersite/imager_profile/migrations/0001_initial.py new file mode 100644 index 0000000..93870a7 --- /dev/null +++ b/imagersite/imager_profile/migrations/0001_initial.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2017-01-17 03:06 +from __future__ import unicode_literals + +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='ImagerProfile', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('travel_distance', models.IntegerField(blank=True, null=True)), + ('phone_number', models.CharField(blank=True, max_length=15, null=True)), + ('photography_type', models.CharField(blank=True, max_length=20, null=True)), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='profile', to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/imagersite/imager_profile/models.py b/imagersite/imager_profile/models.py index 48eba4d..eeb4bba 100644 --- a/imagersite/imager_profile/models.py +++ b/imagersite/imager_profile/models.py @@ -1,6 +1,5 @@ from django.db import models from django.contrib.auth.models import User -import uuid from django.db.models.signals import post_save from django.dispatch import receiver @@ -26,3 +25,10 @@ class ImagerProfile(models.Model): travel_distance = models.IntegerField(null=True, blank=True) phone_number = models.CharField(max_length=15, null=True, blank=True) photography_type = models.CharField(max_length=20, null=True, blank=True) + + +@receiver(post_save, sender=User) +def make_profile_for_user(sender, instance, **kwargs): + """Called when user is made and hooks that user to a profile.""" + new_profile = ImagerProfile(user=instance) + new_profile.save() diff --git a/imagersite/imager_profile/tests.py b/imagersite/imager_profile/tests.py index 7ce503c..f1de2f6 100644 --- a/imagersite/imager_profile/tests.py +++ b/imagersite/imager_profile/tests.py @@ -1,3 +1,50 @@ +"""Tests for the imager_profile app.""" from django.test import TestCase +from django.contrib.auth.models import User +from imager_profile.models import ImagerProfile +import factory # Create your tests here. + + +class ProfileTestCase(TestCase): + """The Profile Model test runner.""" + + class UserFactory(factory.django.DjangoModelFactory): + """Makes users.""" + + class Meta: + """Meta.""" + + model = User + + username = factory.Sequence(lambda n: "The Chosen {}".format(n)) + email = factory.LazyAttribute( + lambda x: "{}@foo.com".format(x.username.replace(" ", "")) + ) + + def setUp(self): + """The appropriate setup for the appropriate test.""" + self.foo = "bar" + self.users = [self.UserFactory.create() for i in range(20)] + + def thing_and_stuff(self): + self.thing = "stuff" + + def test_profile_is_made_when_user_is_saved(self): + """.""" + self.thing_and_stuff() + self.assertTrue(ImagerProfile.objects.count() == 20) + self.assertTrue(self.thing == "stuff") + + def test_profile_is_associated_with_actual_users(self): + """.""" + profile = ImagerProfile.objects.first() + self.assertTrue(hasattr(profile, "user")) + self.assertIsInstance(profile.user, User) + + def test_user_has_profile_attached(self): + """.""" + user = self.users[0] + self.assertTrue(hasattr(user, "profile")) + self.assertIsInstance(user.profile, ImagerProfile) diff --git a/imagersite/imagersite/settings.py b/imagersite/imagersite/settings.py index 70a2211..cdf4ced 100644 --- a/imagersite/imagersite/settings.py +++ b/imagersite/imagersite/settings.py @@ -37,6 +37,7 @@ 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', + 'imager_profile' ] MIDDLEWARE = [ From 3aa40d18c1871871c825a2a30b837f4bbf878ca7 Mon Sep 17 00:00:00 2001 From: Amos Boldor Date: Mon, 16 Jan 2017 19:39:01 -0800 Subject: [PATCH 05/50] add is_active and ActiveProfileManager --- imagersite/imager_profile/models.py | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/imagersite/imager_profile/models.py b/imagersite/imager_profile/models.py index eeb4bba..cfc3350 100644 --- a/imagersite/imager_profile/models.py +++ b/imagersite/imager_profile/models.py @@ -1,22 +1,34 @@ from django.db import models from django.contrib.auth.models import User - +from django.utils.encoding import python_2_unicode_compatible from django.db.models.signals import post_save from django.dispatch import receiver # Create your models here. +class ActiveProfileManager(models.Manager): + """Create Model Manager for Active Profiles.""" + + def get_queryset(self): + """Return active users.""" + qs = super(ActiveProfileManager, self).get_queryset() + return qs.filter(user__is_active__exact=True) + + +@python_2_unicode_compatible class ImagerProfile(models.Model): """The imager user and all their attributes.""" + objects = models.Manager() + active = ActiveProfileManager() + user = models.OneToOneField( User, related_name="profile", on_delete=models.CASCADE ) CAMERA_CHOICES = ('Nikon', 'iPhone', 'Canon'), - ACTIVE = False camera_type = models.CharField(max_length=10, choices=CAMERA_CHOICES, null=True, blank=True), address = models.CharField(max_length=40, null=True, blank=True), bio = models.TextField(), @@ -26,6 +38,11 @@ class ImagerProfile(models.Model): phone_number = models.CharField(max_length=15, null=True, blank=True) photography_type = models.CharField(max_length=20, null=True, blank=True) + @property + def is_active(self): + """Return True if user associated with this profile is active.""" + return self._is_active + @receiver(post_save, sender=User) def make_profile_for_user(sender, instance, **kwargs): From ccb4011867334749562b081ef804a8a1b9976e77 Mon Sep 17 00:00:00 2001 From: pasaunders Date: Mon, 16 Jan 2017 23:11:33 -0800 Subject: [PATCH 06/50] added __str__ method and default app config --- imagersite/imager_profile/__init__.py | 1 + imagersite/imager_profile/models.py | 13 ++++++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/imagersite/imager_profile/__init__.py b/imagersite/imager_profile/__init__.py index e69de29..4a991a4 100644 --- a/imagersite/imager_profile/__init__.py +++ b/imagersite/imager_profile/__init__.py @@ -0,0 +1 @@ +default_app_config = 'imager_profile.apps.ImagerProfileConfig' diff --git a/imagersite/imager_profile/models.py b/imagersite/imager_profile/models.py index cfc3350..ec2e88e 100644 --- a/imagersite/imager_profile/models.py +++ b/imagersite/imager_profile/models.py @@ -1,3 +1,5 @@ +"""Models for imager_profile.""" + from django.db import models from django.contrib.auth.models import User from django.utils.encoding import python_2_unicode_compatible @@ -29,7 +31,12 @@ class ImagerProfile(models.Model): on_delete=models.CASCADE ) CAMERA_CHOICES = ('Nikon', 'iPhone', 'Canon'), - camera_type = models.CharField(max_length=10, choices=CAMERA_CHOICES, null=True, blank=True), + camera_type = models.CharField( + max_length=10, + choices=CAMERA_CHOICES, + null=True, + blank=True + ), address = models.CharField(max_length=40, null=True, blank=True), bio = models.TextField(), personal_website = models.URLField(), @@ -43,6 +50,10 @@ def is_active(self): """Return True if user associated with this profile is active.""" return self._is_active + def __str__(self): + """Display user data as a string.""" + return "User: {}, Camera: {}, Address: {}, Phone number: {} For Hire? {}, Photography style: {}".format(self.user, self.camera_type, self.address, self.phone_number, self.for_hire, self.photography_type) + @receiver(post_save, sender=User) def make_profile_for_user(sender, instance, **kwargs): From c01f3bc82851b7335cefb91bd6e8c3b90ec305e6 Mon Sep 17 00:00:00 2001 From: Amos Boldor Date: Tue, 17 Jan 2017 09:33:57 -0800 Subject: [PATCH 07/50] remove and fix some tests --- imagersite/imager_profile/tests.py | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/imagersite/imager_profile/tests.py b/imagersite/imager_profile/tests.py index f1de2f6..ae18524 100644 --- a/imagersite/imager_profile/tests.py +++ b/imagersite/imager_profile/tests.py @@ -4,8 +4,6 @@ from imager_profile.models import ImagerProfile import factory -# Create your tests here. - class ProfileTestCase(TestCase): """The Profile Model test runner.""" @@ -28,23 +26,18 @@ def setUp(self): self.foo = "bar" self.users = [self.UserFactory.create() for i in range(20)] - def thing_and_stuff(self): - self.thing = "stuff" - def test_profile_is_made_when_user_is_saved(self): - """.""" - self.thing_and_stuff() + """Test profile is made when user is saved.""" self.assertTrue(ImagerProfile.objects.count() == 20) - self.assertTrue(self.thing == "stuff") def test_profile_is_associated_with_actual_users(self): - """.""" + """Test profile is associated with actual users.""" profile = ImagerProfile.objects.first() self.assertTrue(hasattr(profile, "user")) self.assertIsInstance(profile.user, User) def test_user_has_profile_attached(self): - """.""" + """Test user has profile attached.""" user = self.users[0] self.assertTrue(hasattr(user, "profile")) self.assertIsInstance(user.profile, ImagerProfile) From 44bdabfc99cbf5809aef4c8f91eb1ac51e3bec6f Mon Sep 17 00:00:00 2001 From: Amos Boldor Date: Tue, 17 Jan 2017 10:09:47 -0800 Subject: [PATCH 08/50] add requirements.pip --- imagersite/requirements.pip | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 imagersite/requirements.pip diff --git a/imagersite/requirements.pip b/imagersite/requirements.pip new file mode 100644 index 0000000..74feca4 --- /dev/null +++ b/imagersite/requirements.pip @@ -0,0 +1,18 @@ +decorator==4.0.11 +Django==1.10.5 +factory-boy==2.8.1 +Faker==0.7.7 +ipython==5.1.0 +ipython-genutils==0.1.0 +pexpect==4.2.1 +pickleshare==0.7.4 +pkg-resources==0.0.0 +prompt-toolkit==1.0.9 +psycopg2==2.6.2 +ptyprocess==0.5.1 +Pygments==2.1.3 +python-dateutil==2.6.0 +simplegeneric==0.8.1 +six==1.10.0 +traitlets==4.3.1 +wcwidth==0.1.7 From 08a4a1b62850d7da89d02f7104fee49dfc8c96a3 Mon Sep 17 00:00:00 2001 From: Amos Boldor Date: Tue, 17 Jan 2017 10:17:04 -0800 Subject: [PATCH 09/50] add readme --- README.md | 40 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 552d117..45227bc 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,38 @@ -# django-imager -django introduction assignment +# Django-Imager +## Getting Started + +Clone this repository into whatever directory you want to work from. + +```bash +$ git clone https://github.com/pasaunders/django-imager.git +``` + +Assuming that you have access to Python 3 at the system level, start up a new virtual environment. + +```bash +$ cd django-imager +$ python3 -m venv . +$ source bin/activate +``` + +Once your environment has been activated, make sure to install Django and all of this project's required packages. + +```bash +(django-imager) $ pip install -r requirements.pip +``` + +Navigate to the project root, `imagersite`, and apply the migrations for the app. + +```bash +(django-imager) $ cd lending_library +(django-imager) $ ./manage.py migrate +``` + +Finally, run the server in order to server the app on `localhost` + +```bash +(django-imager) $ ./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`. \ No newline at end of file From 0cfedb8a1d93a8da1667612565c87a3ddc6352eb Mon Sep 17 00:00:00 2001 From: Amos Boldor Date: Tue, 17 Jan 2017 10:23:02 -0800 Subject: [PATCH 10/50] change is_active --- imagersite/imager_profile/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/imagersite/imager_profile/models.py b/imagersite/imager_profile/models.py index ec2e88e..19f8b09 100644 --- a/imagersite/imager_profile/models.py +++ b/imagersite/imager_profile/models.py @@ -48,7 +48,7 @@ class ImagerProfile(models.Model): @property def is_active(self): """Return True if user associated with this profile is active.""" - return self._is_active + return self.user.is_active def __str__(self): """Display user data as a string.""" From ee2b05dfd2b24881175b4e41b02553960816e330 Mon Sep 17 00:00:00 2001 From: Amos Boldor Date: Tue, 17 Jan 2017 10:27:42 -0800 Subject: [PATCH 11/50] fixed some stuff --- imagersite/imager_profile/models.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/imagersite/imager_profile/models.py b/imagersite/imager_profile/models.py index 19f8b09..a9779a3 100644 --- a/imagersite/imager_profile/models.py +++ b/imagersite/imager_profile/models.py @@ -30,17 +30,21 @@ class ImagerProfile(models.Model): related_name="profile", on_delete=models.CASCADE ) - CAMERA_CHOICES = ('Nikon', 'iPhone', 'Canon'), + CAMERA_CHOICES = [ + ('Nikon', 'Nikon'), + ('iPhone', 'iPhone'), + ('Canon', 'Canon') + ] camera_type = models.CharField( max_length=10, choices=CAMERA_CHOICES, null=True, blank=True - ), + ) address = models.CharField(max_length=40, null=True, blank=True), - bio = models.TextField(), - personal_website = models.URLField(), - for_hire = models.BooleanField(default=False), + bio = models.TextField(default="") + personal_website = models.URLField(default="") + for_hire = models.BooleanField(default=False) travel_distance = models.IntegerField(null=True, blank=True) phone_number = models.CharField(max_length=15, null=True, blank=True) photography_type = models.CharField(max_length=20, null=True, blank=True) From 27fd374fadb9eef3cfd11dc99005a2c907be15ad Mon Sep 17 00:00:00 2001 From: Amos Boldor Date: Tue, 17 Jan 2017 10:28:58 -0800 Subject: [PATCH 12/50] did makemigrations --- .../migrations/0002_auto_20170117_1828.py | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 imagersite/imager_profile/migrations/0002_auto_20170117_1828.py diff --git a/imagersite/imager_profile/migrations/0002_auto_20170117_1828.py b/imagersite/imager_profile/migrations/0002_auto_20170117_1828.py new file mode 100644 index 0000000..42a32ed --- /dev/null +++ b/imagersite/imager_profile/migrations/0002_auto_20170117_1828.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2017-01-17 18:28 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('imager_profile', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='imagerprofile', + name='bio', + field=models.TextField(default=''), + ), + migrations.AddField( + model_name='imagerprofile', + name='camera_type', + field=models.CharField(blank=True, choices=[('Nikon', 'Nikon'), ('iPhone', 'iPhone'), ('Canon', 'Canon')], max_length=10, null=True), + ), + migrations.AddField( + model_name='imagerprofile', + name='for_hire', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='imagerprofile', + name='personal_website', + field=models.URLField(default=''), + ), + ] From 53d177a191e94cfd46611562b23b2e0ff07def97 Mon Sep 17 00:00:00 2001 From: pasaunders Date: Tue, 17 Jan 2017 15:21:11 -0800 Subject: [PATCH 13/50] built .travis.yml and requirements.pip --- .travis.yml | 15 +++++++++++++++ requirements.pip | 15 +++++++++++++++ 2 files changed, 30 insertions(+) create mode 100644 .travis.yml create mode 100644 requirements.pip diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..07bdb11 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,15 @@ +decorator==4.0.11 +Django==1.10.5 +ipython==5.1.0 +ipython-genutils==0.1.0 +pexpect==4.2.1 +pickleshare==0.7.4 +pkg-resources==0.0.0 +prompt-toolkit==1.0.9 +psycopg2==2.6.2 +ptyprocess==0.5.1 +Pygments==2.1.3 +simplegeneric==0.8.1 +six==1.10.0 +traitlets==4.3.1 +wcwidth==0.1.7 diff --git a/requirements.pip b/requirements.pip new file mode 100644 index 0000000..07bdb11 --- /dev/null +++ b/requirements.pip @@ -0,0 +1,15 @@ +decorator==4.0.11 +Django==1.10.5 +ipython==5.1.0 +ipython-genutils==0.1.0 +pexpect==4.2.1 +pickleshare==0.7.4 +pkg-resources==0.0.0 +prompt-toolkit==1.0.9 +psycopg2==2.6.2 +ptyprocess==0.5.1 +Pygments==2.1.3 +simplegeneric==0.8.1 +six==1.10.0 +traitlets==4.3.1 +wcwidth==0.1.7 From 061bb4ca462d26edefa782e31c9b5a80db8d2b97 Mon Sep 17 00:00:00 2001 From: pasaunders Date: Tue, 17 Jan 2017 15:29:51 -0800 Subject: [PATCH 14/50] added readme widget --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 45227bc..784532d 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,4 @@ +[![Build Status](https://travis-ci.org/pasaunders/django-imager.svg?branch=master)](https://travis-ci.org/pasaunders/django-imager) # Django-Imager ## Getting Started From 071b439c2f2e8e47d343f0efdc2910f274d9c633 Mon Sep 17 00:00:00 2001 From: pasaunders Date: Tue, 17 Jan 2017 16:22:14 -0800 Subject: [PATCH 15/50] travis debugging --- .travis.yml | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/.travis.yml b/.travis.yml index 07bdb11..cbc029a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,15 +1,18 @@ -decorator==4.0.11 -Django==1.10.5 -ipython==5.1.0 -ipython-genutils==0.1.0 -pexpect==4.2.1 -pickleshare==0.7.4 -pkg-resources==0.0.0 -prompt-toolkit==1.0.9 -psycopg2==2.6.2 -ptyprocess==0.5.1 -Pygments==2.1.3 -simplegeneric==0.8.1 -six==1.10.0 -traitlets==4.3.1 -wcwidth==0.1.7 +language: python +python: + - "2.7" + - "3.5" + +# command to install dependencies +install: + - pip install . + - pip install -r requirements.txt + +services: + - postgresql + +before_script: + - psql -c 'create database travis_ci_test;' -U postgres + +# command to run tests +script: python manage.py test \ No newline at end of file From d95f9acaa9bd34a4eeffa411146a6ac8621ce25b Mon Sep 17 00:00:00 2001 From: pasaunders Date: Tue, 17 Jan 2017 16:36:22 -0800 Subject: [PATCH 16/50] travis debugging --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index cbc029a..0610318 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,7 +5,7 @@ python: # command to install dependencies install: - - pip install . + # - pip install . - pip install -r requirements.txt services: From 253909242bc76d5c6e09616b2c6f692330ca7927 Mon Sep 17 00:00:00 2001 From: pasaunders Date: Tue, 17 Jan 2017 16:37:45 -0800 Subject: [PATCH 17/50] travis debugging --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 0610318..3911138 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,7 +6,7 @@ python: # command to install dependencies install: # - pip install . - - pip install -r requirements.txt + - pip install -r requirements.pip services: - postgresql From c1b08cd85ce14c82a0d86a1c71f4eeecaee1e2c4 Mon Sep 17 00:00:00 2001 From: pasaunders Date: Tue, 17 Jan 2017 16:39:19 -0800 Subject: [PATCH 18/50] travis debugging --- requirements.pip | 1 - 1 file changed, 1 deletion(-) diff --git a/requirements.pip b/requirements.pip index 07bdb11..54667b0 100644 --- a/requirements.pip +++ b/requirements.pip @@ -4,7 +4,6 @@ ipython==5.1.0 ipython-genutils==0.1.0 pexpect==4.2.1 pickleshare==0.7.4 -pkg-resources==0.0.0 prompt-toolkit==1.0.9 psycopg2==2.6.2 ptyprocess==0.5.1 From 580c94c2a9fde30c679a67515bd39e2aa802a0f1 Mon Sep 17 00:00:00 2001 From: pasaunders Date: Tue, 17 Jan 2017 16:41:12 -0800 Subject: [PATCH 19/50] travis debugging --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 3911138..792b15f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,4 +15,4 @@ before_script: - psql -c 'create database travis_ci_test;' -U postgres # command to run tests -script: python manage.py test \ No newline at end of file +script: python imagersite.manage.py test \ No newline at end of file From e4b35464b39b96924e8f10c65825b5f2bddb6526 Mon Sep 17 00:00:00 2001 From: pasaunders Date: Tue, 17 Jan 2017 16:43:27 -0800 Subject: [PATCH 20/50] travis debugging --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 792b15f..ffad062 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,4 +15,4 @@ before_script: - psql -c 'create database travis_ci_test;' -U postgres # command to run tests -script: python imagersite.manage.py test \ No newline at end of file +script: python imagersite/manage.py test \ No newline at end of file From f9a852370f5608b12bbb5f56752e9e3d1eb0b737 Mon Sep 17 00:00:00 2001 From: pasaunders Date: Tue, 17 Jan 2017 16:46:11 -0800 Subject: [PATCH 21/50] travis debugging --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 784532d..12d0439 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,4 @@ -[![Build Status](https://travis-ci.org/pasaunders/django-imager.svg?branch=master)](https://travis-ci.org/pasaunders/django-imager) -# Django-Imager +[![Build Status](https://travis-ci.org/pasaunders/django-imager.svg?branch=master)](https://travis-ci.org/pasaunders/django-imager)# Django-Imager ## Getting Started Clone this repository into whatever directory you want to work from. From 7f5d9569acd6f4a3773d34e8f9f3e7ef40a3482f Mon Sep 17 00:00:00 2001 From: pasaunders Date: Tue, 17 Jan 2017 16:49:43 -0800 Subject: [PATCH 22/50] travis debugging --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 12d0439..5152b0d 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -[![Build Status](https://travis-ci.org/pasaunders/django-imager.svg?branch=master)](https://travis-ci.org/pasaunders/django-imager)# Django-Imager +[![Build Status](https://travis-ci.org/pasaunders/django-imager.svg?branch=front-end-1)](https://travis-ci.org/pasaunders/django-imager) ## Getting Started Clone this repository into whatever directory you want to work from. From 00c1b3f3708462c35c58a1cb3881316d5df792dc Mon Sep 17 00:00:00 2001 From: Amos Boldor Date: Tue, 17 Jan 2017 17:16:08 -0800 Subject: [PATCH 23/50] add admin, some teplates and other stuff --- imagersite/imager_profile/admin.py | 4 ++++ imagersite/imagersite/settings.py | 3 ++- imagersite/imagersite/templates/imagersite/base.html | 9 +++++++++ imagersite/imagersite/templates/imagersite/home.html | 3 +++ imagersite/imagersite/urls.py | 2 ++ imagersite/imagersite/views.py | 9 +++++++++ 6 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 imagersite/imagersite/templates/imagersite/base.html create mode 100644 imagersite/imagersite/templates/imagersite/home.html create mode 100644 imagersite/imagersite/views.py diff --git a/imagersite/imager_profile/admin.py b/imagersite/imager_profile/admin.py index 8c38f3f..8295835 100644 --- a/imagersite/imager_profile/admin.py +++ b/imagersite/imager_profile/admin.py @@ -1,3 +1,7 @@ from django.contrib import admin +from imager_profile.models import ImagerProfile # Register your models here. + + +admin.site.register(ImagerProfile) diff --git a/imagersite/imagersite/settings.py b/imagersite/imagersite/settings.py index cdf4ced..d6eacd6 100644 --- a/imagersite/imagersite/settings.py +++ b/imagersite/imagersite/settings.py @@ -37,7 +37,8 @@ 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', - 'imager_profile' + 'imager_profile', + 'imagersite' ] MIDDLEWARE = [ diff --git a/imagersite/imagersite/templates/imagersite/base.html b/imagersite/imagersite/templates/imagersite/base.html new file mode 100644 index 0000000..cfea42a --- /dev/null +++ b/imagersite/imagersite/templates/imagersite/base.html @@ -0,0 +1,9 @@ + + + + App + + +{% block content %}{% endblock %} + + \ No newline at end of file diff --git a/imagersite/imagersite/templates/imagersite/home.html b/imagersite/imagersite/templates/imagersite/home.html new file mode 100644 index 0000000..9c34b75 --- /dev/null +++ b/imagersite/imagersite/templates/imagersite/home.html @@ -0,0 +1,3 @@ +{% extends 'imagersite/base.html' %} + +Home Page \ No newline at end of file diff --git a/imagersite/imagersite/urls.py b/imagersite/imagersite/urls.py index 384e743..d1e0017 100644 --- a/imagersite/imagersite/urls.py +++ b/imagersite/imagersite/urls.py @@ -15,7 +15,9 @@ """ from django.conf.urls import url from django.contrib import admin +from imagersite.views import home_view urlpatterns = [ url(r'^admin/', admin.site.urls), + url(r'^$', home_view, name='homepage') ] diff --git a/imagersite/imagersite/views.py b/imagersite/imagersite/views.py new file mode 100644 index 0000000..c0ce2a7 --- /dev/null +++ b/imagersite/imagersite/views.py @@ -0,0 +1,9 @@ +"""Views.""" +from django.shortcuts import render + + +def home_view(request): + """The home view.""" + return render(request, + "imagersite/home.html" + ) From 19494912c104ae9dc721e1395fc455f8e3948add Mon Sep 17 00:00:00 2001 From: Amos Boldor Date: Tue, 17 Jan 2017 17:24:29 -0800 Subject: [PATCH 24/50] fix home template --- imagersite/imagersite/templates/imagersite/home.html | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/imagersite/imagersite/templates/imagersite/home.html b/imagersite/imagersite/templates/imagersite/home.html index 9c34b75..367883c 100644 --- a/imagersite/imagersite/templates/imagersite/home.html +++ b/imagersite/imagersite/templates/imagersite/home.html @@ -1,3 +1,4 @@ {% extends 'imagersite/base.html' %} - -Home Page \ No newline at end of file +{% block content %} +

Home Page

+{% endblock content %} \ No newline at end of file From d3d1e5778d5d8a87c26e771d30fda52701444fe8 Mon Sep 17 00:00:00 2001 From: Amos Boldor Date: Tue, 17 Jan 2017 18:12:11 -0800 Subject: [PATCH 25/50] change url --- imagersite/imagersite/urls.py | 13 +++++++++---- imagersite/imagersite/views.py | 1 + 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/imagersite/imagersite/urls.py b/imagersite/imagersite/urls.py index d1e0017..e5ded31 100644 --- a/imagersite/imagersite/urls.py +++ b/imagersite/imagersite/urls.py @@ -13,11 +13,16 @@ 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 -from django.contrib import admin -from imagersite.views import home_view +from django.conf.urls import include, url +from django.contrib import ( + admin +) +from imagersite.views import ( + home_view +) urlpatterns = [ url(r'^admin/', admin.site.urls), - url(r'^$', home_view, name='homepage') + url(r'^$', home_view, name='homepage'), + url(r'^accounts/$', include('registration.backends.hmac.urls')), ] diff --git a/imagersite/imagersite/views.py b/imagersite/imagersite/views.py index c0ce2a7..d5681d0 100644 --- a/imagersite/imagersite/views.py +++ b/imagersite/imagersite/views.py @@ -1,5 +1,6 @@ """Views.""" from django.shortcuts import render +import registration.backends.hmac.views.RegistrationView def home_view(request): From 94b6e321ecc8ac538c3a52b7145610381c07e1ae Mon Sep 17 00:00:00 2001 From: pasaunders Date: Tue, 17 Jan 2017 18:34:50 -0800 Subject: [PATCH 26/50] some bootstrap code, one more test, requirements.pip updated --- imagersite/imager_profile/tests.py | 7 +++++++ imagersite/imagersite/settings.py | 1 + imagersite/requirements.pip | 1 - requirements.pip | 5 +++++ 4 files changed, 13 insertions(+), 1 deletion(-) diff --git a/imagersite/imager_profile/tests.py b/imagersite/imager_profile/tests.py index ae18524..35186c3 100644 --- a/imagersite/imager_profile/tests.py +++ b/imagersite/imager_profile/tests.py @@ -41,3 +41,10 @@ def test_user_has_profile_attached(self): user = self.users[0] self.assertTrue(hasattr(user, "profile")) self.assertIsInstance(user.profile, ImagerProfile) + + def test_user_model_has_str(self): + """Test user has a string method.""" + user = self.users[0] + self.assertIsInstance(str(user), str) + + # def test_user_model_has_attributes(self): diff --git a/imagersite/imagersite/settings.py b/imagersite/imagersite/settings.py index d6eacd6..295abbb 100644 --- a/imagersite/imagersite/settings.py +++ b/imagersite/imagersite/settings.py @@ -37,6 +37,7 @@ 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', + 'django-bootstrap3', 'imager_profile', 'imagersite' ] diff --git a/imagersite/requirements.pip b/imagersite/requirements.pip index 74feca4..779cab5 100644 --- a/imagersite/requirements.pip +++ b/imagersite/requirements.pip @@ -6,7 +6,6 @@ ipython==5.1.0 ipython-genutils==0.1.0 pexpect==4.2.1 pickleshare==0.7.4 -pkg-resources==0.0.0 prompt-toolkit==1.0.9 psycopg2==2.6.2 ptyprocess==0.5.1 diff --git a/requirements.pip b/requirements.pip index 54667b0..2436f19 100644 --- a/requirements.pip +++ b/requirements.pip @@ -1,13 +1,18 @@ decorator==4.0.11 Django==1.10.5 +django-bootstrap3==8.1.0 +factory-boy==2.8.1 +Faker==0.7.7 ipython==5.1.0 ipython-genutils==0.1.0 pexpect==4.2.1 pickleshare==0.7.4 +pkg-resources==0.0.0 prompt-toolkit==1.0.9 psycopg2==2.6.2 ptyprocess==0.5.1 Pygments==2.1.3 +python-dateutil==2.6.0 simplegeneric==0.8.1 six==1.10.0 traitlets==4.3.1 From e4121cbfbf4f8c4216b0568e28d132ddbefd91f2 Mon Sep 17 00:00:00 2001 From: pasaunders Date: Wed, 18 Jan 2017 17:37:00 -0800 Subject: [PATCH 27/50] first pass HMAC templates --- .../imagersite/templates/registration/activate.html | 4 ++++ .../templates/registration/activation_complete.html | 4 ++++ .../templates/registration/activation_email.txt | 2 ++ .../templates/registration/activation_email_subject.txt | 1 + .../templates/registration/registration_complete.html | 4 ++++ .../templates/registration/registration_form.html | 8 ++++++++ 6 files changed, 23 insertions(+) create mode 100644 imagersite/imagersite/templates/registration/activate.html create mode 100644 imagersite/imagersite/templates/registration/activation_complete.html create mode 100644 imagersite/imagersite/templates/registration/activation_email.txt create mode 100644 imagersite/imagersite/templates/registration/activation_email_subject.txt create mode 100644 imagersite/imagersite/templates/registration/registration_complete.html create mode 100644 imagersite/imagersite/templates/registration/registration_form.html diff --git a/imagersite/imagersite/templates/registration/activate.html b/imagersite/imagersite/templates/registration/activate.html new file mode 100644 index 0000000..37bc407 --- /dev/null +++ b/imagersite/imagersite/templates/registration/activate.html @@ -0,0 +1,4 @@ +{% extends 'imagersite/base.html' %} +{% block content %} +

Activate Page

+{% endblock content %} \ No newline at end of file diff --git a/imagersite/imagersite/templates/registration/activation_complete.html b/imagersite/imagersite/templates/registration/activation_complete.html new file mode 100644 index 0000000..144d774 --- /dev/null +++ b/imagersite/imagersite/templates/registration/activation_complete.html @@ -0,0 +1,4 @@ +{% extends 'imagersite/base.html' %} +{% block content %} +

Activation complete

+{% endblock content %} \ No newline at end of file diff --git a/imagersite/imagersite/templates/registration/activation_email.txt b/imagersite/imagersite/templates/registration/activation_email.txt new file mode 100644 index 0000000..3c13308 --- /dev/null +++ b/imagersite/imagersite/templates/registration/activation_email.txt @@ -0,0 +1,2 @@ +Click here to register at imagersite: +http://{{ site.domain }}{% url "registration_activate" activation_key %} \ No newline at end of file diff --git a/imagersite/imagersite/templates/registration/activation_email_subject.txt b/imagersite/imagersite/templates/registration/activation_email_subject.txt new file mode 100644 index 0000000..42fd665 --- /dev/null +++ b/imagersite/imagersite/templates/registration/activation_email_subject.txt @@ -0,0 +1 @@ +Account Activation \ No newline at end of file diff --git a/imagersite/imagersite/templates/registration/registration_complete.html b/imagersite/imagersite/templates/registration/registration_complete.html new file mode 100644 index 0000000..37de4ff --- /dev/null +++ b/imagersite/imagersite/templates/registration/registration_complete.html @@ -0,0 +1,4 @@ +{% extends 'imagersite/base.html' %} +{% block content %} +

Registration Complete

+{% endblock content %} \ No newline at end of file diff --git a/imagersite/imagersite/templates/registration/registration_form.html b/imagersite/imagersite/templates/registration/registration_form.html new file mode 100644 index 0000000..d02acd9 --- /dev/null +++ b/imagersite/imagersite/templates/registration/registration_form.html @@ -0,0 +1,8 @@ +{% extends 'imagersite/base.html' %} +{% block content %} +
+ {% csrf_token %} + {{ form.as_table }} + +
+{% endblock content %} \ No newline at end of file From f05f3665deeedcd8d6bd0be03ecc8dafddb05b94 Mon Sep 17 00:00:00 2001 From: pasaunders Date: Wed, 18 Jan 2017 17:37:42 -0800 Subject: [PATCH 28/50] first pass HMAC templates --- imagersite/imagersite/settings.py | 8 +++++++- requirements.pip | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/imagersite/imagersite/settings.py b/imagersite/imagersite/settings.py index 295abbb..78efdcc 100644 --- a/imagersite/imagersite/settings.py +++ b/imagersite/imagersite/settings.py @@ -115,7 +115,7 @@ LANGUAGE_CODE = 'en-us' -TIME_ZONE = 'UTC' +TIME_ZONE = 'America/Los_Angeles' USE_I18N = True @@ -128,3 +128,9 @@ # https://docs.djangoproject.com/en/1.10/howto/static-files/ STATIC_URL = '/static/' + + +# Registration Settings +ACCOUNT_ACTIVATION_DAYS = 3 +EMAIL_HOST = '127.0.0.1' +EMAIL_PORT = 1025 diff --git a/requirements.pip b/requirements.pip index 2436f19..08fffa9 100644 --- a/requirements.pip +++ b/requirements.pip @@ -1,13 +1,13 @@ 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 ipython==5.1.0 ipython-genutils==0.1.0 pexpect==4.2.1 pickleshare==0.7.4 -pkg-resources==0.0.0 prompt-toolkit==1.0.9 psycopg2==2.6.2 ptyprocess==0.5.1 From 156f329199a423e4613cd6b2a61a69e3e65db4fe Mon Sep 17 00:00:00 2001 From: pasaunders Date: Wed, 18 Jan 2017 18:34:14 -0800 Subject: [PATCH 29/50] login logout and register seem to work --- imagersite/imagersite/settings.py | 5 ++++- .../templates/registration/login.html | 0 imagersite/imagersite/urls.py | 9 ++++++--- imagersite/imagersite/views.py | 2 +- imagersite/requirements.pip | 17 ----------------- 5 files changed, 11 insertions(+), 22 deletions(-) create mode 100644 imagersite/imagersite/templates/registration/login.html delete mode 100644 imagersite/requirements.pip diff --git a/imagersite/imagersite/settings.py b/imagersite/imagersite/settings.py index 78efdcc..ad52794 100644 --- a/imagersite/imagersite/settings.py +++ b/imagersite/imagersite/settings.py @@ -37,7 +37,7 @@ 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', - 'django-bootstrap3', + 'bootstrap3', 'imager_profile', 'imagersite' ] @@ -134,3 +134,6 @@ ACCOUNT_ACTIVATION_DAYS = 3 EMAIL_HOST = '127.0.0.1' EMAIL_PORT = 1025 + +# Login/out settings +LOGIN_REDIRECT_URL='/' diff --git a/imagersite/imagersite/templates/registration/login.html b/imagersite/imagersite/templates/registration/login.html new file mode 100644 index 0000000..e69de29 diff --git a/imagersite/imagersite/urls.py b/imagersite/imagersite/urls.py index e5ded31..16ad8dc 100644 --- a/imagersite/imagersite/urls.py +++ b/imagersite/imagersite/urls.py @@ -1,4 +1,4 @@ -"""imagersite URL Configuration +"""imagersite URL Configuration. The `urlpatterns` list routes URLs to views. For more information please see: https://docs.djangoproject.com/en/1.10/topics/http/urls/ @@ -15,7 +15,8 @@ """ from django.conf.urls import include, url from django.contrib import ( - admin + admin, + auth ) from imagersite.views import ( home_view @@ -24,5 +25,7 @@ urlpatterns = [ url(r'^admin/', admin.site.urls), url(r'^$', home_view, name='homepage'), - url(r'^accounts/$', include('registration.backends.hmac.urls')), + 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') ] diff --git a/imagersite/imagersite/views.py b/imagersite/imagersite/views.py index d5681d0..86279ce 100644 --- a/imagersite/imagersite/views.py +++ b/imagersite/imagersite/views.py @@ -1,6 +1,6 @@ """Views.""" from django.shortcuts import render -import registration.backends.hmac.views.RegistrationView +# from registration.backends.hmac.views import RegistrationView def home_view(request): diff --git a/imagersite/requirements.pip b/imagersite/requirements.pip deleted file mode 100644 index 779cab5..0000000 --- a/imagersite/requirements.pip +++ /dev/null @@ -1,17 +0,0 @@ -decorator==4.0.11 -Django==1.10.5 -factory-boy==2.8.1 -Faker==0.7.7 -ipython==5.1.0 -ipython-genutils==0.1.0 -pexpect==4.2.1 -pickleshare==0.7.4 -prompt-toolkit==1.0.9 -psycopg2==2.6.2 -ptyprocess==0.5.1 -Pygments==2.1.3 -python-dateutil==2.6.0 -simplegeneric==0.8.1 -six==1.10.0 -traitlets==4.3.1 -wcwidth==0.1.7 From 4496d4966d4b3b9e0f79f51ca9cee1bd267a0f8e Mon Sep 17 00:00:00 2001 From: pasaunders Date: Wed, 18 Jan 2017 18:34:44 -0800 Subject: [PATCH 30/50] readme change --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 5152b0d..b1d6c64 100644 --- a/README.md +++ b/README.md @@ -35,4 +35,7 @@ Finally, run the server in order to server the app on `localhost` ``` Django will typically serve on port 8000, unless you specify otherwise. -You can access the locally-served site at the address `http://localhost:8000`. \ No newline at end of file +You can access the locally-served site at the address `http://localhost:8000`. + +Resources we used: +http://stackoverflow.com/questions/10180764/django-auth-login-problems \ No newline at end of file From 692f00c53a255e8f9ededd982baa7c34e9fc1566 Mon Sep 17 00:00:00 2001 From: pasaunders Date: Wed, 18 Jan 2017 18:45:05 -0800 Subject: [PATCH 31/50] begin second app models --- imagersite/imager_images/__init__.py | 0 imagersite/imager_images/admin.py | 3 +++ imagersite/imager_images/apps.py | 5 +++++ imagersite/imager_images/migrations/__init__.py | 0 imagersite/imager_images/models.py | 4 ++++ imagersite/imager_images/tests.py | 3 +++ imagersite/imager_images/views.py | 3 +++ 7 files changed, 18 insertions(+) create mode 100644 imagersite/imager_images/__init__.py create mode 100644 imagersite/imager_images/admin.py create mode 100644 imagersite/imager_images/apps.py create mode 100644 imagersite/imager_images/migrations/__init__.py create mode 100644 imagersite/imager_images/models.py create mode 100644 imagersite/imager_images/tests.py create mode 100644 imagersite/imager_images/views.py diff --git a/imagersite/imager_images/__init__.py b/imagersite/imager_images/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/imagersite/imager_images/admin.py b/imagersite/imager_images/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/imagersite/imager_images/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/imagersite/imager_images/apps.py b/imagersite/imager_images/apps.py new file mode 100644 index 0000000..7afb70a --- /dev/null +++ b/imagersite/imager_images/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class ImagerImagesConfig(AppConfig): + name = 'imager_images' diff --git a/imagersite/imager_images/migrations/__init__.py b/imagersite/imager_images/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/imagersite/imager_images/models.py b/imagersite/imager_images/models.py new file mode 100644 index 0000000..77a813f --- /dev/null +++ b/imagersite/imager_images/models.py @@ -0,0 +1,4 @@ +from django.db import models +from imagersite.models import ImagerProfile + +# Create your models here. \ No newline at end of file diff --git a/imagersite/imager_images/tests.py b/imagersite/imager_images/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/imagersite/imager_images/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/imagersite/imager_images/views.py b/imagersite/imager_images/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/imagersite/imager_images/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. From c4fd524728b1ff009d0088f22137243040db52dd Mon Sep 17 00:00:00 2001 From: Amos Boldor Date: Wed, 18 Jan 2017 21:30:01 -0800 Subject: [PATCH 32/50] add MEDIA folder and the gitkeep for it and gitignore lines --- .gitignore | 4 ++++ imagersite/MEDIA/.gitkeep | 0 2 files changed, 4 insertions(+) create mode 100644 imagersite/MEDIA/.gitkeep diff --git a/.gitignore b/.gitignore index d6499d1..ac52e8b 100644 --- a/.gitignore +++ b/.gitignore @@ -92,3 +92,7 @@ ENV/ # Rope project settings .ropeproject + +# Ignore the files in the Media directory, but not the directory itself +imagersite/MEDIA/* +!imagersite/MEDIA/.gitkeep \ No newline at end of file diff --git a/imagersite/MEDIA/.gitkeep b/imagersite/MEDIA/.gitkeep new file mode 100644 index 0000000..e69de29 From 60c823ba769bd3149b7c12ac339c7404e087a1b0 Mon Sep 17 00:00:00 2001 From: Amos Boldor Date: Wed, 18 Jan 2017 21:34:31 -0800 Subject: [PATCH 33/50] add fix for only add profile if created True --- imagersite/imager_profile/models.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/imagersite/imager_profile/models.py b/imagersite/imager_profile/models.py index a9779a3..107d78f 100644 --- a/imagersite/imager_profile/models.py +++ b/imagersite/imager_profile/models.py @@ -62,5 +62,6 @@ def __str__(self): @receiver(post_save, sender=User) def make_profile_for_user(sender, instance, **kwargs): """Called when user is made and hooks that user to a profile.""" - new_profile = ImagerProfile(user=instance) - new_profile.save() + if kwargs["created"]: + new_profile = ImagerProfile(user=instance) + new_profile.save() From 34e633861926130e71f6b82e25e2f2d4c4703ea8 Mon Sep 17 00:00:00 2001 From: Amos Boldor Date: Wed, 18 Jan 2017 21:37:30 -0800 Subject: [PATCH 34/50] add MEDIA_ROOT --- imagersite/imagersite/settings.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/imagersite/imagersite/settings.py b/imagersite/imagersite/settings.py index ad52794..7c00e4d 100644 --- a/imagersite/imagersite/settings.py +++ b/imagersite/imagersite/settings.py @@ -39,7 +39,8 @@ 'django.contrib.staticfiles', 'bootstrap3', 'imager_profile', - 'imagersite' + 'imagersite', + 'imager_images' ] MIDDLEWARE = [ @@ -137,3 +138,5 @@ # Login/out settings LOGIN_REDIRECT_URL='/' +MEDIA_ROOT = os.path.join(BASE_DIR, 'MEDIA') +MEDIA_URL = "/media/" From 55444497e5f3ed97e6b4356d20de03552ad4d909 Mon Sep 17 00:00:00 2001 From: Amos Boldor Date: Wed, 18 Jan 2017 21:38:19 -0800 Subject: [PATCH 35/50] add ability for admin to create model for images --- imagersite/imager_images/admin.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/imagersite/imager_images/admin.py b/imagersite/imager_images/admin.py index 8c38f3f..aae142c 100644 --- a/imagersite/imager_images/admin.py +++ b/imagersite/imager_images/admin.py @@ -1,3 +1,5 @@ from django.contrib import admin +from imager_images.models import Album, Photo -# Register your models here. +admin.site.register(Album) +admin.site.register(Photo) From 533e6e941bc2dd0273f6412785149e233e8c4d6f Mon Sep 17 00:00:00 2001 From: Amos Boldor Date: Wed, 18 Jan 2017 21:39:01 -0800 Subject: [PATCH 36/50] add photo and album models and migration --- .../imager_images/migrations/0001_initial.py | 61 +++++++++++++++++ imagersite/imager_images/models.py | 68 ++++++++++++++++++- 2 files changed, 127 insertions(+), 2 deletions(-) create mode 100644 imagersite/imager_images/migrations/0001_initial.py diff --git a/imagersite/imager_images/migrations/0001_initial.py b/imagersite/imager_images/migrations/0001_initial.py new file mode 100644 index 0000000..1fa1ba8 --- /dev/null +++ b/imagersite/imager_images/migrations/0001_initial.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2017-01-19 05:16 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import imager_images.models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Album', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=60)), + ('description', models.TextField(max_length=200)), + ('date_created', models.DateTimeField(auto_now_add=True)), + ('date_modified', models.DateTimeField(auto_now=True)), + ('date_published', models.DateTimeField(null=True)), + ('published', models.CharField(choices=[('private', 'private'), ('shared', 'shared'), ('public', 'public')], max_length=10)), + ], + ), + migrations.CreateModel( + name='Photo', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('image', models.ImageField(upload_to=imager_images.models.image_path)), + ('title', models.CharField(max_length=60)), + ('description', models.TextField(max_length=120)), + ('date_uploaded', models.DateTimeField(auto_now_add=True)), + ('date_modified', models.DateTimeField(auto_now=True)), + ('date_published', models.DateTimeField(null=True)), + ('published', models.CharField(choices=[('private', 'private'), ('shared', 'shared'), ('public', 'public')], max_length=10)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='photos', to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.AddField( + model_name='album', + name='cover', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='albums_covered', to='imager_images.Photo'), + ), + migrations.AddField( + model_name='album', + name='photos', + field=models.ManyToManyField(related_name='albums', to='imager_images.Photo'), + ), + migrations.AddField( + model_name='album', + name='user', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='albums', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/imagersite/imager_images/models.py b/imagersite/imager_images/models.py index 77a813f..7245076 100644 --- a/imagersite/imager_images/models.py +++ b/imagersite/imager_images/models.py @@ -1,4 +1,68 @@ +from __future__ import unicode_literals +from django.utils.encoding import python_2_unicode_compatible from django.db import models -from imagersite.models import ImagerProfile +from django.contrib.auth.models import User -# Create your models here. \ No newline at end of file +PUBLISHED_OPTIONS = ( + ("private", "private"), + ("shared", "shared"), + ("public", "public"), +) + + +def image_path(instance, file_name): + """Upload file to media root in user folder.""" + return 'user_{0}/{1}'.format(instance.user.id, file_name) + + +@python_2_unicode_compatible +class Photo(models.Model): + """Create Photo Model.""" + + user = models.ForeignKey( + User, + related_name='photos', + on_delete=models.CASCADE, + ) + image = models.ImageField(upload_to=image_path) + title = models.CharField(max_length=60) + description = models.TextField(max_length=120) + date_uploaded = models.DateTimeField(auto_now_add=True) + date_modified = models.DateTimeField(auto_now=True) + date_published = models.DateTimeField(null=True) + published = models.CharField(max_length=10, choices=PUBLISHED_OPTIONS) + + def __str__(self): + """Return string description of album.""" + return "{}: Photo belonging to {}".format(self.title, self.user) + + +@python_2_unicode_compatible +class Album(models.Model): + """Create Album Model.""" + + user = models.ForeignKey( + User, + related_name="albums", + on_delete=models.CASCADE, + ) + cover = models.ForeignKey( + "Photo", + null=True, + related_name="albums_covered" + ) + title = models.CharField(max_length=60) + description = models.TextField(max_length=200) + photos = models.ManyToManyField( + "Photo", + related_name="albums", + symmetrical=False + ) + date_created = models.DateTimeField(auto_now_add=True) + date_modified = models.DateTimeField(auto_now=True) + date_published = models.DateTimeField(null=True) + published = models.CharField(max_length=10, choices=PUBLISHED_OPTIONS) + + def __str__(self): + """Return String Representation of Album.""" + return "{}: Album belonging to {}".format(self.title, self.user) From 399d538e62e4832f8aae5b48336ababb99493894 Mon Sep 17 00:00:00 2001 From: Amos Boldor Date: Wed, 18 Jan 2017 21:40:11 -0800 Subject: [PATCH 37/50] update requirements.pip with Pillow==4.0.0 --- requirements.pip | 3 +++ 1 file changed, 3 insertions(+) diff --git a/requirements.pip b/requirements.pip index 08fffa9..d432505 100644 --- a/requirements.pip +++ b/requirements.pip @@ -6,8 +6,11 @@ factory-boy==2.8.1 Faker==0.7.7 ipython==5.1.0 ipython-genutils==0.1.0 +olefile==0.44 pexpect==4.2.1 pickleshare==0.7.4 +Pillow==4.0.0 +pkg-resources==0.0.0 prompt-toolkit==1.0.9 psycopg2==2.6.2 ptyprocess==0.5.1 From caf4ff7c1519f0cac43633df5e408ada3a03eefd Mon Sep 17 00:00:00 2001 From: pasaunders Date: Thu, 19 Jan 2017 18:44:19 -0800 Subject: [PATCH 38/50] new tests --- .../migrations/0003_auto_20170119_1823.py | 20 ++++++++++++++++ imagersite/imager_profile/models.py | 15 ++++++++++-- imagersite/imager_profile/tests.py | 23 +++++++++++++++++-- 3 files changed, 54 insertions(+), 4 deletions(-) create mode 100644 imagersite/imager_profile/migrations/0003_auto_20170119_1823.py diff --git a/imagersite/imager_profile/migrations/0003_auto_20170119_1823.py b/imagersite/imager_profile/migrations/0003_auto_20170119_1823.py new file mode 100644 index 0000000..59498f7 --- /dev/null +++ b/imagersite/imager_profile/migrations/0003_auto_20170119_1823.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2017-01-20 02:23 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('imager_profile', '0002_auto_20170117_1828'), + ] + + operations = [ + migrations.AlterField( + model_name='imagerprofile', + name='photography_type', + field=models.CharField(blank=True, choices=[('portrait', 'Portrait'), ('landscape', 'Landscape'), ('bw', 'Black and White'), ('sport', 'Sport')], max_length=20, null=True), + ), + ] diff --git a/imagersite/imager_profile/models.py b/imagersite/imager_profile/models.py index 107d78f..0ea6baa 100644 --- a/imagersite/imager_profile/models.py +++ b/imagersite/imager_profile/models.py @@ -47,7 +47,18 @@ class ImagerProfile(models.Model): for_hire = models.BooleanField(default=False) travel_distance = models.IntegerField(null=True, blank=True) phone_number = models.CharField(max_length=15, null=True, blank=True) - photography_type = models.CharField(max_length=20, null=True, blank=True) + STYLE_CHOICES = [ + ('portrait', 'Portrait'), + ('landscape', 'Landscape'), + ('bw', 'Black and White'), + ('sport', 'Sport') + ] + photography_type = models.CharField( + max_length=20, + choices=STYLE_CHOICES, + null=True, + blank=True + ) @property def is_active(self): @@ -56,7 +67,7 @@ def is_active(self): def __str__(self): """Display user data as a string.""" - return "User: {}, Camera: {}, Address: {}, Phone number: {} For Hire? {}, Photography style: {}".format(self.user, self.camera_type, self.address, self.phone_number, self.for_hire, self.photography_type) + return "User: {}, Camera: {}, Address: {}, Phone number: {}, For Hire? {}, Photography style: {}".format(self.user, self.camera_type, self.address, self.phone_number, self.for_hire, self.photography_type) @receiver(post_save, sender=User) diff --git a/imagersite/imager_profile/tests.py b/imagersite/imager_profile/tests.py index 35186c3..37b8ac1 100644 --- a/imagersite/imager_profile/tests.py +++ b/imagersite/imager_profile/tests.py @@ -1,5 +1,5 @@ """Tests for the imager_profile app.""" -from django.test import TestCase +from django.test import TestCase, Client, RequestFactory from django.contrib.auth.models import User from imager_profile.models import ImagerProfile import factory @@ -47,4 +47,23 @@ def test_user_model_has_str(self): user = self.users[0] self.assertIsInstance(str(user), str) - # def test_user_model_has_attributes(self): + def test_user_model_has_attributes(self): + """Test user attributes are present.""" + pass + + def test_active_users_counted(self): + """Test acttive user count meets expectations.""" + self.assertTrue(ImagerProfile.active.count() == User.objects.count()) + + def test_inactive_users_not_counted(self): + """Test inactive users not included with active users.""" + deactivated_user = self.users[0] + deactivated_user.is_active = False + deactivated_user.save() + self.assertTrue(ImagerProfile.active.count() == User.objects.count() - 1) + + + +class frontend_test_cases(TestCase): + """Test the frontend of the imager_profile site.""" + pass From b70a7b6b62603031329f5986f025f8e7060ab902 Mon Sep 17 00:00:00 2001 From: pasaunders Date: Thu, 19 Jan 2017 22:49:29 -0800 Subject: [PATCH 39/50] extend tests, needs debugging --- imagersite/imager_profile/tests.py | 127 +++++++++++++++++++++++++---- 1 file changed, 109 insertions(+), 18 deletions(-) diff --git a/imagersite/imager_profile/tests.py b/imagersite/imager_profile/tests.py index 37b8ac1..0ad5b68 100644 --- a/imagersite/imager_profile/tests.py +++ b/imagersite/imager_profile/tests.py @@ -5,26 +5,27 @@ import factory -class ProfileTestCase(TestCase): - """The Profile Model test runner.""" +class UserFactory(factory.django.DjangoModelFactory): + """Makes users.""" - class UserFactory(factory.django.DjangoModelFactory): - """Makes users.""" + class Meta: + """Meta.""" - class Meta: - """Meta.""" + model = User - model = User + username = factory.Sequence(lambda n: "The Chosen {}".format(n)) + email = factory.LazyAttribute( + lambda x: "{}@foo.com".format(x.username.replace(" ", "")) + ) - username = factory.Sequence(lambda n: "The Chosen {}".format(n)) - email = factory.LazyAttribute( - lambda x: "{}@foo.com".format(x.username.replace(" ", "")) - ) + +class ProfileTestCase(TestCase): + """The Profile Model test runner.""" def setUp(self): """The appropriate setup for the appropriate test.""" self.foo = "bar" - self.users = [self.UserFactory.create() for i in range(20)] + self.users = [UserFactory.create() for i in range(20)] def test_profile_is_made_when_user_is_saved(self): """Test profile is made when user is saved.""" @@ -47,9 +48,9 @@ def test_user_model_has_str(self): user = self.users[0] self.assertIsInstance(str(user), str) - def test_user_model_has_attributes(self): - """Test user attributes are present.""" - pass + # def test_user_model_has_attributes(self): + # """Test user attributes are present.""" + # pass def test_active_users_counted(self): """Test acttive user count meets expectations.""" @@ -63,7 +64,97 @@ def test_inactive_users_not_counted(self): self.assertTrue(ImagerProfile.active.count() == User.objects.count() - 1) - -class frontend_test_cases(TestCase): +class FrontendTestCases(TestCase): """Test the frontend of the imager_profile site.""" - pass + + def setUp(self): + """Set up client and request factory.""" + self.client = Client() + self.request = RequestFactory() + + def test_home_view_status(self): + """Test home view has 200 status.""" + from imagersite.views import home_view + req = self.request.get("/route") + response = home_view(req) + self.assertTrue(response.status_code == 200) + + def test_home_route_status(self): + """Test home route has 200 status.""" + response = self.client.get("/") + self.assertTrue(response.status_code == 200) + + def test_home_route_templates(self): + """Test the home route templates are correct.""" + response = self.client.get("/") + self.assertTemplateUsed(response, "imagersite/base.html") + self.assertTemplateUsed(response, "imagersite/home.html") + + def test_login_redirect_code(self): + """Test built-in login route redirects properly.""" + user_register = UserFactory.create() + user_register.username = "username" + user_register.password = "potatoes" + user_register.save() + response = self.client.post("/login/", { + "username": user_register.username, + "password": "potatoes" + + }) + self.assertTrue(response.status_code == 302) + + def test_login_redirect_home(self): + """Test built-in login route redirects to home.""" + user_register = UserFactory.create() + user_register.username = "username2" + user_register.password = "potatoes2" + user_register.save() + response = self.client.post("/login/", { + "username": user_register.username, + "password": "potatoes" + }, follow=True) + self.assertTrue(response.redirect_chain[0][0] == '/') + + def test_register_user(self): + """Test that tests can register users.""" + self.assertTrue(User.objects.count() == 0) + self.client.post("/registration/register/", { + "username": "Sir_Joseph", + "email": "e@mail.com", + "password1": "rutabega", + "password2": "rutabega" + }) + self.assertTrue(User.objects.count() == 1) + + def test_new_user_inactive(self): + """Test django-created user starts as inactive.""" + self.client.post("/registration/register/", { + "username": "Sir_Joseph", + "email": "e@mail.com", + "password1": "rutabega", + "password2": "rutabega" + }) + inactive_user = User.objects.first() + self.assertFalse(inactive_user.is_active) + + def test_registration_redirect(self): + """Test redirect on registration.""" + response = self.client.post("/registration/register/", { + "username": "Sir_Joseph", + "email": "e@mail.com", + "password1": "rutabega", + "password2": "rutabega" + }) + self.assertTrue(response.status_code == 302) + + def test_registration_reidrect_home(self): + """Test registration redirects home.""" + response = self.client.post("/registration/register/", { + "username": "Sir_Joseph", + "email": "e@mail.com", + "password1": "rutabega", + "password2": "rutabega" + }, follow=True) + self.assertTrue( + response.redirect_chain[0][0] == "/registration/register/complete/" + ) From 4f39b8e013bb186ee311d1d15f6863210a0f0911 Mon Sep 17 00:00:00 2001 From: pasaunders Date: Fri, 27 Jan 2017 15:53:24 -0800 Subject: [PATCH 40/50] update gitignore for future --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index ac52e8b..90f1258 100644 --- a/.gitignore +++ b/.gitignore @@ -28,6 +28,7 @@ lib64 pyvenv.cfg share/ pip-selfcheck.json +MEDIA/ # PyInstaller # Usually these files are written by a python script from a template From c9fbb306262a4e7022ab666821a24c0f5d4f5f90 Mon Sep 17 00:00:00 2001 From: pasaunders Date: Sat, 28 Jan 2017 17:01:35 -0800 Subject: [PATCH 41/50] docstring update. --- imagersite/imager_profile/tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/imagersite/imager_profile/tests.py b/imagersite/imager_profile/tests.py index 0ad5b68..eec1f51 100644 --- a/imagersite/imager_profile/tests.py +++ b/imagersite/imager_profile/tests.py @@ -9,7 +9,7 @@ class UserFactory(factory.django.DjangoModelFactory): """Makes users.""" class Meta: - """Meta.""" + """Metadata for UserFactory.""" model = User From 5c78cc0ea3daebda331eff8abe821e1932fc5141 Mon Sep 17 00:00:00 2001 From: pasaunders Date: Sat, 28 Jan 2017 17:31:59 -0800 Subject: [PATCH 42/50] added additional tests --- .../imager_profile/migrations/0001_initial.py | 13 ++- .../migrations/0002_auto_20170117_1828.py | 35 -------- .../migrations/0003_auto_20170119_1823.py | 20 ----- imagersite/imager_profile/models.py | 31 +++---- imagersite/imager_profile/tests.py | 83 +++++++++++-------- imagersite/imagersite/settings.py | 2 +- 6 files changed, 72 insertions(+), 112 deletions(-) delete mode 100644 imagersite/imager_profile/migrations/0002_auto_20170117_1828.py delete mode 100644 imagersite/imager_profile/migrations/0003_auto_20170119_1823.py diff --git a/imagersite/imager_profile/migrations/0001_initial.py b/imagersite/imager_profile/migrations/0001_initial.py index 93870a7..94299f0 100644 --- a/imagersite/imager_profile/migrations/0001_initial.py +++ b/imagersite/imager_profile/migrations/0001_initial.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Generated by Django 1.10.5 on 2017-01-17 03:06 +# Generated by Django 1.10.5 on 2017-01-29 01:26 from __future__ import unicode_literals from django.conf import settings @@ -20,9 +20,14 @@ class Migration(migrations.Migration): name='ImagerProfile', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('travel_distance', models.IntegerField(blank=True, null=True)), - ('phone_number', models.CharField(blank=True, max_length=15, null=True)), - ('photography_type', models.CharField(blank=True, max_length=20, null=True)), + ('camera_type', models.CharField(blank=True, choices=[('Nikon', 'Nikon'), ('iPhone', 'iPhone'), ('Canon', 'Canon'), ('--------', '--------')], default='--------', max_length=10)), + ('address', models.CharField(blank=True, default='', max_length=70, null=True)), + ('bio', models.TextField(default='')), + ('personal_website', models.URLField(default='')), + ('for_hire', models.BooleanField(default=False)), + ('travel_distance', models.IntegerField(blank=True, default=0)), + ('phone_number', models.CharField(blank=True, default='', max_length=15)), + ('photography_type', models.CharField(blank=True, default='', max_length=20)), ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='profile', to=settings.AUTH_USER_MODEL)), ], ), diff --git a/imagersite/imager_profile/migrations/0002_auto_20170117_1828.py b/imagersite/imager_profile/migrations/0002_auto_20170117_1828.py deleted file mode 100644 index 42a32ed..0000000 --- a/imagersite/imager_profile/migrations/0002_auto_20170117_1828.py +++ /dev/null @@ -1,35 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.10.5 on 2017-01-17 18:28 -from __future__ import unicode_literals - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('imager_profile', '0001_initial'), - ] - - operations = [ - migrations.AddField( - model_name='imagerprofile', - name='bio', - field=models.TextField(default=''), - ), - migrations.AddField( - model_name='imagerprofile', - name='camera_type', - field=models.CharField(blank=True, choices=[('Nikon', 'Nikon'), ('iPhone', 'iPhone'), ('Canon', 'Canon')], max_length=10, null=True), - ), - migrations.AddField( - model_name='imagerprofile', - name='for_hire', - field=models.BooleanField(default=False), - ), - migrations.AddField( - model_name='imagerprofile', - name='personal_website', - field=models.URLField(default=''), - ), - ] diff --git a/imagersite/imager_profile/migrations/0003_auto_20170119_1823.py b/imagersite/imager_profile/migrations/0003_auto_20170119_1823.py deleted file mode 100644 index 59498f7..0000000 --- a/imagersite/imager_profile/migrations/0003_auto_20170119_1823.py +++ /dev/null @@ -1,20 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.10.5 on 2017-01-20 02:23 -from __future__ import unicode_literals - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('imager_profile', '0002_auto_20170117_1828'), - ] - - operations = [ - migrations.AlterField( - model_name='imagerprofile', - name='photography_type', - field=models.CharField(blank=True, choices=[('portrait', 'Portrait'), ('landscape', 'Landscape'), ('bw', 'Black and White'), ('sport', 'Sport')], max_length=20, null=True), - ), - ] diff --git a/imagersite/imager_profile/models.py b/imagersite/imager_profile/models.py index 0ea6baa..72578c9 100644 --- a/imagersite/imager_profile/models.py +++ b/imagersite/imager_profile/models.py @@ -33,32 +33,27 @@ class ImagerProfile(models.Model): CAMERA_CHOICES = [ ('Nikon', 'Nikon'), ('iPhone', 'iPhone'), - ('Canon', 'Canon') + ('Canon', 'Canon'), + ('--------', '--------') + ] + TYPE_OF_PHOTOGRAPHY = [ + ('nature', 'nature'), + ('urban', 'urban'), + ('portraits', 'portraits') ] camera_type = models.CharField( max_length=10, choices=CAMERA_CHOICES, - null=True, - blank=True + blank=True, + default='--------' ) - address = models.CharField(max_length=40, null=True, blank=True), + address = models.CharField(default="", max_length=70, null=True, blank=True) bio = models.TextField(default="") personal_website = models.URLField(default="") for_hire = models.BooleanField(default=False) - travel_distance = models.IntegerField(null=True, blank=True) - phone_number = models.CharField(max_length=15, null=True, blank=True) - STYLE_CHOICES = [ - ('portrait', 'Portrait'), - ('landscape', 'Landscape'), - ('bw', 'Black and White'), - ('sport', 'Sport') - ] - photography_type = models.CharField( - max_length=20, - choices=STYLE_CHOICES, - null=True, - blank=True - ) + travel_distance = models.IntegerField(default=0, blank=True) + phone_number = models.CharField(max_length=15, default="", blank=True) + photography_type = models.CharField(max_length=20, default="", blank=True) @property def is_active(self): diff --git a/imagersite/imager_profile/tests.py b/imagersite/imager_profile/tests.py index eec1f51..11a80e6 100644 --- a/imagersite/imager_profile/tests.py +++ b/imagersite/imager_profile/tests.py @@ -48,10 +48,6 @@ def test_user_model_has_str(self): user = self.users[0] self.assertIsInstance(str(user), str) - # def test_user_model_has_attributes(self): - # """Test user attributes are present.""" - # pass - def test_active_users_counted(self): """Test acttive user count meets expectations.""" self.assertTrue(ImagerProfile.active.count() == User.objects.count()) @@ -63,72 +59,79 @@ def test_inactive_users_not_counted(self): deactivated_user.save() self.assertTrue(ImagerProfile.active.count() == User.objects.count() - 1) + def test_imagerprofile_attributes(self): + """Test that ImagerProfile has the expected attributes.""" + attribute_list = ["user", "camera_type", "address", "bio", "personal_website", "for_hire", "travel_distance", "phone_number", "photography_type"] + for item in attribute_list: + self.assertTrue(hasattr(ImagerProfile, item)) + + def test_field_type(self): + """Test user field types.""" + attribute_list = ["camera_type", "address", "bio", "personal_website", "for_hire", "travel_distance", "phone_number", "photography_type"] + field_list = [str, str, str, str, bool, int, str, str] + test_user = self.users[0] + # import pdb; pdb.set_trace() + self.assertIsInstance(test_user.username, str) + for attribute, field in zip(attribute_list, field_list): + self.assertIsInstance(getattr(test_user.profile, attribute), field) + class FrontendTestCases(TestCase): """Test the frontend of the imager_profile site.""" - def setUp(self): """Set up client and request factory.""" self.client = Client() self.request = RequestFactory() - def test_home_view_status(self): """Test home view has 200 status.""" from imagersite.views import home_view req = self.request.get("/route") response = home_view(req) self.assertTrue(response.status_code == 200) - def test_home_route_status(self): """Test home route has 200 status.""" response = self.client.get("/") self.assertTrue(response.status_code == 200) - def test_home_route_templates(self): """Test the home route templates are correct.""" response = self.client.get("/") self.assertTemplateUsed(response, "imagersite/base.html") self.assertTemplateUsed(response, "imagersite/home.html") - + def test_login_template(self): + """Test the login route templates are correct.""" + response = self.client.get("/login/") + self.assertTemplateUsed(response, "imagersite/base.html") + self.assertTemplateUsed(response, "registration/login.html") + def test_registration_template(self): + """Test the login route templates are correct.""" + response = self.client.get("/accounts/register/") + self.assertTemplateUsed(response, "imagersite/base.html") + self.assertTemplateUsed(response, "registration/registration_form.html") def test_login_redirect_code(self): """Test built-in login route redirects properly.""" user_register = UserFactory.create() + user_register.is_active = True user_register.username = "username" - user_register.password = "potatoes" + user_register.set_password("potatoes") user_register.save() response = self.client.post("/login/", { "username": user_register.username, "password": "potatoes" - }) - self.assertTrue(response.status_code == 302) - - def test_login_redirect_home(self): - """Test built-in login route redirects to home.""" - user_register = UserFactory.create() - user_register.username = "username2" - user_register.password = "potatoes2" - user_register.save() - response = self.client.post("/login/", { - "username": user_register.username, - "password": "potatoes" - }, follow=True) - self.assertTrue(response.redirect_chain[0][0] == '/') - + self.assertRedirects(response, '/') def test_register_user(self): """Test that tests can register users.""" self.assertTrue(User.objects.count() == 0) - self.client.post("/registration/register/", { + self.client.post("/accounts/register/", { "username": "Sir_Joseph", "email": "e@mail.com", "password1": "rutabega", "password2": "rutabega" }) self.assertTrue(User.objects.count() == 1) - def test_new_user_inactive(self): """Test django-created user starts as inactive.""" - self.client.post("/registration/register/", { + self.client.post("/accounts/register/", { "username": "Sir_Joseph", "email": "e@mail.com", "password1": "rutabega", @@ -136,25 +139,37 @@ def test_new_user_inactive(self): }) inactive_user = User.objects.first() self.assertFalse(inactive_user.is_active) - def test_registration_redirect(self): """Test redirect on registration.""" - response = self.client.post("/registration/register/", { + response = self.client.post("/accounts/register/", { "username": "Sir_Joseph", "email": "e@mail.com", "password1": "rutabega", "password2": "rutabega" }) self.assertTrue(response.status_code == 302) - def test_registration_reidrect_home(self): """Test registration redirects home.""" - response = self.client.post("/registration/register/", { + response = self.client.post("/accounts/register/", { "username": "Sir_Joseph", "email": "e@mail.com", "password1": "rutabega", "password2": "rutabega" }, follow=True) - self.assertTrue( - response.redirect_chain[0][0] == "/registration/register/complete/" + self.assertRedirects( + response, + "/accounts/register/complete/" ) + def test_logout_redirects_to_home(self): + """Test logging out redirects to home.""" + user_register = UserFactory.create() + user_register.is_active = True + user_register.username = "username" + user_register.set_password("potatoes") + user_register.save() + self.client.post("/login/", { + "username": user_register.username, + "password": "potatoes" + }) + response = self.client.get('/logout/') + self.assertRedirects(response, '/') diff --git a/imagersite/imagersite/settings.py b/imagersite/imagersite/settings.py index 7c00e4d..f60fad2 100644 --- a/imagersite/imagersite/settings.py +++ b/imagersite/imagersite/settings.py @@ -137,6 +137,6 @@ EMAIL_PORT = 1025 # Login/out settings -LOGIN_REDIRECT_URL='/' +LOGIN_REDIRECT_URL = '/' MEDIA_ROOT = os.path.join(BASE_DIR, 'MEDIA') MEDIA_URL = "/media/" From 86a386127e2c93644e17924ec05282f27a345e75 Mon Sep 17 00:00:00 2001 From: pasaunders Date: Sat, 28 Jan 2017 19:43:18 -0800 Subject: [PATCH 43/50] photo and album factories --- imagersite/imager_images/models.py | 16 ++-- imagersite/imager_images/tests.py | 113 ++++++++++++++++++++++++++++- imagersite/imager_profile/tests.py | 19 +++-- imagersite/imagersite/settings.py | 2 +- 4 files changed, 134 insertions(+), 16 deletions(-) diff --git a/imagersite/imager_images/models.py b/imagersite/imager_images/models.py index 7245076..40554a7 100644 --- a/imagersite/imager_images/models.py +++ b/imagersite/imager_images/models.py @@ -24,13 +24,13 @@ class Photo(models.Model): related_name='photos', on_delete=models.CASCADE, ) - image = models.ImageField(upload_to=image_path) + image = models.ImageField(upload_to=image_path, blank=True, null=True) title = models.CharField(max_length=60) - description = models.TextField(max_length=120) - date_uploaded = models.DateTimeField(auto_now_add=True) - date_modified = models.DateTimeField(auto_now=True) - date_published = models.DateTimeField(null=True) - published = models.CharField(max_length=10, choices=PUBLISHED_OPTIONS) + description = models.TextField(max_length=120, blank=True, null=True) + date_uploaded = models.DateTimeField(auto_now_add=True, blank=True, null=True) + date_modified = models.DateTimeField(auto_now=True, blank=True, null=True) + date_published = models.DateTimeField(null=True, blank=True) + published = models.CharField(max_length=10, choices=PUBLISHED_OPTIONS, default='public') def __str__(self): """Return string description of album.""" @@ -60,8 +60,8 @@ class Album(models.Model): ) date_created = models.DateTimeField(auto_now_add=True) date_modified = models.DateTimeField(auto_now=True) - date_published = models.DateTimeField(null=True) - published = models.CharField(max_length=10, choices=PUBLISHED_OPTIONS) + date_published = models.DateTimeField(null=True, blank=True) + published = models.CharField(max_length=10, choices=PUBLISHED_OPTIONS, default="public") def __str__(self): """Return String Representation of Album.""" diff --git a/imagersite/imager_images/tests.py b/imagersite/imager_images/tests.py index 7ce503c..f3f9304 100644 --- a/imagersite/imager_images/tests.py +++ b/imagersite/imager_images/tests.py @@ -1,3 +1,112 @@ -from django.test import TestCase +"""Test the imager_images app.""" +from django.test import TestCase, Client, RequestFactory +from django.contrib.auth.models import User +from django.db import models +from imager_images.models import Photo, Album +from django.core.files.uploadedfile import SimpleUploadedFile +import factory -# Create your tests here. + +class UserFactory(factory.django.DjangoModelFactory): + """Makes users.""" + + class Meta: + """Meta.""" + + model = User + + username = factory.Sequence(lambda n: "Prisoner number {}".format(n)) + email = factory.LazyAttribute( + lambda x: "{}@foo.com".format(x.username.replace(" ", "")) + ) + + +class PhotoFactory(factory.django.DjangoModelFactory): + """Makes photos.""" + + class Meta: + """Meta.""" + + model = Photo + + title = factory.Sequence(lambda n: "Photo number {}".format(n)) + description = factory.LazyAttribute(lambda a: '{} is a photo'.format(a.title)) + + +class AlbumFacotory(factory.django.DjangoModelFactory): + """Makes albums.""" + + class Meta: + """Meta.""" + + model = Album + + title = factory.Sequence(lambda n: "Album number {}".format(n)) + description = factory.LazyAttribute(lambda a: '{} is a photo'.format(a.title)) + + +class PhotoTestCase(TestCase): + """Photo model and view tests.""" + + def setUp(self): + """The appropriate setup for the appropriate test.""" + self.users = [UserFactory.create() for i in range(20)] + self.photos = [PhotoFactory.create() for i in range(5)] + + """ + To test: + photo model is built + photos are associated with users + assigning values to photo attributes + presence of string method + """ + + def test_photo_made_when_saved(self): + """Test photos are added to the database.""" + self.assertTrue(Photo.objects.count() == 1) + + def test_photo_associated_with_user(self): + """Test that a photo is attached to a user.""" + photo = Photo.objects.first() + self.assertTrue(hasattr(photo, "user")) + self.assertIsInstance(photo.user, User) + + def test_photo_has_str(self): + """Test photo model includes string method.""" + + +class AlbumTestCase(TestCase): + """Album model and view tests.""" + + def setUp(self): + """The appropriate setup for the appropriate test.""" + self.users = [UserFactory.create() for i in range(20)] + for profile in Album.objects.all(): + self.fake_album_attrs(profile) + + """ + To test: + album model is built + albums are associated with users + assigning values to album attributes + presence of string method + photo creation and modified date/times are now + """ + + +class FrontEndTestCase(TestCase): + """Front end tests.""" + + def setUp(self): + """Set up client and requestfactory.""" + self.client = Client() + self.request = RequestFactory() + + """ + To test: + Views return 200 + Routes return 200 + all four templates are used + albums are visible in albums.html + correct number of photos and albums are visible + """ diff --git a/imagersite/imager_profile/tests.py b/imagersite/imager_profile/tests.py index 11a80e6..8ebf27a 100644 --- a/imagersite/imager_profile/tests.py +++ b/imagersite/imager_profile/tests.py @@ -13,7 +13,7 @@ class Meta: model = User - username = factory.Sequence(lambda n: "The Chosen {}".format(n)) + username = factory.Sequence(lambda n: "Prisoner number {}".format(n)) email = factory.LazyAttribute( lambda x: "{}@foo.com".format(x.username.replace(" ", "")) ) @@ -24,7 +24,6 @@ class ProfileTestCase(TestCase): def setUp(self): """The appropriate setup for the appropriate test.""" - self.foo = "bar" self.users = [UserFactory.create() for i in range(20)] def test_profile_is_made_when_user_is_saved(self): @@ -59,7 +58,7 @@ def test_inactive_users_not_counted(self): deactivated_user.save() self.assertTrue(ImagerProfile.active.count() == User.objects.count() - 1) - def test_imagerprofile_attributes(self): + def test_imagerprofile_attributes(self): """Test that ImagerProfile has the expected attributes.""" attribute_list = ["user", "camera_type", "address", "bio", "personal_website", "for_hire", "travel_distance", "phone_number", "photography_type"] for item in attribute_list: @@ -78,35 +77,40 @@ def test_field_type(self): class FrontendTestCases(TestCase): """Test the frontend of the imager_profile site.""" + def setUp(self): """Set up client and request factory.""" self.client = Client() self.request = RequestFactory() + def test_home_view_status(self): """Test home view has 200 status.""" from imagersite.views import home_view req = self.request.get("/route") response = home_view(req) self.assertTrue(response.status_code == 200) + def test_home_route_status(self): """Test home route has 200 status.""" response = self.client.get("/") self.assertTrue(response.status_code == 200) + def test_home_route_templates(self): """Test the home route templates are correct.""" response = self.client.get("/") - self.assertTemplateUsed(response, "imagersite/base.html") self.assertTemplateUsed(response, "imagersite/home.html") + def test_login_template(self): """Test the login route templates are correct.""" response = self.client.get("/login/") - self.assertTemplateUsed(response, "imagersite/base.html") self.assertTemplateUsed(response, "registration/login.html") + def test_registration_template(self): """Test the login route templates are correct.""" response = self.client.get("/accounts/register/") self.assertTemplateUsed(response, "imagersite/base.html") self.assertTemplateUsed(response, "registration/registration_form.html") + def test_login_redirect_code(self): """Test built-in login route redirects properly.""" user_register = UserFactory.create() @@ -119,6 +123,7 @@ def test_login_redirect_code(self): "password": "potatoes" }) self.assertRedirects(response, '/') + def test_register_user(self): """Test that tests can register users.""" self.assertTrue(User.objects.count() == 0) @@ -129,6 +134,7 @@ def test_register_user(self): "password2": "rutabega" }) self.assertTrue(User.objects.count() == 1) + def test_new_user_inactive(self): """Test django-created user starts as inactive.""" self.client.post("/accounts/register/", { @@ -139,6 +145,7 @@ def test_new_user_inactive(self): }) inactive_user = User.objects.first() self.assertFalse(inactive_user.is_active) + def test_registration_redirect(self): """Test redirect on registration.""" response = self.client.post("/accounts/register/", { @@ -148,6 +155,7 @@ def test_registration_redirect(self): "password2": "rutabega" }) self.assertTrue(response.status_code == 302) + def test_registration_reidrect_home(self): """Test registration redirects home.""" response = self.client.post("/accounts/register/", { @@ -160,6 +168,7 @@ def test_registration_reidrect_home(self): response, "/accounts/register/complete/" ) + def test_logout_redirects_to_home(self): """Test logging out redirects to home.""" user_register = UserFactory.create() diff --git a/imagersite/imagersite/settings.py b/imagersite/imagersite/settings.py index f60fad2..4274c76 100644 --- a/imagersite/imagersite/settings.py +++ b/imagersite/imagersite/settings.py @@ -86,7 +86,7 @@ # 'HOST': '127.0.0.1', # 'PORT': '5432', 'TEST': { - 'NAME': os.environ['TEST_IMAGER'] + 'NAME': os.environ['TEST_IMAGER_DATABASE'] } } } From 4c67d4643bb57416ee380c669b018aa6d9dd179a Mon Sep 17 00:00:00 2001 From: pasaunders Date: Sat, 28 Jan 2017 19:45:23 -0800 Subject: [PATCH 44/50] setUp methods updated --- imagersite/imager_images/tests.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/imagersite/imager_images/tests.py b/imagersite/imager_images/tests.py index f3f9304..00b4f2b 100644 --- a/imagersite/imager_images/tests.py +++ b/imagersite/imager_images/tests.py @@ -3,7 +3,6 @@ from django.contrib.auth.models import User from django.db import models from imager_images.models import Photo, Album -from django.core.files.uploadedfile import SimpleUploadedFile import factory @@ -42,7 +41,7 @@ class Meta: model = Album title = factory.Sequence(lambda n: "Album number {}".format(n)) - description = factory.LazyAttribute(lambda a: '{} is a photo'.format(a.title)) + description = factory.LazyAttribute(lambda a: '{} is an album'.format(a.title)) class PhotoTestCase(TestCase): @@ -51,7 +50,7 @@ class PhotoTestCase(TestCase): def setUp(self): """The appropriate setup for the appropriate test.""" self.users = [UserFactory.create() for i in range(20)] - self.photos = [PhotoFactory.create() for i in range(5)] + self.photos = [PhotoFactory.create() for i in range(20)] """ To test: @@ -81,8 +80,8 @@ class AlbumTestCase(TestCase): def setUp(self): """The appropriate setup for the appropriate test.""" self.users = [UserFactory.create() for i in range(20)] - for profile in Album.objects.all(): - self.fake_album_attrs(profile) + self.photos = [PhotoFactory.create() for i in range(20)] + self.album = [AlbumFacotory.create() for i in range(20)] """ To test: From fa770835437a06a99fe6f55e4f48d7e724cc3cf7 Mon Sep 17 00:00:00 2001 From: Amos Boldor Date: Sun, 29 Jan 2017 12:39:16 -0800 Subject: [PATCH 45/50] add test image and made more tests for image model --- imagersite/imager_images/test_img.jpg | Bin 0 -> 73232 bytes imagersite/imager_images/tests.py | 68 +++++++++++++++++++++++++- 2 files changed, 66 insertions(+), 2 deletions(-) create mode 100644 imagersite/imager_images/test_img.jpg diff --git a/imagersite/imager_images/test_img.jpg b/imagersite/imager_images/test_img.jpg new file mode 100644 index 0000000000000000000000000000000000000000..fb59ba4cb87cbdb507f262a794acf204b3ae2c07 GIT binary patch literal 73232 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�YK48=*R9U@nu< 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 Date: Sun, 29 Jan 2017 12:47:11 -0800 Subject: [PATCH 46/50] add code so that photos can displayed --- imagersite/imagersite/urls.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/imagersite/imagersite/urls.py b/imagersite/imagersite/urls.py index 16ad8dc..48b5476 100644 --- a/imagersite/imagersite/urls.py +++ b/imagersite/imagersite/urls.py @@ -18,6 +18,8 @@ admin, auth ) +from django.conf import settings +from django.conf.urls.static import static from imagersite.views import ( home_view ) @@ -28,4 +30,4 @@ 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') -] +] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) From a901ceb47f7857fa178e5705210cb33a582da7a3 Mon Sep 17 00:00:00 2001 From: Amos Boldor Date: Sun, 29 Jan 2017 12:49:11 -0800 Subject: [PATCH 47/50] made migrations from updated model --- .../migrations/0002_auto_20170129_1248.py | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 imagersite/imager_images/migrations/0002_auto_20170129_1248.py diff --git a/imagersite/imager_images/migrations/0002_auto_20170129_1248.py b/imagersite/imager_images/migrations/0002_auto_20170129_1248.py new file mode 100644 index 0000000..a14027a --- /dev/null +++ b/imagersite/imager_images/migrations/0002_auto_20170129_1248.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2017-01-29 20:48 +from __future__ import unicode_literals + +from django.db import migrations, models +import imager_images.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('imager_images', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='album', + name='date_published', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AlterField( + model_name='album', + name='published', + field=models.CharField(choices=[('private', 'private'), ('shared', 'shared'), ('public', 'public')], default='public', max_length=10), + ), + migrations.AlterField( + model_name='photo', + name='date_modified', + field=models.DateTimeField(auto_now=True, null=True), + ), + migrations.AlterField( + model_name='photo', + name='date_published', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AlterField( + model_name='photo', + name='date_uploaded', + field=models.DateTimeField(auto_now_add=True, null=True), + ), + migrations.AlterField( + model_name='photo', + name='description', + field=models.TextField(blank=True, max_length=120, null=True), + ), + migrations.AlterField( + model_name='photo', + name='image', + field=models.ImageField(blank=True, null=True, upload_to=imager_images.models.image_path), + ), + migrations.AlterField( + model_name='photo', + name='published', + field=models.CharField(choices=[('private', 'private'), ('shared', 'shared'), ('public', 'public')], default='public', max_length=10), + ), + ] From 61bdd5acac54fe420f9f90e68f9a34ee3c0ca253 Mon Sep 17 00:00:00 2001 From: Amos Boldor Date: Sun, 29 Jan 2017 12:50:52 -0800 Subject: [PATCH 48/50] finished str test --- imagersite/imager_images/tests.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/imagersite/imager_images/tests.py b/imagersite/imager_images/tests.py index 2139065..6b793df 100644 --- a/imagersite/imager_images/tests.py +++ b/imagersite/imager_images/tests.py @@ -68,11 +68,12 @@ def test_photo_made_when_saved(self): def test_photo_associated_with_user(self): """Test that a photo is attached to a user.""" photo = Photo.objects.first() - self.assertTrue(hasattr(photo, "user")) - self.assertIsInstance(photo.user, User) + self.assertTrue(hasattr(photo, "__str__")) def test_photo_has_str(self): """Test photo model includes string method.""" + photo = Photo.objects.first() + self.assertTrue(hasattr(photo, "user")) def test_image_title(self): """Test that the image has a title.""" From 67bf70aeaada55ce931577f18ea889dc1ad48da8 Mon Sep 17 00:00:00 2001 From: Amos Boldor Date: Sun, 29 Jan 2017 14:25:59 -0800 Subject: [PATCH 49/50] made some tests for album model --- imagersite/imager_images/tests.py | 136 ++++++++++++++++++++++++++---- 1 file changed, 119 insertions(+), 17 deletions(-) diff --git a/imagersite/imager_images/tests.py b/imagersite/imager_images/tests.py index 6b793df..9cbf9cc 100644 --- a/imagersite/imager_images/tests.py +++ b/imagersite/imager_images/tests.py @@ -41,6 +41,7 @@ class Meta: model = Album + user = factory.SubFactory(UserFactory) title = factory.Sequence(lambda n: "Album number {}".format(n)) description = factory.LazyAttribute(lambda a: '{} is an album'.format(a.title)) @@ -53,14 +54,6 @@ def setUp(self): self.users = [UserFactory.create() for i in range(20)] self.photos = [PhotoFactory.create() for i in range(20)] - """ - To test: - photo model is built - photos are associated with users - assigning values to photo attributes - presence of string method - """ - def test_photo_made_when_saved(self): """Test photos are added to the database.""" self.assertTrue(Photo.objects.count() == 20) @@ -146,16 +139,125 @@ def setUp(self): """The appropriate setup for the appropriate test.""" self.users = [UserFactory.create() for i in range(20)] self.photos = [PhotoFactory.create() for i in range(20)] - self.album = [AlbumFacotory.create() for i in range(20)] + self.albums = [AlbumFacotory.create() for i in range(20)] - """ - To test: - album model is built - albums are associated with users - assigning values to album attributes - presence of string method - photo creation and modified date/times are now - """ + def test_image_has_no_album(self): + """Test that the image is in an album.""" + image = Photo.objects.first() + self.assertTrue(image.albums.count() == 0) + + def test_image_has_album(self): + """Test that the image is in an album.""" + image = Photo.objects.first() + album = Album.objects.first() + image.albums.add(album) + self.assertTrue(image.albums.count() == 1) + + def test_album_has_no_image(self): + """Test that an album has no image before assignemnt.""" + album = Album.objects.first() + self.assertTrue(album.photos.count() == 0) + + def test_album_has_image(self): + """Test that an album has an image after assignemnt.""" + image = Photo.objects.first() + album = Album.objects.first() + image.albums.add(album) + self.assertTrue(image.albums.count() == 1) + + def test_two_images_have_album(self): + """Test that two images have same album.""" + image1 = Photo.objects.all()[0] + image2 = Photo.objects.all()[1] + album = Album.objects.first() + image1.albums.add(album) + image2.albums.add(album) + image1.save() + image2.save() + self.assertTrue(image1.albums.all()[0] == album) + self.assertTrue(image2.albums.all()[0] == album) + + def test_album_has_two_images(self): + """Test that an album has two images.""" + image1 = Photo.objects.all()[0] + image2 = Photo.objects.all()[1] + album = Album.objects.first() + image1.albums.add(album) + image2.albums.add(album) + image1.save() + image2.save() + self.assertTrue(album.photos.count() == 2) + + def test_image_has_two_albums(self): + """Test that an image has two albums.""" + image = Photo.objects.first() + album1 = Album.objects.all()[0] + album2 = Album.objects.all()[1] + image.albums.add(album1) + image.albums.add(album2) + image.save() + self.assertTrue(image.albums.count() == 2) + + def test_album_title(self): + """Test that the album has a title.""" + self.assertTrue("Album" in Album.objects.first().title) + + def test_album_has_description(self): + """Test that the album description field exists.""" + self.assertTrue("is an album" in Album.objects.first().description) + + def test_album_has_published(self): + """Test that the album published field exists.""" + album = Album.objects.first() + album.published = 'public' + album.save() + self.assertTrue(Album.objects.first().published == "public") + + def test_album_has_user(self): + """Test that album has an user.""" + self.assertTrue(Album.objects.first().user) + + def test_user_has_album(self): + """Test that the user has the album.""" + album = Album.objects.first() + user = User.objects.first() + self.assertTrue(user.albums.count() == 0) + album.user = user + album.save() + self.assertTrue(user.albums.count() == 1) + + def test_two_albums_have_user(self): + """Test two albums have the same user.""" + album1 = Album.objects.all()[0] + album2 = Album.objects.all()[1] + user = User.objects.first() + album1.user = user + album2.user = user + album1.save() + album2.save() + self.assertTrue(album1.user == user) + self.assertTrue(album2.user == user) + + def test_user_has_two_albums(self): + """Test that user has two albums.""" + album1 = Album.objects.all()[0] + album2 = Album.objects.all()[1] + user = User.objects.first() + album1.user = user + album2.user = user + album1.save() + album2.save() + self.assertTrue(user.albums.count() == 2) + + def test_adding_cover_image(self): + """Test that the image is in an album.""" + image = Photo.objects.first() + album = Album.objects.first() + # import ipdb; ipdb.set_trace() + self.assertTrue(album.cover is None) + album.cover = image + album.save() + self.assertTrue(Album.objects.first().cover is not None) class FrontEndTestCase(TestCase): From bd1a7c16bfa7217e9a522ce05e18588fb75d42ff Mon Sep 17 00:00:00 2001 From: Amos Boldor Date: Mon, 30 Jan 2017 14:58:56 -0800 Subject: [PATCH 50/50] update readme --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b1d6c64..9152cf3 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ [![Build Status](https://travis-ci.org/pasaunders/django-imager.svg?branch=front-end-1)](https://travis-ci.org/pasaunders/django-imager) -## Getting Started +# Django Imager +### Getting Started Clone this repository into whatever directory you want to work from. @@ -24,7 +25,7 @@ Once your environment has been activated, make sure to install Django and all of Navigate to the project root, `imagersite`, and apply the migrations for the app. ```bash -(django-imager) $ cd lending_library +(django-imager) $ cd imagersite (django-imager) $ ./manage.py migrate ```