Skip to content

Commit 8da18df

Browse files
author
Gerard Casas Saez
committed
add reimbursement stats
1 parent f103db8 commit 8da18df

11 files changed

+179
-11
lines changed

Diff for: README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ You can deploy this project into heroku for free. You will need to verify your a
7373
- Create super user by running `python manage.py createsuperuser` once the heroku app is deployed
7474
- Add scheduler addon: https://elements.heroku.com/addons/scheduler
7575
- Open scheduler dashboard: https://scheduler.heroku.com/dashboard (make sure it opens the just created heroku app)
76-
- Add daily job `python manage.py expire_applications`
76+
- Add daily job `python manage.py expire_applications && python manage.py expire_reimbursements`
7777

7878
### Production environment
7979

Diff for: management.sh.template

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,4 @@ export SG_KEY="API_KEY"
77
# Domain where running
88
export DOMAIN="my.hackupc.com"
99

10-
./env/bin/python manage.py expire_applications
10+
./env/bin/python manage.py expire_applications && ./env/bin/python manage.py expire_reimbursements

Diff for: reimbursement/management/__init__.py

Whitespace-only changes.

Diff for: reimbursement/management/commands/__init__.py

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
from django.core.management.base import BaseCommand
2+
from django.utils import timezone
3+
4+
5+
from reimbursement import models
6+
7+
8+
class Command(BaseCommand):
9+
help = 'Checks reimbursements that have expired'
10+
11+
def handle(self, *args, **options):
12+
self.stdout.write('Checking expired reimbursements...')
13+
reimbs = models.Reimbursement.objects.filter(
14+
expiration_time__lte=timezone.now(), status=models.RE_PEND_TICKET)
15+
self.stdout.write('Checking expired reimbursements...%s found' % reimbs.count())
16+
for reimb in reimbs:
17+
reimb.expire()
18+

Diff for: reimbursement/models.py

+7-1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
RE_PEND_TICKET = 'PT'
1919
RE_PEND_APPROVAL = 'PA'
2020
RE_APPROVED = 'A'
21+
RE_EXPIRED = 'X'
2122
RE_FRIEND_SUBMISSION = 'FS'
2223

2324
RE_STATUS = [
@@ -26,6 +27,7 @@
2627
(RE_PEND_TICKET, 'Pending receipt submission'),
2728
(RE_PEND_APPROVAL, 'Pending receipt approval'),
2829
(RE_APPROVED, 'Receipt approved'),
30+
(RE_EXPIRED, 'Expired'),
2931
(RE_FRIEND_SUBMISSION, 'Friend submission'),
3032
]
3133

@@ -105,7 +107,7 @@ def timeleft_expiration(self):
105107

106108
@property
107109
def expired(self):
108-
return timezone.now() > self.expiration_time and self.status == RE_PEND_TICKET
110+
return self.status == RE_EXPIRED
109111

110112
def generate_draft(self, application):
111113
if self.status != RE_DRAFT:
@@ -116,6 +118,10 @@ def generate_draft(self, application):
116118
self.reimbursement_money = None
117119
self.save()
118120

