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
|
from django.db import models
from django.db.models import Q
from django.utils.functional import cached_property
from django.core.serializers.json import DjangoJSONEncoder
from django.core.validators import MinValueValidator
from django.contrib.auth.models import User
from django.utils import timezone
from postgresqleu.confreg.models import Conference, RegistrationType, PrepaidBatch
from postgresqleu.confreg.models import ConferenceRegistration
from postgresqleu.invoices.models import Invoice, InvoicePaymentMethod
from postgresqleu.digisign.models import DigisignDocument
from postgresqleu.util.fields import PdfBinaryField
from postgresqleu.util.validators import validate_lowercase, validate_urlname
from postgresqleu.util.random import generate_random_token
from postgresqleu.util.messaging import get_messaging_class_from_typename
from .benefits import benefit_choices
vat_status_choices = (
(0, 'Company is from inside EU and has VAT number'),
(1, 'Company is from inside EU, but does not have VAT number'),
(2, 'Company is from outside EU'),
)
CONTRACT_LEVEL_CHOICES = (
(0, 'No contract'),
(1, 'Click-through contract'),
(2, 'Full contract'),
)
CONTRACT_LEVEL_MAP = dict(CONTRACT_LEVEL_CHOICES)
class SponsorshipContract(models.Model):
conference = models.ForeignKey(Conference, null=False, blank=False, on_delete=models.CASCADE)
contractname = models.CharField(max_length=100, null=False, blank=False, verbose_name='Contract name')
contractpdf = PdfBinaryField(null=False, blank=False, max_length=1000000, verbose_name='Contract PDF')
fieldjson = models.JSONField(blank=False, null=False, default=dict, encoder=DjangoJSONEncoder)
_safe_attributes = ['conference', 'contractname']
def __str__(self):
return self.contractname
class SponsorshipLevel(models.Model):
conference = models.ForeignKey(Conference, null=False, blank=False, on_delete=models.CASCADE)
levelname = models.CharField(max_length=100, null=False, blank=False, verbose_name="Level name")
urlname = models.CharField(max_length=100, null=False, blank=False,
validators=[validate_lowercase, validate_urlname],
verbose_name="URL name")
levelcost = models.IntegerField(null=False, blank=False, verbose_name="Cost", help_text="Cost excluding VAT.")
available = models.BooleanField(null=False, blank=False, default=True, verbose_name="Available for signup")
public = models.BooleanField(null=False, blank=False, default=True, verbose_name="Publicly visible",
help_text="If unchecked the sponsorship level will be treated as internal, for example for testing")
maxnumber = models.IntegerField(null=False, blank=False, default=0, verbose_name="Maximum number of sponsors")
contractlevel = models.IntegerField(null=False, blank=False, default=0, verbose_name="Contract level",
choices=CONTRACT_LEVEL_CHOICES)
paymentmethods = models.ManyToManyField(InvoicePaymentMethod, blank=False, verbose_name="Payment methods for generated invoices")
invoiceextradescription = models.TextField(
blank=True, null=False, verbose_name="Invoice extra description",
help_text="Extra description to be added to invoices, included in payment information and in the email sent.",
)
contract = models.ForeignKey(SponsorshipContract, blank=True, null=True, on_delete=models.CASCADE)
canbuyvoucher = models.BooleanField(null=False, blank=False, default=True, verbose_name="Can buy vouchers")
canbuydiscountcode = models.BooleanField(null=False, blank=False, default=True, verbose_name="Can buy discount codes")
paymentdays = models.IntegerField(null=False, blank=False, default=30, verbose_name="Number of days until payment is due")
paymentdueby = models.DateField(
null=False, blank=False, verbose_name="The latest date the payment is due by",
help_text="The last acceptable due date for payments. If payment terms go beyond this date then the invoice is due at this date",
)
def __str__(self):
return self.levelname
class Meta:
ordering = ('-levelcost', 'levelname',)
unique_together = (('conference', 'urlname'), )
@cached_property
def num_confirmed(self):
return self.sponsor_set.filter(confirmed=True).count()
@cached_property
def num_unconfirmed(self):
return self.sponsor_set.filter(confirmed=False).count()
@cached_property
def num_total(self):
return self.num_confirmed + self.num_unconfirmed
@cached_property
def can_signup(self):
if self.available:
if self.maxnumber > 0:
return self.num_confirmed < self.maxnumber
else:
return True
return False
@cached_property
def contractlevel_name(self):
return CONTRACT_LEVEL_MAP[self.contractlevel]
def _display_contractlevel(self, cache):
return self.contractlevel_name
class SponsorshipBenefit(models.Model):
level = models.ForeignKey(SponsorshipLevel, null=False, blank=False, on_delete=models.CASCADE)
benefitname = models.CharField(max_length=100, null=False, blank=False, verbose_name="Benefit name")
sortkey = models.PositiveIntegerField(null=False, blank=False, default=100, verbose_name="Sort key")
benefitdescription = models.TextField(null=False, blank=True, verbose_name="Benefit description")
claimprompt = models.TextField(null=False, blank=True, verbose_name="Claim prompt")
maxclaims = models.IntegerField(null=False, blank=False, default=1, verbose_name="Max number of claims",
help_text="Maximum number of times this benefit can be claimed",
validators=[MinValueValidator(1)])
deadline = models.DateTimeField(null=True, blank=True, verbose_name="Claim deadline")
autoconfirm = models.BooleanField(null=False, blank=False, default=False,
verbose_name="Automatically confirm",
help_text="Automatically confirm this benefit when it's claimed")
benefit_class = models.IntegerField(null=True, blank=True, default=None, choices=benefit_choices)
class_parameters = models.JSONField(blank=True, null=False)
tweet_template = models.TextField(null=False, blank=True)
overview_name = models.CharField(max_length=100, null=False, blank=True, verbose_name='Name in overview')
overview_value = models.CharField(max_length=50, null=False, blank=True, verbose_name='Value in overview',
help_text='Specify this to use a direct value instead of the max claims number as the value')
include_in_data = models.BooleanField(null=False, default=True, verbose_name="Include in data",
help_text='Include information about this benefit in sponsorship data from tokens')
_safe_attributes = [
'benefitname', 'sortkey', 'benefitdescription', 'claimprompt', 'maxclaims', 'deadline',
'autoconfirm', 'benefit_class', 'class_parameters',
]
def __str__(self):
return self.benefitname
@property
def expired(self):
if self.deadline:
return self.deadline < timezone.now()
return False
class Meta:
ordering = ('sortkey', 'benefitname', )
class Sponsor(models.Model):
conference = models.ForeignKey(Conference, null=False, blank=False, on_delete=models.CASCADE)
name = models.CharField(max_length=100, null=False, blank=False)
displayname = models.CharField(max_length=100, null=False, blank=False, verbose_name='Display name')
invoiceaddr = models.TextField(max_length=500, null=False, blank=True, verbose_name='Invoice address')
vatstatus = models.IntegerField(null=True, blank=False, choices=vat_status_choices, verbose_name='VAT status')
vatnumber = models.CharField(max_length=100, null=True, blank=True, verbose_name='VAT number')
managers = models.ManyToManyField(User, blank=False)
url = models.URLField(max_length=200, null=False, blank=True)
social = models.JSONField(blank=True, null=False, default=dict)
level = models.ForeignKey(SponsorshipLevel, null=False, blank=False, on_delete=models.CASCADE)
invoice = models.OneToOneField(Invoice, null=True, blank=True, on_delete=models.CASCADE)
confirmed = models.BooleanField(null=False, blank=False, default=False)
confirmedat = models.DateTimeField(null=True, blank=True)
confirmedby = models.CharField(max_length=50, null=False, blank=True)
signupat = models.DateTimeField(null=False, blank=False)
extra_cc = models.EmailField(null=False, blank=True, verbose_name="Extra information address")
signmethod = models.IntegerField(null=False, blank=False, default=1, choices=((0, 'Digital signatures'), (1, 'Manual signatures')), verbose_name='Signing method')
autoapprovesigned = models.BooleanField(null=False, blank=False, default=True, verbose_name="Approve on signing", help_text="Automatically approve once digital signatures are completed")
contract = models.OneToOneField(DigisignDocument, null=True, blank=True, help_text="Contract, when using digital signatures", on_delete=models.SET_NULL)
explicitcontract = models.BooleanField(null=False, blank=False, default=False, verbose_name='Requested explicit contract')
def __str__(self):
return self.name
_safe_attributes = ('id', 'displayname', 'twittername', 'social', 'url', 'level', )
@cached_property
def socials_with_link(self):
for k, v in sorted(self.social.items()):
c = get_messaging_class_from_typename(k)
if c:
yield (k.title(), v, c.get_link_from_identifier(v))
@cached_property
def twittername(self):
return self.social.get('twitter', '')
class SponsorClaimedBenefit(models.Model):
sponsor = models.ForeignKey(Sponsor, null=False, blank=False, on_delete=models.CASCADE)
benefit = models.ForeignKey(SponsorshipBenefit, null=False, blank=False, on_delete=models.CASCADE)
claimedat = models.DateTimeField(null=False, blank=False)
claimedby = models.ForeignKey(User, null=False, blank=False, on_delete=models.CASCADE)
claimnum = models.IntegerField(null=False, blank=False, default=1)
declined = models.BooleanField(null=False, blank=False, default=False)
claimjson = models.JSONField(blank=True, null=False)
confirmed = models.BooleanField(null=False, blank=False, default=False)
class Meta:
constraints = [
models.UniqueConstraint(
name='uniq_sponsor_benefit_num',
fields=('sponsor', 'benefit', 'claimnum'),
deferrable=models.Deferrable.DEFERRED, # This constraint must be deferred so we can renumber the claimnum entry
)
]
class SponsorMail(models.Model):
conference = models.ForeignKey(Conference, null=False, blank=False, on_delete=models.CASCADE)
levels = models.ManyToManyField(SponsorshipLevel, blank=True)
sponsors = models.ManyToManyField(Sponsor, blank=True)
sentat = models.DateTimeField(null=False, blank=False, default=timezone.now, verbose_name="Send at")
sent = models.BooleanField(null=False, blank=False, default=False)
subject = models.CharField(max_length=100, null=False, blank=False)
message = models.TextField(max_length=8000, null=False, blank=False)
_safe_attributes = ('id', 'sentat', 'subject', 'message')
def __str__(self):
return "%s: %s" % (timezone.localtime(self.sentat).strftime("%Y-%m-%d %H:%M"), self.subject)
class Meta:
ordering = ('-sentat',)
indexes = [
models.Index(name="confsponsor_sponsormail_unsent", fields=['sentat'], condition=Q(sent=False)),
]
@property
def future(self):
return self.sentat > timezone.now()
class SponsorScanner(models.Model):
sponsor = models.ForeignKey(Sponsor, null=False, blank=False, on_delete=models.CASCADE)
scanner = models.ForeignKey(ConferenceRegistration, null=False, blank=False, on_delete=models.CASCADE)
token = models.TextField(null=False, blank=False, unique=True)
class Meta:
unique_together = (
('sponsor', 'scanner', ),
)
_safe_attributes = ('id', 'sponsor', 'token', )
class ScannedAttendee(models.Model):
sponsor = models.ForeignKey(Sponsor, null=False, blank=False, on_delete=models.CASCADE)
scannedby = models.ForeignKey(ConferenceRegistration, null=False, blank=False, related_name='scanned_attendees', on_delete=models.CASCADE)
attendee = models.ForeignKey(ConferenceRegistration, null=False, blank=False, related_name='scanned_by', on_delete=models.CASCADE)
scannedat = models.DateTimeField(null=False, blank=False, auto_now_add=True)
firstscan = models.BooleanField(null=False, blank=False, default=True)
note = models.TextField(null=False, blank=True)
class Meta:
ordering = ('-scannedat', )
unique_together = (
('sponsor', 'scannedby', 'attendee', )
)
_safe_attributes = ('sponsor', 'scannedat', )
class PurchasedVoucher(models.Model):
conference = models.ForeignKey(Conference, null=False, blank=False, on_delete=models.CASCADE)
sponsor = models.ForeignKey(Sponsor, null=True, blank=True, on_delete=models.CASCADE)
user = models.ForeignKey(User, null=False, blank=False, on_delete=models.CASCADE)
regtype = models.ForeignKey(RegistrationType, null=False, blank=False, on_delete=models.CASCADE)
num = models.IntegerField(null=False, blank=False)
invoice = models.OneToOneField(Invoice, null=False, blank=False, on_delete=models.CASCADE)
batch = models.OneToOneField(PrepaidBatch, null=True, blank=True, on_delete=models.CASCADE)
class ShipmentAddress(models.Model):
conference = models.ForeignKey(Conference, null=False, blank=False, on_delete=models.CASCADE)
available_to = models.ManyToManyField(SponsorshipLevel, blank=True,
help_text="Which sponsorsihp levels is this address available to")
active = models.BooleanField(null=False, blank=False, default=False,
help_text="Can address be viewed?")
startdate = models.DateField(null=True, blank=True,
help_text="Shipments cannot arrive before")
enddate = models.DateField(null=True, blank=True,
help_text="Shipments cannot arrive after")
token = models.TextField(null=False, blank=False, unique=True,
help_text="Token used by arriving party to indicate shipments",
default=generate_random_token)
title = models.CharField(max_length=100, null=False, blank=False)
address = models.TextField(null=False, blank=False)
description = models.TextField(null=False, blank=True)
_safe_attributes = ('active', 'startdate', 'enddate', 'token',
'title', 'address', 'description')
class Meta:
ordering = ('startdate', 'enddate', 'title', )
class Shipment(models.Model):
conference = models.ForeignKey(Conference, null=False, blank=False, on_delete=models.CASCADE)
sponsor = models.ForeignKey(Sponsor, null=True, blank=True, on_delete=models.CASCADE)
address = models.ForeignKey(ShipmentAddress, null=False, blank=False, on_delete=models.CASCADE)
addresstoken = models.BigIntegerField(null=False, blank=False)
description = models.CharField(max_length=200, null=False, blank=False)
sent_parcels = models.IntegerField(null=False, blank=False,
help_text="Number of parcels sent",
verbose_name="Parcel count")
sent_at = models.DateTimeField(null=True, blank=True, verbose_name="Shipment sent at")
arrived_at = models.DateTimeField(null=True, blank=True,
help_text="Parcels arrived at")
arrived_parcels = models.IntegerField(null=False, blank=False,
help_text="Number of parcels arrived")
trackingnumber = models.CharField(max_length=100, null=False, blank=True,
verbose_name="Tracking number")
shippingcompany = models.CharField(max_length=100, null=False, blank=True,
verbose_name="Shipping company")
trackinglink = models.URLField(max_length=200, null=False, blank=True,
verbose_name="Tracking link")
_safe_attributes = ('sponsor', 'address', 'addresstoken', 'description',
'sent_parcels', 'sent_at', 'arrived_at', 'arrived_parcels',
'trackingnumber', 'shippingcompany', 'trackinglink', )
class Meta:
unique_together = (
('conference', 'addresstoken'),
)
@property
def full_address(self):
return self.address.address.replace('%%', str(self.addresstoken))
@property
def status_label_class(self):
if self.sent_at is None:
# Not sent yet
return "warning"
if self.arrived_at is not None:
# Has arrived. Check the number of parcels.
# They must be the same, or if sent parcels is set to 0 = Unknown,
# we just ignore it.
if self.arrived_parcels == self.sent_parcels or self.sent_parcels == 0:
return "success"
else:
return "danger"
return ""
@property
def sender(self):
if self.sponsor:
return self.sponsor.name
return "{0} organizers".format(self.conference)
class SponsorAdditionalContract(models.Model):
sponsor = models.ForeignKey(Sponsor, null=False, blank=False, on_delete=models.CASCADE)
subject = models.CharField(max_length=100, null=False, blank=False)
contract = models.ForeignKey(SponsorshipContract, null=False, blank=False, on_delete=models.CASCADE)
sent_to_manager = models.ForeignKey(User, null=True, blank=True, on_delete=models.SET_NULL)
digitalcontract = models.OneToOneField(DigisignDocument, null=True, blank=True, help_text="Contract, when using digital signatures", on_delete=models.SET_NULL)
sponsorsigned = models.DateTimeField(null=True, blank=True)
completed = models.DateTimeField(null=True, blank=True)
|