From d066ad0815c346163ccf64295c633df4ddea0403 Mon Sep 17 00:00:00 2001 From: Aaron Gutierrez Date: Sun, 5 Feb 2017 22:19:31 -0500 Subject: [PATCH] Plumb most of the basic interactions --- fincom/committee/models.py | 2 +- fincom/fincom/settings.py | 15 ++- fincom/items/models.py | 20 +++- fincom/items/urls.py | 5 + fincom/items/views.py | 152 ++++++++++++++++++++++++++-- fincom/static/sass/details.scss | 28 +++++ fincom/static/sass/globals.scss | 22 ++-- fincom/static/sass/include.scss | 1 + fincom/static/sass/items.scss | 45 +++++++- fincom/static/sass/new.scss | 30 +++++- fincom/templates/boilerplate.html | 2 + fincom/templates/fincom/index.html | 2 +- fincom/templates/items/details.html | 22 ++++ fincom/templates/items/edit.html | 65 ++++++++++++ fincom/templates/items/item.html | 6 ++ fincom/templates/items/list.html | 54 +++++++++- fincom/templates/items/new.html | 12 +-- 17 files changed, 442 insertions(+), 41 deletions(-) create mode 100644 fincom/static/sass/details.scss create mode 100644 fincom/templates/items/details.html create mode 100644 fincom/templates/items/edit.html diff --git a/fincom/committee/models.py b/fincom/committee/models.py index c3b4d8d..6f9e746 100644 --- a/fincom/committee/models.py +++ b/fincom/committee/models.py @@ -4,7 +4,7 @@ from django.contrib.auth.models import User from django.db import models class Committee(models.Model): - name = models.CharField(max_length=100) + name = models.CharField(max_length=100, unique=True) chair = models.ForeignKey(User, null=True) def __str__(self): diff --git a/fincom/fincom/settings.py b/fincom/fincom/settings.py index de7c5c3..9d68e2b 100644 --- a/fincom/fincom/settings.py +++ b/fincom/fincom/settings.py @@ -41,9 +41,13 @@ INSTALLED_APPS = [ 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', + 'django_cleanup', + 'social_django', + 'storages', + 'stdimage', + 'items', 'committee', - 'social_django', ] MIDDLEWARE = [ @@ -93,6 +97,14 @@ DATABASES = { } } +# S3 File Storage +DEFAULT_FILE_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage' +AWS_ACCESS_KEY_ID=os.environ['AWS_ACCESS_KEY_ID'] +AWS_SECRET_ACCESS_KEY=os.environ['AWS_SECRET_ACCESS_KEY'] +AWS_AUTO_CREATE_BUCKET=True +AWS_STORAGE_BUCKET_NAME='fincom' +AWS_S3_FILE_OVERWRITE=False + # Password validation # https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators @@ -118,6 +130,7 @@ AUTH_PASSWORD_VALIDATORS = [ SOCIAL_AUTH_GOOGLE_OAUTH2_KEY = os.environ['SOCIAL_AUTH_GOOGLE_OAUTH2_KEY'] SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET = os.environ['SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET'] SOCIAL_AUTH_GOOGLE_OAUTH2_WHITELISTED_DOMAINS = ['andrew.cmu.edu'] +SOCIAL_AUTH_GOOGLE_OAUTH2_AUTH_EXTRA_ARGUMENTS = {'hd': 'andrew.cmu.edu' } SOCIAL_AUTH_LOGIN_REDIRECT_URL = '/items/' # Internationalization diff --git a/fincom/items/models.py b/fincom/items/models.py index f0c8a2f..c3359b0 100644 --- a/fincom/items/models.py +++ b/fincom/items/models.py @@ -1,11 +1,16 @@ from __future__ import unicode_literals from django.contrib.auth.models import User +from storages.backends.s3boto3 import S3Boto3Storage +from stdimage.models import StdImageField +from datetime import datetime, date from committee.models import Committee from django.db import models class Item(models.Model): + S3 = S3Boto3Storage() + NEW = 'N' PREAPPROVED = 'C' PROCESSED = 'P' @@ -24,24 +29,29 @@ class Item(models.Model): details = models.TextField() cost = models.DecimalField(max_digits=7, decimal_places=2) date_purchased = models.DateField('date purchased') + image = StdImageField(upload_to='images/%Y/%m/%d', + variations={'thumbnail': (600, 600)} + ) created_by = models.ForeignKey(User, related_name='created_by') approved_by = models.ManyToManyField(User, blank=True, related_name='approved_by') date_filed = models.DateTimeField('date filed') status = models.CharField(max_length=2, choices=STATUS) - task_id = models.CharField(max_length=30) def approved(self): - return self.status == 'P' + return self.status == Item.PREAPPROVED def processed(self): - return self.status == 'C' + return self.status == Item.PROCESSED def rejected(self): - return self.status == 'R' + return self.status == Item.REJECTED def new(self): - return self.status == 'N' + return self.status == Item.NEW + + def statusText(self): + return dict(Item.STATUS)[self.status] def comName(self): return self.committee.name diff --git a/fincom/items/urls.py b/fincom/items/urls.py index 71b8345..4db5837 100644 --- a/fincom/items/urls.py +++ b/fincom/items/urls.py @@ -3,5 +3,10 @@ from . import views urlpatterns = [ url(r'^$', views.list, name='list'), + url(r'^(?P\d+)/$', views.details, name='details'), + url(r'^(?P\d+)/approve$', views.approve, name='approve'), + url(r'^(?P\d+)/reject$', views.reject, name='reject'), + url(r'^(?P\d+)/edit$', views.edit, name='edit'), + url(r'^(?P\d+)/delete$', views.delete, name='delete'), url(r'^new$', views.new_form, name='new_form'), ] diff --git a/fincom/items/views.py b/fincom/items/views.py index 960e02f..0167e31 100644 --- a/fincom/items/views.py +++ b/fincom/items/views.py @@ -1,22 +1,158 @@ -from django.shortcuts import HttpResponse +from django.shortcuts import HttpResponse, HttpResponseRedirect from django.template import loader +from django.utils import timezone +from django.contrib.auth.decorators import login_required +from django.db.models import Q from models import Item from committee.models import Committee +def isAuthorised(request, item): + return (request.user == item.committee.chair + or request.user.groups.filter(name='Fincom').exists()) -# Create your views here. +def authError(): + return HttpResponseRedirect('/items') + +def myItems(user): + if (user.groups.filter(name='Fincom').exists()): + return Items.objects.order_by('-date_filed', 'desc') + + comms = [] + for c in Committee.objects.all(): + if (c.chair == user): + comms.append(c) + + return Item.objects.filter(Q(created_by=user) | Q(committee__in=comms)) \ + .order_by('-date_filed', 'desc') + +@login_required def list(request): template = loader.get_template('items/list.html') + items = myItems(request.user) + context = { - 'items': Item.objects.all(), + 'preapproved': items.filter(status=Item.PREAPPROVED), + 'processed': items.filter(status=Item.PROCESSED), + 'newitems': items.filter(status=Item.NEW), + 'rejected': items.filter(status=Item.REJECTED), } return HttpResponse(template.render(context, request)) +@login_required +def details(request, item_id): + I = Item.objects.get(pk=item_id) + if (not isAuthorised(request, I)): + return HttpResponseRedirect('/items/' + str(item_id) + '/edit') + + template = loader.get_template('items/details.html') + + context = { + 'I': I, + } + return HttpResponse(template.render(context, request)) + +def approve(request, item_id): + I = Item.objects.get(pk=item_id) + + if (not isAuthorised(request, I)): + return authError() + + I.approved_by.add(request.user) + if (I.committee.chair == request.user and + I.status == Item.NEW): + I.status = Item.PREAPPROVED + elif (request.user.groups.filter(name='Fincom').exists()): + I.status = Item.PROCESSED + + I.save() + + return HttpResponseRedirect('/items') + +def reject(request, item_id): + I = Item.objects.get(pk=item_id) + + if (not isAuthorised(request, I)): + return authError() + + I.status = Item.REJECTED + I.save() + + return HttpResponseRedirect('/items') + +def delete(request, item_id): + I = Item.objects.get(pk=item_id) + + if (not isAuthorised(request, I)): + return authError() + + I.delete() + + return HttpResponseRedirect('/items') + +def edit(request, item_id): + I = Item.objects.get(pk=item_id) + + if (not isAuthorised(request, I) + and request.user != I.created_by): + return authError() + + if request.method == 'POST': + if (request.POST.get('desc', None)): + I.desc = request.POST['desc'] + if (request.POST.get('event', None)): + I.event = request.POST['event'] + if (request.POST.get('committee', None)): + I.committee = Committee.objects.get(name=request.POST['committee']) + if (request.POST.get('cost', None)): + I.cost = request.POST['cost'] + if (request.POST.get('date', None)): + I.date_purchased = Item.parseDate(request.POST['date']), + if (request.POST.get('details', None)): + I.details = request.POST['details'] + + I.save() + + return HttpResponseRedirect('/items/' + str(item_id)) + else: + template = loader.get_template('items/edit.html') + + context = { + 'I': I, + 'committees': Committee.objects.order_by('name'), + } + return HttpResponse(template.render(context, request)) + def new_form(request): - template = loader.get_template('items/new.html') - context = { - 'committees': Committee.objects.order_by('name'), - } + if request.method == 'POST': + if request.FILES['image'].size > 10 * (1 << 20): + template = loader.get_template('items/new.html') + context = { + 'committees': Committee.objects.order_by('name'), + 'error': 'Your image file is too large. Maximum size is 20MB', + } + return HttpResponse(template.render(context, request)) + item = Item( + desc = request.POST['desc'], + event = request.POST['event'], + committee = Committee.objects.get(name=request.POST['committee']), + cost = request.POST['cost'], + date_purchased = Item.parseDate(request.POST['date']), + details = request.POST['details'], + date_filed = timezone.now(), + created_by = request.user, + status = Item.NEW, + image = request.FILES['image'], + ) - return HttpResponse(template.render(context, request)) + item.save() + + return HttpResponseRedirect('/items') + + else: + template = loader.get_template('items/new.html') + context = { + 'committees': Committee.objects.order_by('name'), + } + + return HttpResponse(template.render(context, request)) diff --git a/fincom/static/sass/details.scss b/fincom/static/sass/details.scss new file mode 100644 index 0000000..50a1c55 --- /dev/null +++ b/fincom/static/sass/details.scss @@ -0,0 +1,28 @@ +.approve { + padding: $pad-xl; + + .status { + font-size: 16px; + color: $midgray; + font-weight: lighter; + + span { + color: $text; + } + } + + img { + border: 1px solid $midgray; + max-width: 100%; + } +} + +.actions { + margin: $pad-m 0; + + a { + text-decoration: none; + font-size: 12px; + font-weight: normal; + } +} diff --git a/fincom/static/sass/globals.scss b/fincom/static/sass/globals.scss index b803f53..67878e5 100644 --- a/fincom/static/sass/globals.scss +++ b/fincom/static/sass/globals.scss @@ -1,6 +1,6 @@ $purple: #563d7c; $gold: #fdd017; -$lightgray: #fcfcfc; +$lightgray: #f8f8f8; $midgray: #969499; $darkgray: #4b4a4d; $text: #252526; @@ -41,18 +41,20 @@ body { } .container { - height: 100%; - margin-left: auto; - margin-right: auto; - margin-top: 64px; - max-width: 960; - background-color: #fff; - border: solid 1px $midgray; + & > div { + margin-left: auto; + margin-right: auto; + margin-top: 64px; + max-width: 960; + background-color: #fff; + border: solid 1px $midgray; + box-shadow: 0px 3px 9px lighten($shadowgray, 20%); + } } - .btn { border-radius: $pad-s; + border: 1px solid $midgray; color: $purple; display: inline-block; @@ -63,6 +65,6 @@ body { &:hover { - background-color: darken($purple, 15%); + background-color: lighten($purple, 35%); } } diff --git a/fincom/static/sass/include.scss b/fincom/static/sass/include.scss index bad9912..6db8a5d 100644 --- a/fincom/static/sass/include.scss +++ b/fincom/static/sass/include.scss @@ -1,4 +1,5 @@ @import 'globals'; +@import 'details'; @import 'items'; @import 'login'; @import 'new'; diff --git a/fincom/static/sass/items.scss b/fincom/static/sass/items.scss index a7d3bd5..b314e88 100644 --- a/fincom/static/sass/items.scss +++ b/fincom/static/sass/items.scss @@ -1,3 +1,11 @@ +section { + color: $midgray; + font-weight: lighter; + font-size: 16px; + margin-top: 2*$pad-xl; + margin-bottom: -2*$pad-xl + $pad-l; +} + .item { padding: $pad-m $pad-xl; margin: 0px; @@ -12,6 +20,10 @@ margin: 0px; } + a { + text-decoration: none; + } + .details-row { margin: $pad-m 0; font-size: 16px; @@ -36,7 +48,6 @@ } .cost { - float: right; display: inline-block; text-align: center; width: 120px; @@ -58,22 +69,52 @@ background-color: #d9ffd9; } + .rejected { + background-color: $midgray; + color: $lightgray; + } + .newItem { background-color: #fff7d9; } } +@media (min-width: 800px) { + .item .cost { + float: right; + } +} + +.empty { + margin: $pad-l; + color: $midgray; + font-weight: lighter; + font-size: 16px; + text-align: center; +} + $button-size: 24px; .btn-floating { position: fixed; bottom: $button-size; right: $button-size; - padding: $button-size; + padding: $button-size/2; + color: $gold; + background-color: $purple; + text-decoration: none; + font-weight: bold; z-index: 1000; border-radius: 50%; text-align: center; box-shadow: 0 0 6px rgba(0,0,0,.16),0 6px 12px rgba(0,0,0,.32); + + span { + display: block; + width: 36; + height: 36; + font-size: 28; + } } diff --git a/fincom/static/sass/new.scss b/fincom/static/sass/new.scss index aa833b3..60fd399 100644 --- a/fincom/static/sass/new.scss +++ b/fincom/static/sass/new.scss @@ -14,14 +14,12 @@ hr { form { div { margin: $pad-l; - width: 376px; * { display: inline-block; } label { - text-align: right; width: 180px; color: $text; vertical-align: top; @@ -29,7 +27,6 @@ form { input, select, textarea { width: 180px; - float: right; } &.clear { @@ -46,9 +43,32 @@ form { } .rcol { - float: right; - width: 40%; margin: $pad-l; color: $midgray; font-weight: lighter; } + +@media (min-width: 430px) { + form { + div { + width: 376px; + + + label { + text-align: right; + } + + input, select, textarea { + width: 180px; + float: right; + } + } + } +} + +@media (min-width: 800px) { + .rcol { + float: right; + width: 40%; + } +} diff --git a/fincom/templates/boilerplate.html b/fincom/templates/boilerplate.html index ef2270a..6b4de0d 100644 --- a/fincom/templates/boilerplate.html +++ b/fincom/templates/boilerplate.html @@ -13,6 +13,8 @@ {% block main %} {% endblock %} + {% block bottom %} + {% endblock %}