diff --git a/.gitignore b/.gitignore
index 8312747..ac52e8b 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
@@ -91,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/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..ffad062
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,18 @@
+language: python
+python:
+ - "2.7"
+ - "3.5"
+
+# command to install dependencies
+install:
+ # - pip install .
+ - pip install -r requirements.pip
+
+services:
+ - postgresql
+
+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
diff --git a/README.md b/README.md
index 552d117..b1d6c64 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,41 @@
-# django-imager
-django introduction assignment
+[](https://travis-ci.org/pasaunders/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`.
+
+Resources we used:
+http://stackoverflow.com/questions/10180764/django-auth-login-problems
\ No newline at end of file
diff --git a/imagersite/MEDIA/.gitkeep b/imagersite/MEDIA/.gitkeep
new file mode 100644
index 0000000..e69de29
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..aae142c
--- /dev/null
+++ b/imagersite/imager_images/admin.py
@@ -0,0 +1,5 @@
+from django.contrib import admin
+from imager_images.models import Album, Photo
+
+admin.site.register(Album)
+admin.site.register(Photo)
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/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/migrations/0002_auto_20170129_1804.py b/imagersite/imager_images/migrations/0002_auto_20170129_1804.py
new file mode 100644
index 0000000..27b7944
--- /dev/null
+++ b/imagersite/imager_images/migrations/0002_auto_20170129_1804.py
@@ -0,0 +1,56 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.10.5 on 2017-01-30 02:04
+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),
+ ),
+ ]
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..40554a7
--- /dev/null
+++ b/imagersite/imager_images/models.py
@@ -0,0 +1,68 @@
+from __future__ import unicode_literals
+from django.utils.encoding import python_2_unicode_compatible
+from django.db import models
+from django.contrib.auth.models import User
+
+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, blank=True, null=True)
+ title = models.CharField(max_length=60)
+ 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."""
+ 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, blank=True)
+ published = models.CharField(max_length=10, choices=PUBLISHED_OPTIONS, default="public")
+
+ def __str__(self):
+ """Return String Representation of Album."""
+ return "{}: Album belonging to {}".format(self.title, self.user)
diff --git a/imagersite/imager_images/templates/imager_images/album.html b/imagersite/imager_images/templates/imager_images/album.html
new file mode 100644
index 0000000..9076d10
--- /dev/null
+++ b/imagersite/imager_images/templates/imager_images/album.html
@@ -0,0 +1,17 @@
+{% extends 'imagersite/base.html' %}
+{% block content %}
+
Your Album:
+
+

+{% for photo in album.photos.all %}
+

+{% endfor %}
+
+{{ album.description }}
+
+{% endblock content %}
\ No newline at end of file
diff --git a/imagersite/imager_images/templates/imager_images/albums.html b/imagersite/imager_images/templates/imager_images/albums.html
new file mode 100644
index 0000000..c6995f8
--- /dev/null
+++ b/imagersite/imager_images/templates/imager_images/albums.html
@@ -0,0 +1,15 @@
+{% extends 'imagersite/base.html' %}
+{% load thumbnail %}
+{% block content %}
+Public Albums
+
+{% endblock content %}
\ No newline at end of file
diff --git a/imagersite/imager_images/templates/imager_images/library.html b/imagersite/imager_images/templates/imager_images/library.html
new file mode 100644
index 0000000..f17dad6
--- /dev/null
+++ b/imagersite/imager_images/templates/imager_images/library.html
@@ -0,0 +1,29 @@
+{% extends 'imagersite/base.html' %}
+{% load thumbnail %}
+{% block content %}
+
+{% endblock content %}
\ No newline at end of file
diff --git a/imagersite/imager_images/templates/imager_images/photo.html b/imagersite/imager_images/templates/imager_images/photo.html
new file mode 100644
index 0000000..7858f2e
--- /dev/null
+++ b/imagersite/imager_images/templates/imager_images/photo.html
@@ -0,0 +1,7 @@
+{% extends 'imagersite/base.html' %}
+{% block content %}
+Photo
+{{ photo.title }}
+
+{{ photo.description }}
+{% endblock content %}
\ No newline at end of file
diff --git a/imagersite/imager_images/templates/imager_images/photos.html b/imagersite/imager_images/templates/imager_images/photos.html
new file mode 100644
index 0000000..643224d
--- /dev/null
+++ b/imagersite/imager_images/templates/imager_images/photos.html
@@ -0,0 +1,15 @@
+{% extends 'imagersite/base.html' %}
+{% block content %}
+Photos
+
+{% for photo in photos %}
+

+{% endfor %}
+
+
+{% endblock content %}
\ No newline at end of file
diff --git a/imagersite/imager_images/test_img.jpg b/imagersite/imager_images/test_img.jpg
new file mode 100644
index 0000000..fb59ba4
Binary files /dev/null and b/imagersite/imager_images/test_img.jpg differ
diff --git a/imagersite/imager_images/tests.py b/imagersite/imager_images/tests.py
new file mode 100644
index 0000000..1910b33
--- /dev/null
+++ b/imagersite/imager_images/tests.py
@@ -0,0 +1,418 @@
+"""Test the imager_images app."""
+from django.test import TestCase, Client, RequestFactory
+from django.contrib.auth.models import User
+from django.core.files.uploadedfile import SimpleUploadedFile
+from imager_images.models import Photo, Album
+import factory
+from django.core.urlresolvers import reverse_lazy
+from .views import photo_view, all_photos, single_album, all_albums, library
+
+
+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
+
+ user = factory.SubFactory(UserFactory)
+ 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
+
+ user = factory.SubFactory(UserFactory)
+ title = factory.Sequence(lambda n: "Album number {}".format(n))
+ description = factory.LazyAttribute(lambda a: '{} is an album'.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(20)]
+
+ def test_photo_made_when_saved(self):
+ """Test photos are added to the database."""
+ self.assertTrue(Photo.objects.count() == 20)
+
+ def test_photo_associated_with_user(self):
+ """Test that a photo is attached to a user."""
+ photo = Photo.objects.first()
+ 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."""
+ self.assertTrue("Photo" in Photo.objects.first().title)
+
+ def test_image_has_description(self):
+ """Test that the Photo description field can be assigned."""
+ image = Photo.objects.first()
+ description = "This is a test of description field."
+ image.description = description
+ image.save()
+ self.assertTrue(Photo.objects.first().description == description)
+
+ def test_image_has_published(self):
+ """Test the image published field."""
+ image = Photo.objects.first()
+ image.published = 'public'
+ image.save()
+ self.assertTrue(Photo.objects.first().published == "public")
+
+ def test_user_has_image(self):
+ """Test that the user has the image."""
+ image = Photo.objects.first()
+ user = User.objects.first()
+ self.assertTrue(user.photos.count() == 0)
+ image.user = user
+ image.save()
+ self.assertTrue(user.photos.count() == 1)
+
+ def test_two_images_have_user(self):
+ """Test two images have the same user."""
+ image1 = Photo.objects.all()[0]
+ image2 = Photo.objects.all()[1]
+ user = User.objects.first()
+ image1.user = user
+ image2.user = user
+ image1.save()
+ image2.save()
+ self.assertTrue(image1.user == user)
+ self.assertTrue(image2.user == user)
+
+ def test_user_has_two_images(self):
+ """Test that user has two image."""
+ image1 = Photo.objects.all()[0]
+ image2 = Photo.objects.all()[1]
+ user = User.objects.first()
+ image1.user = user
+ image2.user = user
+ image1.save()
+ image2.save()
+ self.assertTrue(user.photos.count() == 2)
+
+ def test_user_has_photo_uploaded(self):
+ """Test user has photo uploaded."""
+ photo = self.photos[4]
+ self.assertTrue(photo.image.name is None)
+ image = SimpleUploadedFile(
+ name='test_image.jpg',
+ content=open('imager_images/test_img.jpg', 'rb').read(),
+ content_type='image/jpeg'
+ )
+ photo.image = image
+ self.assertTrue(photo.image.name is not None)
+
+
+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)]
+ self.photos = [PhotoFactory.create() for i in range(20)]
+ self.albums = [AlbumFacotory.create() for i in range(20)]
+
+ 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()
+ self.assertTrue(album.cover is None)
+ album.cover = image
+ album.save()
+ self.assertTrue(Album.objects.first().cover is not None)
+
+
+class FrontEndTestCase(TestCase):
+ """Front end tests."""
+
+ def setUp(self):
+ """Set up client and requestfactory."""
+ self.client = Client()
+ self.request = RequestFactory()
+ self.users = [UserFactory.create() for i in range(20)]
+ self.photos = [PhotoFactory.create() for i in range(20)]
+ self.albums = [AlbumFacotory.create() for i in range(20)]
+
+ """
+ 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
+ """
+ def test_libary_view_returns_200(self):
+ """Test Library View returns a 200."""
+ user = UserFactory.create()
+ user.save()
+ view = library
+ req = self.request.get(reverse_lazy('library'))
+ req.user = user
+ response = view(req)
+ self.assertTrue(response.status_code == 200)
+
+ def test_logged_in_user_has_library(self):
+ """A logged in user gets a 200 resposne."""
+ user = UserFactory.create()
+ user.save()
+ self.client.force_login(user)
+ response = self.client.get(reverse_lazy("library"))
+ self.assertTrue(response.status_code == 200)
+
+ def test_logged_in_user_sees_their_albums(self):
+ """Test that a logged in user can see their images in library."""
+ user = UserFactory.create()
+ album1 = Album.objects.first()
+ user.albums.add(album1)
+ user.save()
+ self.client.force_login(user)
+ response = self.client.get(reverse_lazy("library"))
+ self.assertTrue(album1.title in str(response.content))
+
+ def test_album_view_returns_200(self):
+ """Test that the album view returns a 200."""
+ req = self.request.get(reverse_lazy('all_albums'))
+ response = all_albums(req)
+ self.assertTrue(response.status_code == 200)
+
+ def test_photoid_view_returns_200(self):
+ """Test that the photo id view returns a 200."""
+ photo = self.photos[6]
+ image = SimpleUploadedFile(
+ name='test_image.jpg',
+ content=open('imager_images/test_img.jpg', 'rb').read(),
+ content_type='image/jpeg'
+ )
+ photo.image = image
+ photo.save()
+ response = self.client.get(reverse_lazy('single_photo',
+ kwargs={'photo_id': photo.id}))
+ self.assertTrue(response.status_code == 200)
+
+ def test_photoid_view_returns_error_private_photo(self):
+ """Test that a user cannot view a private photo of another user."""
+ photo = self.photos[12]
+ photo.published = 'private'
+ image = SimpleUploadedFile(
+ name='test_image.jpg',
+ content=open('imager_images/test_img.jpg', 'rb').read(),
+ content_type='image/jpeg'
+ )
+ photo.image = image
+ photo.save()
+ response = self.client.get(reverse_lazy('single_photo',
+ kwargs={'photo_id': photo.id}))
+ self.assertTrue(response.status_code == 401)
+
+ def test_photo_id_user_views_own_private_photo(self):
+ """Test that a user can view their own private photo."""
+ user = self.users[2]
+ user.save()
+ self.client.force_login(user)
+ photo = self.photos[15]
+ photo.published = 'private'
+ photo.user = user
+ image = SimpleUploadedFile(
+ name='test_image.jpg',
+ content=open('imager_images/test_img.jpg', 'rb').read(),
+ content_type='image/jpeg'
+ )
+ photo.image = image
+ photo.save()
+ response = self.client.get(reverse_lazy('single_photo',
+ kwargs={'photo_id': photo.id}))
+ self.assertTrue(response.status_code == 200)
+
+ def test_albumid_view_returns_200(self):
+ """Test that the album id view returns a 200."""
+ user = self.users[0]
+ self.client.force_login(user)
+ album = self.albums[9]
+ album.user = user
+ album.save()
+ response = self.client.get(reverse_lazy('single_album',
+ kwargs={'album_id': album.id}))
+ self.assertTrue(response.status_code == 200)
+
+ def test_album_id_view_doesnt_return_private_album(self):
+ """Test that a user cannot view a private album."""
+ album = self.albums[9]
+ album.published = 'private'
+ album.save()
+ response = self.client.get(reverse_lazy('single_album',
+ kwargs={'album_id': album.id}))
+ self.assertTrue(response.status_code == 401)
+
+ def test_description_of_album_shows(self):
+ """Test that the description of an album shows."""
+ album = self.albums[17]
+ response = self.client.get(reverse_lazy('single_album',
+ kwargs={'album_id': album.id}))
+ self.assertTrue('is an album' in response.content.decode())
+
+ def test_description_of_photo_shows(self):
+ """Test that the description of an photo shows."""
+ photo = self.photos[17]
+ image = SimpleUploadedFile(
+ name='test_image.jpg',
+ content=open('imager_images/test_img.jpg', 'rb').read(),
+ content_type='image/jpeg'
+ )
+ photo.image = image
+ photo.save()
+ response = self.client.get(reverse_lazy('single_photo',
+ kwargs={'photo_id': photo.id}))
+ self.assertTrue('is a photo' in response.content.decode())
+
+ def test_title_of_photo_shows(self):
+ """Test that the title of an photo shows."""
+ photo = self.photos[17]
+ image = SimpleUploadedFile(
+ name='test_image.jpg',
+ content=open('imager_images/test_img.jpg', 'rb').read(),
+ content_type='image/jpeg'
+ )
+ photo.image = image
+ photo.save()
+ response = self.client.get(reverse_lazy('single_photo',
+ kwargs={'photo_id': photo.id}))
+ self.assertTrue('Photo number' in response.content.decode())
diff --git a/imagersite/imager_images/urls.py b/imagersite/imager_images/urls.py
new file mode 100644
index 0000000..803dd0c
--- /dev/null
+++ b/imagersite/imager_images/urls.py
@@ -0,0 +1,11 @@
+"""Images urls."""
+from django.conf.urls import url
+from .views import photo_view, all_photos, single_album, all_albums, library
+
+urlpatterns = [
+ url(r'^photos/(?P\d+)', photo_view, name='single_photo'),
+ url(r'^photos/$', all_photos, name='private_profile'), # display all of the public photos that have been uploaded
+ url(r'^albums/(?P\d+)/$', single_album, name='single_album'), # display a single selected album
+ url(r'^albums/$', all_albums, name='all_albums'),
+ url(r'^library/$', library, name='library'),
+]
diff --git a/imagersite/imager_images/views.py b/imagersite/imager_images/views.py
new file mode 100644
index 0000000..de6ccf0
--- /dev/null
+++ b/imagersite/imager_images/views.py
@@ -0,0 +1,59 @@
+"""Views for images."""
+from django.shortcuts import render
+from imager_images.models import Photo, Album
+from django.http import HttpResponse
+from django.contrib.auth.decorators import login_required
+
+
+def photo_view(request, photo_id):
+ """Render individual image by id."""
+ photo = Photo.objects.get(id=photo_id)
+ if photo.published != 'private' or photo.user.username == request.user.username:
+ return render(request, 'imager_images/photo.html', {"photo": photo})
+ return HttpResponse('Unauthorized', status=401)
+
+
+def all_photos(request):
+ """Render all photos in app."""
+ public_photos = []
+ photos = Photo.objects.all()
+ for photo in photos:
+ if photo.published != 'private' or photo.user.username == request.user.username:
+ public_photos.append(photo)
+ return render(request, 'imager_images/photos.html', {"photos": public_photos})
+
+
+def single_album(request, album_id):
+ """Render a specific album."""
+ album = Album.objects.get(id=album_id)
+ if album.published != 'private' or album.user.username == request.user.username:
+ return render(
+ request,
+ 'imager_images/album.html',
+ {'album': album}
+ )
+ return HttpResponse('Unauthorized', status=401)
+
+
+def all_albums(request):
+ """Render all public albums."""
+ public_albums = []
+ albums = Album.objects.all()
+ for album in albums:
+ if album.published != 'private' or album.user.username == request.user.username:
+ public_albums.append(album)
+ return render(
+ request,
+ 'imager_images/albums.html',
+ {'albums': public_albums}
+ )
+
+
+@login_required(login_url='/accounts/login/')
+def library(request):
+ """Library view."""
+ albums = request.user.albums.all()
+ photos = request.user.photos.all()
+ return render(request,
+ 'imager_images/library.html',
+ context={'albums': albums, 'photos': photos})
diff --git a/imagersite/imager_profile/__init__.py b/imagersite/imager_profile/__init__.py
new file mode 100644
index 0000000..4a991a4
--- /dev/null
+++ b/imagersite/imager_profile/__init__.py
@@ -0,0 +1 @@
+default_app_config = 'imager_profile.apps.ImagerProfileConfig'
diff --git a/imagersite/imager_profile/admin.py b/imagersite/imager_profile/admin.py
new file mode 100644
index 0000000..8295835
--- /dev/null
+++ b/imagersite/imager_profile/admin.py
@@ -0,0 +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/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/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/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=''),
+ ),
+ ]
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/migrations/0004_imagerprofile_address.py b/imagersite/imager_profile/migrations/0004_imagerprofile_address.py
new file mode 100644
index 0000000..3df6819
--- /dev/null
+++ b/imagersite/imager_profile/migrations/0004_imagerprofile_address.py
@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.10.5 on 2017-01-22 00:01
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('imager_profile', '0003_auto_20170119_1823'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='imagerprofile',
+ name='address',
+ field=models.CharField(blank=True, max_length=40, null=True),
+ ),
+ ]
diff --git a/imagersite/imager_profile/migrations/0005_auto_20170121_1732.py b/imagersite/imager_profile/migrations/0005_auto_20170121_1732.py
new file mode 100644
index 0000000..7411c93
--- /dev/null
+++ b/imagersite/imager_profile/migrations/0005_auto_20170121_1732.py
@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.10.5 on 2017-01-22 01:32
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('imager_profile', '0004_imagerprofile_address'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='imagerprofile',
+ name='photography_type',
+ field=models.CharField(blank=True, choices=[('Portrait', 'Portrait'), ('Landscape', 'Landscape'), ('Black and White', 'Black and White'), ('Sport', 'Sport')], max_length=20, null=True),
+ ),
+ ]
diff --git a/imagersite/imager_profile/migrations/0006_auto_20170124_1837.py b/imagersite/imager_profile/migrations/0006_auto_20170124_1837.py
new file mode 100644
index 0000000..ad6d0dd
--- /dev/null
+++ b/imagersite/imager_profile/migrations/0006_auto_20170124_1837.py
@@ -0,0 +1,25 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.10.5 on 2017-01-25 02:37
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('imager_profile', '0005_auto_20170121_1732'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='imagerprofile',
+ name='address',
+ field=models.CharField(blank=True, max_length=70, null=True),
+ ),
+ migrations.AlterField(
+ model_name='imagerprofile',
+ name='phone_number',
+ field=models.CharField(blank=True, max_length=17, null=True),
+ ),
+ ]
diff --git a/imagersite/imager_profile/migrations/0007_auto_20170124_1838.py b/imagersite/imager_profile/migrations/0007_auto_20170124_1838.py
new file mode 100644
index 0000000..38e5473
--- /dev/null
+++ b/imagersite/imager_profile/migrations/0007_auto_20170124_1838.py
@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.10.5 on 2017-01-25 02:38
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('imager_profile', '0006_auto_20170124_1837'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='imagerprofile',
+ name='phone_number',
+ field=models.CharField(blank=True, max_length=20, null=True),
+ ),
+ ]
diff --git a/imagersite/imager_profile/migrations/0008_auto_20170129_1804.py b/imagersite/imager_profile/migrations/0008_auto_20170129_1804.py
new file mode 100644
index 0000000..2ed8ecb
--- /dev/null
+++ b/imagersite/imager_profile/migrations/0008_auto_20170129_1804.py
@@ -0,0 +1,40 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.10.5 on 2017-01-30 02:04
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('imager_profile', '0007_auto_20170124_1838'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='imagerprofile',
+ name='address',
+ field=models.CharField(blank=True, default='', max_length=70, null=True),
+ ),
+ migrations.AlterField(
+ model_name='imagerprofile',
+ name='camera_type',
+ field=models.CharField(blank=True, choices=[('Nikon', 'Nikon'), ('iPhone', 'iPhone'), ('Canon', 'Canon'), ('--------', '--------')], default='--------', max_length=10),
+ ),
+ migrations.AlterField(
+ model_name='imagerprofile',
+ name='phone_number',
+ field=models.CharField(blank=True, default='', max_length=15),
+ ),
+ migrations.AlterField(
+ model_name='imagerprofile',
+ name='photography_type',
+ field=models.CharField(blank=True, default='', max_length=20),
+ ),
+ migrations.AlterField(
+ model_name='imagerprofile',
+ name='travel_distance',
+ field=models.IntegerField(blank=True, default=0),
+ ),
+ ]
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..72578c9
--- /dev/null
+++ b/imagersite/imager_profile/models.py
@@ -0,0 +1,73 @@
+"""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
+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', 'Nikon'),
+ ('iPhone', 'iPhone'),
+ ('Canon', 'Canon'),
+ ('--------', '--------')
+ ]
+ TYPE_OF_PHOTOGRAPHY = [
+ ('nature', 'nature'),
+ ('urban', 'urban'),
+ ('portraits', 'portraits')
+ ]
+ camera_type = models.CharField(
+ max_length=10,
+ choices=CAMERA_CHOICES,
+ blank=True,
+ default='--------'
+ )
+ 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(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):
+ """Return True if user associated with this profile is active."""
+ return self.user.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):
+ """Called when user is made and hooks that user to a profile."""
+ if kwargs["created"]:
+ new_profile = ImagerProfile(user=instance)
+ new_profile.save()
diff --git a/imagersite/imager_profile/templates/imager_profile/profile.html b/imagersite/imager_profile/templates/imager_profile/profile.html
new file mode 100644
index 0000000..b094d75
--- /dev/null
+++ b/imagersite/imager_profile/templates/imager_profile/profile.html
@@ -0,0 +1,22 @@
+{% extends 'imagersite/base.html' %}
+{% block content %}
+
+Welcome {{user.username}}!
+
+ - Name: {{user.first_name|title}} {{user.last_name|title}}
+ - Email: {{user.email}}
+ - Bio: {{user.profile.bio}}
+ - Website: {{user.profile.personal_website}}
+ - For Hire: {{user.profile.for_hire}}
+ - Travel Distance: {{user.profile.travel_distance}} Miles
+ - Phone Number: {{user.profile.phone_number}}
+ - Photography Type: {{user.profile.photography_type}}
+ - Camera: {{user.profile.camera_type}}
+ {% ifequal request.get_full_path '/profile/' %}
+ - Location: {{user.profile.address}}
+ - Photos Uploaded: {{user.photos.count}}
+
- Albums Created: {{user.albums.count}}
+ {% endifequal %}
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/imagersite/imager_profile/tests.py b/imagersite/imager_profile/tests.py
new file mode 100644
index 0000000..6a034fc
--- /dev/null
+++ b/imagersite/imager_profile/tests.py
@@ -0,0 +1,184 @@
+"""Tests for the imager_profile app."""
+from django.test import TestCase, Client, RequestFactory
+from django.contrib.auth.models import User
+from imager_profile.models import ImagerProfile
+import factory
+
+
+class UserFactory(factory.django.DjangoModelFactory):
+ """Makes users."""
+
+ class Meta:
+ """Metadata for UserFactory."""
+
+ model = User
+
+ username = factory.Sequence(lambda n: "Prisoner number {}".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.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."""
+ self.assertTrue(ImagerProfile.objects.count() == 20)
+
+ 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)
+
+ def test_user_model_has_str(self):
+ """Test user has a string method."""
+ user = self.users[0]
+ self.assertIsInstance(str(user), str)
+
+ 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)
+
+ 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/home.html")
+
+ def test_login_template(self):
+ """Test the login route templates are correct."""
+ response = self.client.get("/login/")
+ 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.set_password("potatoes")
+ user_register.save()
+ response = self.client.post("/login/", {
+ "username": user_register.username,
+ "password": "potatoes"
+ })
+ self.assertRedirects(response, '/profile/')
+
+ def test_register_user(self):
+ """Test that tests can register users."""
+ self.assertTrue(User.objects.count() == 0)
+ 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("/accounts/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("/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("/accounts/register/", {
+ "username": "Sir_Joseph",
+ "email": "e@mail.com",
+ "password1": "rutabega",
+ "password2": "rutabega"
+ }, follow=True)
+ 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/imager_profile/urls.py b/imagersite/imager_profile/urls.py
new file mode 100644
index 0000000..faa8a1e
--- /dev/null
+++ b/imagersite/imager_profile/urls.py
@@ -0,0 +1,8 @@
+"""Profile urls."""
+from django.conf.urls import url
+from .views import public_profile, profile_view
+
+urlpatterns = [
+ url(r'^(?P\w+)', public_profile, name='public_profile'),
+ url(r'^$', profile_view, name='private_profile')
+]
diff --git a/imagersite/imager_profile/views.py b/imagersite/imager_profile/views.py
new file mode 100644
index 0000000..18d42d0
--- /dev/null
+++ b/imagersite/imager_profile/views.py
@@ -0,0 +1,21 @@
+"""Profile views."""
+from django.shortcuts import render
+from django.contrib.auth.decorators import login_required
+from django.contrib.auth.models import User
+
+
+@login_required(login_url='/accounts/login/')
+def profile_view(request):
+ """The user profile view."""
+ return render(request,
+ "imager_profile/profile.html",
+ {"user": request.user}
+ )
+
+
+def public_profile(request, username):
+ """Public profile view."""
+ return render(request,
+ "imager_profile/profile.html",
+ {"user": User.objects.get(username=username)}
+ )
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..b625d95
--- /dev/null
+++ b/imagersite/imagersite/settings.py
@@ -0,0 +1,146 @@
+"""
+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',
+ 'bootstrap3',
+ 'imager_profile',
+ 'imagersite',
+ 'imager_images',
+ 'sorl.thumbnail'
+]
+
+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_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_DATABASE']
+ }
+ }
+}
+
+
+# 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 = 'America/Los_Angeles'
+
+USE_I18N = True
+
+USE_L10N = True
+
+USE_TZ = True
+
+
+# Static files (CSS, JavaScript, Images)
+# https://docs.djangoproject.com/en/1.10/howto/static-files/
+
+STATIC_URL = '/static/'
+
+
+# Registration Settings
+ACCOUNT_ACTIVATION_DAYS = 3
+EMAIL_HOST = '127.0.0.1'
+EMAIL_PORT = 1025
+
+# Login/out settings
+LOGIN_REDIRECT_URL = '/profile/'
+MEDIA_ROOT = os.path.join(BASE_DIR, 'MEDIA')
+MEDIA_URL = "/media/"
+THUMBNAIL_DEBUG = True
+
+EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
diff --git a/imagersite/imagersite/templates/imagersite/album.html b/imagersite/imagersite/templates/imagersite/album.html
new file mode 100644
index 0000000..93ed697
--- /dev/null
+++ b/imagersite/imagersite/templates/imagersite/album.html
@@ -0,0 +1,4 @@
+{% extends 'imagersite/base.html' %}
+{% block content %}
+Album Page
+{% endblock content %}
\ No newline at end of file
diff --git a/imagersite/imagersite/templates/imagersite/base.html b/imagersite/imagersite/templates/imagersite/base.html
new file mode 100644
index 0000000..462657c
--- /dev/null
+++ b/imagersite/imagersite/templates/imagersite/base.html
@@ -0,0 +1,37 @@
+
+
+
+ App
+
+
+ {% load bootstrap3 %}
+ {% bootstrap_css %}
+ {% bootstrap_javascript %}
+
+
+
+
+{% 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..367883c
--- /dev/null
+++ b/imagersite/imagersite/templates/imagersite/home.html
@@ -0,0 +1,4 @@
+{% extends 'imagersite/base.html' %}
+{% block content %}
+Home Page
+{% endblock content %}
\ No newline at end of file
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..fcc3f0c
--- /dev/null
+++ b/imagersite/imagersite/templates/registration/activation_complete.html
@@ -0,0 +1,4 @@
+{% extends 'imagersite/base.html' %}
+{% block content %}
+Activation complete, welcome to imagersite
+{% 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/login.html b/imagersite/imagersite/templates/registration/login.html
new file mode 100644
index 0000000..f66f308
--- /dev/null
+++ b/imagersite/imagersite/templates/registration/login.html
@@ -0,0 +1,17 @@
+{% extends 'imagersite/base.html' %}
+{% block content %}
+{% load bootstrap3 %}
+
+{% endblock content %}
\ 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..46c770c
--- /dev/null
+++ b/imagersite/imagersite/templates/registration/registration_form.html
@@ -0,0 +1,9 @@
+{% extends 'imagersite/base.html' %}
+{% block content %}
+{% load bootstrap3 %}
+
+{% endblock content %}
\ No newline at end of file
diff --git a/imagersite/imagersite/urls.py b/imagersite/imagersite/urls.py
new file mode 100644
index 0000000..1273391
--- /dev/null
+++ b/imagersite/imagersite/urls.py
@@ -0,0 +1,35 @@
+"""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 include, url
+from django.contrib import (
+ admin,
+ auth
+)
+from imagersite.views import (
+ home_view
+)
+from django.conf import settings
+from django.conf.urls.static import static
+
+urlpatterns = [
+ url(r'^admin/', admin.site.urls),
+ url(r'^$', home_view, name='homepage'),
+ url(r'^accounts/', include('registration.backends.hmac.urls')),
+ url(r'^login/', auth.views.login, name='login'),
+ url(r'^logout/', auth.views.logout, {'next_page': '/'}, name='logout'),
+ url(r'^profile/', include('imager_profile.urls')),
+ url(r'^images/', include('imager_images.urls'))
+] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
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"
+ )
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)
diff --git a/requirements.pip b/requirements.pip
new file mode 100644
index 0000000..261e22a
--- /dev/null
+++ b/requirements.pip
@@ -0,0 +1,26 @@
+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
+ipdb==0.10.1
+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
+pudb==2016.2
+Pygments==2.1.3
+python-dateutil==2.6.0
+simplegeneric==0.8.1
+six==1.10.0
+sorl-thumbnail==12.3
+traitlets==4.3.1
+urwid==1.3.1
+wcwidth==0.1.7