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
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):

View File

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

View File

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

View File

@@ -3,5 +3,10 @@ from . import views
urlpatterns = [
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'),
]

View File

@@ -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))

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;
$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%);
}
}

View File

@@ -1,4 +1,5 @@
@import 'globals';
@import 'details';
@import 'items';
@import 'login';
@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 {
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;
}
}

View File

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

View File

@@ -13,6 +13,8 @@
{% block main %}
{% endblock %}
</div>
{% block bottom %}
{% endblock %}
<script>
(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),

View File

@@ -6,7 +6,7 @@
<link href="{% static "css/site.css" %}" rel="stylesheet">
</head>
<body class="login">
<div class="container">
<div>
<h3>Delta Beta Chapter</h3>
<h2>Fincom Webapp</h2>
{% 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">
{% elif I.processed %}
<div class="cost processed">
{% elif I.rejected %}
<div class="cost rejected">
{% else %}
<div class="cost newItem">
{% endif %}
${{ I.cost|floatformat:"2" }}
</div>
<a href="/items/{{ I.pk }}">
<div class="details-row">
<b>{{ I.event }}:</b>
{{ I.desc }}
@@ -19,12 +22,15 @@
<em>on</em> {{ I.date_purchased }}
<em>(filed {{ I.date_filed }})</em>
</div>
</a>
<div class="status">
{% if I.approved or I.processed %}
Approved By:
{% for u in I.approved_by.all %}
{{ u.first_name }} {{ u.last_name }}
{% endfor %}
{% elif I.rejected %}
Reimbursement Declined
{% else %}
Needs Approval
{% endif %}

View File

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