Exemple #1
0
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)
Exemple #2
0
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))
Exemple #3
0
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),
        )
Exemple #4
0
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)
Exemple #5
0
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)
Exemple #6
0
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
Exemple #7
0
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)
Exemple #8
0
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)
Exemple #9
0
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