Skip to content

Commit

Permalink
Merge pull request #170 from HackAssistant/stats
Browse files Browse the repository at this point in the history
Add apps stats
  • Loading branch information
casassg authored Jan 28, 2018
2 parents 92097e4 + 12037b0 commit f103db8
Show file tree
Hide file tree
Showing 12 changed files with 323 additions and 3 deletions.
17 changes: 16 additions & 1 deletion app/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@
'checkin',
'user',
'applications',
'teams'
'teams',
'stats',
]

if REIMBURSEMENT_ENABLED:
Expand Down Expand Up @@ -194,6 +195,20 @@
'required_css_class': 'required',
}

if DEBUG:
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.dummy.DummyCache',
}
}
else:
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.filebased.FileBasedCache',
'LOCATION': os.path.join(BASE_DIR, 'cache'),
}
}

# Add domain to allowed hosts
ALLOWED_HOSTS.append(HACKATHON_DOMAIN)

Expand Down
1 change: 1 addition & 0 deletions app/static/lib/c3.min.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions app/static/lib/c3.min.js

Large diffs are not rendered by default.

6 changes: 4 additions & 2 deletions app/templates/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -95,12 +95,14 @@
{% endif %}
{% if request.user.email_verified %}
{% if request.user.is_organizer %}
<li class="{% if 'stats' in request.build_absolute_uri %}active{% endif %}"><a
href="{% url 'app_stats' %}">Stats</a></li>
<li class="{% if 'applications' in request.build_absolute_uri %}active{% endif %}"><a
href="{% url 'app_list' %}">Applications</a></li>

{% if h_r_enabled %}
<li class="{% if 'reimbursement' in request.build_absolute_uri %}active{% endif %}">
<a
href="{% url 'reimbursement_list' %}">Reimbursements</a></li>
<a href="{% url 'reimbursement_list' %}">Reimbursements</a></li>
{% endif %}
{% endif %}
{% if request.user.is_organizer or request.user.is_volunteer %}
Expand Down
1 change: 1 addition & 0 deletions app/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
url(r'^favicon.ico', RedirectView.as_view(url=static('favicon.ico'))),
url(r'^checkin/', include('checkin.urls')),
url(r'^teams/', include('teams.urls')),
url(r'^stats/', include('stats.urls')),
url(r'code_conduct/$', views.code_conduct, name='code_conduct'),

]
Expand Down
Empty file added stats/__init__.py
Empty file.
5 changes: 5 additions & 0 deletions stats/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from django.apps import AppConfig


class StatsConfig(AppConfig):
name = 'stats'
197 changes: 197 additions & 0 deletions stats/templates/application_stats.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
{% extends 'c3_base.html' %}

{% block head_title %}Application stats{% endblock %}
{% block panel %}
<h1>Application stats</h1>
<small class="pull-right"><b>Last updated:</b> <span id="update_date"></span></small>
<div class="row">
<div class="col-md-12">
<div id="timeseries">
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<h3>Status</h3>
<div id="applications_stats"></div>
</div>
<div class="col-md-6">
<h3>Gender</h3>
<div id="gender_stats"></div>
</div>
</div>
<h2>T-Shirts sizes</h2>

<div class="row">
<div class="col-md-6">
<h3>All</h3>
<div id="shirts_stats"></div>
</div>
<div class="col-md-6">
<h3>Confirmed only</h3>
<div id="shirts_stats_confirmed"></div>
</div>
</div>
<h2>Dietary restrictions</h2>

<div class="row">
<div class="col-md-6">
<h3>All</h3>
<div id="diet_stats"></div>
</div>
<div class="col-md-6">
<h3>Confirmed only</h3>
<div id="diet_stats_confirmed"></div>
</div>
<div class="col-md-12">
<p><b>Other diet requirements</b> <span id="other_diet"></span></p>
</div>

</div>


{% endblock %}
{% block c3script %}
<script>
$.getJSON('{% url 'api_app_stats' %}', function (data) {
c3.generate({
bindto: '#timeseries',
data: {
json: data['timeseries'],
keys: {
x: 'date',
value: ['applications']
}
},

axis: {
x: {
type: 'timeseries',
tick: {
format: '%Y-%m-%d'
}
}
}
});
c3.generate({
bindto: '#shirts_stats_confirmed',
data: {
json: data['shirt_count_confirmed'],
keys: {
x: 'tshirt_size',
value: ['applications']
},
type: 'bar'

},

axis: {
x: {
type: 'category'
}
}
});

var status_data = {};
var sites = [];
$(data['status']).each(function (c, e) {
sites.push(e.status_name);
status_data[e.status_name] = e.applications;
});
c3.generate({
bindto: '#applications_stats',
data: {
json: status_data,
type: 'donut'

},
donut: {
label: {
format: function (value, ratio, id) {
return value;
}
}
}
});
var gender_data = {};
var genders = [];
$(data['gender']).each(function (c, e) {
genders.push(e.gender_name);
gender_data[e.gender_name] = e.applications;
});
c3.generate({
bindto: '#gender_stats',
data: {
json: gender_data,
type: 'donut'

},
donut: {
label: {
format: function (value, ratio, id) {
return value;
}
}
}
});
c3.generate({
bindto: '#shirts_stats',
data: {
json: data['shirt_count'],
keys: {
x: 'tshirt_size',
value: ['applications']
},
type: 'bar'

},

axis: {
x: {
type: 'category'
}
}
});
c3.generate({
bindto: '#diet_stats',
data: {
json: data['diet'],
keys: {
x: 'diet',
value: ['applications']
},
type: 'bar'

},

axis: {
x: {
type: 'category'
}
}
});
c3.generate({
bindto: '#diet_stats_confirmed',
data: {
json: data['diet_confirmed'],
keys: {
x: 'diet',
value: ['applications']
},
type: 'bar'

},

axis: {
x: {
type: 'category'
}
}
});
$('#other_diet').html(data['other_diet']);
$('#update_date').html(data['update_time']);
})
;

</script>
{% endblock %}
13 changes: 13 additions & 0 deletions stats/templates/c3_base.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{% extends 'base_tabs.html' %}
{% load static %}


{% block extra_head %}
<link rel="stylesheet" href="{% static 'lib/c3.min.css' %}">
{% endblock %}

{% block extra_scripts %}
<script src="https://d3js.org/d3.v3.min.js"></script>
<script src="{% static 'lib/c3.min.js' %}" charset="utf-8"></script>
{% block c3script %}{% endblock %}
{% endblock %}
9 changes: 9 additions & 0 deletions stats/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from django.conf.urls import url
from django.views.decorators.cache import cache_page

from stats import views

urlpatterns = [
url(r'^api/apps/$', cache_page(60)(views.app_stats_api), name='api_app_stats'),
url(r'^$', views.AppStats.as_view(), name='app_stats'),
]
57 changes: 57 additions & 0 deletions stats/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
from django.db.models import Count
from django.db.models.functions import TruncDate
from django.http import JsonResponse
from django.utils import timezone

from app.views import TabsView
from applications.models import Application, STATUS, APP_CONFIRMED, GENDERS
from user.mixins import is_organizer, IsOrganizerMixin

STATUS_DICT = dict(STATUS)
GENDER_DICT = dict(GENDERS)


@is_organizer
def app_stats_api(request):
# Status analysis
status_count = Application.objects.all().values('status') \
.annotate(applications=Count('status'))
status_count = map(lambda x: dict(status_name=STATUS_DICT[x['status']], **x), status_count)

gender_count = Application.objects.all().values('gender') \
.annotate(applications=Count('gender'))
gender_count = map(lambda x: dict(gender_name=GENDER_DICT[x['gender']], **x), gender_count)

shirt_count = Application.objects.values('tshirt_size') \
.annotate(applications=Count('tshirt_size'))
shirt_count_confirmed = Application.objects.filter(status=APP_CONFIRMED).values('tshirt_size') \
.annotate(applications=Count('tshirt_size'))

diet_count = Application.objects.values('diet') \
.annotate(applications=Count('diet'))
diet_count_confirmed = Application.objects.filter(status=APP_CONFIRMED).values('diet') \
.annotate(applications=Count('diet'))
other_diets = Application.objects.values('other_diet')

timeseries = Application.objects.all().annotate(date=TruncDate('submission_date')).values('date').annotate(
applications=Count('date'))
return JsonResponse(
{
'update_time': timezone.now(),
'status': list(status_count),
'shirt_count': list(shirt_count),
'shirt_count_confirmed': list(shirt_count_confirmed),
'timeseries': list(timeseries),
'gender': list(gender_count),
'diet': list(diet_count),
'diet_confirmed': list(diet_count_confirmed),
'other_diet': ';'.join([el['other_diet'] for el in other_diets if el['other_diet']])
}
)


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

def get_context_data(self, **kwargs):
return {}
19 changes: 19 additions & 0 deletions user/mixins.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from django.contrib.auth.decorators import user_passes_test
from django.contrib.auth.mixins import UserPassesTestMixin
from django.core.exceptions import PermissionDenied


class IsOrganizerMixin(UserPassesTestMixin):
Expand Down Expand Up @@ -34,3 +36,20 @@ def test_func(self):
return False
return \
self.request.user.is_authenticated and self.request.user.is_director


def is_organizer(f, raise_exception=True):
"""
Decorator for views that checks whether a user is an organizer or not
"""

def check_perms(user):
if user.is_authenticated and user.email_verified and user.is_organizer:
return True
# In case the 403 handler should be called raise the exception
if raise_exception:
raise PermissionDenied
# As the last resort, show the login form
return False

return user_passes_test(check_perms)(f)

0 comments on commit f103db8

Please sign in to comment.