Plumb most of the basic interactions

This commit is contained in:
2017-02-05 22:19:31 -05:00
parent 37461eb4a7
commit d066ad0815
17 changed files with 442 additions and 41 deletions

View File

@@ -4,7 +4,7 @@ from django.contrib.auth.models import User
from django.db import models from django.db import models
class Committee(models.Model): class Committee(models.Model):
name = models.CharField(max_length=100) name = models.CharField(max_length=100, unique=True)
chair = models.ForeignKey(User, null=True) chair = models.ForeignKey(User, null=True)
def __str__(self): def __str__(self):

View File

@@ -41,9 +41,13 @@ INSTALLED_APPS = [
'django.contrib.sessions', 'django.contrib.sessions',
'django.contrib.messages', 'django.contrib.messages',
'django.contrib.staticfiles', 'django.contrib.staticfiles',
'django_cleanup',
'social_django',
'storages',
'stdimage',
'items', 'items',
'committee', 'committee',
'social_django',
] ]
MIDDLEWARE = [ 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 # Password validation
# https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators # 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_KEY = os.environ['SOCIAL_AUTH_GOOGLE_OAUTH2_KEY']
SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET = os.environ['SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET'] 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_WHITELISTED_DOMAINS = ['andrew.cmu.edu']
SOCIAL_AUTH_GOOGLE_OAUTH2_AUTH_EXTRA_ARGUMENTS = {'hd': 'andrew.cmu.edu' }
SOCIAL_AUTH_LOGIN_REDIRECT_URL = '/items/' SOCIAL_AUTH_LOGIN_REDIRECT_URL = '/items/'
# Internationalization # Internationalization

View File

@@ -1,11 +1,16 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from django.contrib.auth.models import User 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 committee.models import Committee
from django.db import models from django.db import models
class Item(models.Model): class Item(models.Model):
S3 = S3Boto3Storage()
NEW = 'N' NEW = 'N'
PREAPPROVED = 'C' PREAPPROVED = 'C'
PROCESSED = 'P' PROCESSED = 'P'
@@ -24,24 +29,29 @@ class Item(models.Model):
details = models.TextField() details = models.TextField()
cost = models.DecimalField(max_digits=7, decimal_places=2) cost = models.DecimalField(max_digits=7, decimal_places=2)
date_purchased = models.DateField('date purchased') 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') created_by = models.ForeignKey(User, related_name='created_by')
approved_by = models.ManyToManyField(User, blank=True, related_name='approved_by') approved_by = models.ManyToManyField(User, blank=True, related_name='approved_by')
date_filed = models.DateTimeField('date filed') date_filed = models.DateTimeField('date filed')
status = models.CharField(max_length=2, choices=STATUS) status = models.CharField(max_length=2, choices=STATUS)
task_id = models.CharField(max_length=30)
def approved(self): def approved(self):
return self.status == 'P' return self.status == Item.PREAPPROVED
def processed(self): def processed(self):
return self.status == 'C' return self.status == Item.PROCESSED
def rejected(self): def rejected(self):
return self.status == 'R' return self.status == Item.REJECTED
def new(self): def new(self):
return self.status == 'N' return self.status == Item.NEW
def statusText(self):
return dict(Item.STATUS)[self.status]
def comName(self): def comName(self):
return self.committee.name return self.committee.name

View File

@@ -3,5 +3,10 @@ from . import views
urlpatterns = [ urlpatterns = [
url(r'^$', views.list, name='list'), url(r'^$', views.list, name='list'),
url(r'^(?P<item_id>\d+)/$', views.details, name='details'),
url(r'^(?P<item_id>\d+)/approve$', views.approve, name='approve'),
url(r'^(?P<item_id>\d+)/reject$', views.reject, name='reject'),
url(r'^(?P<item_id>\d+)/edit$', views.edit, name='edit'),
url(r'^(?P<item_id>\d+)/delete$', views.delete, name='delete'),
url(r'^new$', views.new_form, name='new_form'), url(r'^new$', views.new_form, name='new_form'),
] ]

View File

@@ -1,22 +1,158 @@
from django.shortcuts import HttpResponse from django.shortcuts import HttpResponse, HttpResponseRedirect
from django.template import loader 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 models import Item
from committee.models import Committee 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): def list(request):
template = loader.get_template('items/list.html') template = loader.get_template('items/list.html')
items = myItems(request.user)
context = { 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)) 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): def new_form(request):
template = loader.get_template('items/new.html') if request.method == 'POST':
context = { if request.FILES['image'].size > 10 * (1 << 20):
'committees': Committee.objects.order_by('name'), 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))

View File

@@ -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;
}
}

View File

