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)
|