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 +[![Build Status](https://travis-ci.org/pasaunders/django-imager.svg?branch=front-end-1)](https://travis-ci.org/pasaunders/django-imager) +## Getting Started + +Clone this repository into whatever directory you want to work from. + +```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 %} +
+ {% bootstrap_form form %} + {% csrf_token %} + {% bootstrap_button "Submit" button_type="submit" button_class="btn-primary" %} +
+{% 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 %} +
+ {% csrf_token %} + {% bootstrap_form form %} + {% bootstrap_button "Submit" button_type="submit" button_class="btn-primary" %} +
+{% 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

+
+{% for album in albums %} +

{{ album.title }}

+ +{% thumbnail album.cover.image "200x200" crop="center" as im %} + +{% endthumbnail %} + +{% endfor %} +
+{% 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 %} +
+ {% if user.first_name %} +

{{user.first_name|title }}'s Library

+ {% else %} +

{{user.username }}'s Library

+ {% endif %} +

Albums:

+ {% for album in albums %} +

{{album.title}}

+ + {% thumbnail album.cover.image "200x200" crop="center" as im %} + + {% endthumbnail %} + + + {% endfor %} +

Images:

+ {% for photo in photos %} +
+ {% thumbnail photo.image "200x200" crop="center" as im %} + + {% endthumbnail %} + + {{single_photo.title}} + + {% for tag in photo.tags.all %} + {{ tag }} + {% endfor %} + {% endfor %} +
+{% 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 %} +
+ {% csrf_token %} +
+ {{ form.username.label_tag }} + {{ form.username }} +
+
+ {{ form.password.label_tag }} + {{ form.password }} +
+ {% bootstrap_button "Login" button_type="submit" button_class="btn-primary" %} + +
+{% 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 %} +
+ {% bootstrap_form form %} + {% csrf_token %} + {% bootstrap_button "Register" button_type="submit" button_class="btn-primary" %} +
+{% 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