Skip to content

Commit

Permalink
Merge pull request #264 from VSEScala/maarten-statistics
Browse files Browse the repository at this point in the history
Statistics page
  • Loading branch information
mhvis authored Jun 21, 2023
2 parents d16a050 + 6058538 commit 386ae94
Show file tree
Hide file tree
Showing 6 changed files with 148 additions and 43 deletions.
9 changes: 3 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,13 @@ Cooking website.
- `python manage.py migrate`
- `python manage.py runserver`

### Docker

- Install Docker and Docker Compose
- `docker-compose build`
- `docker-compose up`
- `docker-compose exec app python manage.py migrate`
The app can also be built and run with Docker.

## Development commands

* Lint code: `flake8`
* Blacken the code `black .`
* Sort the imports with `isort --ac .`
* Run unit tests: `python manage.py test`
* Create superuser: `python manage.py createsuperuser`
* Coverage: `coverage run manage.py test`
Expand Down
5 changes: 5 additions & 0 deletions assets/templates/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,11 @@
<span class="badge badge-pill badge-warning">!</span>
{% endif %}
</a>
{# <div class="dropdown-divider"></div>#}
{# <a class="dropdown-item {{ justify }}" href="{% url "statistics" %}">#}
{# <span>Statistics <span class="badge badge-warning">New</span></span>#}
{# <i class="fas fa-chart-simple"></i>#}
{# </a>#}
{# <a class="dropdown-item" href="{% url 'site_updates' %}">#}
{# News and updates#}
{# {% if user.requires_information_updates %}#}
Expand Down
77 changes: 77 additions & 0 deletions assets/templates/dining_lists/statistics.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
{% extends "base.html" %}

{% block content %}
<h3>Date range</h3>
<p>
Currently set:
<strong>{{ range_from|date:"SHORT_DATE_FORMAT" }} – {{ range_to|date:"SHORT_DATE_FORMAT" }}</strong>
</p>

<form method="get" action="{% url "statistics" %}" class="form-inline">
<label for="rangeFrom" class="my-1 mr-2">From</label>
<input type="date" id="rangeFrom" name="from" value="{{ range_from.isoformat }}"
class="form-control my-1 mr-sm-2">
<label for="rangeTo" class="my-1 mr-2">To (exclusive)</label>
<input type="date" id="rangeTo" name="to" value="{{ range_to.isoformat }}"
class="form-control my-1 mr-sm-2">
<button type="submit" class="btn btn-primary my-1">Change</button>
</form>

<a href="{% url "statistics" %}?from={{ prev.isoformat }}&to={{ range_from.isoformat }}" class="btn btn-secondary">
« Previous
</a>

<a href="{% url "statistics" %}?from={{ range_to.isoformat }}&to={{ next.isoformat }}" class="btn btn-secondary">
Next »
</a>
<a href="{% url "statistics" %}" class="btn btn-secondary">
Reset
</a>

<h3 class="mt-3">Dining counts</h3>
<table class="table table-sm">
<thead>
<tr>
<th scope="col">Association</th>
<th scope="col">Lists</th>
<th scope="col">Individual diners*</th>
<th scope="col">Total diner count**</th>
</tr>
</thead>
<tbody>
{% for association, queryset in per_association.items %}
<tr>
<th scope="row">{{ association.name }}</th>
<td>{{ queryset.lists.count }}</td>
<td>{{ queryset.users.count }}</td>
<td>{{ queryset.entries.count }}</td>
</tr>
{% endfor %}
<tr>
<th scope="row"><em>Total</em></th>
<td><em>{{ lists.count }}</em></td>
<td><em>{{ users.count }}</em></td>
<td><em>{{ entries.count }}</em></td>
</tr>
</tbody>
</table>

<p>
<strong>*Individual diners:</strong> the number of individuals from each association who joined
a dining list in the given period.
When the same individual joined multiple lists this is counted as 1.
An individual is counted when
they joined <em>any</em> dining list, not necessarily a list of their own association.
Some individuals are member of multiple associations, in which case they are counted once for each
association. Therefore, the sum of all associations can be more than the total number of individuals.
Unverified members and guest diners are not counted.
</p>

<p>
<strong>**Total diner count:</strong> the sum of all diners for the lists that are <em>owned</em>
by the association,
in the given period.
</p>


{% endblock %}
8 changes: 4 additions & 4 deletions dining/urls.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
from django.urls import include, path

from . import views
from .views import DayView, index
from dining import views

urlpatterns = [
path("", index, name="index"),
path("", views.index, name="index"),
path("csv/", views.DailyDinersCSVView.as_view(), name="diners_csv"),
path(
"<int:year>/<int:month>/<int:day>/",
include(
[
path("", DayView.as_view(), name="day_view"),
path("", views.DayView.as_view(), name="day_view"),
path("add/", views.NewSlotView.as_view(), name="new_slot"),
path(
"<slug:identifier>/",
Expand Down Expand Up @@ -54,4 +53,5 @@
path(
"entries/<int:pk>/delete/", views.EntryDeleteView.as_view(), name="entry_delete"
),
path("statistics/", views.StatisticsView.as_view(), name="statistics"),
]
59 changes: 59 additions & 0 deletions dining/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -642,3 +642,62 @@ def form_valid(self, form):
else:
messages.success(self.request, "Diners have been informed")
return super().form_valid(form)


class StatisticsView(LoginRequiredMixin, TemplateView):
template_name = "dining_lists/statistics.html"

def get_range(self):
"""Returns the user-defined date range or a default range."""
today = timezone.now().date()
# Default boundary is August 1st. We could also do September 1st but then we
# might miss some early year dining lists.
range_from = date(today.year - 1 if today.month < 8 else today.year, 8, 1)
range_to = range_from.replace(year=range_from.year + 1)
try:
range_from = date.fromisoformat(self.request.GET.get("from") or "")
except ValueError:
pass
try:
range_to = date.fromisoformat(self.request.GET.get("to") or "")
except ValueError:
pass
return range_from, range_to

def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
range_from, range_to = self.get_range()

# Dining lists in the period
lists = DiningList.objects.filter(date__gte=range_from, date__lt=range_to)
# Users who dined in the given period (excludes external entries)
users = User.objects.filter(
diningentry__dining_list__in=lists, diningentry__external_name=""
).distinct()
# Entries in given period
entries = DiningEntry.objects.filter(dining_list__in=lists)
# Filter on association
per_association = {
a: {
"users": users.filter(
usermembership__association=a, usermembership__is_verified=True
),
"lists": lists.filter(association=a),
"entries": entries.filter(dining_list__association=a),
}
for a in Association.objects.order_by("name")
}

context.update(
{
"range_from": range_from,
"range_to": range_to,
"lists": lists,
"users": users,
"entries": entries,
"per_association": per_association,
"next": range_to + (range_to - range_from),
"prev": range_from - (range_to - range_from),
}
)
return context
33 changes: 0 additions & 33 deletions docker-compose.yml

This file was deleted.

0 comments on commit 386ae94

Please sign in to comment.