initial layout

This commit is contained in:
Evert Prants 2017-10-05 13:37:09 +03:00
parent a815e29f65
commit 4ca7c86ec5
Signed by: evert
GPG Key ID: 1688DA83D222D0B5
10 changed files with 573 additions and 14 deletions

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
/static/
/media/
/options.ini
/database.*
*.py[cod]

View File

@ -46,6 +46,7 @@ INSTALLED_APPS = [
'LandingPage.apps.LandingpageConfig',
'Show.apps.ShowConfig',
'Discussions.apps.DiscussionsConfig',
'pipeline',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
@ -129,10 +130,39 @@ USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.11/howto/static-files/
MEDIA_URL = '/media/'
MEDIA_ROOT = 'media/'
STATIC_URL = '/static/'
STATIC_ROOT = 'static/'
PIPELINE = {
'PIPELINE_ENABLED': not DEBUG,
'STYLESHEETS': {
'style': {
'source_filenames': (
'css/style.styl',
'css/footer.styl',
),
'output_filename': 'css/style.css',
},
},
'COMPILERS': [
'pipeline.compilers.stylus.StylusCompiler'
],
'CSS_COMPRESSOR': 'pipeline.compressors.NoopCompressor',
'JS_COMPRESSOR': 'pipeline.compressors.NoopCompressor',
}
AUTH_TOKEN_ENDPOINT = oauth_options.get('token_endpoint','https://icynet.eu/oauth/')
STATICFILES_STORAGE = 'pipeline.storage.PipelineCachedStorage'
STATICFILES_FINDERS = (
'django.contrib.staticfiles.finders.FileSystemFinder',
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
'pipeline.finders.PipelineFinder',
)
AUTH_TOKEN_ENDPOINT = oauth_options.get('token_endpoint','https://icynet.eu/oauth2/')
AUTH_CLIENT_ID = oauth_options.get('client_id')
AUTH_B64 = base64.b64encode(bytearray('%s:%s'%(AUTH_CLIENT_ID,oauth_options.get('client_secret')),'utf-8')).decode("utf-8")
AUTH_REDIRECT_URL = oauth_options.get('redirect_url')

View File

@ -13,10 +13,12 @@ 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 import settings
from django.conf.urls import url, include
from django.contrib import admin
from django.conf.urls.static import static
urlpatterns = [
url(r'^admin/', admin.site.urls),
url(r'^', include('LandingPage.urls'))
]
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

View File

@ -0,0 +1,317 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.4 on 2017-10-04 19:15
from __future__ import unicode_literals
import LandingPage.models
import django.core.files.storage
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Ban',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('timestamp', models.DateTimeField(auto_now=True, help_text='The date and time this object was created')),
('reason', models.CharField(blank=True, help_text='The reason this user was banned', max_length=50, verbose_name='Ban Reason')),
('expiration', models.DateField(blank=True, help_text='The date this user will become unbanned', verbose_name='Expiration Date')),
('permanent', models.BooleanField(help_text='If checked, this user will never be unbanned, even if the expiration date passes', verbose_name='Permanent')),
('site_wide', models.BooleanField(help_text='If checked, this is a site-wide ban, and the user is automatically banned from all shows, not just those in the Banned From (scope) paramenter', verbose_name='Site Wide Ban')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='DiscussionBoard',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('timestamp', models.DateTimeField(auto_now=True, help_text='The date and time this object was created')),
('title', models.CharField(help_text='The title of the discussion', max_length=100)),
('body', models.TextField(help_text='The body of the post', verbose_name='Body')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='DiscussionReply',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('timestamp', models.DateTimeField(auto_now=True, help_text='The date and time this object was created')),
('body', models.TextField(help_text='The body of the response', verbose_name='Body')),
('board', models.ForeignKey(help_text='The discussion board this was created in reply to', on_delete=django.db.models.deletion.CASCADE, related_name='replies', to='LandingPage.DiscussionBoard')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='DiscussionVote',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('timestamp', models.DateTimeField(auto_now=True, help_text='The date and time this object was created')),
('postive', models.BooleanField(help_text='If true, the vote is an upvote. Otherwise, it is a downvote. Neutral votes are not recorded')),
('board', models.ForeignKey(help_text='The board this vote was cast on', on_delete=django.db.models.deletion.CASCADE, related_name='votes', to='LandingPage.DiscussionBoard')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='Episode',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('episode', models.IntegerField(help_text='The position of this episode in the season. The first episode of the season to air would be episode number 1', verbose_name='Episode Number')),
('name', models.CharField(help_text='The name given to this episode by its producers', max_length=40, verbose_name='Episode Season')),
('summary', models.TextField(help_text='A summary of this episode')),
('airdate', models.DateField(help_text='The date this episode officially aired for the first time', verbose_name='Original Air Date')),
],
),
migrations.CreateModel(
name='Favorite',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('timestamp', models.DateTimeField(auto_now=True, help_text='The date and time this object was created')),
('episode', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='LandingPage.Episode')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='Report',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('timestamp', models.DateTimeField(auto_now=True, help_text='The date and time this object was created')),
('title', models.CharField(help_text='A brief summary of the report', max_length=50, verbose_name='Title')),
('details', models.TextField(help_text='The details of the report, preferably including why the content should be removed', verbose_name='Details')),
('url', models.URLField(help_text='The URL of the content being reported', max_length=100, verbose_name='Content URL')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='Season',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(blank=True, help_text='The name given to this season by its producers. Can be blank if no name was given', max_length=40, verbose_name='Season Name')),
('number', models.IntegerField(help_text='The number of this season, starting at 1; For example, the first season to be aired would be number 1, and the second would be number 2')),
('description', models.TextField(help_text="A description of this season's happenings")),
('artwork', models.ImageField(blank=True, help_text='The artwork associated with the season. Should display the name of the show in a movie-poster esque format. Aspect ration should be about 2:3', storage=django.core.files.storage.FileSystemStorage(base_url='showstatic', location='uploaded_resources'), upload_to=LandingPage.models.name_season_artwork, verbose_name='Artwork')),
],
),
migrations.CreateModel(
name='Show',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('timestamp', models.DateTimeField(auto_now=True, help_text='The date and time this object was created')),
('name', models.CharField(help_text='The full name of the show', max_length=40, verbose_name='Full Name')),
('abbr', models.SlugField(help_text='A short abbreviation of the show, for use in urls', max_length=5, unique=True, verbose_name='Abbreviation')),
('description', models.TextField(help_text='A description of the show', verbose_name='Description')),
('release', models.DateField(help_text='The release date of the first episode of the show', verbose_name='Release Date')),
('artwork', models.ImageField(help_text='The artwork associated with the show. Should display the name of the show in a movie-poster esque format. Aspect ration should be about 2:3', storage=django.core.files.storage.FileSystemStorage(base_url='showstatic', location='uploaded_resources'), upload_to=LandingPage.models.name_artwork, verbose_name='Artwork')),
('imdb', models.URLField(help_text='The url of the IMDb page for this show', verbose_name='IMDb Page')),
('moderated', models.BooleanField(help_text='Whether or not this show is user-moderated', verbose_name='User Moderated')),
('css', models.FileField(help_text="The CSS stylesheet applied to this show's page", storage=django.core.files.storage.FileSystemStorage(base_url='showstatic', location='uploaded_resources'), upload_to=LandingPage.models.name_css, verbose_name='Custom Style')),
('banner', models.ImageField(help_text="A banner used for the show's page.", storage=django.core.files.storage.FileSystemStorage(base_url='showstatic', location='uploaded_resources'), upload_to=LandingPage.models.name_banner, verbose_name='Artwork')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='ShowModerator',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('timestamp', models.DateTimeField(auto_now=True, help_text='The date and time this object was created')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='ShowSubmission',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('timestamp', models.DateTimeField(auto_now=True, help_text='The date and time this object was created')),
('details', models.TextField(help_text='Some details about the show. Why it should be added, where information about it can be found, etc.', verbose_name='Details')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='Submission',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('timestamp', models.DateTimeField(auto_now=True, help_text='The date and time this object was created')),
('url', models.URLField(help_text='The link that was submitted')),
('tags', models.CharField(help_text='Tags applied to this link submission', max_length=200)),
('episode', models.ForeignKey(help_text='What episode this link contains a mirror of', on_delete=django.db.models.deletion.CASCADE, related_name='submissions', to='LandingPage.Episode', verbose_name='Submitted For')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='SubmissionVote',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('timestamp', models.DateTimeField(auto_now=True, help_text='The date and time this object was created')),
('positive', models.BooleanField(help_text='If this is true, the vote is an upvote. Otherwise, it is a downvote')),
('submission', models.ForeignKey(help_text='What this submission was cast on', on_delete=django.db.models.deletion.CASCADE, related_name='votes', to='LandingPage.Submission')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='User',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('timestamp', models.DateTimeField(auto_now=True, help_text='The date and time this object was created')),
('user_id', models.CharField(help_text="The UUID assigned to this user by IcyNet's auth servers", max_length=36)),
('email', models.EmailField(help_text="This user's email address", max_length=254)),
('display_name', models.CharField(help_text='The name shown to other users', max_length=20, verbose_name='Display Name')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='Watch',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('timestamp', models.DateTimeField(auto_now=True, help_text='The date and time this object was created')),
('episode', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='LandingPage.Episode')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='Admin',
fields=[
('user_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='LandingPage.User')),
],
options={
'abstract': False,
},
bases=('LandingPage.user',),
),
migrations.AddField(
model_name='watch',
name='user',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='LandingPage.User'),
),
migrations.AddField(
model_name='user',
name='favorites',
field=models.ManyToManyField(related_name='favorited_by', through='LandingPage.Favorite', to='LandingPage.Episode'),
),
migrations.AddField(
model_name='user',
name='watches',
field=models.ManyToManyField(related_name='watched_by', through='LandingPage.Watch', to='LandingPage.Episode'),
),
migrations.AddField(
model_name='submissionvote',
name='user',
field=models.ForeignKey(help_text='The user who cast this vote', on_delete=django.db.models.deletion.CASCADE, related_name='votes', to='LandingPage.User'),
),
migrations.AddField(
model_name='submission',
name='user',
field=models.ForeignKey(help_text='The user who submitted this link', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='submissions', to='LandingPage.User'),
),
migrations.AddField(
model_name='showsubmission',
name='user',
field=models.ForeignKey(help_text='The user who submitted this show', on_delete=django.db.models.deletion.CASCADE, related_name='show_submissions', to='LandingPage.User', verbose_name='Submitter'),
),
migrations.AddField(
model_name='showmoderator',
name='appointed_by',
field=models.ForeignKey(help_text='The user who appointed this moderator', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='appointed_mods', to='LandingPage.User', verbose_name='Appointed by'),
),
migrations.AddField(
model_name='showmoderator',
name='show',
field=models.ForeignKey(help_text='The show this user moderates', on_delete=django.db.models.deletion.CASCADE, related_name='moderators', to='LandingPage.Show', verbose_name='Moderated Show'),
),
migrations.AddField(
model_name='showmoderator',
name='user',
field=models.ForeignKey(help_text='The user who moderates this show', on_delete=django.db.models.deletion.CASCADE, related_name='moderated_shows', to='LandingPage.User', verbose_name='Moderator'),
),
migrations.AddField(
model_name='season',
name='show',
field=models.ForeignKey(help_text='The show this season belongs to', on_delete=django.db.models.deletion.CASCADE, related_name='seasons', to='LandingPage.Show'),
),
migrations.AddField(
model_name='report',
name='reporter',
field=models.ForeignKey(help_text='The user who created this report', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='reports', to='LandingPage.User', verbose_name='Reporter'),
),
migrations.AddField(
model_name='favorite',
name='user',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='LandingPage.User'),
),
migrations.AddField(
model_name='episode',
name='season',
field=models.ForeignKey(help_text='The season this episode is from', on_delete=django.db.models.deletion.CASCADE, related_name='episodes', to='LandingPage.Season'),
),
migrations.AddField(
model_name='episode',
name='show',
field=models.ForeignKey(help_text='The show this episode belongs to', on_delete=django.db.models.deletion.CASCADE, related_name='episodes', to='LandingPage.Show'),
),
migrations.AddField(
model_name='discussionvote',
name='user',
field=models.ForeignKey(help_text='The user which cast this vote', on_delete=django.db.models.deletion.CASCADE, related_name='discussion_votes', to='LandingPage.User'),
),
migrations.AddField(
model_name='discussionreply',
name='user',
field=models.ForeignKey(help_text='The user that posted this reply', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='replies', to='LandingPage.User'),
),
migrations.AddField(
model_name='discussionboard',
name='show',
field=models.ForeignKey(help_text='The show this discussion was created for', on_delete=django.db.models.deletion.CASCADE, related_name='discussion_boards', to='LandingPage.Show'),
),
migrations.AddField(
model_name='discussionboard',
name='user',
field=models.ForeignKey(help_text='The user that created this discussion', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='discussion_boards', to='LandingPage.User'),
),
migrations.AddField(
model_name='ban',
name='scope',
field=models.ManyToManyField(help_text='The shows this user is banned from interacting with', related_name='bans', to='LandingPage.Show', verbose_name='Banned From'),
),
migrations.AddField(
model_name='ban',
name='user',
field=models.OneToOneField(help_text='The user this ban applies to', on_delete=django.db.models.deletion.CASCADE, to='LandingPage.User', verbose_name='Banned User'),
),
migrations.AddField(
model_name='ban',
name='admin',
field=models.ForeignKey(help_text='The admin which banned this user', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='bans', to='LandingPage.Admin', verbose_name='Banned By'),
),
]

View File

@ -0,0 +1,76 @@
footer
padding: 20px
background: #e9f6fd
background: -moz-linear-gradient(top, #e9f6fd 0%, #d3eefb 100%)
background: -webkit-linear-gradient(top, #e9f6fd 0%,#d3eefb 100%)
background: linear-gradient(to bottom, #e9f6fd 0%,#d3eefb 100%)
border-top: 1px solid #ddd
text-align: center
.copyright
display: inline-block
text-align: center
font-size: 90%
vertical-align: top
margin-top: 35px
padding: 15px
margin-left: 5vw
.squeebot
width: 200px
.logo
font-family: "Open Sans"
font-weight: bold
text-transform: uppercase
text-shadow: 2px 2px 1px #007104
color: #00b300
letter-spacing: 5px
user-select: none
font-size: 30px
text-align: inherit
cursor: pointer
display: inline-block
.part1, .part2
display: inline-block
.part1
color: #03A9F4
text-shadow: 2px 2px 1px #0059a0
margin-right: 5px
.socialbtn
width: 28px
height: 28px
display: inline-block
line-height: 28px
color: #fff
font-size: 160%
border-radius: 100px
padding: 2px
margin: 5px
&#github
background-color: #000
&#twitter
background-color: #03a9f4
&#discord
background-color: #2C2F33
.discordlogo
position: relative
top: 3px
background: url(https://icynet.eu/static/image/Discord-Logo-White.svg)
width: 22px
height: 22px
display: inline-block
i
position: relative
right: 1px
span.divider
color: #ddd
margin: 0 5px
cursor: default
@media all and (max-width: 800px)
footer
.squeebot
margin: 0
width: 150px
margin: auto
display: block
.copyright
margin-left: 0

View File

@ -0,0 +1,58 @@
body
background-color: #fff
font-family: "Open Sans", Helvetica
margin: 0
padding: 0
.unibar
padding: 20px
border-bottom: 1px solid #ddd
box-shadow: 2px 2px 5px #9c9c9c
.logo
font-size: 200%
.userdata
display: inline-block
float: right
padding: 12px
.wrapper
min-height: 100vh
.show-promo
display: inline-block
width: 200px
text-align: center
margin: 5px
vertical-align: text-top
.artwork
max-width: 180px
max-height: 260px
.blocklayout
.block
margin: 15px
box-shadow: 2px 2px 5px #9c9c9c
h1
background-color: #f5f5f5
border: 2px solid #d0d0d0
border-bottom: 0
padding: 8px
margin: 0
.content
overflow: hidden
border: 2px solid #d0d0d0
padding: 10px
&.columns
position: relative
.column
position: absolute
left: 0px
top: 0
right: 0
bottom: 0
&.primary
width: 80%
&.smaller
width: 20%
right: 0
left: auto

View File

@ -0,0 +1,37 @@
{% load pipeline %}
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" type="text/css" href="//fonts.googleapis.com/css?family=Open+Sans">
<link rel="stylesheet" type="text/css" href="//cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
{% stylesheet 'style' %}
<meta charset="utf-8">
{% block meta %}
<meta name="description" content="Episodes.Community - Share, Discuss and Watch your Favourite Shows">
<meta name="keywords" content="episodes,community,television,shows,discussion">
<meta name="author" content="Icy Network">
{% endblock %}
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Episodes.Community{% endblock %}</title>
</head>
<body>
{% block unibar %}
<div class="unibar">
<span class="logo">Episodes<span class="period">.</span>Community</span>
<div class="userdata">
{% if request.session.user_id %}
logged in
{% else %}
<a href="/login">Log in</a>
{% endif %}
</div>
</div>
{% endblock %}
<div class="wrapper">
{% block content %}{% endblock %}
</div>
{% block footer %}
<footer class="icy"><img class="squeebot" src="https://icynet.eu/static/image/squeebot.svg"><span class="copyright"><a href="https://icynet.eu" target="_blank"><div class="logo small"><div class="part1">Icy</div><div class="part2">Network</div></div></a><div class="social"><a class="socialbtn" id="github" href="https://github.com/IcyNet/" target="_blank"><i class="fa fa-fw fa-github"></i></a><a class="socialbtn" id="twitter" href="https://twitter.com/IcyNet" target="_blank"><i class="fa fa-fw fa-twitter"></i></a><a class="socialbtn" id="discord" href="https://discord.gg/Xe7MKSx" target="_blank"><span class="discordlogo"></span></a></div><span>© 2017 - Icy Network - Some Rights Reserved</span><br><span> <a href="https://icynet.eu/docs/terms-of-service">Terms of Service</a><span class="divider">|</span><a href="https://icynet.eu/docs/privacy-policy">Privacy Policy</a><span class="divider">|</span><a href="https://icynet.eu/donate">Donate</a></span></span></footer>
{% endblock %}
</body>
</html>

View File

@ -1,11 +1,37 @@
{% for show in shows %}
<div>
<h3>{{show.name}} [{{show.abbr}}]</h3>
<p>{{show.description}}</p>
<ul>
{% for ep in show.episodes.all %}
<li>Episode {{ep.episode}} — {{ep.airdate}}</li>
{% endfor %}
</ul>
{% extends "base.html" %}
{% block content %}
<div class="blocklayout columns">
<div class="column primary">
<div class="block">
<h1>Welcome to Episodes.Community!</h1>
<div class="content">
<p>Episodes.Community is a platform for sharing, watching and discussing your favorite shows!</p>
<p>Currently hosting {{ stats.shows }} shows, {{ stats.episodes }} submissions and {{ stats.boards }} discussions</p>
</div>
</div>
<div class="block">
<h1>Latest releases</h1>
<div class="content">
{% if not recent %} Nothing to show {% endif %}
{% for show in recent %}
<a class="show-promo" href="#">
<img class="artwork" src="/media/uploaded_resources/{{show.artwork}}">
<span>{{show.name}}</span>
</a>
{% endfor %}
</div>
</div>
</div>
<div class="column smaller">
<div class="block">
<h1>Donate</h1>
<div class="content">
<p>Donate to Icy Network to help us keep up our communities.</p>
<a href="https://icynet.eu/donate">Click Here to Donate</a>
</div>
</div>
</div>
</div>
{% endfor %}
{% endblock %}

View File

@ -10,6 +10,8 @@ import hashlib
import json
from .models import User
from .models import Show
from .models import Submission
from .models import DiscussionBoard
# Create your views here.
# Redirect url should point to this view
@ -87,7 +89,12 @@ class LandingPage(TemplateView):
def get_context_data(self, **kwargs):
ctx = super().get_context_data()
ctx['shows'] = Show.objects.annotate(recency=Max('episodes__airdate')).order_by('-recency')[:8]
ctx['recent'] = Show.objects.annotate(recency=Max('episodes__airdate')).order_by('-recency')[:8]
ctx['stats'] = {
'shows': Show.objects.count(),
'episodes': Submission.objects.count(),
'boards': DiscussionBoard.objects.count()
}
return ctx

View File

@ -1,3 +1,4 @@
Django==1.11.4
Pillow=4.2.1
Pillow==4.2.1
dj-database-url==0.4.2
django-pipeline==1.6.13