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 Country(CountryBase): iso = models.CharField("ISO", max_length=3, primary_key=True) iso_code = models.CharField("Iso alpha 2", max_length=2, default="") name = models.CharField("Name", max_length=128) class Meta: verbose_name = "Country" verbose_name_plural = "Countries" ordering = ("name",) db_table = "country" def __str__(self): return self.name
class Country(CountryBase): iso = models.CharField('ISO', max_length=3, primary_key=True) iso_code = models.CharField('Iso alpha 2', max_length=2, default='') name = models.CharField('Name', max_length=128) class Meta: verbose_name = 'Country' verbose_name_plural = 'Countries' ordering = ('name', ) db_table = "country" def __str__(self): return self.name
class UserProfile(models.Model): user = models.OneToOneField(User, on_delete=models.CASCADE) home_country = models.ForeignKey( Country, on_delete=models.CASCADE, related_name="home_country" ) countries = models.ManyToManyField(Country) affiliation = models.CharField( "Institutional affiliation", max_length=255, null=True, blank=True, ) position = models.CharField( "Position", max_length=255, null=True, blank=True, ) approve_url = models.URLField("Approve URL", max_length=255, null=True, blank=True) other_role = models.CharField( 'Other role', max_length=255, null=True, blank=True, ) @property def roles(self): return get_user_roles(self.user) @property def flag(self): """Returns alpha3 from iso3.""" return pycountry.countries.get(alpha_3=self.home_country.iso).alpha_3 @property def country(self): if self.home_country is None: # @TODO Please remove this when User management is added # in the frontend. return "User without a country" return self.home_country.name def __str__(self): return self.user.username
class TaxonomyTag(models.Model): # NOTE: The name must not contain the character ";". name = models.CharField(max_length=255) group = models.ForeignKey(TaxonomyTagGroup, related_name='tags') def __str__(self): return "Tag " + self.name
class BidSuggestion(models.Model): bid = models.ForeignKey('Bid', related_name='suggestions', null=False, on_delete=models.PROTECT) name = models.CharField(max_length=64, blank=False, null=False, verbose_name='Name') class Meta: app_label = 'tracker' ordering = ['name'] def __init__(self): raise Exception('Nothing should be using this any more') def clean(self): sameBid = BidSuggestion.objects.filter( Q(name__iexact=self.name) & (Q(bid__event=self.bid.get_event()) | Q(bid__speedrun__event=self.bid.get_event()))) if sameBid.exists(): if sameBid.count() > 1 or sameBid[0].id != self.id: raise ValidationError( 'Cannot have a bid suggestion with the same name within the same event.' ) # If set, limit the length of suggestions based on the parent bid's # setting def __str__(self): return self.name + ' -- ' + str(self.bid)
class Nonce(UUIDModelMixin): """ Stores temporary nonce values used for cryptographic handshakes during syncing. These nonces are requested by the client, and then generated and stored by the server. When the client then goes to initiate a sync session, it signs the nonce value using the private key from the certificate it is using for the session, to prove to the server that it owns the certificate. The server checks that the nonce exists and hasn't expired, and then deletes it. """ uuid_input_fields = "RANDOM" timestamp = models.DateTimeField(default=timezone.now) ip = models.CharField(max_length=100, blank=True) @classmethod def use_nonce(cls, nonce_value): with transaction.atomic(): # try fetching the nonce try: nonce = cls.objects.get(id=nonce_value) except cls.DoesNotExist: raise NonceDoesNotExist() # check that the nonce hasn't expired if not (0 < (timezone.now() - nonce.timestamp).total_seconds() < 60): nonce.delete() raise NonceExpired() # now that we've used it, delete the nonce nonce.delete()
class TaxonomyTagGroup(models.Model): name = models.CharField(max_length=255) def __str__(self): return 'Tagging by ' + self.name class Meta: ordering = ['id']
class Region(models.Model): name = models.CharField('Name', max_length=128) def __str__(self): return self.name def countries(self): return (object for object in self.country_set.all())
class LegislationArticle(_TaxonomyModel): text = models.CharField(max_length=65535) legislation = models.ForeignKey( Legislation, on_delete=models.CASCADE, related_name="articles" ) legislation_page = models.IntegerField(null=True, blank=True) code = models.CharField(max_length=256) # aka Article number 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) # objects = LegislationSectionManager() class Meta(_TaxonomyModel.Meta): ordering = ["number", "code"] 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 LogicalCategory(models.Model): name = models.CharField(max_length=1024) code = models.IntegerField() class Meta: verbose_name = "Logical category" verbose_name_plural = "Logical categories" ordering = ["code"] def __str__(self): return self.name
class BidSuggestion(models.Model): bid = models.ForeignKey('Bid', related_name='suggestions', null=False, on_delete=models.PROTECT) name = models.CharField(max_length=64, blank=False, null=False, verbose_name="Name") class Meta: app_label = 'tracker' ordering = ['name'] def clean(self): sameBid = BidSuggestion.objects.filter(Q(name__iexact=self.name) & ( Q(bid__event=self.bid.get_event()) | Q(bid__speedrun__event=self.bid.get_event()))) if sameBid.exists(): if sameBid.count() > 1 or sameBid[0].id != self.id: raise ValidationError( "Cannot have a bid suggestion with the same name within the same event.") def __unicode__(self): return self.name + " -- " + unicode(self.bid)
class Question(mptt.models.MPTTModel): text = models.CharField(max_length=1024) parent = mptt.models.TreeForeignKey('self', null=True, blank=True, related_name='children') parent_answer = models.NullBooleanField(default=None) order = models.IntegerField(blank=True) classification = models.ForeignKey(TaxonomyClassification, null=True, blank=True) class Meta: verbose_name = 'Question' verbose_name_plural = 'Questions' class MPTTMeta: order_insertion_by = ['order'] def save(self, *args, **kwargs): if not self.order: self.order = utils.set_order(self.classification, self.parent) super(Question, self).save(*args, **kwargs) @property def full_order(self): return ".".join([ str(question.order) for question in self.get_ancestors(include_self=True) ]) def __str__(self): if self.parent: return "Question: %s with parent answer: %s" % (self.full_order, self.parent_answer) else: return "C: %s Question: %s" % (self.classification.code, self.order)
class PrioritySector(models.Model): name = models.CharField('Name', max_length=255) def __str__(self): return self.name
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 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 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 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 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 SubRegion(models.Model): name = models.CharField('Name', max_length=128) def __str__(self): return self.name
class LegalSystem(models.Model): name = models.CharField('Name', max_length=128) def __str__(self): return self.name
class Legislation(_TaxonomyModel): title = models.CharField(max_length=256) abstract = models.CharField(max_length=1024, blank=True, null=True) country = models.ForeignKey(Country, related_name="legislations") language = models.CharField(choices=constants.ALL_LANGUAGES, default=constants.DEFAULT_LANGUAGE_VALUE, max_length=64) law_type = models.CharField(choices=constants.LEGISLATION_TYPE, default=constants.LEGISLATION_DEFAULT_VALUE, max_length=64) year = models.IntegerField(default=constants.LEGISLATION_YEAR_RANGE[-1]) year_amendment = models.IntegerField( default=constants.LEGISLATION_DEFAULT_YEAR, blank=True, null=True) year_mention = models.CharField(max_length=1024, blank=True, null=True) geo_coverage = models.CharField( choices=constants.GEOGRAPHICAL_COVERAGE, default=constants.GEOGRAPHICAL_COVERAGE_DEFAULT_VALUE, max_length=64, null=True) source = models.CharField(max_length=256, blank=True, null=True) source_type = models.CharField(choices=constants.SOURCE_TYPE, default=constants.SOURCE_TYPE_DEFAULT_VALUE, max_length=64, blank=True, null=True) website = models.URLField(max_length=2000, blank=True, null=True) legispro_article = models.CharField(max_length=512, blank=True, null=True) import_from_legispro = models.BooleanField(default=False) date_created = models.DateTimeField(auto_now_add=True) date_updated = models.DateTimeField(auto_now=True) pdf_file = models.FileField(null=True, blank=True) pdf_file_name = models.CharField(null=True, max_length=256) objects = LegislationManager() @property def country_name(self): return self.country.name @property def country_iso(self): return self.country.iso @property def other_legislations(self): other = {} for classification in self.classifications.all(): other[classification] = Legislation.objects.filter( classifications__id__exact=classification.pk).exclude( pk=self.pk).all()[:3] return other # @TODO: Change the __str__ to something more appropriate def __str__(self): return "Legislation: " + ' | '.join([self.country.name, self.law_type]) def highlighted_title(self): """ If this law was returned as a result of an elasticsearch query, return the title with the search terms highlighted. If not, return the original title. """ return getattr(self, '_highlighted_title', self.title) def highlighted_abstract(self): """ If this law was returned as a result of an elasticsearch query, return the abstract with the search terms highlighted. If not, return an empty string. """ return getattr(self, '_highlighted_abstract', '') def highlighted_pdf_text(self): """ If this law was returned as a result of an elasticsearch query, return the pdf_text with the search terms highlighted. If not, return an empty string. """ return getattr(self, '_highlighted_pdf_text', '') def highlighted_classifications(self): """ If this law was returned as a result of an elasticsearch query, return a list of classification names with the search terms highlighted. If not, return the original list of classification names. """ return getattr( self, '_highlighted_classifications', self.classifications.all().values_list('name', flat=True)) def highlighted_tags(self): """ If this law was returned as a result of an elasticsearch query, return a list of tag names with the search terms highlighted. If not, return the original list of tag names. """ return getattr(self, '_highlighted_tags', self.tags.all().values_list('name', flat=True)) def highlighted_articles(self): """ If this law was returned as a result of an elasticsearch query, return a list of dictionaries representing articles with the search terms highlighted in the text field. If not, return an empty list. """ return getattr(self, '_highlighted_articles', []) def save_pdf_pages(self): if settings.DEBUG: time_to_load_pdf = time.time() if settings.DEBUG: print("INFO: FS pdf file load time: %fs" % (time.time() - time_to_load_pdf)) time_begin_transaction = time.time() with transaction.atomic(): pdf = pdftotext.PDF(self.pdf_file) for idx, page in enumerate(pdf): page = page.replace('\x00', '') LegislationPage(page_text="<pre>%s</pre>" % page, page_number=idx + 1, legislation=self).save() if settings.DEBUG: print("INFO: ORM models.LegislationPages save time: %fs" % (time.time() - time_begin_transaction)) # This is necessary in order to trigger the signal that will update the # ElasticSearch index. self.save()
class FocusArea(models.Model): name = models.CharField('Name', max_length=255) def __str__(self): return self.name
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