diff --git a/.gitignore b/.gitignore index 8312747..ac52e8b 100644 --- a/.gitignore +++ b/.gitignore @@ -27,6 +27,7 @@ bin/ lib64 pyvenv.cfg share/ +pip-selfcheck.json # PyInstaller # Usually these files are written by a python script from a template @@ -91,3 +92,7 @@ ENV/ # Rope project settings .ropeproject + +# Ignore the files in the Media directory, but not the directory itself +imagersite/MEDIA/* +!imagersite/MEDIA/.gitkeep \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..ffad062 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,18 @@ +language: python +python: + - "2.7" + - "3.5" + +# command to install dependencies +install: + # - pip install . + - pip install -r requirements.pip + +services: + - postgresql + +before_script: + - psql -c 'create database travis_ci_test;' -U postgres + +# command to run tests +script: python imagersite/manage.py test \ No newline at end of file diff --git a/README.md b/README.md index 552d117..b1d6c64 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,41 @@ -# django-imager -django introduction assignment +[![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 \ No newline at end of file diff --git a/imagersite/MEDIA/.gitkeep b/imagersite/MEDIA/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/imagersite/imager_images/__init__.py b/imagersite/imager_images/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/imagersite/imager_images/admin.py b/imagersite/imager_images/admin.py new file mode 100644 index 0000000..aae142c --- /dev/null +++ b/imagersite/imager_images/admin.py @@ -0,0 +1,5 @@ +from django.contrib import admin +from imager_images.models import Album, Photo + +admin.site.register(Album) +admin.site.register(Photo) diff --git a/imagersite/imager_images/apps.py b/imagersite/imager_images/apps.py new file mode 100644 index 0000000..7afb70a --- /dev/null +++ b/imagersite/imager_images/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class ImagerImagesConfig(AppConfig): + name = 'imager_images' diff --git a/imagersite/imager_images/migrations/0001_initial.py b/imagersite/imager_images/migrations/0001_initial.py new file mode 100644 index 0000000..1fa1ba8 --- /dev/null +++ b/imagersite/imager_images/migrations/0001_initial.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2017-01-19 05:16 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import imager_images.models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Album', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=60)), + ('description', models.TextField(max_length=200)), + ('date_created', models.DateTimeField(auto_now_add=True)), + ('date_modified', models.DateTimeField(auto_now=True)), + ('date_published', models.DateTimeField(null=True)), + ('published', models.CharField(choices=[('private', 'private'), ('shared', 'shared'), ('public', 'public')], max_length=10)), + ], + ), + migrations.CreateModel( + name='Photo', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('image', models.ImageField(upload_to=imager_images.models.image_path)), + ('title', models.CharField(max_length=60)), + ('description', models.TextField(max_length=120)), + ('date_uploaded', models.DateTimeField(auto_now_add=True)), + ('date_modified', models.DateTimeField(auto_now=True)), + ('date_published', models.DateTimeField(null=True)), + ('published', models.CharField(choices=[('private', 'private'), ('shared', 'shared'), ('public', 'public')], max_length=10)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='photos', to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.AddField( + model_name='album', + name='cover', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='albums_covered', to='imager_images.Photo'), + ), + migrations.AddField( + model_name='album', + name='photos', + field=models.ManyToManyField(related_name='albums', to='imager_images.Photo'), + ), + migrations.AddField( + model_name='album', + name='user', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='albums', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/imagersite/imager_images/migrations/0002_auto_20170129_1804.py b/imagersite/imager_images/migrations/0002_auto_20170129_1804.py new file mode 100644 index 0000000..27b7944 --- /dev/null +++ b/imagersite/imager_images/migrations/0002_auto_20170129_1804.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2017-01-30 02:04 +from __future__ import unicode_literals + +from django.db import migrations, models +import imager_images.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('imager_images', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='album', + name='date_published', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AlterField( + model_name='album', + name='published', + field=models.CharField(choices=[('private', 'private'), ('shared', 'shared'), ('public', 'public')], default='public', max_length=10), + ), + migrations.AlterField( + model_name='photo', + name='date_modified', + field=models.DateTimeField(auto_now=True, null=True), + ), + migrations.AlterField( + model_name='photo', + name='date_published', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AlterField( + model_name='photo', + name='date_uploaded', + field=models.DateTimeField(auto_now_add=True, null=True), + ), + migrations.AlterField( + model_name='photo', + name='description', + field=models.TextField(blank=True, max_length=120, null=True), + ), + migrations.AlterField( + model_name='photo', + name='image', + field=models.ImageField(blank=True, null=True, upload_to=imager_images.models.image_path), + ), + migrations.AlterField( + model_name='photo', + name='published', + field=models.CharField(choices=[('private', 'private'), ('shared', 'shared'), ('public', 'public')], default='public', max_length=10), + ), + ] diff --git a/imagersite/imager_images/migrations/__init__.py b/imagersite/imager_images/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/imagersite/imager_images/models.py b/imagersite/imager_images/models.py new file mode 100644 index 0000000..40554a7 --- /dev/null +++ b/imagersite/imager_images/models.py @@ -0,0 +1,68 @@ +from __future__ import unicode_literals +from django.utils.encoding import python_2_unicode_compatible +from django.db import models +from django.contrib.auth.models import User + +PUBLISHED_OPTIONS = ( + ("private", "private"), + ("shared", "shared"), + ("public", "public"), +) + + +def image_path(instance, file_name): + """Upload file to media root in user folder.""" + return 'user_{0}/{1}'.format(instance.user.id, file_name) + + +@python_2_unicode_compatible +class Photo(models.Model): + """Create Photo Model.""" + + user = models.ForeignKey( + User, + related_name='photos', + on_delete=models.CASCADE, + ) + image = models.ImageField(upload_to=image_path, blank=True, null=True) + title = models.CharField(max_length=60) + description = models.TextField(max_length=120, blank=True, null=True) + date_uploaded = models.DateTimeField(auto_now_add=True, blank=True, null=True) + date_modified = models.DateTimeField(auto_now=True, blank=True, null=True) + date_published = models.DateTimeField(null=True, blank=True) + published = models.CharField(max_length=10, choices=PUBLISHED_OPTIONS, default='public') + + def __str__(self): + """Return string description of album.""" + return "{}: Photo belonging to {}".format(self.title, self.user) + + +@python_2_unicode_compatible +class Album(models.Model): + """Create Album Model.""" + + user = models.ForeignKey( + User, + related_name="albums", + on_delete=models.CASCADE, + ) + cover = models.ForeignKey( + "Photo", + null=True, + related_name="albums_covered" + ) + title = models.CharField(max_length=60) + description = models.TextField(max_length=200) + photos = models.ManyToManyField( + "Photo", + related_name="albums", + symmetrical=False + ) + date_created = models.DateTimeField(auto_now_add=True) + date_modified = models.DateTimeField(auto_now=True) + date_published = models.DateTimeField(null=True, blank=True) + published = models.CharField(max_length=10, choices=PUBLISHED_OPTIONS, default="public") + + def __str__(self): + """Return String Representation of Album.""" + return "{}: Album belonging to {}".format(self.title, self.user) diff --git a/imagersite/imager_images/templates/imager_images/album.html b/imagersite/imager_images/templates/imager_images/album.html new file mode 100644 index 0000000..9076d10 --- /dev/null +++ b/imagersite/imager_images/templates/imager_images/album.html @@ -0,0 +1,17 @@ +{% extends 'imagersite/base.html' %} +{% block content %} +