121+
def expire(self):
122+
self.status = RE_EXPIRED
123+
self.save()
124+
119125
def send(self, user):
120126
if not self.assigned_money:
121127
raise ValidationError('Reimbursement can\'t be sent because '

Diff for: reimbursement/signals.py

+9-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
from django.db.models.signals import post_save
22
from django.dispatch import receiver
3+
from django.utils import timezone
34

45
from applications.models import Application
5-
from reimbursement.models import Reimbursement
6+
from reimbursement.models import Reimbursement, RE_EXPIRED, RE_PEND_TICKET
67

78

89
@receiver(post_save, sender=Application)
@@ -18,3 +19,10 @@ def reimbursement_create(sender, instance, created, *args, **kwargs):
1819
else:
1920
reimb = Reimbursement()
2021
reimb.generate_draft(instance)
22+
23+
24+
@receiver(post_save, sender=Reimbursement)
25+
def reimbursement_unexpire(sender, instance, created, *args, **kwargs):
26+
if instance.status == RE_EXPIRED and instance.expiration_time > timezone.now():
27+
instance.status = RE_PEND_TICKET
28+
instance.save()

Diff for: stats/templates/application_stats.html

+3-1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ <h1>Application stats</h1>
1414
<div class="col-md-6">
1515
<h3>Status</h3>
1616
<div id="applications_stats"></div>
17+
<p><b>Application count:</b> <span id="app_count"></span></p>
1718
</div>
1819
<div class="col-md-6">
1920
<h3>Gender</h3>
@@ -44,7 +45,7 @@ <h3>Confirmed only</h3>
4445
<div id="diet_stats_confirmed"></div>
4546
</div>
4647
<div class="col-md-12">
47-
<p><b>Other diet requirements</b> <span id="other_diet"></span></p>
48+
<p><b>Confirmed extra diet requirements:</b><br> <span id="other_diet"></span></p>
4849
</div>
4950

5051
</div>
@@ -190,6 +191,7 @@ <h3>Confirmed only</h3>
190191
});
191192
$('#other_diet').html(data['other_diet']);
192193
$('#update_date').html(data['update_time']);
194+
$('#app_count').html(data['app_count']);
193195
})
194196
;
195197

Diff for: stats/templates/reimbursement_stats.html

+90
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
{% extends 'c3_base.html' %}
2+
3+
{% block head_title %}Reimbursement stats{% endblock %}
4+
{% block panel %}
5+
<h1>Reimbursement stats</h1>
6+
<small class="pull-right"><b>Last updated:</b> <span id="update_date"></span></small>
7+
<div class="row">
8+
<div class="col-md-12">
9+
<div id="amounts">
10+
</div>
11+
</div>
12+
</div>
13+
<div class="row">
14+
<div class="col-md-6">
15+
<h3>Status</h3>
16+
<div id="reimb_stats"></div>
17+
<p><b>Reimbursement count:</b> <span id="reimb_count"></span></p>
18+
</div>
19+
<div class="col-md-6">
20+
<h3>Applications</h3>
21+
<div id="reimb_apps"></div>
22+
</div>
23+
</div>
24+
{% endblock %}
25+
{% block c3script %}
26+
<script>
27+
$.getJSON('{% url 'api_reimb_stats' %}', function (data) {
28+
c3.generate({
29+
bindto: '#amounts',
30+
data: {
31+
json: data['amounts'],
32+
keys: {
33+
x: 'status_name',
34+
value: ['final_amount', 'max_amount']
35+
},
36+
type: 'bar'
37+
},
38+
39+
axis: {
40+
x: {
41+
type: 'category'
42+
}
43+
}
44+
});
45+
46+
47+
var status_data = {};
48+
var sites = [];
49+
$(data['status']).each(function (c, e) {
50+
sites.push(e.status_name);
51+
status_data[e.status_name] = e.reimbursements;
52+
});
53+
c3.generate({
54+
bindto: '#reimb_stats',
55+
data: {
56+
json: status_data,
57+
type: 'donut'
58+
59+
},
60+
donut: {
61+
label: {
62+
format: function (value, ratio, id) {
63+
return value;
64+
}
65+
}
66+
}
67+
});
68+
69+
c3.generate({
70+
bindto: '#reimb_apps',
71+
data: {
72+
json: data['reimb_apps'],
73+
type: 'donut'
74+
75+
},
76+
donut: {
77+
label: {
78+
format: function (value, ratio, id) {
79+
return value;
80+
}
81+
}
82+
}
83+
});
84+
$('#update_date').html(data['update_time']);
85+
$('#reimb_count').html(data['reimb_count']);
86+
})
87+
;
88+
89+
</script>
90+
{% endblock %}

Diff for: stats/urls.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,7 @@
55

66
urlpatterns = [
77
url(r'^api/apps/$', cache_page(60)(views.app_stats_api), name='api_app_stats'),
8-
url(r'^$', views.AppStats.as_view(), name='app_stats'),
8+
url(r'^api/reimb/$', cache_page(60)(views.reimb_stats_api), name='api_reimb_stats'),
9+
url(r'^apps/$', views.AppStats.as_view(), name='app_stats'),
10+
url(r'^reimb/$', views.ReimbStats.as_view(), name='reimb_stats'),
911
]