@@ -1,6 +1,6 @@
$purple: #563d7c; $purple: #563d7c;
$gold: #fdd017; $gold: #fdd017;
$lightgray: #fcfcfc; $lightgray: #f8f8f8;
$midgray: #969499; $midgray: #969499;
$darkgray: #4b4a4d; $darkgray: #4b4a4d;
$text: #252526; $text: #252526;
@@ -41,18 +41,20 @@ body {
} }
.container { .container {
height: 100%; & > div {
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
margin-top: 64px; margin-top: 64px;
max-width: 960; max-width: 960;
background-color: #fff; background-color: #fff;
border: solid 1px $midgray; border: solid 1px $midgray;
box-shadow: 0px 3px 9px lighten($shadowgray, 20%);
}
} }
.btn { .btn {
border-radius: $pad-s; border-radius: $pad-s;
border: 1px solid $midgray;
color: $purple; color: $purple;
display: inline-block; display: inline-block;
@@ -63,6 +65,6 @@ body {
&:hover { &:hover {
background-color: darken($purple, 15%); background-color: lighten($purple, 35%);
} }
} }

View File

@@ -1,4 +1,5 @@
@import 'globals'; @import 'globals';
@import 'details';
@import 'items'; @import 'items';
@import 'login'; @import 'login';
@import 'new'; @import 'new';

View File

@@ -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 { .item {
padding: $pad-m $pad-xl; padding: $pad-m $pad-xl;
margin: 0px; margin: 0px;
@@ -12,6 +20,10 @@
margin: 0px; margin: 0px;
} }
a {
text-decoration: none;
}
.details-row { .details-row {
margin: $pad-m 0; margin: $pad-m 0;
font-size: 16px; font-size: 16px;
@@ -36,7 +48,6 @@
} }
.cost { .cost {
float: right;
display: inline-block; display: inline-block;
text-align: center; text-align: center;
width: 120px; width: 120px;
@@ -58,22 +69,52 @@
background-color: #d9ffd9; background-color: #d9ffd9;
} }
.rejected {
background-color: $midgray;
color: $lightgray;
}
.newItem { .newItem {
background-color: #fff7d9; 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; $button-size: 24px;
.btn-floating { .btn-floating {
position: fixed; position: fixed;
bottom: $button-size; bottom: $button-size;
right: $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; z-index: 1000;
border-radius: 50%; border-radius: 50%;
text-align: center; text-align: center;
box-shadow: 0 0 6px rgba(0,0,0,.16),0 6px 12px rgba(0,0,0,.32); 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;
}
} }

View File

@@ -14,14 +14,12 @@ hr {
form { form {
div { div {
margin: $pad-l; margin: $pad-l;
width: 376px;
* { * {
display: inline-block; display: inline-block;
} }
label { label {
text-align: right;
width: 180px; width: 180px;
color: $text; color: $text;
vertical-align: top; vertical-align: top;
@@ -29,7 +27,6 @@ form {
input, select, textarea { input, select, textarea {
width: 180px; width: 180px;
float: right;
} }
&.clear { &.clear {
@@ -46,9 +43,32 @@ form {
} }
.rcol { .rcol {
float: right;
width: 40%;
margin: $pad-l; margin: $pad-l;
color: $midgray; color: $midgray;
font-weight: lighter; 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%;
}
}

View File

@@ -13,6 +13,8 @@
{% block main %} {% block main %}
{% endblock %} {% endblock %}
</div> </div>
{% block bottom %}
{% endblock %}
<script> <script>
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),

View File

@@ -6,7 +6,7 @@
<link href="{% static "css/site.css" %}" rel="stylesheet"> <link href="{% static "css/site.css" %}" rel="stylesheet">
</head> </head>
<body class="login"> <body class="login">
<div class="container"> <div>
<h3>Delta Beta Chapter</h3> <h3>Delta Beta Chapter</h3>
<h2>Fincom Webapp</h2> <h2>Fincom Webapp</h2>
{% if error %} {% if error %}

View File

@@ -0,0 +1,22 @@
{% extends "../boilerplate.html" %}
{% block main %}
<div>
{% include "./item.html" %}
<div class="approve">
<div class="status">Status: <span>{{ I.statusText }}</span></div>
<div class="actions">
<a href="/items/{{ I.pk }}/approve" class="btn">Approve</a>
<a href="/items/{{ I.pk }}/reject" class="btn">Reject</a>
<a href="/items/{{ I.pk }}/delete" class="btn">Delete</a>
<a href="/items/{{ I.pk }}/edit" class="btn">Edit</a>
</div>
<a href="{{ I.image.url }}"><img src="{{ I.image.thumbnail.url }}"></a>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,65 @@
{% extends "../boilerplate.html" %}
{% block title %}Edit Reimbursement{% endblock %}
{% block main %}
<div>
<h1>Edit Reimbursements</h1>
<hr>
<form action="/items/{{ I.pk }}/edit" method="post">
{% csrf_token %}
<div>
<label for="desc">Item</label>
<input id="desc" type="text" name="desc" placeholder="What you bought"
value="{{ I.desc }}">
</div>
<div>
<label for="event">Event</label>
<input id="event" type="text" name="event" placeholder="When we need it"
value="{{ I.event }}">
</div>
<div>
<label for="committee">Committee or Budget</label>
<select name="committee" id="committee">
{% for C in committees %}
{% if I.committee == C %}
<option selected>{{ C.name }}</option>
{% else %}
<option>{{ C.name }}</option>
{% endif %}
{% endfor %}
</select>
</div>
<div>
<label for="cost">Cost</label>
<input id="cost" type="text" name="cost" placeholder="15.00"
value="{{ I.cost }}">
</div>
<div>
<label for="date">Date Purchased</label>
<input id="date" type="date" name="date" placeholder="mm/dd/yyyy"
value="{{ I.date_purchased }}">
</div>
<div class="clear">
<div>
<label for="details">Details</label>
<textarea id="details" name="details" rows="4" class="clear">{{ I.details }}</textarea>
</div>
</div>
<div class="clear">
<div>
<input type="submit">
</div>
</div>
</form>
</div>
{% endblock %}

View File

@@ -6,11 +6,14 @@
<div class="cost approved"> <div class="cost approved">
{% elif I.processed %} {% elif I.processed %}
<div class="cost processed"> <div class="cost processed">
{% elif I.rejected %}
<div class="cost rejected">
{% else %} {% else %}
<div class="cost newItem"> <div class="cost newItem">
{% endif %} {% endif %}
${{ I.cost|floatformat:"2" }} ${{ I.cost|floatformat:"2" }}
</div> </div>
<a href="/items/{{ I.pk }}">
<div class="details-row"> <div class="details-row">
<b>{{ I.event }}:</b> <b>{{ I.event }}:</b>
{{ I.desc }} {{ I.desc }}
@@ -19,12 +22,15 @@
<em>on</em> {{ I.date_purchased }} <em>on</em> {{ I.date_purchased }}
<em>(filed {{ I.date_filed }})</em> <em>(filed {{ I.date_filed }})</em>
</div> </div>
</a>
<div class="status"> <div class="status">
{% if I.approved or I.processed %} {% if I.approved or I.processed %}
Approved By: Approved By:
{% for u in I.approved_by.all %} {% for u in I.approved_by.all %}
{{ u.first_name }} {{ u.last_name }} {{ u.first_name }} {{ u.last_name }}
{% endfor %} {% endfor %}
{% elif I.rejected %}
Reimbursement Declined
{% else %} {% else %}
Needs Approval Needs Approval
{% endif %} {% endif %}

View File

@@ -2,10 +2,60 @@
{% block main %} {% block main %}
{% for I in items %} {% if error %}
<div>
<span class="error">{{ error }}</span>
</div>
{% endif %}
{% if preapproved %}
<section>Pre-Approved</section>
<div>
{% for I in preapproved %}
{% include "./item.html" %} {% include "./item.html" %}
{% empty %} {% empty %}
<div>No Reimbursements</div> <div class="empty">No Reimbursements</div>
{% endfor %} {% endfor %}
</div>
{% endif %}
{% if newitems %}
<section>New Items</section>
<div>
{% for I in newitems %}
{% include "./item.html" %}
{% empty %}
<div class="empty">No Reimbursements</div>
{% endfor %}
</div>
{% endif %}
{% if processed %}
<section>Approved</section>
<div>
{% for I in processed %}
{% include "./item.html" %}
{% empty %}
<div class="empty">No Reimbursements</div>
{% endfor %}
</div>
{% endif %}
{% if rejected %}
<section>Rejected</section>
<div>
{% for I in rejected %}
{% include "./item.html" %}
{% empty %}
<div class="empty">No Reimbursements</div>
{% endfor %}
</div>
{% endif %}
{% endblock %} {% endblock %}
{% block bottom %}
<a href="/items/new" title="Submit new item" class="btn-floating">
<span>+</span>
</a>
{% endblock %}

View File

@@ -4,6 +4,7 @@
{% block main %} {% block main %}
<div>
<h1>Add New Reimbursements</h1> <h1>Add New Reimbursements</h1>
<hr> <hr>
@@ -12,7 +13,7 @@
you select the correct committee. you select the correct committee.
</div> </div>
<form action="/new/" method="post"> <form action="/items/new" method="post" enctype="multipart/form-data">
{% csrf_token %} {% csrf_token %}
<div> <div>
<label for="desc">Item</label> <label for="desc">Item</label>
@@ -53,16 +54,15 @@
<div> <div>
<label for="image">Receipt</label> <label for="image">Receipt</label>
<input type="file" name="image" id="image"> <input type="file" name="image" id="image" accept="image/*">
</div> </div>
<div class="clear">
<div> <div>
<input type="submit"> <input type="submit">
</div> </div>
</div>
</form> </form>
</div>
{% endblock %} {% endblock %}