class ScopeDefinition(models.Model): # the identifier used to specify this scope within a certificate id = models.CharField(primary_key=True, max_length=20) # the Morango profile with which this scope is associated profile = models.CharField(max_length=20) # version number is incremented whenever scope definition is updated version = models.IntegerField() # the scope_param key that the primary partition value will be inserted into when generating a root cert # (if this is not set, then this scope definition cannot be used to generate a root cert) primary_scope_param_key = models.CharField(max_length=20, blank=True) # human-readable description # (can include string template refs to scope params e.g. "Allows syncing data for user ${username}") description = models.TextField() # filter templates, in the form of a newline-delimited list of colon-delimited partition strings # (can include string template refs to scope params e.g. "122211:singleuser:${user_id}") read_filter_template = models.TextField() write_filter_template = models.TextField() read_write_filter_template = models.TextField() @classmethod def retrieve_by_id(cls, scope_def_id): try: return cls.objects.get(id=scope_def_id) except ScopeDefinition.DoesNotExist: call_command("loaddata", "scopedefinitions") return cls.objects.get(id=scope_def_id) def get_scope(self, params): return Scope(definition=self, params=params)
class LegislationPage(models.Model): page_text = models.TextField(max_length=65535) page_number = models.IntegerField() legislation = models.ForeignKey(Legislation, related_name="pages") def __str__(self): return "Page %d of Legislation %s" % (self.page_number, str(self.legislation.title))
class LegislationPage(models.Model): page_text = models.TextField() page_number = models.IntegerField() legislation = models.ForeignKey( Legislation, on_delete=models.CASCADE, related_name="pages" ) def __str__(self): return "Page %d of Legislation %s" % ( self.page_number, str(self.legislation.title), )
class Certificate(mptt.models.MPTTModel, UUIDModelMixin): uuid_input_fields = ("public_key", "profile") parent = models.ForeignKey("Certificate", blank=True, null=True) # the Morango profile with which this certificate is associated profile = models.CharField(max_length=20) # scope of this certificate, and version of the scope, along with associated params scope_definition = models.ForeignKey("ScopeDefinition") scope_version = models.IntegerField() scope_params = models.TextField() # JSON dict of values to insert into scope definitions # track the certificate's public key so we can verify any certificates it signs public_key = PublicKeyField() # the JSON-serialized copy of all the fields above serialized = models.TextField() # signature from the private key of the parent certificate, of the "serialized" field text signature = models.TextField() # when we own a certificate, we'll have the private key for it (otherwise not) _private_key = PrivateKeyField(blank=True, null=True, db_column="private_key") @property def private_key(self): return self._private_key @private_key.setter def private_key(self, value): self._private_key = value if value and not self.public_key: self.public_key = Key(public_key_string=self._private_key.get_public_key_string()) @classmethod def generate_root_certificate(cls, scope_def_id, **extra_scope_params): # attempt to retrieve the requested scope definition object scope_def = ScopeDefinition.retrieve_by_id(scope_def_id) # create a certificate model instance cert = cls() # set the scope definition foreign key, and read some values off of the scope definition model cert.scope_definition = scope_def cert.scope_version = scope_def.version cert.profile = scope_def.profile primary_scope_param_key = scope_def.primary_scope_param_key assert primary_scope_param_key, "Root cert can only be created for ScopeDefinition that has primary_scope_param_key defined" # generate a key and extract the public key component cert.private_key = Key() cert.public_key = Key(public_key_string=cert.private_key.get_public_key_string()) # calculate the certificate's ID on the basis of the profile and public key cert.id = cert.calculate_uuid() # set the scope params to include the primary partition value and any additional params scope_params = {primary_scope_param_key: cert.id} scope_params.update(extra_scope_params) cert.scope_params = json.dumps(scope_params) # self-sign the certificate cert.sign_certificate(cert) # save and return the certificate cert.save() return cert def serialize(self): if not self.id: self.id = self.calculate_uuid() data = { "id": self.id, "parent_id": self.parent_id, "profile": self.profile, "scope_definition_id": self.scope_definition_id, "scope_version": self.scope_version, "scope_params": self.scope_params, "public_key_string": self.public_key.get_public_key_string(), } return json.dumps(data) @classmethod def deserialize(cls, serialized, signature): data = json.loads(serialized) model = cls( id=data["id"], parent_id=data["parent_id"], profile=data["profile"], scope_definition_id=data["scope_definition_id"], scope_version=data["scope_version"], scope_params=data["scope_params"], public_key=Key(public_key_string=data["public_key_string"]), serialized=serialized, signature=signature, ) return model def sign_certificate(self, cert_to_sign): if not cert_to_sign.serialized: cert_to_sign.serialized = cert_to_sign.serialize() cert_to_sign.signature = self.sign(cert_to_sign.serialized) def check_certificate(self): # check that the certificate's ID is properly calculated if self.id != self.calculate_uuid(): raise CertificateIDInvalid("Certificate ID is {} but should be {}".format(self.id, self.calculate_uuid())) if not self.parent: # self-signed root certificate # check that the certificate is properly self-signed if not self.verify(self.serialized, self.signature): raise CertificateSignatureInvalid() # check that the certificate scopes all start with the primary partition value scope = self.get_scope() for item in scope.read_filter + scope.write_filter: if not item.startswith(self.id): raise CertificateRootScopeInvalid("Scope entry {} does not start with primary partition {}".format(item, self.id)) else: # non-root child certificate # check that the certificate is properly signed by its parent if not self.parent.verify(self.serialized, self.signature): raise CertificateSignatureInvalid() # check that certificate's scope is a subset of parent's scope self.get_scope().verify_subset_of(self.parent.get_scope()) # check that certificate is for same profile as parent if self.profile != self.parent.profile: raise CertificateProfileInvalid("Certificate profile is {} but parent's is {}" \ .format(self.profile, self.parent.profile)) def sign(self, value): assert self.private_key, "Can only sign using certificates that have private keys" return self.private_key.sign(value) def verify(self, value, signature): return self.public_key.verify(value, signature) def get_scope(self): return self.scope_definition.get_scope(self.scope_params)
class Certificate(mptt.models.MPTTModel, UUIDModelMixin): uuid_input_fields = ("public_key", "profile", "salt") parent = models.ForeignKey("Certificate", blank=True, null=True) # the Morango profile with which this certificate is associated profile = models.CharField(max_length=20) # scope of this certificate, and version of the scope, along with associated params scope_definition = models.ForeignKey("ScopeDefinition") scope_version = models.IntegerField() scope_params = models.TextField( ) # JSON dict of values to insert into scope definitions # track the certificate's public key so we can verify any certificates it signs public_key = PublicKeyField() # a salt value to include in the UUID calculation, to prevent CSR requests from forcing ID collisions salt = models.CharField(max_length=32, blank=True) # the JSON-serialized copy of all the fields above serialized = models.TextField() # signature from the private key of the parent certificate, of the "serialized" field text signature = models.TextField() # when we own a certificate, we'll have the private key for it (otherwise not) _private_key = PrivateKeyField(blank=True, null=True, db_column="private_key") @property def private_key(self): return self._private_key @private_key.setter def private_key(self, value): self._private_key = value if value and not self.public_key: self.public_key = Key( public_key_string=self._private_key.get_public_key_string()) @classmethod def generate_root_certificate(cls, scope_def_id, **extra_scope_params): # attempt to retrieve the requested scope definition object scope_def = ScopeDefinition.retrieve_by_id(scope_def_id) # create a certificate model instance cert = cls() # set the scope definition foreign key, and read some values off of the scope definition model cert.scope_definition = scope_def cert.scope_version = scope_def.version cert.profile = scope_def.profile primary_scope_param_key = scope_def.primary_scope_param_key assert primary_scope_param_key, "Root cert can only be created for ScopeDefinition that has primary_scope_param_key defined" # generate a key and extract the public key component cert.private_key = Key() cert.public_key = Key( public_key_string=cert.private_key.get_public_key_string()) # calculate the certificate's ID on the basis of the profile and public key cert.id = cert.calculate_uuid() # set the scope params to include the primary partition value and any additional params scope_params = {primary_scope_param_key: cert.id} scope_params.update(extra_scope_params) cert.scope_params = json.dumps(scope_params) # self-sign the certificate cert.sign_certificate(cert) # save and return the certificate cert.save() return cert def has_private_key(self): return self._private_key is not None def serialize(self): if not self.id: self.id = self.calculate_uuid() data = { "id": self.id, "parent_id": self.parent_id, "profile": self.profile, "salt": self.salt, "scope_definition_id": self.scope_definition_id, "scope_version": self.scope_version, "scope_params": self.scope_params, "public_key_string": self.public_key.get_public_key_string(), } return json.dumps(data) @classmethod def deserialize(cls, serialized, signature): data = json.loads(serialized) model = cls( id=data["id"], parent_id=data["parent_id"], profile=data["profile"], salt=data.get('salt') or '', scope_definition_id=data["scope_definition_id"], scope_version=data["scope_version"], scope_params=data["scope_params"], public_key=Key(public_key_string=data["public_key_string"]), serialized=serialized, signature=signature, ) return model def _serialize_if_needed(self): if not self.serialized: self.serialized = self.serialize() def sign_certificate(self, cert_to_sign): cert_to_sign._serialize_if_needed() cert_to_sign.signature = self.sign(cert_to_sign.serialized) def check_certificate(self): # check that the certificate's ID is properly calculated if self.id != self.calculate_uuid(): raise CertificateIDInvalid( "Certificate ID is {} but should be {}".format( self.id, self.calculate_uuid())) if not self.parent: # self-signed root certificate # check that the certificate is properly self-signed if not self.verify(self.serialized, self.signature): raise CertificateSignatureInvalid() # check that the certificate scopes all start with the primary partition value scope = self.get_scope() for item in scope.read_filter + scope.write_filter: if not item.startswith(self.id): raise CertificateRootScopeInvalid( "Scope entry {} does not start with primary partition {}" .format(item, self.id)) else: # non-root child certificate # check that the certificate is properly signed by its parent if not self.parent.verify(self.serialized, self.signature): raise CertificateSignatureInvalid() # check that certificate's scope is a subset of parent's scope if not self.get_scope().is_subset_of(self.parent.get_scope()): raise CertificateScopeNotSubset() # check that certificate is for same profile as parent if self.profile != self.parent.profile: raise CertificateProfileInvalid("Certificate profile is {} but parent's is {}" \ .format(self.profile, self.parent.profile)) @classmethod def save_certificate_chain(cls, cert_chain, expected_last_id=None): # parse the chain from json if needed if isinstance(cert_chain, string_types): cert_chain = json.loads(cert_chain) # start from the bottom of the chain cert_data = cert_chain[-1] # create an in-memory instance of the cert from the serialized data and signature cert = cls.deserialize(cert_data["serialized"], cert_data["signature"]) # verify the id of the cert matches the id of the outer serialized data assert cert_data["id"] == cert.id # check that the expected ID matches, if specified if expected_last_id: assert cert.id == expected_last_id # if cert already exists locally, it's already been verified, so no need to continue # (this also means we have the full cert chain for it, given the `parent` relations) try: return cls.objects.get(id=cert.id) except cls.DoesNotExist: pass # recurse up the certificate chain, until we hit a cert that exists or is the root if len(cert_chain) > 1: cls.save_certificate_chain(cert_chain[:-1], expected_last_id=cert.parent_id) else: assert not cert.parent_id, "First cert in chain must be a root cert (no parent)" # ensure the certificate checks out (now that we know its parent, if any, is saved) cert.check_certificate() # save the certificate, as it's now fully verified cert.save() return cert def sign(self, value): assert self.private_key, "Can only sign using certificates that have private keys" return self.private_key.sign(value) def verify(self, value, signature): return self.public_key.verify(value, signature) def get_scope(self): return self.scope_definition.get_scope(self.scope_params) def __str__(self): if self.scope_definition: return self.scope_definition.get_description(self.scope_params)
class Bid(mptt.models.MPTTModel): objects = BidManager() event = models.ForeignKey( 'Event', on_delete=models.PROTECT, verbose_name='Event', null=True, blank=True, related_name='bids', help_text='Required for top level bids if Run is not set', ) speedrun = models.ForeignKey( 'SpeedRun', on_delete=models.PROTECT, verbose_name='Run', null=True, blank=True, related_name='bids', ) parent = mptt.models.TreeForeignKey( 'self', on_delete=models.PROTECT, verbose_name='Parent', editable=False, null=True, blank=True, related_name='options', ) name = models.CharField(max_length=64) state = models.CharField( max_length=32, db_index=True, default='OPENED', choices=( ('PENDING', 'Pending'), ('DENIED', 'Denied'), ('HIDDEN', 'Hidden'), ('OPENED', 'Opened'), ('CLOSED', 'Closed'), ), ) description = models.TextField(max_length=1024, blank=True) shortdescription = models.TextField( max_length=256, blank=True, verbose_name='Short Description', help_text='Alternative description text to display in tight spaces', ) goal = models.DecimalField(decimal_places=2, max_digits=20, null=True, blank=True, default=None) istarget = models.BooleanField( default=False, verbose_name='Target', help_text= "Set this if this bid is a 'target' for donations (bottom level choice or challenge)", ) allowuseroptions = models.BooleanField( default=False, verbose_name='Allow User Options', help_text= 'If set, this will allow donors to specify their own options on the donate page (pending moderator approval)', ) option_max_length = models.PositiveSmallIntegerField( 'Max length of user suggestions', blank=True, null=True, default=None, validators=[MinValueValidator(1), MaxValueValidator(64)], help_text= 'If allowuseroptions is set, this sets the maximum length of user-submitted bid suggestions', ) revealedtime = models.DateTimeField(verbose_name='Revealed Time', null=True, blank=True) biddependency = models.ForeignKey( 'self', on_delete=models.PROTECT, verbose_name='Dependency', null=True, blank=True, related_name='dependent_bids', ) total = models.DecimalField(decimal_places=2, max_digits=20, editable=False, default=Decimal('0.00')) count = models.IntegerField(editable=False) class Meta: app_label = 'tracker' unique_together = (( 'event', 'name', 'speedrun', 'parent', ), ) ordering = [ 'event__datetime', 'speedrun__starttime', 'parent__name', 'name' ] permissions = ( ('top_level_bid', 'Can create new top level bids'), ('delete_all_bids', 'Can delete bids with donations attached'), ('view_hidden_bid', 'Can view hidden bids'), ) class MPTTMeta: order_insertion_by = ['name'] def get_absolute_url(self): return reverse('tracker:bid', args=(self.id, )) def natural_key(self): if self.parent: return ( self.event.natural_key(), self.name, self.speedrun.natural_key() if self.speedrun else None, self.parent.natural_key(), ) elif self.speedrun: return (self.event.natural_key(), self.name, self.speedrun.natural_key()) else: return (self.event.natural_key(), self.name) def clean(self): # Manually de-normalize speedrun/event/state to help with searching # TODO: refactor this logic, it should be correct, but is probably not minimal if self.option_max_length: if not self.allowuseroptions: raise ValidationError( _('Cannot set option_max_length without allowuseroptions'), code='invalid', ) # FIXME: why is this printing 'please enter a whole number'? # raise ValidationError( # { # 'option_max_length': ValidationError( # _('Cannot set option_max_length without allowuseroptions'), # code='invalid', # ), # } # ) if self.pk: for child in self.get_children(): if len(child.name) > self.option_max_length: raise ValidationError( _('Cannot set option_max_length to %(length)d, child name `%(name)s` is too long' ), code='invalid', params={ 'length': self.option_max_length, 'name': child.name, }, ) # TODO: why is this printing 'please enter a whole number'? # raise ValidationError({ # 'option_max_length': ValidationError( # _('Cannot set option_max_length to %(length), child name %(name) is too long'), # code='invalid', # params={ # 'length': self.option_max_length, # 'name': child.name, # } # ), # }) if self.parent: max_len = self.parent.option_max_length if max_len and len(self.name) > max_len: raise ValidationError({ 'name': ValidationError( _('Name is longer than %(limit)s characters'), params={'limit': max_len}, code='invalid', ), }) if self.biddependency: if self.parent or self.speedrun: if self.event != self.biddependency.event: raise ValidationError( 'Dependent bids must be on the same event') if not self.parent: if not self.get_event(): raise ValidationError( 'Top level bids must have their event set') if not self.goal: self.goal = None elif self.goal <= Decimal('0.0'): raise ValidationError('Goal should be a positive value') if self.state in ['PENDING', 'DENIED' ] and (not self.istarget or not self.parent or not self.parent.allowuseroptions): raise ValidationError({ 'state': f'State `{self.state}` can only be set on targets with parents that allow user options' }) if self.istarget and self.options.count() != 0: raise ValidationError('Targets cannot have children') if self.parent and self.parent.istarget: raise ValidationError('Cannot set that parent, parent is a target') if self.istarget and self.allowuseroptions: raise ValidationError( 'A bid target cannot allow user options, since it cannot have children.' ) if (not self.allowuseroptions and self.pk and self.get_children().filter(state__in=['PENDING', 'DENIED'])): raise ValidationError({ 'allowuseroptions': 'Bid has pending/denied children, cannot remove allowing user options' }) same_name = Bid.objects.filter( speedrun=self.speedrun, event=self.event, parent=self.parent, name__iexact=self.name, ).exclude(pk=self.pk) if same_name.exists(): raise ValidationError( 'Cannot have a bid under the same event/run/parent with the same name' ) def save(self, *args, skip_parent=False, **kwargs): if self.parent: self.check_parent() if self.speedrun: self.event = self.speedrun.event if self.state in ['OPENED', 'CLOSED'] and not self.revealedtime: self.revealedtime = datetime.utcnow().replace(tzinfo=pytz.utc) if self.biddependency: self.event = self.biddependency.event if not self.speedrun: self.speedrun = self.biddependency.speedrun self.update_total() super(Bid, self).save(*args, **kwargs) if self.pk: for option in self.get_descendants(): if option.check_parent(): option.save(skip_parent=True) if self.parent and not skip_parent: self.parent.save() def check_parent(self): changed = False if self.speedrun != self.parent.speedrun: self.speedrun = self.parent.speedrun changed = True if self.event != self.parent.event: self.event = self.parent.event changed = True if self.state not in ['PENDING', 'DENIED' ] and self.state != self.parent.state: self.state = self.parent.state changed = True return changed @property def has_options(self): return self.allowuseroptions or self.public_options.exists() @property def public_options(self): return self.options.filter(Q(state='OPENED') | Q(state='CLOSED')).order_by('-total') def update_total(self): if self.istarget: self.total = self.bids.filter( donation__transactionstate='COMPLETED').aggregate( Sum('amount'))['amount__sum'] or Decimal('0.00') self.count = self.bids.filter( donation__transactionstate='COMPLETED').count() # auto close this if it's a challenge with no children and the goal's been met if (self.goal and self.state == 'OPENED' and self.total >= self.goal and self.istarget): self.state = 'CLOSED' else: options = self.options.exclude(state__in=('DENIED', 'PENDING')).aggregate( Sum('total'), Sum('count')) self.total = options['total__sum'] or Decimal('0.00') self.count = options['count__sum'] or 0 def get_event(self): if self.speedrun: return self.speedrun.event else: return self.event def full_label(self, addMoney=True): result = [self.fullname()] if self.speedrun: result = [self.speedrun.name_with_category(), ' : '] + result if addMoney: result += [' $', '%0.2f' % self.total] if self.goal: result += [' / ', '%0.2f' % self.goal] return ''.join(result) def __str__(self): if self.parent: return f'{self.parent} (Parent) -- {self.name}' elif self.speedrun: return f'{self.speedrun.name_with_category()} (Run) -- {self.name}' else: return f'{self.event} (Event) -- {self.name}' def fullname(self): parent = self.parent.fullname() + ' -- ' if self.parent else '' return parent + self.name
class TaxonomyClassification(mptt.models.MPTTModel): # NOTE: The name must not contain the character ";". name = models.CharField(max_length=255) code = models.CharField(max_length=16, unique=True, blank=True) legispro_code = models.CharField(max_length=16, blank=True) details = models.TextField(null=True, default='') parent = mptt.models.TreeForeignKey('self', null=True, blank=True, related_name='children') class Meta: verbose_name = 'Taxonomy Classification' verbose_name_plural = 'Taxonomy Classifications' ordering = ('code', ) class MPTTMeta: order_insertion_by = ['code'] @classmethod def _pre_save_classification_code_on_create(cls, instance): """Logic executed before saving a new TaxonomyClassification instance. Set the next code for the classification. """ instance.code = utils.generate_code(cls, instance) @staticmethod def _pre_save_classification_code_on_edit(instance): """Logic executed before editing an TaxonomyClassification instance. Update the code for every child to match the parent classification. """ for classification in instance.children.all(): parts = classification.code.split('.') suffix_code = parts[-1] classification.code = '{0}.{1}'.format(instance.code, suffix_code) classification.save() @staticmethod def pre_save_classification_code(**kwargs): instance = kwargs['instance'] if instance.code: TaxonomyClassification._pre_save_classification_code_on_edit( instance) else: TaxonomyClassification._pre_save_classification_code_on_create( instance) def get_classification_level(self): # The logical classification of taxonomy starts from 1 # The tree level of an object starts from 0 return self.get_level() + 1 def get_children(self): return super().get_children().extra(select={ 'code_fix': "string_to_array(code, '.')::int[]", }, ).order_by('code_fix') def __str__(self): return "{} classification: {}".format(self.code, self.name)
class LegislationSection(_TaxonomyModel, mptt.models.MPTTModel): text = models.TextField() legislation = models.ForeignKey( Legislation, on_delete=models.CASCADE, related_name="sections" ) legislation_page = models.IntegerField(null=True, blank=True) number = models.IntegerField(blank=True, null=True) # populated from code identifier = models.IntegerField(blank=True, null=True, default=None) legispro_identifier = models.CharField(max_length=256, null=True, blank=True) code = models.CharField(max_length=256, blank=True) code_order = models.CharField(max_length=256, blank=True) parent = mptt.models.TreeForeignKey( "self", null=True, blank=True, on_delete=models.CASCADE, related_name="children" ) objects = LegislationSectionManager() class Meta(_TaxonomyModel.Meta): ordering = ['code_order'] class MPTTMeta: order_insertion_by = ["code_order"] def get_children(self): return ( super() .get_children() .extra( select={ "code_order_fix": "string_to_array(code_order, '.')::int[]", }, ) .order_by("code_order_fix") ) def __str__(self): return self.code def classifications_text(self): return settings.TAXONOMY_CONNECTOR.join( self.classifications.values_list("name", flat=True) ) def tags_text(self): return settings.TAXONOMY_CONNECTOR.join( self.tags.values_list("name", flat=True) ) def parent_tags(self): return settings.TAXONOMY_CONNECTOR.join( self.legislation.tags.values_list("name", flat=True) ) def parent_classifications(self): return settings.TAXONOMY_CONNECTOR.join( self.legislation.classifications.values_list("name", flat=True) ) def save(self, *args, **kwargs): match = re.search("\d+", self.code) if match: self.number = int(match.group(0)) return super().save(*args, **kwargs)
class Bid(mptt.models.MPTTModel): objects = BidManager() event = models.ForeignKey('Event', on_delete=models.PROTECT, verbose_name='Event', null=True, blank=True, related_name='bids', help_text='Required for top level bids if Run is not set') speedrun = models.ForeignKey('SpeedRun', on_delete=models.PROTECT, verbose_name='Run', null=True, blank=True, related_name='bids') parent = mptt.models.TreeForeignKey('self', on_delete=models.PROTECT, verbose_name='Parent', editable=False, null=True, blank=True, related_name='options') name = models.CharField(max_length=64) state = models.CharField(max_length=32, choices=(('PENDING', 'Pending'), ('DENIED', 'Denied'), ( 'HIDDEN', 'Hidden'), ('OPENED', 'Opened'), ('CLOSED', 'Closed')), default='OPENED') description = models.TextField(max_length=1024, blank=True) shortdescription = models.TextField(max_length=256, blank=True, verbose_name='Short Description', help_text="Alternative description text to display in tight spaces") goal = models.DecimalField( decimal_places=2, max_digits=20, null=True, blank=True, default=None) istarget = models.BooleanField(default=False, verbose_name='Target', help_text="Set this if this bid is a 'target' for donations (bottom level choice or challenge)") allowuseroptions = models.BooleanField(default=False, verbose_name="Allow User Options", help_text="If set, this will allow donors to specify their own options on the donate page (pending moderator approval)") revealedtime = models.DateTimeField( verbose_name='Revealed Time', null=True, blank=True) biddependency = models.ForeignKey('self', on_delete=models.PROTECT, verbose_name='Dependency', null=True, blank=True, related_name='dependent_bids') total = models.DecimalField( decimal_places=2, max_digits=20, editable=False, default=Decimal('0.00')) count = models.IntegerField(editable=False) class Meta: app_label = 'tracker' unique_together = (('event', 'name', 'speedrun', 'parent',),) ordering = ['event__datetime', 'speedrun__starttime', 'parent__name', 'name'] permissions = ( ('top_level_bid', 'Can create new top level bids'), ('delete_all_bids', 'Can delete bids with donations attached'), ('view_hidden', 'Can view hidden bids'), ) class MPTTMeta: order_insertion_by = ['name'] def natural_key(self): if self.parent: return (self.event.natural_key(), self.name, self.speedrun.natural_key() if self.speedrun else None, self.parent.natural_key()) elif self.speedrun: return (self.event.natural_key(), self.name, self.speedrun.natural_key()) else: return (self.event.natural_key(), self.name) def clean(self): # Manually de-normalize speedrun/event/state to help with searching # TODO: refactor this logic, it should be correct, but is probably not minimal if self.speedrun: self.event = self.speedrun.event if self.parent: curr = self.parent while curr.parent != None: curr = curr.parent root = curr self.speedrun = root.speedrun self.event = root.event if self.state != 'PENDING' and self.state != 'DENIED': self.state = root.state if self.biddependency: if self.parent or self.speedrun: if self.event != self.biddependency.event: raise ValidationError( 'Dependent bids must be on the same event') self.event = self.biddependency.event if not self.speedrun: self.speedrun = self.biddependency.speedrun if not self.parent: if not self.get_event(): raise ValidationError( 'Top level bids must have their event set') if self.id: for option in self.get_descendants(): option.speedrun = self.speedrun option.event = self.event if option.state != 'PENDING' and option.state != 'DENIED': option.state = self.state option.save() if not self.goal: self.goal = None elif self.goal <= Decimal('0.0'): raise ValidationError('Goal should be a positive value') if self.istarget and self.options.count() != 0: raise ValidationError('Targets cannot have children') if self.parent and self.parent.istarget: raise ValidationError('Cannot set that parent, parent is a target') if self.istarget and self.allowuseroptions: raise ValidationError( 'A bid target cannot allow user options, since it cannot have children.') sameName = Bid.objects.filter( speedrun=self.speedrun, event=self.event, parent=self.parent, name__iexact=self.name) if sameName.exists(): if sameName.count() > 1 or sameName[0].id != self.id: raise ValidationError( 'Cannot have a bid under the same event/run/parent with the same name') if self.id == None or (sameName.exists() and sameName[0].state == 'HIDDEN' and self.state == 'OPENED'): self.revealedtime = datetime.utcnow().replace(tzinfo=pytz.utc) self.update_total() @property def has_options(self): return self.allowuseroptions or self.public_options.exists() @property def public_options(self): return self.options.filter(Q(state='OPENED') | Q(state='CLOSED')).order_by('-total') def update_total(self): if self.istarget: self.total = self.bids.filter(donation__transactionstate='COMPLETED').aggregate( Sum('amount'))['amount__sum'] or Decimal('0.00') self.count = self.bids.filter( donation__transactionstate='COMPLETED').count() # auto close this if it's a challenge with no children and the goal's been met if self.goal and self.state == 'OPENED' and self.total >= self.goal and self.istarget: self.state = 'CLOSED' else: options = self.options.exclude(state__in=( 'HIDDEN', 'DENIED', 'PENDING')).aggregate(Sum('total'), Sum('count')) self.total = options['total__sum'] or Decimal('0.00') self.count = options['count__sum'] or 0 def get_event(self): if self.speedrun: return self.speedrun.event else: return self.event def full_label(self, addMoney=True): result = [self.fullname()] if self.speedrun: result = [self.speedrun.name_with_category(), ' : '] + result if addMoney: result += [' $', '%0.2f' % self.total] if self.goal: result += [' / ', '%0.2f' % self.goal] return ''.join(result) def __unicode__(self): if self.parent: return unicode(self.parent) + ' -- ' + self.name elif self.speedrun: return self.speedrun.name_with_category() + ' -- ' + self.name else: return unicode(self.event) + ' -- ' + self.name def fullname(self): return ((self.parent.fullname() + ' -- ') if self.parent else '') + self.name