Diff for: stats/views.py

+47-5
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,50 @@
1-
from django.db.models import Count
1+
from django.db.models import Count, Sum
22
from django.db.models.functions import TruncDate
33
from django.http import JsonResponse
4+
from django.urls import reverse
45
from django.utils import timezone
56

67
from app.views import TabsView
78
from applications.models import Application, STATUS, APP_CONFIRMED, GENDERS
9+
from reimbursement.models import Reimbursement, RE_STATUS, RE_DRAFT
810
from user.mixins import is_organizer, IsOrganizerMixin
911

1012
STATUS_DICT = dict(STATUS)
13+
RE_STATUS_DICT = dict(RE_STATUS)
1114
GENDER_DICT = dict(GENDERS)
1215

1316

17+
def stats_tabs():
18+
return [('Applications', reverse('app_stats'), False),
19+
('Reimbursements', reverse('reimb_stats'), False)]
20+
21+
22+
@is_organizer
23+
def reimb_stats_api(request):
24+
# Status analysis
25+
status_count = Reimbursement.objects.all().values('status') \
26+
.annotate(reimbursements=Count('status'))
27+
status_count = map(lambda x: dict(status_name=RE_STATUS_DICT[x['status']], **x), status_count)
28+
29+
total_apps = Application.objects.count()
30+
no_reimb_apps = Application.objects.filter(reimb__isnull=True).count()
31+
32+
amounts =Reimbursement.objects.all().exclude(status=RE_DRAFT).values('assigned_money', 'reimbursement_money', 'status') \
33+
.annotate(final_amount=Sum('reimbursement_money'), max_amount=Sum('assigned_money'))
34+
amounts = map(lambda x: dict(status_name=RE_STATUS_DICT[x['status']], **x), amounts)
35+
36+
37+
return JsonResponse(
38+
{
39+
'update_time': timezone.now(),
40+
'reimb_count': Reimbursement.objects.count(),
41+
'reimb_apps': {'Reimbursement needed': total_apps - no_reimb_apps, 'No reimbursement': no_reimb_apps},
42+
'status': list(status_count),
43+
'amounts': list(amounts),
44+
}
45+
)
46+
47+
1448
@is_organizer
1549
def app_stats_api(request):
1650
# Status analysis
@@ -31,27 +65,35 @@ def app_stats_api(request):
3165
.annotate(applications=Count('diet'))
3266
diet_count_confirmed = Application.objects.filter(status=APP_CONFIRMED).values('diet') \
3367
.annotate(applications=Count('diet'))
34-
other_diets = Application.objects.values('other_diet')
68+
other_diets = Application.objects.filter(status=APP_CONFIRMED).values('other_diet')
3569

3670
timeseries = Application.objects.all().annotate(date=TruncDate('submission_date')).values('date').annotate(
3771
applications=Count('date'))
3872
return JsonResponse(
3973
{
4074
'update_time': timezone.now(),
75+
'app_count': Application.objects.count(),
4176
'status': list(status_count),
4277
'shirt_count': list(shirt_count),
4378
'shirt_count_confirmed': list(shirt_count_confirmed),
4479
'timeseries': list(timeseries),
4580
'gender': list(gender_count),
4681
'diet': list(diet_count),
4782
'diet_confirmed': list(diet_count_confirmed),
48-
'other_diet': ';'.join([el['other_diet'] for el in other_diets if el['other_diet']])
83+
'other_diet': '<br>'.join([el['other_diet'] for el in other_diets if el['other_diet']])
4984
}
5085
)
5186

5287

5388
class AppStats(IsOrganizerMixin, TabsView):
5489
template_name = 'application_stats.html'
5590

56-
def get_context_data(self, **kwargs):
57-
return {}
91+
def get_current_tabs(self):
92+
return stats_tabs()
93+
94+
95+
class ReimbStats(IsOrganizerMixin, TabsView):
96+
template_name = 'reimbursement_stats.html'
97+
98+
def get_current_tabs(self):
99+
return stats_tabs()

0 commit comments

Comments
 (0)