summaryrefslogtreecommitdiff
path: root/postgresqleu/confreg/reporting.py
blob: ad2552e98ae6176fc4805136d6d89a21f468e254 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
from django.shortcuts import render
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.http import HttpResponseForbidden
from django.db import connection

from postgresqleu.confreg.models import ConferenceSeries

# XXX: How to handle timezones when two different conferences are involved (in time reports)?
# XXX: Probably need to show the timezone on the output as well somehow!


class ReportException(Exception):
    pass


class Header(object):
    def __init__(self, hdr):
        self.hdr = hdr

    def __str__(self):
        return self.hdr


@login_required
def timereport(request):
    if not (request.user.is_superuser or ConferenceSeries.objects.filter(administrators=request.user).exists()):
        return HttpResponseForbidden()

    from .reportingforms import TimeReportForm
    if request.method == 'POST':
        form = TimeReportForm(request.user, data=request.POST)
        if form.is_valid():
            reporttype = int(form.cleaned_data['reporttype'])
            conferences = form.cleaned_data['conferences']
            trendlines = form.cleaned_data['trendline']

            report = None
            try:
                report = reporttypes[reporttype - 1][1](reporttypes[reporttype - 1][0], conferences)
                report.run()
                return render(request, 'confreg/timereport.html', {
                    'form': form,
                    'title': report.title,
                    'ylabel': report.ylabel,
                    'xlabel': 'Days',
                    'series': report.series,
                    'dayvals': report.dayvals,
                    'trendlines': report.does_trendlines and trendlines,
                    'helplink': 'reports#time',
                    })
            except ReportException as e:
                messages.error(request, e)
                return render(request, 'confreg/timereport.html', {
                    'form': form,
                    })
    else:
        form = TimeReportForm(request.user)

    return render(request, 'confreg/timereport.html', {
        'form': form,
        'helplink': 'reports#time',
        })


# Dynamically built list of all available report types
reporttypes = []


# ##########################################################3
# Base classes for reports
# ##########################################################3
class MultiConferenceReport(object):
    def __init__(self, title, ylabel, conferences):
        self.title = title
        self.ylabel = ylabel
        self.conferences = conferences
        self.does_trendlines = True
        self.curs = connection.cursor()
        self.series = []

    def run(self):
        (maxday, minday) = self.maxmin()
        if not maxday:
            raise ReportException("There are no %s at this conference." % self.title.lower())
        self.dayvals = list(range(maxday, minday - 1 if minday <= 0 else -1, -1))
        for c in self.conferences:
            myvals = [r[0] for r in self.fetch_all_data(c, minday, maxday)]
            self.series.append({
                'label': c.conferencename,
                'values': myvals,
            })


class SingleConferenceReport(object):
    def __init__(self, title, conferences):
        self.title = title
        self.ylabel = 'Number of registrations'
        self.does_trendlines = False

        if len(conferences) != 1:
            raise ReportException('For this report type you must pick a single conference')
        self.conference = conferences[0]
        self.curs = connection.cursor()
        self.series = []

    def maxmin(self):
        self.curs.execute("SELECT max(startdate-payconfirmedat::date), min(startdate-payconfirmedat::date),max(startdate) FROM confreg_conferenceregistration r INNER JOIN confreg_conference c ON r.conference_id=c.id WHERE r.conference_id=%(id)s AND r.payconfirmedat IS NOT NULL", {
            'id': self.conference.id
        })
        return self.curs.fetchone()

    def run(self):
        (maxday, minday, startdate) = self.maxmin()
        if not maxday:
            raise ReportException("There are no %s at this conference." % self.title.lower())
        self.dayvals = list(range(maxday, minday - 1, -1))
        for header, rows in self.fetch_all_data(minday, maxday, startdate):
            self.series.append({
                'label': header,
                'values': [r[0] for r in rows],
            })


