summaryrefslogtreecommitdiff
path: root/postgresqleu/confsponsor/backendforms.py
blob: ffa9b959a507b813d21635114fe295ac9f509e15 (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
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
from django.forms import ValidationError
import django.forms
from django.shortcuts import get_object_or_404
from django.conf import settings

from collections import OrderedDict
import datetime
import json

from postgresqleu.util.db import exec_to_scalar
from postgresqleu.util.widgets import StaticTextWidget, SimpleTreeviewWidget
from postgresqleu.util.widgets import MonospaceTextarea
from postgresqleu.util.backendforms import BackendForm, BackendBeforeNewForm
from postgresqleu.util.backendlookups import GeneralAccountLookup
from postgresqleu.confreg.jinjafunc import JinjaTemplateValidator, filter_social
from postgresqleu.confreg.jinjafunc import get_all_available_attributes
from postgresqleu.confreg.twitter import get_all_conference_social_media
from postgresqleu.confreg.twitter import render_multiprovider_tweet

from postgresqleu.confreg.models import Conference
from .models import Sponsor
from .models import SponsorshipLevel, SponsorshipContract, SponsorshipBenefit
from .models import ShipmentAddress

from .benefits import get_benefit_class, benefit_choices
from .benefitclasses import all_benefits


class BackendSponsorForm(BackendForm):
    helplink = 'sponsors#sponsor'
    selectize_multiple_fields = {
        'managers': GeneralAccountLookup(),
    }

    auto_cascade_delete_to = ['sponsor_managers', ]

    class Meta:
        model = Sponsor
        fields = ['name', 'displayname', 'url',
                  'invoiceaddr', 'vatstatus', 'vatnumber',
                  'extra_cc', 'managers', 'autoapprovesigned', ]

    @property
    def fieldsets(self):
        fs = [
            {'id': 'base_info', 'legend': 'Basic information', 'fields': ['name', 'displayname', 'url', ]},
            {'id': 'social', 'legend': 'Social media', 'fields': self.nosave_fields},
            {'id': 'financial', 'legend': 'Financial information', 'fields': ['invoiceaddr', 'vatstatus', 'vatnumber']},
            {'id': 'management', 'legend': 'Management', 'fields': ['extra_cc', 'managers']},
        ]
        if self.instance.conference.contractprovider and self.instance.conference.autocontracts:
            fs.append(
                {'id': 'contract', 'legend': 'Contract', 'fields': ['autoapprovesigned', ]}
            )

        return fs

    @property
    def nosave_fields(self):
        return ['social_{}'.format(social) for classname, social, impl in get_all_conference_social_media('sponsor')]

    def fix_fields(self):
        for classname, social, impl in sorted(get_all_conference_social_media('sponsor'), key=lambda x: x[1]):
            fn = "social_{}".format(social)
            self.fields[fn] = django.forms.CharField(label="{} name".format(social.title()), max_length=250, required=False)
            self.fields[fn].initial = self.instance.social.get(social, '')

        if not self.instance.conference.contractprovider or not self.instance.conference.autocontracts:
            del self.fields['autoapprovesigned']

        self.update_protected_fields()

    def post_save(self):
        for classname, social, impl in get_all_conference_social_media('sponsor'):
            v = self.cleaned_data['social_{}'.format(social)]
            if v:
                self.instance.social[social] = v
            elif social in self.instance.social:
                del self.instance.social[social]
        self.instance.save(update_fields=['social'])

    def clean(self):
        cleaned_data = super().clean()

        for classname, social, impl in get_all_conference_social_media('sponsor'):
            fn = 'social_{}'.format(social)
            if cleaned_data.get(fn, None):
                try:
                    cleaned_data[fn] = impl.clean_identifier_form_value('sponsor', cleaned_data[fn])
                except ValidationError as v:
                    self.add_error(fn, v)

        return cleaned_data


class BackendSponsorshipNewBenefitForm(BackendBeforeNewForm):
    helplink = 'sponsors#benefit'
    benefitclass = django.forms.ChoiceField(choices=benefit_choices)

    def get_newform_data(self):
        return self.cleaned_data['benefitclass']


def _get_sample_sponsor():
    return Sponsor(name='TestName', displayname="TestDisplayName", social={"twitter": "@testuser", "mastodon": "@testuser@example.com"})


class BackendSponsorshipLevelBenefitForm(BackendForm):
    helplink = 'sponsors#benefit'
    markdown_fields = ['benefitdescription', 'claimprompt', ]
    dynamic_preview_fields = ['tweet_template']
    form_before_new = BackendSponsorshipNewBenefitForm
    readonly_fields = ['benefit_class_name', 'available_fields', ]
    exclude_date_validators = ['deadline', ]

    class_param_fields = []  # Overridden in subclass!

    benefit_class_name = django.forms.CharField(required=False)
    available_fields = django.forms.CharField(required=False)

    @property
    def fieldsets(self):
        basefields = ['benefitname', 'benefit_class_name', 'benefitdescription', 'sortkey', 'claimprompt', 'deadline']
        if self.can_multiclaim:
            basefields.append('maxclaims')
        if self.can_autoconfirm:
            basefields.append('autoconfirm')

        return [
            {'id': 'base', 'legend': 'Base', 'fields': basefields},
            {'id': 'download', 'legend': 'Token download data', 'fields': ['overview_name', 'overview_value', 'include_in_data']},
            {'id': 'marketing', 'legend': 'Marketing', 'fields': ['tweet_template', 'available_fields', ]},
            {'id': 'params', 'legend': 'Parameters', 'fields': self.class_param_fields},
        ]

    @property
    def json_form_fields(self):
        return {
            'class_parameters': self.class_param_fields,
        }

    class Meta:
        model = SponsorshipBenefit
        fields = ['benefitname', 'benefitdescription', 'sortkey', 'maxclaims',
                  'claimprompt', 'deadline', 'tweet_template', 'available_fields', 'benefit_class_name', 'autoconfirm',
                  'overview_name', 'overview_value', 'include_in_data', ]

    _can_multiclaim = None
    _can_autoconfirm = None

    @property
    def can_autoconfirm(self):
        if self._can_autoconfirm is None and self.instance.benefit_class is not None:
            self._can_autoconfirm = get_benefit_class(self.instance.benefit_class).can_autoconfirm
        return self._can_autoconfirm

    @property
    def can_multiclaim(self):
        if self._can_multiclaim is None and self.instance.benefit_class is not None:
            self._can_multiclaim = get_benefit_class(self.instance.benefit_class).can_multiclaim
        return self._can_multiclaim

    def clean_maxclaims(self):
        if not self.can_multiclaim:
            return

        if not self.instance.pk:
            return self.cleaned_data['maxclaims']

        # Count the max number of claims a single sponsor has made
        already_claimed = exec_to_scalar("SELECT count(*) FROM confsponsor_sponsorclaimedbenefit WHERE benefit_id=%(bid)s GROUP BY sponsor_id ORDER BY 1 DESC LIMIT 1", {
            'bid': self.instance.pk,
        })
        if self.cleaned_data['maxclaims'] < (already_claimed or 0):
            raise ValidationError("There is already a sponsor that has claimed this benefit {} times, cannot adjust to a value below that.".format(already_claimed))

        return self.cleaned_data['maxclaims']

    def fix_fields(self):
        if self.newformdata:
            if int(self.newformdata) != 0:
                self.instance.benefit_class = int(self.newformdata)
            else:
                self.instance_benefit_class = None

        self.initial['benefit_class_name'] = self.instance.benefit_class and all_benefits[self.instance.benefit_class]['description'] or ''

        self.fields['tweet_template'].validators = [
            JinjaTemplateValidator({
                'conference': self.conference,
                'benefit': self.instance,
                'level': self.instance.level,
                'sponsor': _get_sample_sponsor(),
            }, {
                'social': filter_social,
            }),
        ]
        self.fields['tweet_template'].widget = MonospaceTextarea()

        self.fields['available_fields'].widget = SimpleTreeviewWidget(treedata=self.get_contextrefs())

        if not self.can_multiclaim:
            del self.fields['maxclaims']
            self.update_protected_fields()

        if not self.can_autoconfirm:
            del self.fields['autoconfirm']
            self.update_protected_fields()

    @classmethod
    def get_dynamic_preview(self, fieldname, s, objid):
        if fieldname == 'tweet_template':
            if objid:
                o = self.Meta.model.objects.get(pk=objid)
                p = render_multiprovider_tweet(o.level.conference, s, {
                    'benefit': o,
                    'level': o.level,
                    'conference': o.level.conference,
                    'sponsor': _get_sample_sponsor(),
                })
                if p is None:
                    return 'No messaging providers configured, cannot render preview'
                elif isinstance(p, dict):
                    # If we had multiple valid providers, just use the first one for preview
                    return list(p.values())[0]
                else:
                    return p
            return ''

    @classmethod
    def get_contextrefs(self):
        return {
            'benefit': dict(get_all_available_attributes(SponsorshipBenefit)),
            'level': dict(get_all_available_attributes(SponsorshipLevel)),
            'conference': dict(get_all_available_attributes(Conference)),
            'sponsor': dict(get_all_available_attributes(Sponsor)),
        }


class BackendSponsorshipLevelBenefitManager(object):
    title = 'Benefits'
    singular = 'benefit'
    can_add = True
    can_copy = True
    fieldset = {
        'id': 'benefits',
        'legend': 'Benefits',
    }

    def get_list(self, instance):
        if instance.id:
            return [(b.id, b.benefitname, b.benefitdescription) for b in instance.sponsorshipbenefit_set.all()]

    def get_form(self, obj, POST):
        if obj and obj.benefit_class:
            return get_benefit_class(obj.benefit_class).get_backend_form()
        elif POST.get('_newformdata'):
            bc = get_benefit_class(int(POST.get('_newformdata')))
            if bc:
                return bc.get_backend_form()
        elif POST.get('benefitclass', None):
            bc = get_benefit_class(int(POST.get('benefitclass')))
            if bc:
                return bc.get_backend_form()
        return BackendSponsorshipLevelBenefitForm

    def get_copy_form(self):
        return BackendSponsorshipLevelBenefitCopyForm

    def get_object(self, masterobj, subjid):
        try:
            return SponsorshipBenefit.objects.get(level=masterobj, pk=subjid)
        except SponsorshipBenefit.DoesNotExist:
            return None

    def get_instancemaker(self, masterobj):
        return lambda: SponsorshipBenefit(level=masterobj, class_parameters={})

    def copy_instance(self, masterobj, cleaned_form):
        b = get_object_or_404(SponsorshipBenefit, pk=cleaned_form['copyfrom'].value())

        # Create a copy of the existing benefit
        b.pk = None
        b._state.adding = True

        # Override the level and save
        b.level = masterobj
        b.save()

        return b.pk


class BackendSponsorshipLevelBenefitCopyForm(django.forms.Form):
    helplink = 'sponsors#benefit'
    copyfrom = django.forms.ModelChoiceField(label="Copy beneift", queryset=SponsorshipBenefit.objects.all())

    def __init__(self, conference, *args, **kwargs):
        self.conference = conference
        super().__init__(*args, **kwargs)

        self.fields['copyfrom'].queryset = SponsorshipBenefit.objects.select_related('level').filter(level__conference=conference).order_by('level__levelcost', 'level__levelname', 'sortkey', 'benefitname')
        self.fields['copyfrom'].label_from_instance = lambda x: '{}: {}'.format(x.level.levelname, x.benefitname)


class BackendSponsorshipLevelForm(BackendForm):
    helplink = 'sponsors#level'
    list_fields = ['levelname', 'levelcost', 'available', 'public', 'contractlevel']
    linked_objects = OrderedDict({
        'benefit': BackendSponsorshipLevelBenefitManager(),
    })
    vat_fields = {'levelcost': 'sponsor'}
    allow_copy_previous = True
    auto_cascade_delete_to = ['sponsorshiplevel_paymentmethods', 'sponsorshipbenefit']
    exclude_date_validators = ['paymentdueby', ]

    class Meta:
        model = SponsorshipLevel
        fields = ['levelname', 'urlname', 'levelcost', 'available', 'public', 'maxnumber', 'contractlevel',
                  'paymentdays', 'paymentdueby', 'paymentmethods', 'invoiceextradescription', 'contract', 'canbuyvoucher', 'canbuydiscountcode']
        widgets = {
            'paymentmethods': django.forms.CheckboxSelectMultiple,
        }

    fieldsets = [
        {
            'id': 'base_info',
            'legend': 'Basic information',
            'fields': ['levelname', 'urlname', 'levelcost', 'available', 'public', 'maxnumber', ]
        },
        {
            'id': 'contract',
            'legend': 'Contract information',
            'fields': ['contractlevel', 'contract', 'paymentdays', 'paymentdueby'],
        },
        {
            'id': 'payment',
            'legend': 'Payment information',
            'fields': ['paymentmethods', 'invoiceextradescription', ],
        },
        {
            'id': 'services',
            'legend': 'Services',
            'fields': ['canbuyvoucher', 'canbuydiscountcode', ],
        },
    ]

    def fix_fields(self):
        self.fields['contract'].queryset = SponsorshipContract.objects.filter(conference=self.conference)
        self.fields['paymentmethods'].label_from_instance = lambda x: "{0}{1}".format(x.internaldescription, x.active and " " or " (INACTIVE)")
        if not self.initial.get('paymentdueby', None):
            self.initial['paymentdueby'] = self.conference.startdate - datetime.timedelta(days=5)

    def clean(self):
        cleaned_data = super(BackendSponsorshipLevelForm, self).clean()

        if cleaned_data['contractlevel'] == 0:
            if cleaned_data['contract']:
                self.add_error('contract', 'Contracts cannot be specified when contract level is No contract')
            if cleaned_data['levelcost'] == 0:
                self.add_error('levelcost', 'Cost cannot be zero when contract level is No contract')
        elif cleaned_data['contractlevel'] == 1:
            if not cleaned_data['contract']:
                self.add_error('contract', 'Contract is required when contract level is Click-through')
            if cleaned_data['levelcost'] == 0:
                self.add_error('levelcost', 'Cost cannot be zero when contract level is Click-through')
        else:
            if not cleaned_data['contract']:
                self.add_error('contract', 'Contract is required when contract level is Full')

        return cleaned_data

    def clean_urlname(self):
        val = self.cleaned_data['urlname']
        if val and SponsorshipLevel.objects.filter(conference=self.conference, urlname=val).exclude(pk=self.instance.pk).exists():
            raise ValidationError("A sponsorship level with this URL name already exists")
        return val

    @classmethod
    def copy_from_conference(self, targetconf, sourceconf, idlist):
        for id in idlist:
            level = SponsorshipLevel.objects.get(pk=id, conference=sourceconf)
            if SponsorshipLevel.objects.filter(conference=targetconf, urlname=level.urlname).exists():
                yield 'A sponsorship level with urlname {0} already exists.'.format(level.urlname)
                continue

            # Get a separate instance that we will modify
            newlevel = SponsorshipLevel.objects.get(pk=id, conference=sourceconf)
            # Set pk to None to make a copy
            newlevel.pk = None
            newlevel.conference = targetconf
            newlevel.contract = None
            newlevel.save()
            for pm in level.paymentmethods.all():
                newlevel.paymentmethods.add(pm)
            newlevel.save()
            for b in level.sponsorshipbenefit_set.all():
                b.pk = None
                b.level = newlevel
                if b.benefit_class is not None:
                    c = get_benefit_class(b.benefit_class)(b.level, b.class_parameters)
                    try:
                        c.transform_parameters(level.conference, newlevel.conference)
                        c.validate_parameters()
                    except ValidationError as e:
                        yield 'Cannot copy level {}, benefit {} cannot be copied: {}'.format(level.levelname, b.benefitname, e.message)
                        continue
                b.save()


class BackendSponsorshipContractForm(BackendForm):
    helplink = 'sponsors#contract'
    list_fields = ['contractname', ]
    exclude_fields_from_validation = ['contractpdf', ]
    allow_copy_previous = True

    class Meta:
        model = SponsorshipContract
        fields = ['contractname', 'contractpdf', ]

    @property
    def extrabuttons(self):
        yield ('Edit field locations', 'editfields/')
        yield ('Preview with fields', 'previewfields/')
        if self.conference.contractprovider:
            yield ('Edit digital signage fields', 'editdigifields/')
            if self.conference.contractprovider.get_implementation().can_send_preview:
                yield ('Send test contract', 'sendtest/')
        yield ('Copy fields from another contract', 'copyfields/')

    def fix_fields(self):
        # Field must be non-required so we can save things. The widget is still required,
        # so things cannot be removed. Yes, that's kind of funky.
        if self.instance.pk:
            self.fields['contractpdf'].required = False

    @classmethod
    def copy_from_conference(self, targetconf, sourceconf, idlist):
        for id in idlist:
            contract = SponsorshipContract.objects.get(pk=id, conference=sourceconf)
            if SponsorshipContract.objects.filter(conference=targetconf, contractname=contract.contractname).exists():
                yield 'A sponsorship contract with name {} already exists.'.format(contract.contractname)
                continue

            newcontract = SponsorshipContract.objects.get(pk=id, conference=sourceconf)
            newcontract.pk = None
            newcontract.conference = targetconf
            newcontract.save()


class BackendShipmentAddressForm(BackendForm):
    helplink = 'sponsors#shpiment'
    list_fields = ['title', 'active', 'startdate', 'enddate', ]
    exclude_date_validators = ['startdate', 'enddate']
    markdown_fields = ['description', ]
    readonly_fields = ['receiverlink', ]

    receiverlink = django.forms.CharField(required=False, label="Recipient link", widget=StaticTextWidget)

    class Meta:
        model = ShipmentAddress
        fields = ['title', 'active', 'startdate', 'enddate', 'available_to', 'address', 'description', ]

    def fix_fields(self):
        self.fields['available_to'].queryset = SponsorshipLevel.objects.filter(conference=self.conference)
        self.fields['address'].help_text = "Full address. %% will be substituted with the unique address number, so don't forget to include it!"
        self.initial['receiverlink'] = 'The recipient should use the link <a href="{0}/events/sponsor/shipments/{1}/">{0}/events/sponsor/shipments/{1}/</a> to access the system.'.format(settings.SITEBASE, self.instance.token)


class BackendSponsorshipSendTestForm(django.forms.Form):
    recipientname = django.forms.CharField(max_length=100, label='Recipient name')
    recipientemail = django.forms.EmailField(max_length=100, label='Recipient email')

    def __init__(self, contract, user, *args, **kwargs):
        self.contract = contract
        self.user = user
        super().__init__(*args, **kwargs)
        self.initial = {
            'recipientname': '{} {}'.format(user.first_name, user.last_name),
            'recipientemail': user.email,
        }


class BackendCopyContractFieldsForm(django.forms.Form):
    currentval = django.forms.CharField(required=False, label="Current fields", widget=StaticTextWidget(monospace=True),
                                        help_text="NOTE! This value will be completely overwritten!")
    copyfrom = django.forms.ChoiceField(choices=[], label="Copy from contract")

    def __init__(self, contract, *args, **kwargs):
        self.contract = contract
        super().__init__(*args, **kwargs)
        self.fields['copyfrom'].choices = [(c.id, c.contractname) for c in SponsorshipContract.objects.filter(conference=contract.conference).exclude(pk=contract.pk).order_by('contractname')]

        if not contract.fieldjson:
            del self.fields['currentval']
        else:
            self.initial['currentval'] = json.dumps(contract.fieldjson, indent=2)