Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add apps stats #170

Merged
merged 1 commit into from
Jan 28, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)