# ##########################################################3
# Actually report classes
# ##########################################################3
class ConfirmedRegistrationsReport(MultiConferenceReport):
    def __init__(self, title, conferences):
        super(ConfirmedRegistrationsReport, self).__init__(title, 'Number of registrations', conferences)

    def maxmin(self):
        self.curs.execute("SELECT max(startdate-payconfirmedat::date), min(startdate-payconfirmedat::date) FROM confreg_conference c INNER JOIN confreg_conferenceregistration r ON c.id=r.conference_id WHERE c.id=ANY(%(idlist)s) AND r.payconfirmedat IS NOT NULL AND r.canceledat IS NULL", {'idlist': [c.id for c in self.conferences]})
        return self.curs.fetchone()

    def fetch_all_data(self, conference, min, max):
        self.curs.execute("WITH t AS (SELECT startdate-payconfirmedat::date AS d, count(*) AS num FROM confreg_conferenceregistration r INNER JOIN confreg_conference c ON c.id=r.conference_id WHERE c.id=%(id)s AND r.payconfirmedat IS NOT NULL AND r.canceledat IS NULL GROUP BY d), tt AS (SELECT g.g, num FROM t RIGHT JOIN generate_series (%(min)s, %(max)s) g(g) ON g.g=t.d) SELECT COALESCE(sum(num) OVER (ORDER BY g DESC),0)::integer FROM tt ORDER BY g DESC", {
            'id': conference.id,
            'min': min,
            'max': max,
        })
        return self.curs.fetchall()


reporttypes.append(('Confirmed registrations', ConfirmedRegistrationsReport))


class CanceledRegistrationsReport(MultiConferenceReport):
    def __init__(self, title, conferences):
        super(CanceledRegistrationsReport, self).__init__(title, 'Number of registrations', conferences)

    def maxmin(self):
        self.curs.execute("SELECT max(startdate-canceledat::date), min(startdate-canceledat::date) FROM confreg_conference c INNER JOIN confreg_conferenceregistration r ON c.id=r.conference_id WHERE c.id=ANY(%(idlist)s) AND r.payconfirmedat IS NOT NULL AND r.canceledat IS NOT NULL", {'idlist': [c.id for c in self.conferences]})
        return self.curs.fetchone()

    def fetch_all_data(self, conference, min, max):
        self.curs.execute("WITH t AS (SELECT startdate-canceledat::date AS d, count(*) AS num FROM confreg_conferenceregistration r INNER JOIN confreg_conference c ON c.id=r.conference_id WHERE c.id=%(id)s AND r.payconfirmedat IS NOT NULL AND r.canceledat IS NOT NULL GROUP BY d), tt AS (SELECT g.g, num FROM t RIGHT JOIN generate_series (%(min)s, %(max)s) g(g) ON g.g=t.d) SELECT COALESCE(sum(num) OVER (ORDER BY g DESC),0)::integer FROM tt ORDER BY g DESC", {
            'id': conference.id,
            'min': min,
            'max': max,
        })
        return self.curs.fetchall()


reporttypes.append(('Canceled registrations', CanceledRegistrationsReport))


class RegistrationsAndCancelesReport(SingleConferenceReport):
    def maxmin(self):
        self.curs.execute("SELECT max(greatest(startdate-payconfirmedat::date, startdate-canceledat::date)), min(least(startdate-payconfirmedat::date, startdate-canceledat::date)),max(startdate) FROM confreg_conferenceregistration r INNER JOIN confreg_conference c ON r.conference_id=c.id WHERE r.conference_id=%(id)s AND r.payconfirmedat IS NOT NULL", {
            'id': self.conference.id
        })
        return self.curs.fetchone()

    def fetch_all_data(self, min, max, startdate):
        self.curs.execute("WITH t AS (SELECT %(startdate)s-payconfirmedat::date AS d, count(*) AS num FROM confreg_conferenceregistration r WHERE r.conference_id=%(cid)s AND r.payconfirmedat IS NOT NULL GROUP BY d), tt AS (SELECT g.g, num FROM t RIGHT JOIN generate_series (%(min)s, %(max)s) g(g) ON g.g=t.d) SELECT COALESCE(sum(num) OVER (ORDER BY g DESC),0)::integer FROM tt ORDER BY g DESC", {
            'cid': self.conference.id,
            'min': min,
            'max': max,
            'startdate': startdate,
        })
        yield ('Confirmed', self.curs.fetchall())

        self.curs.execute("WITH t AS (SELECT %(startdate)s-canceledat::date AS d, count(*) AS num FROM confreg_conferenceregistration r WHERE r.conference_id=%(cid)s AND r.payconfirmedat IS NOT NULL AND r.canceledat IS NOT NULL GROUP BY d), tt AS (SELECT g.g, num FROM t RIGHT JOIN generate_series (%(min)s, %(max)s) g(g) ON g.g=t.d) SELECT COALESCE(sum(num) OVER (ORDER BY g DESC),0)::integer FROM tt ORDER BY g DESC", {
            'cid': self.conference.id,
            'min': min,
            'max': max,
            'startdate': startdate,
        })
        yield ('Canceled', self.curs.fetchall())


reporttypes.append(('Registrations and cancels', RegistrationsAndCancelesReport))


class SubmittedSessionsReport(MultiConferenceReport):
    def __init__(self, title, conferences):
        super(SubmittedSessionsReport, self).__init__(title, 'Number of sessions', conferences)

    def maxmin(self):
        self.curs.execute("SELECT max(extract(days from startdate-initialsubmit)::integer), min(extract(days from startdate-initialsubmit)::integer) FROM confreg_conference c INNER JOIN confreg_conferencesession s ON c.id=s.conference_id WHERE c.id=ANY(%(idlist)s) AND s.initialsubmit IS NOT NULL AND NOT s.cross_schedule", {'idlist': [c.id for c in self.conferences]})
        return self.curs.fetchone()

    def fetch_all_data(self, conference, min, max):
        self.curs.execute("WITH t AS (SELECT extract(days from startdate-initialsubmit) AS d, count(*) AS num FROM confreg_conferencesession s INNER JOIN confreg_conference c ON c.id=s.conference_id WHERE c.id=%(id)s AND s.initialsubmit IS NOT NULL AND NOT s.cross_schedule GROUP BY d), tt AS (SELECT g.g, num FROM t RIGHT JOIN generate_series (%(min)s, %(max)s) g(g) ON g.g=t.d) SELECT COALESCE(sum(num) OVER (ORDER BY g DESC),0)::integer FROM tt ORDER BY g DESC", {
            'id': conference.id,
            'min': min,
            'max': max,
        })
        return self.curs.fetchall()


reporttypes.append(('Submitted sessions', SubmittedSessionsReport))


class SubmittingSpeakersReport(MultiConferenceReport):
    def __init__(self, title, conferences):
        super(SubmittingSpeakersReport, self).__init__(title, 'Number of speakers', conferences)

    def maxmin(self):
        self.curs.execute("SELECT max(extract(days from startdate-initialsubmit)::integer), min(extract(days from startdate-initialsubmit)::integer) FROM confreg_conference c INNER JOIN confreg_conferencesession s ON c.id=s.conference_id WHERE c.id=ANY(%(idlist)s) AND s.initialsubmit IS NOT NULL AND NOT s.cross_schedule", {'idlist': [c.id for c in self.conferences]})
        return self.curs.fetchone()

    def fetch_all_data(self, conference, min, max):
        self.curs.execute("WITH t AS (SELECT extract(days from startdate-initialsubmit) AS d, count(distinct speaker_id) AS num FROM confreg_conferencesession s INNER JOIN confreg_conference c ON c.id=s.conference_id INNER JOIN confreg_conferencesession_speaker spk ON s.id=spk.conferencesession_id WHERE c.id=%(id)s AND s.initialsubmit IS NOT NULL AND NOT s.cross_schedule GROUP BY d), tt AS (SELECT g.g, num FROM t RIGHT JOIN generate_series (%(min)s, %(max)s) g(g) ON g.g=t.d) SELECT COALESCE(sum(num) OVER (ORDER BY g DESC),0)::integer FROM tt ORDER BY g DESC", {
            'id': conference.id,
            'min': min,
            'max': max,
        })
        return self.curs.fetchall()


reporttypes.append(('Submitting speakers', SubmittingSpeakersReport))


class ConfirmedSponsorsReport(MultiConferenceReport):
    def __init__(self, title, conferences):
        super(ConfirmedSponsorsReport, self).__init__(title, 'Confirmed sponsors', conferences)

    def maxmin(self):
        self.curs.execute("SELECT max(extract(days FROM startdate-confirmedat)::integer), min(extract(days FROM startdate-confirmedat)::integer) FROM confreg_conference c INNER JOIN confsponsor_sponsor s ON c.id=s.conference_id WHERE c.id=ANY(%(idlist)s) AND confirmedat IS NOT NULL", {'idlist': [c.id for c in self.conferences]})
        return self.curs.fetchone()

    def fetch_all_data(self, conference, min, max):
        self.curs.execute("WITH t AS (SELECT extract(days FROM startdate-confirmedat)::integer AS d, count(*) AS num FROM confreg_conference c INNER JOIN confsponsor_sponsor s ON c.id=s.conference_id WHERE c.id=%(id)s AND confirmedat IS NOT NULL GROUP BY d), tt AS (SELECT g.g, num FROM t RIGHT JOIN generate_series(%(min)s, %(max)s) g(g) ON g.g=t.d) SELECT COALESCE(sum(num) OVER (ORDER BY g DESC)::integer, 0) FROM tt ORDER BY g DESC", {
            'id': conference.id,
            'min': min,
            'max': max,
        })
        return self.curs.fetchall()


reporttypes.append(('Confirmed sponsors', ConfirmedSponsorsReport))


class SponsorLevelsReport(SingleConferenceReport):
    def maxmin(self):
        self.curs.execute("SELECT max(extract(days FROM startdate-confirmedat)::integer)+1, min(extract(days FROM startdate-confirmedat)::integer), max(startdate) FROM confreg_conference c INNER JOIN confsponsor_sponsor s ON c.id=s.conference_id WHERE c.id=%(id)s AND confirmedat IS NOT NULL", {
            'id': self.conference.id
        })
        return self.curs.fetchone()

    def fetch_all_data(self, min, max, startdate):
        self.curs.execute("SELECT id, levelname FROM confsponsor_sponsorshiplevel l WHERE l.conference_id=%(id)s AND EXISTS (SELECT 1 FROM confsponsor_sponsor s WHERE s.level_id=l.id AND confirmedat IS NOT NULL AND s.conference_id=%(id)s)", {
            'id': self.conference.id,
        })
        for levelid, levelname in self.curs.fetchall():
            self.curs.execute("WITH t AS (SELECT %(startdate)s-confirmedat::date AS d, count(*) AS num FROM confsponsor_sponsor s WHERE s.conference_id=%(cid)s AND s.confirmedat IS NOT NULL AND s.level_id=%(lid)s GROUP BY d), tt AS (SELECT g.g, num FROM t RIGHT JOIN generate_series(%(min)s, %(max)s) g(g) ON g.g=t.d) SELECT COALESCE(sum(num) OVER (ORDER BY g DESC), 0)::integer FROM tt ORDER BY g DESC", {
                'cid': self.conference.id,
                'min': min,
                'max': max,
                'lid': levelid,
                'startdate': startdate,
            })
            yield (levelname, self.curs.fetchall())


reporttypes.append(('Sponsor levels', SponsorLevelsReport))


class RegistrationTypesReport(SingleConferenceReport):
    def fetch_all_data(self, min, max, startdate):
        self.curs.execute("SELECT id, regtype FROM confreg_registrationtype rt WHERE EXISTS (SELECT * FROM confreg_conferenceregistration r WHERE r.regtype_id=rt.id AND r.payconfirmedat IS NOT NULL AND r.conference_id=%(id)s)", {
            'id': self.conference.id,
        })
        for regid, regtype in self.curs.fetchall():
            self.curs.execute("WITH t AS (SELECT %(startdate)s-payconfirmedat::date AS d, count(*) AS num FROM confreg_conferenceregistration r WHERE r.conference_id=%(cid)s AND r.payconfirmedat IS NOT NULL AND r.regtype_id=%(rid)s GROUP BY d), tt AS (SELECT g.g, num FROM t RIGHT JOIN generate_series (%(min)s, %(max)s) g(g) ON g.g=t.d) SELECT COALESCE(sum(num) OVER (ORDER BY g DESC),0)::integer FROM tt ORDER BY g DESC", {
                'cid': self.conference.id,
                'min': min,
                'max': max,
                'rid': regid,
                'startdate': startdate,
            })
            yield (regtype, self.curs.fetchall())


reporttypes.append(('Registration types', RegistrationTypesReport))


class CountryReport(SingleConferenceReport):
    def fetch_all_data(self, min, max, startdate):
        self.curs.execute("SELECT DISTINCT country_id FROM confreg_conferenceregistration r WHERE r.payconfirmedat IS NOT NULL AND r.conference_id=%(id)s AND country_id IS NOT NULL", {
            'id': self.conference.id,
        })
        for countryid, in self.curs.fetchall():
            self.curs.execute("WITH t AS (SELECT %(startdate)s-payconfirmedat::date AS d, count(*) AS num FROM confreg_conferenceregistration r WHERE r.conference_id=%(cid)s AND r.payconfirmedat IS NOT NULL AND r.country_id=%(country)s GROUP BY d), tt AS (SELECT g.g, num FROM t RIGHT JOIN generate_series (%(min)s, %(max)s) g(g) ON g.g=t.d) SELECT COALESCE(sum(num) OVER (ORDER BY g DESC),0)::integer FROM tt ORDER BY g DESC", {
                'cid': self.conference.id,
                'min': min,
                'max': max,
                'country': countryid,
                'startdate': startdate,
            })
            yield (countryid, self.curs.fetchall())


reporttypes.append(('Countries', CountryReport))


class AdditionalOptionsReport(SingleConferenceReport):
    def fetch_all_data(self, min, max, startdate):
        self.curs.execute("SELECT DISTINCT id, name FROM confreg_conferenceadditionaloption ao WHERE conference_id=%(id)s", {
            'id': self.conference.id,
        })
        for optionid, optionname in self.curs.fetchall():
            self.curs.execute("WITH t AS (SELECT startdate-payconfirmedat::date AS d, count(*) AS num FROM confreg_conferenceregistration r INNER JOIN confreg_conference c ON c.id=r.conference_id INNER JOIN confreg_conferenceregistration_additionaloptions rao ON rao.conferenceregistration_id=r.id WHERE c.id=%(cid)s AND r.payconfirmedat IS NOT NULL AND rao.conferenceadditionaloption_id=%(aoid)s GROUP BY d), tt AS (SELECT g.g, num FROM t RIGHT JOIN generate_series (%(min)s, %(max)s) g(g) ON g.g=t.d) SELECT COALESCE(sum(num) OVER (ORDER BY g DESC),0)::integer FROM tt ORDER BY g DESC", {
                'cid': self.conference.id,
                'min': min,
                'max': max,
                'aoid': optionid,
                'startdate': startdate,
            })
            yield (optionname, self.curs.fetchall())


reporttypes.append(('Additional options', AdditionalOptionsReport))