Your Album:

+
+ +{% for photo in album.photos.all %} + +{% endfor %} +
+{{ album.description }} + +{% endblock content %} \ No newline at end of file diff --git a/imagersite/imager_images/templates/imager_images/albums.html b/imagersite/imager_images/templates/imager_images/albums.html new file mode 100644 index 0000000..c6995f8 --- /dev/null +++ b/imagersite/imager_images/templates/imager_images/albums.html @@ -0,0 +1,15 @@ +{% extends 'imagersite/base.html' %} +{% load thumbnail %} +{% block content %} +

Public Albums

+
+{% 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..f17dad6 --- /dev/null +++ b/imagersite/imager_images/templates/imager_images/library.html @@ -0,0 +1,29 @@ +{% 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 %} + +

{{ photo.title }}

+{% thumbnail photo.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/photo.html b/imagersite/imager_images/templates/imager_images/photo.html new file mode 100644 index 0000000..7858f2e --- /dev/null +++ b/imagersite/imager_images/templates/imager_images/photo.html @@ -0,0 +1,7 @@ +{% extends 'imagersite/base.html' %} +{% block content %} +

Photo

+

{{ photo.title }}

+ +

{{ photo.description }}

+{% endblock content %} \ No newline at end of file diff --git a/imagersite/imager_images/templates/imager_images/photos.html b/imagersite/imager_images/templates/imager_images/photos.html new file mode 100644 index 0000000..643224d --- /dev/null +++ b/imagersite/imager_images/templates/imager_images/photos.html @@ -0,0 +1,15 @@ +{% extends 'imagersite/base.html' %} +{% block content %} +

Photos

+
+{% for photo in photos %} + +{% endfor %} +
+ +{% endblock content %} \ No newline at end of file diff --git a/imagersite/imager_images/test_img.jpg b/imagersite/imager_images/test_img.jpg new file mode 100644 index 0000000..fb59ba4 Binary files /dev/null and b/imagersite/imager_images/test_img.jpg differ diff --git a/imagersite/imager_images/tests.py b/imagersite/imager_images/tests.py new file mode 100644 index 0000000..1910b33 --- /dev/null +++ b/imagersite/imager_images/tests.py @@ -0,0 +1,418 @@ +"""Test the imager_images app.""" +from django.test import TestCase, Client, RequestFactory +from django.contrib.auth.models import User +from django.core.files.uploadedfile import SimpleUploadedFile +from imager_images.models import Photo, Album +import factory +from django.core.urlresolvers import reverse_lazy +from .views import photo_view, all_photos, single_album, all_albums, library + + +class UserFactory(factory.django.DjangoModelFactory): + """Makes users.""" + + class Meta: + """Meta.""" + + model = User + + username = factory.Sequence(lambda n: "Prisoner number {}".format(n)) + email = factory.LazyAttribute( + lambda x: "{}@foo.com".format(x.username.replace(" ", "")) + ) + + +class PhotoFactory(factory.django.DjangoModelFactory): + """Makes photos.""" + + class Meta: + """Meta.""" + + model = Photo + + user = factory.SubFactory(UserFactory) + title = factory.Sequence(lambda n: "Photo number {}".format(n)) + description = factory.LazyAttribute(lambda a: '{} is a photo'.format(a.title)) + + +class AlbumFacotory(factory.django.DjangoModelFactory): + """Makes albums.""" + + class Meta: + """Meta.""" + + model = Album + + user = factory.SubFactory(UserFactory) + title = factory.Sequence(lambda n: "Album number {}".format(n)) + description = factory.LazyAttribute(lambda a: '{} is an album'.format(a.title)) + + +class PhotoTestCase(TestCase): + """Photo model and view tests.""" + + def setUp(self): + """The appropriate setup for the appropriate test.""" + self.users = [UserFactory.create() for i in range(20)] + self.photos = [PhotoFactory.create() for i in range(20)] + + def test_photo_made_when_saved(self): + """Test photos are added to the database.""" + self.assertTrue(Photo.objects.count() == 20) + + def test_photo_associated_with_user(self): + """Test that a photo is attached to a user.""" + photo = Photo.objects.first() + self.assertTrue(hasattr(photo, "__str__")) + + def test_photo_has_str(self): + """Test photo model includes string method.""" + photo = Photo.objects.first() + self.assertTrue(hasattr(photo, "user")) + + def test_image_title(self): + """Test that the image has a title.""" + self.assertTrue("Photo" in Photo.objects.first().title) + + def test_image_has_description(self): + """Test that the Photo description field can be assigned.""" + image = Photo.objects.first() + description = "This is a test of description field." + image.description = description + image.save() + self.assertTrue(Photo.objects.first().description == description) + + def test_image_has_published(self): + """Test the image published field.""" + image = Photo.objects.first() + image.published = 'public' + image.save() + self.assertTrue(Photo.objects.first().published == "public") + + def test_user_has_image(self): + """Test that the user has the image.""" + image = Photo.objects.first() + user = User.objects.first() + self.assertTrue(user.photos.count() == 0) + image.user = user + image.save() + self.assertTrue(user.photos.count() == 1) + + def test_two_images_have_user(self): + """Test two images have the same user.""" + image1 = Photo.objects.all()[0] + image2 = Photo.objects.all()[1] + user = User.objects.first() + image1.user = user + image2.user = user + image1.save() + image2.save() + self.assertTrue(image1.user == user) + self.assertTrue(image2.user == user) + + def test_user_has_two_images(self): + """Test that user has two image.""" + image1 = Photo.objects.all()[0] + image2 = Photo.objects.all()[1] + user = User.objects.first() + image1.user = user + image2.user = user + image1.save() + image2.save() + self.assertTrue(user.photos.count() == 2) + + def test_user_has_photo_uploaded(self): + """Test user has photo uploaded.""" + photo = self.photos[4] + self.assertTrue(photo.image.name is None) + image = SimpleUploadedFile( + name='test_image.jpg', + content=open('imager_images/test_img.jpg', 'rb').read(), + content_type='image/jpeg' + ) + photo.image = image + self.assertTrue(photo.image.name is not None) + + +class AlbumTestCase(TestCase): + """Album model and view tests.""" + + def setUp(self): + """The appropriate setup for the appropriate test.""" + self.users = [UserFactory.create() for i in range(20)] + self.photos = [PhotoFactory.create() for i in range(20)] + self.albums = [AlbumFacotory.create() for i in range(20)] + + def test_image_has_no_album(self): + """Test that the image is in an album.""" + image = Photo.objects.first() + self.assertTrue(image.albums.count() == 0) + + def test_image_has_album(self): + """Test that the image is in an album.""" + image = Photo.objects.first() + album = Album.objects.first() + image.albums.add(album) + self.assertTrue(image.albums.count() == 1) + + def test_album_has_no_image(self): + """Test that an album has no image before assignemnt.""" + album = Album.objects.first() + self.assertTrue(album.photos.count() == 0) + + def test_album_has_image(self): + """Test that an album has an image after assignemnt.""" + image = Photo.objects.first() + album = Album.objects.first() + image.albums.add(album) + self.assertTrue(image.albums.count() == 1) + + def test_two_images_have_album(self): + """Test that two images have same album.""" + image1 = Photo.objects.all()[0] + image2 = Photo.objects.all()[1] + album = Album.objects.first() + image1.albums.add(album) + image2.albums.add(album) + image1.save() + image2.save() + self.assertTrue(image1.albums.all()[0] == album) + self.assertTrue(image2.albums.all()[0] == album) + + def test_album_has_two_images(self): + """Test that an album has two images.""" + image1 = Photo.objects.all()[0] + image2 = Photo.objects.all()[1] + album = Album.objects.first() + image1.albums.add(album) + image2.albums.add(album) + image1.save() + image2.save() + self.assertTrue(album.photos.count() == 2) + + def test_image_has_two_albums(self): + """Test that an image has two albums.""" + image = Photo.objects.first() + album1 = Album.objects.all()[0] + album2 = Album.objects.all()[1] + image.albums.add(album1) + image.albums.add(album2) + image.save() + self.assertTrue(image.albums.count() == 2) + + def test_album_title(self): + """Test that the album has a title.""" + self.assertTrue("Album" in Album.objects.first().title) + + def test_album_has_description(self): + """Test that the album description field exists.""" + self.assertTrue("is an album" in Album.objects.first().description) + + def test_album_has_published(self): + """Test that the album published field exists.""" + album = Album.objects.first() + album.published = 'public' + album.save() + self.assertTrue(Album.objects.first().published == "public") + + def test_album_has_user(self): + """Test that album has an user.""" + self.assertTrue(Album.objects.first().user) + + def test_user_has_album(self): + """Test that the user has the album.""" + album = Album.objects.first() + user = User.objects.first() + self.assertTrue(user.albums.count() == 0) + album.user = user + album.save() + self.assertTrue(user.albums.count() == 1) + + def test_two_albums_have_user(self): + """Test two albums have the same user.""" + album1 = Album.objects.all()[0] + album2 = Album.objects.all()[1] + user = User.objects.first() + album1.user = user + album2.user = user + album1.save() + album2.save() + self.assertTrue(album1.user == user) + self.assertTrue(album2.user == user) + + def test_user_has_two_albums(self): + """Test that user has two albums.""" + album1 = Album.objects.all()[0] + album2 = Album.objects.all()[1] + user = User.objects.first() + album1.user = user + album2.user = user + album1.save() + album2.save() + self.assertTrue(user.albums.count() == 2) + + def test_adding_cover_image(self): + """Test that the image is in an album.""" + image = Photo.objects.first() + album = Album.objects.first() + self.assertTrue(album.cover is None) + album.cover = image + album.save() + self.assertTrue(Album.objects.first().cover is not None) + + +class FrontEndTestCase(TestCase): + """Front end tests.""" + + def setUp(self): + """Set up client and requestfactory.""" + self.client = Client() + self.request = RequestFactory() + self.users = [UserFactory.create() for i in range(20)] + self.photos = [PhotoFactory.create() for i in range(20)] + self.albums = [AlbumFacotory.create() for i in range(20)] + + """ + To test: + Views return 200 + Routes return 200 + all four templates are used + albums are visible in albums.html + correct number of photos and albums are visible + """ + def test_libary_view_returns_200(self): + """Test Library View returns a 200.""" + user = UserFactory.create() + user.save() + view = library + req = self.request.get(reverse_lazy('library')) + req.user = user + response = view(req) + self.assertTrue(response.status_code == 200) + + def test_logged_in_user_has_library(self): + """A logged in user gets a 200 resposne.""" + user = UserFactory.create() + user.save() + self.client.force_login(user) + response = self.client.get(reverse_lazy("library")) + self.assertTrue(response.status_code == 200) + + def test_logged_in_user_sees_their_albums(self): + """Test that a logged in user can see their images in library.""" + user = UserFactory.create() + album1 = Album.objects.first() + user.albums.add(album1) + user.save() + self.client.force_login(user) + response = self.client.get(reverse_lazy("library")) + self.assertTrue(album1.title in str(response.content)) + + def test_album_view_returns_200(self): + """Test that the album view returns a 200.""" + req = self.request.get(reverse_lazy('all_albums')) + response = all_albums(req) + self.assertTrue(response.status_code == 200) + + def test_photoid_view_returns_200(self): + """Test that the photo id view returns a 200.""" + photo = self.photos[6] + image = SimpleUploadedFile( + name='test_image.jpg', + content=open('imager_images/test_img.jpg', 'rb').read(), + content_type='image/jpeg' + ) + photo.image = image + photo.save() + response = self.client.get(reverse_lazy('single_photo', + kwargs={'photo_id': photo.id})) + self.assertTrue(response.status_code == 200) + + def test_photoid_view_returns_error_private_photo(self): + """Test that a user cannot view a private photo of another user.""" + photo = self.photos[12] + photo.published = 'private' + image = SimpleUploadedFile( + name='test_image.jpg', + content=open('imager_images/test_img.jpg', 'rb').read(), + content_type='image/jpeg' + ) + photo.image = image + photo.save() + response = self.client.get(reverse_lazy('single_photo', + kwargs={'photo_id': photo.id})) + self.assertTrue(response.status_code == 401) + + def test_photo_id_user_views_own_private_photo(self): + """Test that a user can view their own private photo.""" + user = self.users[2] + user.save() + self.client.force_login(user) + photo = self.photos[15] + photo.published = 'private' + photo.user = user + image = SimpleUploadedFile( + name='test_image.jpg', + content=open('imager_images/test_img.jpg', 'rb').read(), + content_type='image/jpeg' + ) + photo.image = image + photo.save() + response = self.client.get(reverse_lazy('single_photo', + kwargs={'photo_id': photo.id})) + self.assertTrue(response.status_code == 200) + + def test_albumid_view_returns_200(self): + """Test that the album id view returns a 200.""" + user = self.users[0] + self.client.force_login(user) + album = self.albums[9] + album.user = user + album.save() + response = self.client.get(reverse_lazy('single_album', + kwargs={'album_id': album.id})) + self.assertTrue(response.status_code == 200) + + def test_album_id_view_doesnt_return_private_album(self): + """Test that a user cannot view a private album.""" + album = self.albums[9] + album.published = 'private' + album.save() + response = self.client.get(reverse_lazy('single_album', + kwargs={'album_id': album.id})) + self.assertTrue(response.status_code == 401) + + def test_description_of_album_shows(self): + """Test that the description of an album shows.""" + album = self.albums[17] + response = self.client.get(reverse_lazy('single_album', + kwargs={'album_id': album.id})) + self.assertTrue('is an album' in response.content.decode()) + + def test_description_of_photo_shows(self): + """Test that the description of an photo shows.""" + photo = self.photos[17] + image = SimpleUploadedFile( + name='test_image.jpg', + content=open('imager_images/test_img.jpg', 'rb').read(), + content_type='image/jpeg' + ) + photo.image = image + photo.save() + response = self.client.get(reverse_lazy('single_photo', + kwargs={'photo_id': photo.id})) + self.assertTrue('is a photo' in response.content.decode()) + + def test_title_of_photo_shows(self): + """Test that the title of an photo shows.""" + photo = self.photos[17] + image = SimpleUploadedFile( + name='test_image.jpg', + content=open('imager_images/test_img.jpg', 'rb').read(), + content_type='image/jpeg' + ) + photo.image = image + photo.save() + response = self.client.get(reverse_lazy('single_photo', + kwargs={'photo_id': photo.id})) + self.assertTrue('Photo number' in response.content.decode()) diff --git a/imagersite/imager_images/urls.py b/imagersite/imager_images/urls.py new file mode 100644 index 0000000..803dd0c --- /dev/null +++ b/imagersite/imager_images/urls.py @@ -0,0 +1,11 @@ +"""Images urls.""" +from django.conf.urls import url +from .views import photo_view, all_photos, single_album, all_albums, library + +urlpatterns = [ + url(r'^photos/(?P\d+)', photo_view, name='single_photo'), + url(r'^photos/$', all_photos, name='private_profile'), # display all of the public photos that have been uploaded + url(r'^albums/(?P\d+)/$', single_album, name='single_album'), # display a single selected album + url(r'^albums/$', all_albums, name='all_albums'), + url(r'^library/$', library, name='library'), +] diff --git a/imagersite/imager_images/views.py b/imagersite/imager_images/views.py new file mode 100644 index 0000000..de6ccf0 --- /dev/null +++ b/imagersite/imager_images/views.py @@ -0,0 +1,59 @@ +"""Views for images.""" +from django.shortcuts import render +from imager_images.models import Photo, Album +from django.http import HttpResponse +from django.contrib.auth.decorators import login_required + + +def photo_view(request, photo_id): + """Render individual image by id.""" + photo = Photo.objects.get(id=photo_id) + if photo.published != 'private' or photo.user.username == request.user.username: + return render(request, 'imager_images/photo.html', {"photo": photo}) + return HttpResponse('Unauthorized', status=401) + + +def all_photos(request): + """Render all photos in app.""" + public_photos = [] + photos = Photo.objects.all() + for photo in photos: + if photo.published != 'private' or photo.user.username == request.user.username: + public_photos.append(photo) + return render(request, 'imager_images/photos.html', {"photos": public_photos}) + + +def single_album(request, album_id): + """Render a specific album.""" + album = Album.objects.get(id=album_id) + if album.published != 'private' or album.user.username == request.user.username: + return render( + request, + 'imager_images/album.html', + {'album': album} + ) + return HttpResponse('Unauthorized', status=401) + + +def all_albums(request): + """Render all public albums.""" + public_albums = [] + albums = Album.objects.all() + for album in albums: + if album.published != 'private' or album.user.username == request.user.username: + public_albums.append(album) + return render( + request, + 'imager_images/albums.html', + {'albums': public_albums} + ) + + +@login_required(login_url='/accounts/login/') +def library(request): + """Library view.""" + albums = request.user.albums.all() + photos = request.user.photos.all() + return render(request, + 'imager_images/library.html', + context={'albums': albums, 'photos': photos}) diff --git a/imagersite/imager_profile/__init__.py b/imagersite/imager_profile/__init__.py new file mode 100644 index 0000000..4a991a4 --- /dev/null +++ b/imagersite/imager_profile/__init__.py @@ -0,0 +1 @@ +default_app_config = 'imager_profile.apps.ImagerProfileConfig' diff --git a/imagersite/imager_profile/admin.py b/imagersite/imager_profile/admin.py new file mode 100644 index 0000000..8295835 --- /dev/null +++ b/imagersite/imager_profile/admin.py @@ -0,0 +1,7 @@ +from django.contrib import admin +from imager_profile.models import ImagerProfile + +# Register your models here. + + +admin.site.register(ImagerProfile) diff --git a/imagersite/imager_profile/apps.py b/imagersite/imager_profile/apps.py new file mode 100644 index 0000000..8900e6d --- /dev/null +++ b/imagersite/imager_profile/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class ImagerProfileConfig(AppConfig): + name = 'imager_profile' diff --git a/imagersite/imager_profile/migrations/0001_initial.py b/imagersite/imager_profile/migrations/0001_initial.py new file mode 100644 index 0000000..93870a7 --- /dev/null +++ b/imagersite/imager_profile/migrations/0001_initial.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2017-01-17 03:06 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='ImagerProfile', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('travel_distance', models.IntegerField(blank=True, null=True)), + ('phone_number', models.CharField(blank=True, max_length=15, null=True)), + ('photography_type', models.CharField(blank=True, max_length=20, null=True)), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='profile', to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/imagersite/imager_profile/migrations/0002_auto_20170117_1828.py b/imagersite/imager_profile/migrations/0002_auto_20170117_1828.py new file mode 100644 index 0000000..42a32ed --- /dev/null +++ b/imagersite/imager_profile/migrations/0002_auto_20170117_1828.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2017-01-17 18:28 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('imager_profile', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='imagerprofile', + name='bio', + field=models.TextField(default=''), + ), + migrations.AddField( + model_name='imagerprofile', + name='camera_type', + field=models.CharField(blank=True, choices=[('Nikon', 'Nikon'), ('iPhone', 'iPhone'), ('Canon', 'Canon')], max_length=10, null=True), + ), + migrations.AddField( + model_name='imagerprofile', + name='for_hire', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='imagerprofile', + name='personal_website', + field=models.URLField(default=''), + ), + ] diff --git a/imagersite/imager_profile/migrations/0003_auto_20170119_1823.py b/imagersite/imager_profile/migrations/0003_auto_20170119_1823.py new file mode 100644 index 0000000..59498f7 --- /dev/null +++ b/imagersite/imager_profile/migrations/0003_auto_20170119_1823.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2017-01-20 02:23 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('imager_profile', '0002_auto_20170117_1828'), + ] + + operations = [ + migrations.AlterField( + model_name='imagerprofile', + name='photography_type', + field=models.CharField(blank=True, choices=[('portrait', 'Portrait'), ('landscape', 'Landscape'), ('bw', 'Black and White'), ('sport', 'Sport')], max_length=20, null=True), + ), + ] diff --git a/imagersite/imager_profile/migrations/0004_imagerprofile_address.py b/imagersite/imager_profile/migrations/0004_imagerprofile_address.py new file mode 100644 index 0000000..3df6819 --- /dev/null +++ b/imagersite/imager_profile/migrations/0004_imagerprofile_address.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2017-01-22 00:01 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('imager_profile', '0003_auto_20170119_1823'), + ] + + operations = [ + migrations.AddField( + model_name='imagerprofile', + name='address', + field=models.CharField(blank=True, max_length=40, null=True), + ), + ] diff --git a/imagersite/imager_profile/migrations/0005_auto_20170121_1732.py b/imagersite/imager_profile/migrations/0005_auto_20170121_1732.py new file mode 100644 index 0000000..7411c93 --- /dev/null +++ b/imagersite/imager_profile/migrations/0005_auto_20170121_1732.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2017-01-22 01:32 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('imager_profile', '0004_imagerprofile_address'), + ] + + operations = [ + migrations.AlterField( + model_name='imagerprofile', + name='photography_type', + field=models.CharField(blank=True, choices=[('Portrait', 'Portrait'), ('Landscape', 'Landscape'), ('Black and White', 'Black and White'), ('Sport', 'Sport')], max_length=20, null=True), + ), + ] diff --git a/imagersite/imager_profile/migrations/0006_auto_20170124_1837.py b/imagersite/imager_profile/migrations/0006_auto_20170124_1837.py new file mode 100644 index 0000000..ad6d0dd --- /dev/null +++ b/imagersite/imager_profile/migrations/0006_auto_20170124_1837.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2017-01-25 02:37 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('imager_profile', '0005_auto_20170121_1732'), + ] + + operations = [ + migrations.AlterField( + model_name='imagerprofile', + name='address', + field=models.CharField(blank=True, max_length=70, null=True), + ), + migrations.AlterField( + model_name='imagerprofile', + name='phone_number', + field=models.CharField(blank=True, max_length=17, null=True), + ), + ] diff --git a/imagersite/imager_profile/migrations/0007_auto_20170124_1838.py b/imagersite/imager_profile/migrations/0007_auto_20170124_1838.py new file mode 100644 index 0000000..38e5473 --- /dev/null +++ b/imagersite/imager_profile/migrations/0007_auto_20170124_1838.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2017-01-25 02:38 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('imager_profile', '0006_auto_20170124_1837'), + ] + + operations = [ + migrations.AlterField( + model_name='imagerprofile', + name='phone_number', + field=models.CharField(blank=True, max_length=20, null=True), + ), + ] diff --git a/imagersite/imager_profile/migrations/0008_auto_20170129_1804.py b/imagersite/imager_profile/migrations/0008_auto_20170129_1804.py new file mode 100644 index 0000000..2ed8ecb --- /dev/null +++ b/imagersite/imager_profile/migrations/0008_auto_20170129_1804.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2017-01-30 02:04 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('imager_profile', '0007_auto_20170124_1838'), + ] + + operations = [ + migrations.AlterField( + model_name='imagerprofile', + name='address', + field=models.CharField(blank=True, default='', max_length=70, null=True), + ), + migrations.AlterField( + model_name='imagerprofile', + name='camera_type', + field=models.CharField(blank=True, choices=[('Nikon', 'Nikon'), ('iPhone', 'iPhone'), ('Canon', 'Canon'), ('--------', '--------')], default='--------', max_length=10), + ), + migrations.AlterField( + model_name='imagerprofile', + name='phone_number', + field=models.CharField(blank=True, default='', max_length=15), + ), + migrations.AlterField( + model_name='imagerprofile', + name='photography_type', + field=models.CharField(blank=True, default='', max_length=20), + ), + migrations.AlterField( + model_name='imagerprofile', + name='travel_distance', + field=models.IntegerField(blank=True, default=0), + ), + ] diff --git a/imagersite/imager_profile/migrations/__init__.py b/imagersite/imager_profile/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/imagersite/imager_profile/models.py b/imagersite/imager_profile/models.py new file mode 100644 index 0000000..72578c9 --- /dev/null +++ b/imagersite/imager_profile/models.py @@ -0,0 +1,73 @@ +"""Models for imager_profile.""" + +from django.db import models +from django.contrib.auth.models import User +from django.utils.encoding import python_2_unicode_compatible +from django.db.models.signals import post_save +from django.dispatch import receiver + +# Create your models here. + + +class ActiveProfileManager(models.Manager): + """Create Model Manager for Active Profiles.""" + + def get_queryset(self): + """Return active users.""" + qs = super(ActiveProfileManager, self).get_queryset() + return qs.filter(user__is_active__exact=True) + + +@python_2_unicode_compatible +class ImagerProfile(models.Model): + """The imager user and all their attributes.""" + + objects = models.Manager() + active = ActiveProfileManager() + + user = models.OneToOneField( + User, + related_name="profile", + on_delete=models.CASCADE + ) + CAMERA_CHOICES = [ + ('Nikon', 'Nikon'), + ('iPhone', 'iPhone'), + ('Canon', 'Canon'), + ('--------', '--------') + ] + TYPE_OF_PHOTOGRAPHY = [ + ('nature', 'nature'), + ('urban', 'urban'), + ('portraits', 'portraits') + ] + camera_type = models.CharField( + max_length=10, + choices=CAMERA_CHOICES, + blank=True, + default='--------' + ) + address = models.CharField(default="", max_length=70, null=True, blank=True) + bio = models.TextField(default="") + personal_website = models.URLField(default="") + for_hire = models.BooleanField(default=False) + travel_distance = models.IntegerField(default=0, blank=True) + phone_number = models.CharField(max_length=15, default="", blank=True) + photography_type = models.CharField(max_length=20, default="", blank=True) + + @property + def is_active(self): + """Return True if user associated with this profile is active.""" + return self.user.is_active + + def __str__(self): + """Display user data as a string.""" + return "User: {}, Camera: {}, Address: {}, Phone number: {}, For Hire? {}, Photography style: {}".format(self.user, self.camera_type, self.address, self.phone_number, self.for_hire, self.photography_type) + + +@receiver(post_save, sender=User) +def make_profile_for_user(sender, instance, **kwargs): + """Called when user is made and hooks that user to a profile.""" + if kwargs["created"]: + new_profile = ImagerProfile(user=instance) + new_profile.save() diff --git a/imagersite/imager_profile/templates/imager_profile/profile.html b/imagersite/imager_profile/templates/imager_profile/profile.html new file mode 100644 index 0000000..b094d75 --- /dev/null +++ b/imagersite/imager_profile/templates/imager_profile/profile.html @@ -0,0 +1,22 @@ +{% extends 'imagersite/base.html' %} +{% block content %} +
+

Welcome {{user.username}}!

+
    +
  • Name: {{user.first_name|title}} {{user.last_name|title}}
  • +
  • Email: {{user.email}}
  • +
  • Bio: {{user.profile.bio}}
  • +
  • Website: {{user.profile.personal_website}}
  • +
  • For Hire: {{user.profile.for_hire}}
  • +
  • Travel Distance: {{user.profile.travel_distance}} Miles
  • +
  • Phone Number: {{user.profile.phone_number}}
  • +
  • Photography Type: {{user.profile.photography_type}}
  • +
  • Camera: {{user.profile.camera_type}}
  • + {% ifequal request.get_full_path '/profile/' %} +
  • Location: {{user.profile.address}}
  • +
  • Photos Uploaded: {{user.photos.count}} +
  • Albums Created: {{user.albums.count}} + {% endifequal %} +
+
+{% endblock %} \ No newline at end of file diff --git a/imagersite/imager_profile/tests.py b/imagersite/imager_profile/tests.py new file mode 100644 index 0000000..6a034fc --- /dev/null +++ b/imagersite/imager_profile/tests.py @@ -0,0 +1,184 @@ +"""Tests for the imager_profile app.""" +from django.test import TestCase, Client, RequestFactory +from django.contrib.auth.models import User +from imager_profile.models import ImagerProfile +import factory + + +class UserFactory(factory.django.DjangoModelFactory): + """Makes users.""" + + class Meta: + """Metadata for UserFactory.""" + + model = User + + username = factory.Sequence(lambda n: "Prisoner number {}".format(n)) + email = factory.LazyAttribute( + lambda x: "{}@foo.com".format(x.username.replace(" ", "")) + ) + + +class ProfileTestCase(TestCase): + """The Profile Model test runner.""" + + def setUp(self): + """The appropriate setup for the appropriate test.""" + self.users = [UserFactory.create() for i in range(20)] + + def test_profile_is_made_when_user_is_saved(self): + """Test profile is made when user is saved.""" + self.assertTrue(ImagerProfile.objects.count() == 20) + + def test_profile_is_associated_with_actual_users(self): + """Test profile is associated with actual users.""" + profile = ImagerProfile.objects.first() + self.assertTrue(hasattr(profile, "user")) + self.assertIsInstance(profile.user, User) + + def test_user_has_profile_attached(self): + """Test user has profile attached.""" + user = self.users[0] + self.assertTrue(hasattr(user, "profile")) + self.assertIsInstance(user.profile, ImagerProfile) + + def test_user_model_has_str(self): + """Test user has a string method.""" + user = self.users[0] + self.assertIsInstance(str(user), str) + + def test_active_users_counted(self): + """Test acttive user count meets expectations.""" + self.assertTrue(ImagerProfile.active.count() == User.objects.count()) + + def test_inactive_users_not_counted(self): + """Test inactive users not included with active users.""" + deactivated_user = self.users[0] + deactivated_user.is_active = False + deactivated_user.save() + self.assertTrue(ImagerProfile.active.count() == User.objects.count() - 1) + + def test_imagerprofile_attributes(self): + """Test that ImagerProfile has the expected attributes.""" + attribute_list = ["user", "camera_type", "address", "bio", "personal_website", "for_hire", "travel_distance", "phone_number", "photography_type"] + for item in attribute_list: + self.assertTrue(hasattr(ImagerProfile, item)) + + def test_field_type(self): + """Test user field types.""" + attribute_list = ["camera_type", "address", "bio", "personal_website", "for_hire", "travel_distance", "phone_number", "photography_type"] + field_list = [str, str, str, str, bool, int, str, str] + test_user = self.users[0] + # import pdb; pdb.set_trace() + self.assertIsInstance(test_user.username, str) + for attribute, field in zip(attribute_list, field_list): + self.assertIsInstance(getattr(test_user.profile, attribute), field) + + +class FrontendTestCases(TestCase): + """Test the frontend of the imager_profile site.""" + + def setUp(self): + """Set up client and request factory.""" + self.client = Client() + self.request = RequestFactory() + + def test_home_view_status(self): + """Test home view has 200 status.""" + from imagersite.views import home_view + req = self.request.get("/route") + response = home_view(req) + self.assertTrue(response.status_code == 200) + + def test_home_route_status(self): + """Test home route has 200 status.""" + response = self.client.get("/") + self.assertTrue(response.status_code == 200) + + def test_home_route_templates(self): + """Test the home route templates are correct.""" + response = self.client.get("/") + self.assertTemplateUsed(response, "imagersite/home.html") + + def test_login_template(self): + """Test the login route templates are correct.""" + response = self.client.get("/login/") + self.assertTemplateUsed(response, "registration/login.html") + + def test_registration_template(self): + """Test the login route templates are correct.""" + response = self.client.get("/accounts/register/") + self.assertTemplateUsed(response, "imagersite/base.html") + self.assertTemplateUsed(response, "registration/registration_form.html") + + def test_login_redirect_code(self): + """Test built-in login route redirects properly.""" + user_register = UserFactory.create() + user_register.is_active = True + user_register.username = "username" + user_register.set_password("potatoes") + user_register.save() + response = self.client.post("/login/", { + "username": user_register.username, + "password": "potatoes" + }) + self.assertRedirects(response, '/profile/') + + def test_register_user(self): + """Test that tests can register users.""" + self.assertTrue(User.objects.count() == 0) + self.client.post("/accounts/register/", { + "username": "Sir_Joseph", + "email": "e@mail.com", + "password1": "rutabega", + "password2": "rutabega" + }) + self.assertTrue(User.objects.count() == 1) + + def test_new_user_inactive(self): + """Test django-created user starts as inactive.""" + self.client.post("/accounts/register/", { + "username": "Sir_Joseph", + "email": "e@mail.com", + "password1": "rutabega", + "password2": "rutabega" + }) + inactive_user = User.objects.first() + self.assertFalse(inactive_user.is_active) + + def test_registration_redirect(self): + """Test redirect on registration.""" + response = self.client.post("/accounts/register/", { + "username": "Sir_Joseph", + "email": "e@mail.com", + "password1": "rutabega", + "password2": "rutabega" + }) + self.assertTrue(response.status_code == 302) + + def test_registration_reidrect_home(self): + """Test registration redirects home.""" + response = self.client.post("/accounts/register/", { + "username": "Sir_Joseph", + "email": "e@mail.com", + "password1": "rutabega", + "password2": "rutabega" + }, follow=True) + self.assertRedirects( + response, + "/accounts/register/complete/" + ) + + def test_logout_redirects_to_home(self): + """Test logging out redirects to home.""" + user_register = UserFactory.create() + user_register.is_active = True + user_register.username = "username" + user_register.set_password("potatoes") + user_register.save() + self.client.post("/login/", { + "username": user_register.username, + "password": "potatoes" + }) + response = self.client.get('/logout/') + self.assertRedirects(response, '/') diff --git a/imagersite/imager_profile/urls.py b/imagersite/imager_profile/urls.py new file mode 100644 index 0000000..faa8a1e --- /dev/null +++ b/imagersite/imager_profile/urls.py @@ -0,0 +1,8 @@ +"""Profile urls.""" +from django.conf.urls import url +from .views import public_profile, profile_view + +urlpatterns = [ + url(r'^(?P\w+)', public_profile, name='public_profile'), + url(r'^$', profile_view, name='private_profile') +] diff --git a/imagersite/imager_profile/views.py b/imagersite/imager_profile/views.py new file mode 100644 index 0000000..18d42d0 --- /dev/null +++ b/imagersite/imager_profile/views.py @@ -0,0 +1,21 @@ +"""Profile views.""" +from django.shortcuts import render +from django.contrib.auth.decorators import login_required +from django.contrib.auth.models import User + + +@login_required(login_url='/accounts/login/') +def profile_view(request): + """The user profile view.""" + return render(request, + "imager_profile/profile.html", + {"user": request.user} + ) + + +def public_profile(request, username): + """Public profile view.""" + return render(request, + "imager_profile/profile.html", + {"user": User.objects.get(username=username)} + ) diff --git a/imagersite/imagersite/__init__.py b/imagersite/imagersite/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/imagersite/imagersite/settings.py b/imagersite/imagersite/settings.py new file mode 100644 index 0000000..b625d95 --- /dev/null +++ b/imagersite/imagersite/settings.py @@ -0,0 +1,146 @@ +""" +Django settings for imagersite project. + +Generated by 'django-admin startproject' using Django 1.10.5. + +For more information on this file, see +https://docs.djangoproject.com/en/1.10/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/1.10/ref/settings/ +""" + +import os + +# Build paths inside the project like this: os.path.join(BASE_DIR, ...) +BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/1.10/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = '5m(o9rkvq-2$u452_313+-m9&gn*8%mqj+&^yum=r!%h%@(%!j' + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = [] + + +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'bootstrap3', + 'imager_profile', + 'imagersite', + 'imager_images', + 'sorl.thumbnail' +] + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'imagersite.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'imagersite.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/1.10/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.postgresql_psycopg2', + 'NAME': os.environ['IMAGER_DATABASE'], + # 'USER': os.environ['DATABASE_USER'], + # 'PASSWORD': os.environ['DATABASE_PASSWORD'], + # 'HOST': '127.0.0.1', + # 'PORT': '5432', + 'TEST': { + 'NAME': os.environ['TEST_IMAGER_DATABASE'] + } + } +} + + +# Password validation +# https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/1.10/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'America/Los_Angeles' + +USE_I18N = True + +USE_L10N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/1.10/howto/static-files/ + +STATIC_URL = '/static/' + + +# Registration Settings +ACCOUNT_ACTIVATION_DAYS = 3 +EMAIL_HOST = '127.0.0.1' +EMAIL_PORT = 1025 + +# Login/out settings +LOGIN_REDIRECT_URL = '/profile/' +MEDIA_ROOT = os.path.join(BASE_DIR, 'MEDIA') +MEDIA_URL = "/media/" +THUMBNAIL_DEBUG = True + +EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' diff --git a/imagersite/imagersite/templates/imagersite/album.html b/imagersite/imagersite/templates/imagersite/album.html new file mode 100644 index 0000000..93ed697 --- /dev/null +++ b/imagersite/imagersite/templates/imagersite/album.html @@ -0,0 +1,4 @@ +{% extends 'imagersite/base.html' %} +{% block content %} +

Album Page

+{% endblock content %} \ No newline at end of file diff --git a/imagersite/imagersite/templates/imagersite/base.html b/imagersite/imagersite/templates/imagersite/base.html new file mode 100644 index 0000000..462657c --- /dev/null +++ b/imagersite/imagersite/templates/imagersite/base.html @@ -0,0 +1,37 @@ + + + + App + + + {% load bootstrap3 %} + {% bootstrap_css %} + {% bootstrap_javascript %} + + + +
+{% block content %} +{% endblock %} +
+ + \ No newline at end of file diff --git a/imagersite/imagersite/templates/imagersite/home.html b/imagersite/imagersite/templates/imagersite/home.html new file mode 100644 index 0000000..367883c --- /dev/null +++ b/imagersite/imagersite/templates/imagersite/home.html @@ -0,0 +1,4 @@ +{% extends 'imagersite/base.html' %} +{% block content %} +

Home Page

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

Activate Page

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

Activation complete, welcome to imagersite

+{% endblock content %} \ No newline at end of file diff --git a/imagersite/imagersite/templates/registration/activation_email.txt b/imagersite/imagersite/templates/registration/activation_email.txt new file mode 100644 index 0000000..3c13308 --- /dev/null +++ b/imagersite/imagersite/templates/registration/activation_email.txt @@ -0,0 +1,2 @@ +Click here to register at imagersite: +http://{{ site.domain }}{% url "registration_activate" activation_key %} \ No newline at end of file diff --git a/imagersite/imagersite/templates/registration/activation_email_subject.txt b/imagersite/imagersite/templates/registration/activation_email_subject.txt new file mode 100644 index 0000000..42fd665 --- /dev/null +++ b/imagersite/imagersite/templates/registration/activation_email_subject.txt @@ -0,0 +1 @@ +Account Activation \ No newline at end of file diff --git a/imagersite/imagersite/templates/registration/login.html b/imagersite/imagersite/templates/registration/login.html new file mode 100644 index 0000000..f66f308 --- /dev/null +++ b/imagersite/imagersite/templates/registration/login.html @@ -0,0 +1,17 @@ +{% extends 'imagersite/base.html' %} +{% block content %} +{% load bootstrap3 %} +
+
+ {{ form.username.label_tag }} + {{ form.username }} +
+
+ {{ form.password.label_tag }} + {{ form.password }} +
+ {% csrf_token %} + {% 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..1273391 --- /dev/null +++ b/imagersite/imagersite/urls.py @@ -0,0 +1,35 @@ +"""imagersite URL Configuration. + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/1.10/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.conf.urls import url, include + 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) +""" +from django.conf.urls import include, url +from django.contrib import ( + admin, + auth +) +from imagersite.views import ( + home_view +) +from django.conf import settings +from django.conf.urls.static import static + +urlpatterns = [ + url(r'^admin/', admin.site.urls), + url(r'^$', home_view, name='homepage'), + url(r'^accounts/', include('registration.backends.hmac.urls')), + url(r'^login/', auth.views.login, name='login'), + url(r'^logout/', auth.views.logout, {'next_page': '/'}, name='logout'), + url(r'^profile/', include('imager_profile.urls')), + url(r'^images/', include('imager_images.urls')) +] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/imagersite/imagersite/views.py b/imagersite/imagersite/views.py new file mode 100644 index 0000000..c0ce2a7 --- /dev/null +++ b/imagersite/imagersite/views.py @@ -0,0 +1,9 @@ +"""Views.""" +from django.shortcuts import render + + +def home_view(request): + """The home view.""" + return render(request, + "imagersite/home.html" + ) diff --git a/imagersite/imagersite/wsgi.py b/imagersite/imagersite/wsgi.py new file mode 100644 index 0000000..8ef8781 --- /dev/null +++ b/imagersite/imagersite/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for imagersite project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/1.10/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "imagersite.settings") + +application = get_wsgi_application() diff --git a/imagersite/manage.py b/imagersite/manage.py new file mode 100755 index 0000000..b28a0f2 --- /dev/null +++ b/imagersite/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +import os +import sys + +if __name__ == "__main__": + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "imagersite.settings") + try: + from django.core.management import execute_from_command_line + except ImportError: + # The above import may fail for some other reason. Ensure that the + # issue is really that Django is missing to avoid masking other + # exceptions on Python 2. + try: + import django + except ImportError: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) + raise + execute_from_command_line(sys.argv) diff --git a/requirements.pip b/requirements.pip new file mode 100644 index 0000000..261e22a --- /dev/null +++ b/requirements.pip @@ -0,0 +1,26 @@ +decorator==4.0.11 +Django==1.10.5 +django-bootstrap3==8.1.0 +django-registration==2.2 +factory-boy==2.8.1 +Faker==0.7.7 +ipdb==0.10.1 +ipython==5.1.0 +ipython-genutils==0.1.0 +olefile==0.44 +pexpect==4.2.1 +pickleshare==0.7.4 +Pillow==4.0.0 +pkg-resources==0.0.0 +prompt-toolkit==1.0.9 +psycopg2==2.6.2 +ptyprocess==0.5.1 +pudb==2016.2 +Pygments==2.1.3 +python-dateutil==2.6.0 +simplegeneric==0.8.1 +six==1.10.0 +sorl-thumbnail==12.3 +traitlets==4.3.1 +urwid==1.3.1 +wcwidth==0.1.7