diff --git a/.gitignore b/.gitignore
index 72364f9..ac52e8b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -23,6 +23,11 @@ var/
*.egg-info/
.installed.cfg
*.egg
+bin/
+lib64
+pyvenv.cfg
+share/
+pip-selfcheck.json
# PyInstaller
# Usually these files are written by a python script from a template
@@ -87,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..b625dea 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,44 @@
-# 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
+
+library template requires:
+pip install -e git+https://github.com/mariocesar/sorl-thumbnail.git#egg=sorl-thumbnail
\ 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_20170131_1449.py b/imagersite/imager_images/migrations/0002_auto_20170131_1449.py
new file mode 100644
index 0000000..701c1dd
--- /dev/null
+++ b/imagersite/imager_images/migrations/0002_auto_20170131_1449.py
@@ -0,0 +1,46 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.10.5 on 2017-01-31 22:49
+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_published',
+ field=models.DateTimeField(blank=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/0003_photo_tags.py b/imagersite/imager_images/migrations/0003_photo_tags.py
new file mode 100644
index 0000000..7e82026
--- /dev/null
+++ b/imagersite/imager_images/migrations/0003_photo_tags.py
@@ -0,0 +1,22 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.10.5 on 2017-02-02 02:16
+from __future__ import unicode_literals
+
+from django.db import migrations
+import taggit.managers
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('taggit', '0002_auto_20150616_2121'),
+ ('imager_images', '0002_auto_20170131_1449'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='photo',
+ name='tags',
+ field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags'),
+ ),
+ ]
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..2f2b957
--- /dev/null
+++ b/imagersite/imager_images/models.py
@@ -0,0 +1,71 @@
+"""Album and Photo models."""
+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
+from taggit.managers import TaggableManager
+
+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."""
+
+ tags = TaggableManager()
+ 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)
+ date_modified = models.DateTimeField(auto_now=True)
+ date_published = models.DateTimeField(blank=True, null=True)
+ published = models.CharField(max_length=10, choices=PUBLISHED_OPTIONS, default='public')
+
+ def __str__(self):
+ """Return string description of photo."""
+ 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(blank=True, null=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/add_album.html b/imagersite/imager_images/templates/imager_images/add_album.html
new file mode 100644
index 0000000..c9314a6
--- /dev/null
+++ b/imagersite/imager_images/templates/imager_images/add_album.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/imager_images/templates/imager_images/add_photo.html b/imagersite/imager_images/templates/imager_images/add_photo.html
new file mode 100644
index 0000000..0296efa
--- /dev/null
+++ b/imagersite/imager_images/templates/imager_images/add_photo.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/imager_images/templates/imager_images/album.html b/imagersite/imager_images/templates/imager_images/album.html
new file mode 100644
index 0000000..4fdd2cb
--- /dev/null
+++ b/imagersite/imager_images/templates/imager_images/album.html
@@ -0,0 +1,33 @@
+{% extends 'imagersite/base.html' %}
+{% block content %}
+Your Album:
+
+

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

+
+{% for tag in photo.tags.all %}
+
{{ tag }}
+{% endfor %}
+
+{% 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..2454f54
--- /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..747e33a
--- /dev/null
+++ b/imagersite/imager_images/templates/imager_images/library.html
@@ -0,0 +1,36 @@
+{% extends 'imagersite/base.html' %}
+{% load thumbnail %}
+{% block content %}
+
+{% endblock content %}
+
+{% url 'imager_images:edit_photo' single_photo.id %}
\ No newline at end of file
diff --git a/imagersite/imager_images/templates/imager_images/list.html b/imagersite/imager_images/templates/imager_images/list.html
new file mode 100644
index 0000000..fb49633
--- /dev/null
+++ b/imagersite/imager_images/templates/imager_images/list.html
@@ -0,0 +1,17 @@
+{% extends 'imagersite/base.html' %}
+{% block content %}
+{% if tag %}
+Photos Tagged As {{ tag }}
+{% endif %}
+
+ {% for photo in photo_list %}
+

+ {% endfor %}
+
+
+{% endblock %}
\ 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..6660e7e
--- /dev/null
+++ b/imagersite/imager_images/templates/imager_images/photo.html
@@ -0,0 +1,18 @@
+{% extends 'imagersite/base.html' %}
+{% load thumbnail %}
+{% block content %}
+Photo
+{{ photo.title }}
+
+{{ photo.description }}
+{% for photo in tag_match %}
+
+ {% thumbnail photo.image "200x200" crop="center" as im %}
+
+ {% endthumbnail %}
+{% for tag in photo.tags.all %}
+ {{ tag }}
+{% endfor %}
+{% endfor %}
+
+{% 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..09c9161
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..e49ad6a
--- /dev/null
+++ b/imagersite/imager_images/tests.py
@@ -0,0 +1,422 @@
+"""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 PhotosView, AlbumView, AlbumsView, Library, PhotoView, AddAlbum, AddPhoto, EditAlbum, EditPhoto
+
+
+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))
+ published = 'public'
+
+
+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'
+ )
+ # import pdb; pdb.set_trace()
+ 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.as_view()
+ 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."""
+ response = self.client.get(reverse_lazy('AlbumsView'))
+ 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_photo_id_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 == 404)
+
+ 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_album_id_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('AlbumView',
+ 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]
+ # import pdb; pdb.set_trace()
+ album.published = 'private'
+ album.save()
+ # Album.objects.get(id=album.id)
+ # import pdb; pdb.set_trace()
+ response = self.client.get(reverse_lazy('AlbumView',
+ kwargs={'album_id': album.id}))
+ self.assertTrue(response.status_code == 404)
+
+ 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('AlbumView',
+ 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 a 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..0d57dfc
--- /dev/null
+++ b/imagersite/imager_images/urls.py
@@ -0,0 +1,29 @@
+"""Images urls."""
+from django.conf.urls import url
+from .views import(
+ PhotosView,
+ AlbumView,
+ AlbumsView,
+ Library,
+ PhotoView,
+ AddAlbum,
+ AddPhoto,
+ EditAlbum,
+ EditPhoto,
+ TagListView
+)
+from django.contrib.auth.decorators import login_required
+
+app_name = 'imager_images'
+urlpatterns = [
+ url(r'^photos/add/$', AddPhoto.as_view(), name='AddPhoto'),
+ url(r'^photos/(?P\d+)$', PhotoView.as_view(), name='single_photo'),
+ url(r'^photos/(?P\d+)/edit/$', EditPhoto.as_view(), name='edit_photo'),
+ url(r'^photos/$', PhotosView.as_view(), name='photos'),
+ url(r'^albums/(?P\d+)/$', AlbumView.as_view(), name='AlbumView'),
+ url(r'^albums/(?P\d+)/edit/$', EditAlbum.as_view(), name='edit_album'),
+ url(r'^albums/add/$', AddAlbum.as_view(), name='AddAlbum'),
+ url(r'^albums/$', AlbumsView.as_view(), name='AlbumsView'),
+ url(r'^library/$', login_required(Library.as_view()), name='library'),
+ url(r'^tagged/(?P[-\w]+)/$', TagListView.as_view(), name="tagged_photos")
+]
diff --git a/imagersite/imager_images/views.py b/imagersite/imager_images/views.py
new file mode 100644
index 0000000..e0a2683
--- /dev/null
+++ b/imagersite/imager_images/views.py
@@ -0,0 +1,166 @@
+"""Views for images."""
+from imager_images.models import Photo, Album
+from django.http import HttpResponse # reimplement if we get around to fixing the views, otherwise use below.
+from django.http import Http404
+from django.views.generic import ListView, TemplateView, CreateView, UpdateView
+from django.urls import reverse_lazy
+from django.contrib.auth.mixins import PermissionRequiredMixin, LoginRequiredMixin
+import random
+
+
+class PhotoView(ListView):
+ """Photo View."""
+
+ model = Photo
+ template_name = "imager_images/photo.html"
+
+ def get_context_data(self):
+ """Return photo."""
+ photo = Photo.objects.get(id=self.kwargs['photo_id'])
+ tag_match = []
+ for tag in photo.tags.all():
+ check_photo = Photo.objects.filter(tags__slug=tag).all()
+ if check_photo:
+ next_photo = random.choice(check_photo)
+ if next_photo not in tag_match:
+ tag_match.append(next_photo)
+ if len(tag_match) >= 5:
+ break
+ if photo.published != 'private' or photo.user.username == self.request.user.username:
+ return {"photo": photo, "tag_match": tag_match}
+ else:
+ raise Http404('Unauthorized')
+ return {}
+
+
+class PhotosView(TemplateView):
+ """Photos View."""
+
+ template_name = "imager_images/photos.html"
+
+ def get_context_data(self):
+ """Return photos."""
+ public_photos = []
+ photos = Photo.objects.all()
+ for photo in photos:
+ if photo.published != 'private' or photo.user.username == self.request.user.username:
+ public_photos.append(photo)
+ return {"photos": public_photos}
+
+
+class TagListView(ListView):
+ """The listing for tagged photos."""
+
+ template_name = "imager_images/list.html"
+
+ def get_queryset(self):
+ """Return all photos with __ tag name."""
+ return Photo.objects.filter(tags__slug=self.kwargs.get("slug")).all()
+
+ def get_context_data(self, **kwargs):
+ """Make tag in context and sets it to self.kwargs.get("slug")."""
+ context = super(TagListView, self).get_context_data(**kwargs)
+ context["tag"] = self.kwargs.get("slug")
+ return context
+
+
+class AlbumView(ListView):
+ """Album View."""
+
+ model = Album
+ template_name = "imager_images/album.html"
+
+ def get_context_data(self):
+ """Return album."""
+ album = Album.objects.get(id=self.kwargs['album_id'])
+ if album.published != 'private' or album.user.username == self.request.user.username:
+ return {'album': album}
+ else:
+ raise Http404('Unauthorized')
+ return {}
+
+
+class AlbumsView(TemplateView):
+ """Albums View."""
+
+ template_name = "imager_images/albums.html"
+
+ def get_context_data(self):
+ """Return albums."""
+ public_albums = []
+ albums = Album.objects.all()
+ for album in albums:
+ if album.published != 'private' or album.user.username == self.request.user.username:
+ public_albums.append(album)
+ return {'albums': public_albums}
+
+
+class Library(LoginRequiredMixin, TemplateView):
+ """Library View."""
+
+ login_url = reverse_lazy('login')
+
+ template_name = "imager_images/library.html"
+
+ def get_context_data(self):
+ """Return albums."""
+ albums = self.request.user.albums.all()
+ photos = self.request.user.photos.all()
+ return {'albums': albums, 'photos': photos}
+
+
+class AddAlbum(PermissionRequiredMixin, CreateView):
+ """Add Album."""
+
+ permission_required = "imager_images.add_album"
+
+ template_name = "imager_images/add_album.html"
+ model = Album
+ fields = ['title', "cover", "description", "photos", "published", "date_published"]
+ success_url = reverse_lazy('library')
+
+ def form_valid(self, form):
+ """Make the form user instance the current user."""
+ form.instance.user = self.request.user
+ return super(AddAlbum, self).form_valid(form)
+
+
+class EditAlbum(PermissionRequiredMixin, UpdateView):
+ """Add Album."""
+
+ permission_required = "imager_images.change_album"
+
+ template_name = "imager_images/add_album.html"
+ model = Album
+ fields = ['title', "cover", "description", "photos", "published", "date_published"]
+ success_url = reverse_lazy('library')
+
+
+class AddPhoto(PermissionRequiredMixin, CreateView):
+ """Add a photo."""
+
+ login_url = reverse_lazy('login')
+ permission_required = "imager_images.add_photo"
+
+ template_name = "imager_images/add_photo.html"
+ model = Photo
+ fields = ['image', 'title', 'description', 'date_published', 'published', 'tags']
+ # success_url = '/images/library'
+ success_url = reverse_lazy('imager_images:library')
+
+ def form_valid(self, form):
+ """Make the form user instance the current user."""
+ form.instance.user = self.request.user
+ return super(AddPhoto, self).form_valid(form)
+
+
+class EditPhoto(PermissionRequiredMixin, UpdateView):
+ """Add a photo."""
+
+ permission_required = "imager_images.change_photo"
+
+ template_name = "imager_images/add_photo.html"
+ model = Photo
+ fields = ['image', 'title', 'description', 'date_published', 'published', 'tags']
+ # success_url = '/images/library'
+ success_url = reverse_lazy('imager_images:library')
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_20170201_1816.py b/imagersite/imager_profile/migrations/0008_auto_20170201_1816.py
new file mode 100644
index 0000000..9e75856
--- /dev/null
+++ b/imagersite/imager_profile/migrations/0008_auto_20170201_1816.py
@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.10.5 on 2017-02-02 02:16
+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='travel_distance',
+ field=models.IntegerField(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..9ca95d7
--- /dev/null
+++ b/imagersite/imager_profile/models.py
@@ -0,0 +1,78 @@
+"""Models for imager_profile."""
+
+from django.db import models
+from django.contrib.auth.models import User, Group
+from django.utils.encoding import python_2_unicode_compatible
+from django.db.models.signals import post_save
+from django.dispatch import receiver
+
+# 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')
+ ]
+ camera_type = models.CharField(
+ max_length=10,
+ choices=CAMERA_CHOICES,
+ default=""
+ )
+ address = models.CharField(max_length=70, default="")
+ bio = models.TextField(default="")
+ personal_website = models.URLField(default="")
+ for_hire = models.BooleanField(default=False)
+ travel_distance = models.IntegerField(default=0)
+ phone_number = models.CharField(max_length=20, default="")
+ STYLE_CHOICES = [
+ ('Portrait', 'Portrait'),
+ ('Landscape', 'Landscape'),
+ ('Black and White', 'Black and White'),
+ ('Sport', 'Sport')
+ ]
+ photography_type = models.CharField(
+ max_length=20,
+ choices=STYLE_CHOICES,
+ default=""
+ )
+
+ @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"]:
+ group = Group.objects.get(name="user")
+ instance.groups.add(group)
+ 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..59dc6c5
--- /dev/null
+++ b/imagersite/imager_profile/templates/imager_profile/profile.html
@@ -0,0 +1,27 @@
+{% 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 %}
+
+{% if user.is_authenticated %}
+
+
+
+{% endif %}
+
+{% 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..2584bfc
--- /dev/null
+++ b/imagersite/imager_profile/tests.py
@@ -0,0 +1,163 @@
+"""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
+from faker import Faker
+
+
+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 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)]
+ for profile in ImagerProfile.objects.all():
+ self.fake_profile_attrs(profile)
+
+ def fake_profile_attrs(self, profile):
+ """Build a fake user."""
+ fake = Faker()
+ profile.address = fake.street_name()
+ profile.bio = fake.paragraph()
+ profile.personal_website = fake.url()
+ profile.for_hire = fake.boolean()
+ profile.travel_distance = fake.random_int()
+ profile.phone_number = fake.phone_number()
+ profile.photography_type = 'Nikon'
+ profile.save()
+
+ 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_user_model_has_attributes(self):
+ """Test user attributes are present."""
+ user = User.objects.get(pk=self.users[0].id)
+ self.assertTrue(user.profile.bio)
+
+ # def test_user_model_has_attributes(self): add more of these!
+ # """Test user attributes are present."""
+ # user = User.objects.get(pk=self.users[0].id)
+
+ 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 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_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.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/"
+ )
diff --git a/imagersite/imager_profile/urls.py b/imagersite/imager_profile/urls.py
new file mode 100644
index 0000000..5aa67f5
--- /dev/null
+++ b/imagersite/imager_profile/urls.py
@@ -0,0 +1,9 @@
+"""Profile urls."""
+from django.conf.urls import url
+from .views import Profile, PublicProfile
+from django.contrib.auth.decorators import login_required
+
+urlpatterns = [
+ url(r'^(?P\w+)', PublicProfile.as_view(), name='public_profile'),
+ url(r'^$', login_required(Profile.as_view()), name='private_profile')
+]
diff --git a/imagersite/imager_profile/views.py b/imagersite/imager_profile/views.py
new file mode 100644
index 0000000..256b7a1
--- /dev/null
+++ b/imagersite/imager_profile/views.py
@@ -0,0 +1,27 @@
+"""Profile views."""
+from django.contrib.auth.models import User
+from django.contrib.auth.mixins import LoginRequiredMixin
+from django.urls import reverse_lazy
+from django.views.generic import ListView
+
+PROFILE_TEMPLATE_PATH = "imager_profile/profile.html"
+
+
+class Profile(LoginRequiredMixin, ListView):
+ """The user profile view."""
+
+ login_url = reverse_lazy('login')
+
+ model = User
+ template_name = PROFILE_TEMPLATE_PATH
+
+
+class PublicProfile(ListView):
+ """Public profile view."""
+
+ model = User
+ template_name = PROFILE_TEMPLATE_PATH
+
+ def get_context_data(self):
+ """Return user object."""
+ return {"user": User.objects.get(username=self.kwargs['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..d859ee6
--- /dev/null
+++ b/imagersite/imagersite/settings.py
@@ -0,0 +1,147 @@
+"""
+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',
+ 'taggit'
+]
+
+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']
+ }
+ }
+}
+
+
+# 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..e60fa0f
--- /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..00a8ea2
--- /dev/null
+++ b/imagersite/imagersite/urls.py
@@ -0,0 +1,33 @@
+"""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 django.conf import settings
+from django.conf.urls.static import static
+from django.views.generic import TemplateView
+
+urlpatterns = [
+ url(r'^admin/', admin.site.urls),
+ url(r'^$', TemplateView.as_view(template_name="imagersite/home.html"), 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..27f3173
--- /dev/null
+++ b/imagersite/imagersite/views.py
@@ -0,0 +1,2 @@
+"""Views."""
+from django.shortcuts import render
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..b9510f6
--- /dev/null
+++ b/requirements.pip
@@ -0,0 +1,33 @@
+coverage==4.3.4
+decorator==4.0.11
+Django==1.10.5
+django-bootstrap3==8.1.0
+django-registration==2.2
+django-taggit==0.22.0
+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
+pluggy==0.4.0
+prompt-toolkit==1.0.9
+psycopg2==2.6.2
+ptyprocess==0.5.1
+pudb==2016.2
+py==1.4.32
+Pygments==2.1.3
+pytest==3.0.6
+pytest-cov==2.4.0
+python-dateutil==2.6.0
+simplegeneric==0.8.1
+six==1.10.0
+sorl-thumbnail==12.3
+tox==2.5.0
+traitlets==4.3.1
+urwid==1.3.1
+virtualenv==15.1.0
+wcwidth==0.1.7
diff --git a/src/sorl-thumbnail b/src/sorl-thumbnail
new file mode 160000
index 0000000..7ce76bc
--- /dev/null
+++ b/src/sorl-thumbnail
@@ -0,0 +1 @@
+Subproject commit 7ce76bc1ef798a63cbf89b5df83de4da4b12e98d