Add daily and cumulative attendee graphs to the order statistics page (#5792)

The order statistics page previously only showed order-based graphs. This change
adds attendee-based daily and cumulative graphs.
This commit is contained in:
Kian Cross
2026-01-16 15:57:04 +00:00
committed by GitHub
parent c4792800f0
commit 6b65cb4e33
3 changed files with 112 additions and 0 deletions

View File

@@ -19,6 +19,30 @@ $(function () {
fillOpacity: 0.3,
behaveLikeLine: true
});
new Morris.Area({
element: 'abd_chart',
data: JSON.parse($("#abd-data").html()),
xkey: 'date',
ykeys: ['ordered', 'paid'],
labels: [gettext('Attendees (ordered)'), gettext('Attendees (paid)')],
lineColors: ['#3b1c4a', '#50a167'],
smooth: false,
resize: true,
fillOpacity: 0.3,
behaveLikeLine: true
});
new Morris.Area({
element: 'abt_chart',
data: JSON.parse($("#abt-data").html()),
xkey: 'date',
ykeys: ['ordered', 'paid'],
labels: [gettext('Attendees (ordered)'), gettext('Attendees (paid)')],
lineColors: ['#3b1c4a', '#50a167'],
smooth: false,
resize: true,
fillOpacity: 0.3,
behaveLikeLine: true
});
new Morris.Area({
element: 'rev_chart',
data: JSON.parse($("#rev-data").html()),

View File

@@ -31,6 +31,46 @@
</p>
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">{% trans "Attendees by day" %}</h3>
</div>
<div class="panel-body">
<div id="abd_chart" class="chart"></div>
<p class="help-block">
<small>
{% blocktrans trimmed %}
Attendees in orders paid in multiple instalments are shown using the date of the
final payment. Order dates reflect when the order was first placed; attendees added
later via additional order positions still use the original order date. Attendees in
placed orders include those from all order states (pending, paid, cancelled, and
expired); attendees in paid orders include only those from paid orders and exclude
those from cancelled orders.
{% endblocktrans %}
</small>
</p>
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">{% trans "Attendees by time" %}</h3>
</div>
<div class="panel-body">
<div id="abt_chart" class="chart"></div>
<p class="help-block">
<small>
{% blocktrans trimmed %}
Attendees in orders paid in multiple instalments are shown using the date of the
final payment. Order dates reflect when the order was first placed; attendees added
later via additional order positions still use the original order date. Attendees in
placed orders include those from all order states (pending, paid, cancelled, and
expired); attendees in paid orders include only those from paid orders and exclude
those from cancelled orders.
{% endblocktrans %}
</small>
</p>
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">{% trans "Revenue over time" %}</h3>
@@ -177,6 +217,8 @@
</div>
{% endif %}
<script type="application/json" id="obd-data">{{ obd_data|escapejson }}</script>
<script type="application/json" id="abd-data">{{ abd_data|escapejson }}</script>
<script type="application/json" id="abt-data">{{ abt_data|escapejson }}</script>
<script type="application/json" id="rev-data">{{ rev_data|escapejson }}</script>
<script type="application/json" id="obp-data">{{ obp_data|escapejson }}</script>
<script type="application/text" id="currency">{{ request.event.currency }}</script>

View File

@@ -128,6 +128,52 @@ class IndexView(EventPermissionRequiredMixin, ChartContainingView, TemplateView)
ctx['obd_data'] = json.dumps(data)
cache.set('statistics_obd_data' + ckey, ctx['obd_data'])
# Attendees by day/time
ctx['abd_data'] = cache.get('statistics_abd_data' + ckey)
ctx['abt_data'] = cache.get('statistics_abt_data' + ckey)
if not ctx['abd_data'] or not ctx['abt_data']:
opqs = OrderPosition.all.filter(order__event=self.request.event, item__admission=True).annotate(
payment_date=Subquery(op_date, output_field=DateTimeField())
)
if subevent:
opqs = opqs.filter(subevent=subevent)
ordered_by_day = {}
for p in opqs.values('order__datetime'):
day = p['order__datetime'].astimezone(tz).date()
ordered_by_day[day] = ordered_by_day.get(day, 0) + 1
paid_by_day = {}
for p in opqs.filter(payment_date__isnull=False, canceled=False, order__status=Order.STATUS_PAID).values('payment_date'):
day = p['payment_date'].astimezone(tz).date()
paid_by_day[day] = paid_by_day.get(day, 0) + 1
day_data = []
time_data = []
for d in dateutil.rrule.rrule(
dateutil.rrule.DAILY,
dtstart=min(ordered_by_day.keys()) if ordered_by_day else datetime.date.today(),
until=max(
max(ordered_by_day.keys() if paid_by_day else [datetime.date.today()]),
max(paid_by_day.keys() if paid_by_day else [datetime.date(1970, 1, 1)])
)):
d = d.date()
day_data.append({
'date': d.strftime('%Y-%m-%d'),
'ordered': ordered_by_day.get(d, 0),
'paid': paid_by_day.get(d, 0)
})
time_data.append({
'date': d.strftime('%Y-%m-%d'),
'ordered': (time_data[-1]["ordered"] if time_data else 0) + ordered_by_day.get(d, 0),
'paid': (time_data[-1]["paid"] if time_data else 0) + paid_by_day.get(d, 0)
})
ctx['abd_data'] = json.dumps(day_data)
ctx['abt_data'] = json.dumps(time_data)
cache.set('statistics_abd_data' + ckey, ctx['abd_data'])
cache.set('statistics_abt_data' + ckey, ctx['abt_data'])
# Orders by product
ctx['obp_data'] = cache.get('statistics_obp_data' + ckey)
if not ctx['obp_data']: