Exemple #1
0
class Price(models.Model):
    prices_path=settings.MEDIA_ROOT+'/files/generated_prices'
    head_name=models.CharField(null=True, blank=True, max_length=100,verbose_name=_("Price name"))

    name=EavSlugField(_("Document name"), max_length=50, help_text=_("Use this name like '{% load price_file %} {% price_file site='auto' lang='auto' %}'." ),
                   unique=True)
    last_update= TextField(_("Last document update"), default='Price never generated', editable=False)
    price_page=EavSlugField(_("Reverse id of price page"), max_length=100)

    #todo show normal datetime everywhere

    def generate_new_prices(self):
        
        self.last_update='Generating (start: '+ str(now()).split('.')[0] +')' 
        self.save()
        try:
            for template in self.pricetemplate_set.all():
                if template.template_file and template.template_file.hash:
                    template.generate_price(self.prices_path)
            self.last_update= str(now()).split('.')[0] 
            self.save()
        except Exception as e:
            s = str(e)
            self.last_update="ERROR: "+s 
            self.save()
    class Meta:
        verbose_name = _('price list')
        verbose_name_plural = _('Price lists')
Exemple #2
0
class ProductTag(models.Model):
    name_ru = models.CharField(max_length=30,
                               verbose_name=_("Tag name (ru)"),
                               unique=True)
    name_en = models.CharField(max_length=30,
                               verbose_name=_("Tag name (en)"),
                               unique=True)
    slug = EavSlugField(max_length=30,
                        verbose_name=_("Tag slug"),
                        help_text=_("Short unique tag label."),
                        unique=True)

    class Meta:
        verbose_name = _('product tag')
        verbose_name_plural = _('Product tags')

    def count_tagged_products(self):
        return self.product_set.count()

    count_tagged_products.short_description = _("Tagged producs")

    def __unicode__(self):
        current_lang = get_language()
        if current_lang == 'ru':
            return self.name_ru
        else:
            return self.name_en
Exemple #3
0
class ProductType(models.Model):
    name = models.CharField(max_length=30, verbose_name=_("Type name"))
    slug = EavSlugField(max_length=30,
                        verbose_name=_("Type slug"),
                        help_text=_("Short unique type label."),
                        unique=True)
    fields = models.ManyToManyField(
        Attribute,
        verbose_name=_("Fields of type"),
        help_text=_("Data fields always assigned to products of this type."),
        blank=True)
    type_description = models.TextField(
        null=True,
        blank=True,
        verbose_name=_("Product description"),
        help_text=
        _("Common description for products of this type. Default: {% block type_description %}{% endblock %}"
          ))
    template = models.TextField(
        null=True,
        blank=True,
        verbose_name=_("Template"),
        help_text=
        _("Common template to render product on its page. Default: {% block left_content %}{% block product_description %}{% endblock %}{{ block.super }}{% endblock %}"
          ))
    changed = models.DateTimeField(auto_now=True)

    class Meta:
        verbose_name = _('product type')
        verbose_name_plural = _('Product types')
        ordering = ['name']

    def count_products_of_type(self):
        return self.product_set.count()

    count_products_of_type.short_description = _("Tagged producs")

    def get_products_of_type_and_accessoires(self):
        accs_list = []
        accesoires = Product.objects.filter(product_type__slug='accessoires')
        for acc in accesoires:
            try:
                if str(self.pk) in acc.type_accessoires:
                    accs_list.append(acc)
            except AttributeError:
                pass
        products = list(self.product_set.all())
        if self.slug == "sewerage_wells":
            products.insert(0, Product.objects.get(slug='well_pe_kl_660'))
        return products, accs_list

    def __unicode__(self):
        current_lang = get_language()
        if current_lang == 'ru':
            return self.name_ru
        else:
            return self.name_en
Exemple #4
0
    def save(self, *args, **kwargs):
        """
        Saves the Attribute and auto-generates a slug field
        if one wasn't provided.
        """
        if not self.slug:
            self.slug = EavSlugField.create_slug_from_name(self.name)

        self.full_clean()
        super(Attribute, self).save(*args, **kwargs)
    def derive_datatype(self):
        """
        We map our field_type to the appropriate data_type here.
        """
        # set our slug based on our command and keyword
        self.slug = "%s_%s" % (self.xform.get_primary_keyword(), EavSlugField.create_slug_from_name(self.command))
        typedef = self.lookup_type(self.field_type)

        if not typedef:
            raise ValidationError("Field '%s' has an unknown data type: %s" % (self.command, self.datatype))

        self.datatype = typedef['db_type']
    def derive_datatype(self):
        """
        We map our field_type to the appropriate data_type here.
        """
        # set our slug based on our command and keyword
        self.slug = "%s_%s" % (self.xform.get_primary_keyword(), EavSlugField.create_slug_from_name(self.command))
        typedef = self.lookup_type(self.field_type)

        if not typedef:
            raise ValidationError("Field '%s' has an unknown data type: %s" % (self.command, self.datatype))

        self.datatype = typedef['db_type']
Exemple #7
0
    def save(self, *args, **kwargs):
        '''
        Saves the Attribute and auto-generates a slug field if one wasn't
        provided.

        If parent provided is not already a ContentType, calculate this.
        Yes, this means you can't add Attributes for the ContentType model.
        '''
        if not getattr(self, 'slug', None):
            self.slug = EavSlugField.create_slug_from_name(self.name)
        if not getattr(self, 'site', None):
            self.site = Site.objects.get_current()
        self.full_clean()
        super(Attribute, self).save(*args, **kwargs)
Exemple #8
0
    def derive_datatype(self):
        """
        We map our field_type to the appropriate data_type here.
        """
        # set our slug based on our command and keyword
        self.slug = "%s_%s" % (self.xform.keyword, EavSlugField.create_slug_from_name(self.command))

        for (field_type, name, datatype, func, xforms_type) in XFormField.TYPE_CHOICES:
            if field_type == self.field_type:
                self.datatype = datatype
                break

        if not self.datatype:
            raise ValidationError("Field '%s' has an unknown data type: %s" % (self.command, self.datatype))
Exemple #9
0
class Album(models.Model):
    name_en = models.CharField(max_length=70,
                               verbose_name=_("Album name (en)"),
                               null=True,
                               blank=True)
    name_ru = models.CharField(max_length=70,
                               verbose_name=_("Album name (ru)"),
                               null=True,
                               blank=True)
    slug = EavSlugField(max_length=30,
                        verbose_name=_("album slug"),
                        help_text=_("Short unique label."),
                        unique=True)
    show = models.BooleanField(verbose_name=_("Show album"), default=True)
    face_photo = ElfinderField(help_text=_("Choose photo"))
    position = models.IntegerField(verbose_name=_('Position in list'))

    class Meta:
        ordering = ['position']
        verbose_name = _('Album')
        verbose_name_plural = _('Albums')

    # def save(self,*args,**kwargs):
    # super(Album,self).save(*args,**kwargs)
    # folder_url='/'.join(self.face_photo.url.split('/')[:-1])
    # folder_path=url_to_path(folder_url)
    # all_files=listdir(folder_path)
    # photos_urls = [ folder_url+'/'+file_name for file_name in all_files if '_face' not in file_name ]

    # for photo in photos_urls:
    # if not self.media_set.filter(slug=photo).exists():
    # a=Media(slug=photo,album=self)
    # a.save()

    def __unicode__(self):
        current_lang = get_language()
        if current_lang == 'ru':
            return self.name_ru
        else:
            return self.name_en

    def name(self):
        current_lang = get_language()
        if current_lang == 'ru':
            return self.name_ru
        else:
            return self.name_en
Exemple #10
0
class XFormField(Attribute):
    """
    A field within an XForm.  Fields can be one of the types:
        int: An integer
        dec: A decimal or float value
        str: A string
        gps: A lat and long pairing

    Note that when defining a field you must also define it's ``command`` which will
    be used to 'tag' the field in an SMS message.  ie: ``+age 10``

    """ 

    TYPE_INT = Attribute.TYPE_INT
    TYPE_FLOAT = Attribute.TYPE_FLOAT
    TYPE_TEXT = Attribute.TYPE_TEXT
    TYPE_OBJECT = Attribute.TYPE_OBJECT
    TYPE_GEOPOINT = 'geopoint'
    TYPE_IMAGE = 'image'
    TYPE_AUDIO = 'audio'
    TYPE_VIDEO = 'video'    

    # These are the choices of types available for XFormFields.
    #
    # The first field is the field 'field_type'
    # The second is the label displayed in the XForm UI for this type
    # The third is the EAV datatype used for this type
    # And the last field is a method pointer to a parsing method which takes care of
    # deriving a field value from a string
    #
    # This list is manipulated when new fields are added at runtime via the register_field_type
    # hook.

    TYPE_CHOICES = {
        TYPE_INT:   dict( label='Integer', type=TYPE_INT, db_type=TYPE_INT, xforms_type='integer', parser=None, puller=None, xform_only=False),
        TYPE_FLOAT: dict( label='Decimal', type=TYPE_FLOAT, db_type=TYPE_FLOAT, xforms_type='decimal', parser=None, puller=None, xform_only=False),
        TYPE_TEXT:  dict( label='String', type=TYPE_TEXT, db_type=TYPE_TEXT, xforms_type='string', parser=None, puller=None, xform_only=False),
    }

    xform = models.ForeignKey(XForm, related_name='fields', on_delete=models.CASCADE)
    field_type = models.SlugField(max_length=8, null=True, blank=True)
    command = EavSlugField(max_length=32)
    order = models.IntegerField(default=0)

    objects = models.Manager()
    on_site = CurrentSiteManager()    

    class Meta:
        ordering = ('order', 'id')

    @classmethod
    def register_field_type(cls, field_type, label, parserFunc, db_type=TYPE_TEXT, xforms_type='string', puller=None, xform_only=False):
        """
        Used to register a new field type for XForms.  You can use this method to build new field types that are
        available when building XForms.  These types may just do custom parsing of the SMS text sent in, then stuff
        those results in a normal core datatype, or they may lookup and reference completely custom attributes.

        Refer to GeoPoint implementation to see an example of the latter.

        Arguments are:
           label:       The label used for this field type in the user interface
           field_type:  A slug to identify this field type, must be unique across all field types
           parser:      The function called to turn the raw string into the appropriate type, should take two arguments.
                        Takes two arguments, 'command', which is the command of the field, and 'value' the string value submitted.
           db_type:     How the value will be stored in the database, can be one of: TYPE_INT, TYPE_FLOAT, TYPE_TEXT or TYPE_OBJECT
           xforms_type: The type as defined in an XML xforms specification, likely one of: 'integer', 'decimal' or 'string'
           puller:      A method that can be used to 'pull' the value from an SMS submission.  This can be useful when the value of
                        the field is actually derived from attributes in the message itself.  Note that the string value returned will
                        then be passed off to the parser.
        """
        XFormField.TYPE_CHOICES[field_type] = dict(type=field_type, label=label, 
                                                   db_type=db_type, parser=parserFunc,
                                                   xforms_type=xforms_type, puller=puller, xform_only=xform_only)

    @classmethod
    def lookup_type(cls, otype):
        if otype in XFormField.TYPE_CHOICES:
            return XFormField.TYPE_CHOICES[otype]
        else:
            raise ValidationError("Unable to find parser for field: '%s'" % otype)

    def derive_datatype(self):
        """
        We map our field_type to the appropriate data_type here.
        """
        # set our slug based on our command and keyword
        self.slug = "%s_%s" % (self.xform.get_primary_keyword(), EavSlugField.create_slug_from_name(self.command))
        typedef = self.lookup_type(self.field_type)

        if not typedef:
            raise ValidationError("Field '%s' has an unknown data type: %s" % (self.command, self.datatype))

        self.datatype = typedef['db_type']

    def full_clean(self, exclude=None):
        self.derive_datatype()
        return super(XFormField, self).full_clean(['datatype'])

    def save(self, force_insert=False, force_update=False, using=None):
        self.derive_datatype()
        return super(XFormField, self).save(force_insert, force_update, using)

    def clean_submission(self, value, submission_type, raw=None, connection=None):
        """
        Takes the passed in string value and does two steps:

        1) tries to coerce the value into the appropriate type for the field.  This means changing
        a string to an integer or decimal, or splitting it into two for a gps location.

        2) if the coercion into the appropriate type succeeds, then validates then validates the
        value against any constraints on this field.  

        If either of these steps fails, a ValidationError is raised.  If both are successful
        then the cleaned, Python typed value is returned.
        """
        # this will be our properly Python typed value
        cleaned_value = None

        # check against our type first if we have a value
        if value is not None and len(value) > 0:
            # integer
            if self.field_type == Attribute.TYPE_INT:
                try:
                    cleaned_value = int(value)
                except ValueError:
                    raise ValidationError("+%s parameter must be an even number." % self.command)

            # float
            elif self.field_type == Attribute.TYPE_FLOAT:
                try:
                    cleaned_value = float(value)
                except ValueError:
                    raise ValidationError("+%s parameter must be a number." % self.command)

            # string
            elif self.field_type == Attribute.TYPE_TEXT:
                cleaned_value = value.strip()

            # something custom, pass it to our parser
            else:
                typedef = XFormField.lookup_type(self.field_type)
                cleaned_value = typedef['parser'](self.command, value, raw=raw, connection=connection)

        # now check our actual constraints if any
        for constraint in self.constraints.order_by('order'):
            constraint.validate(value, self.field_type, submission_type)
        
        return cleaned_value

    def xform_type(self):
        """
        Returns the XForms type for the field type.
        """
        typedef = self.lookup_type(self.field_type)
        if typedef:
            return typedef['xforms_type']
        else:
            raise RuntimeError("Field type: '%s' not supported in XForms" % self.field_type)

    def constraints_as_xform(self):
        """
        Returns the attributes for an xform bind element that corresponds to the
        constraints that are present on this field.

        See: http://www.w3.org/TR/xforms11/
        """

        # some examples:
        # <bind nodeset="/data/location" type="geopoint" required="true()"/>
        # <bind nodeset="/data/comment" type="string" constraint="(. &gt;  and . &lt; 100)"/>

        full = ""
        constraints = ""
        delim = ""

        for constraint in self.constraints.all():
            if constraint.type == 'req_val':
                full = "required=\"true()\""

            elif constraint.type == 'min_val':
                constraints += delim + ". &gt;= %s" % constraint.test
                delim = " and "

            elif constraint.type == 'max_val':
                constraints += delim + ". &lt;= %s" % constraint.test
                delim = " and "

            # hack in min and max length using regular expressions
            elif constraint.type == 'min_len':
                constraints += delim + "regex(., '^.{%s,}$')" % constraint.test
                delim = " and "

            elif constraint.type == 'max_len':
                constraints += delim + "regex(., '^.{0,%s}$')" % constraint.test
                delim = " and "
            
            elif constraint.type == 'regex':
                constraints += delim + "regex(., '%s')" % constraint.test
                delim = " and "

        if constraints:
            constraints = " constraint=\"(%s)\"" % constraints

        return "%s %s" % (full, constraints)


    def __unicode__(self): # pragma: no cover
        return self.name
Exemple #11
0
class XForm(models.Model):
    """
    An XForm, which is just a collection of fields.

    XForms also define their keyword which will be used when submitting via SMS.
    """
    PREFIX_CHOICES = (
        ('+','Plus (+)'),
        ('-','Dash (-)'),
    )
    SEPARATOR_CHOICES = (
        (',','Comma (,)'),
        (';','Semicolon (;)'),
        (':','Colon (:)'),
        ('*','Asterisk (*)'),
    )

    name = models.CharField(max_length=32,
                            help_text="Human readable name.")
    keyword = EavSlugField(max_length=32,
                           help_text="The SMS keyword for this form, must be a slug.")
    description = models.TextField(max_length=255,
                               help_text="The purpose of this form.")
    response = models.CharField(max_length=255,
                                help_text="The response sent when this form is successfully submitted.")
    active = models.BooleanField(default=True,
                                 help_text="Inactive forms will not accept new submissions.")

    command_prefix = models.CharField(max_length=1, choices=PREFIX_CHOICES, null=True, blank=True,default='+',
                                      help_text="The prefix required for optional field commands, defaults to '+'")

    keyword_prefix = models.CharField(max_length=1, choices=PREFIX_CHOICES, null=True, blank=True,
                                      help_text="The prefix required for form keywords, defaults to no prefix.")

    separator = models.CharField(max_length=1, choices=SEPARATOR_CHOICES, null=True, blank=True,
                                 help_text="The separator character for fields, field values will be split on this character.")

    restrict_to = models.ManyToManyField(Group, null=True, blank=True,
                                         help_text="Restrict submissions to users of this group (if unset, anybody can submit this form)")
    restrict_message = models.CharField(max_length=160, null=True, blank=True,
                                        help_text="The error message that will be returned to users if they do not have the right privileges to submit this form.  Only required if the field is restricted.")

    owner = models.ForeignKey(User,  on_delete=models.CASCADE)
    created = models.DateTimeField(auto_now_add=True)
    modified = models.DateTimeField(auto_now=True)

    site = models.ForeignKey(Site, on_delete=models.CASCADE)
    objects = models.Manager()
    on_site = CurrentSiteManager()

    unique_together = (("name", "site"), ("keyword", "site"))

    _original_keyword = None

    def __init__(self, *args, **kwargs):
        """
        We overload init so we can keep track of our original keyword.  This is because when
        the user changes the keyword we need to remap all the slugs for our fields as well.
        """
        super(XForm, self).__init__(*args, **kwargs)
        self.__original_keyword = self.get_primary_keyword()

    def get_extra_keywords(self):
        return self.keyword.split()[1:]

    def get_primary_keyword(self):
        return self.keyword.split()[0]

    @classmethod
    def find_form(cls, message):
        """
        This method is responsible for find a matching XForm on this site given the passed in message.

        We do a few things to be agressive in matching the keyword, specifically:
            - if no exact matches are found for the keyword, we tests for levenshtein distance of <= 1
            - we also take into account each form's keyword prefix parameter

        The return value is the matched form, as well as the remainder of the message apart from the keyword
        """

        # first pass, match exactly
        matched = None
        for form in XForm.on_site.filter(active=True):
            remainder = form.parse_keyword(message, False)
            if not remainder is None:
                matched = form
                break

        # found it with an exact match, return it
        if matched:
            return matched

        matched = []
        for form in XForm.on_site.filter(active=True):
            remainder = form.parse_keyword(message, True)
            if remainder:
                matched.append(form)

        # if we found one and only one match using fuzzy matching, return it
        if len(matched) == 1:
            return matched[0]
        else:
            return None

    def parse_keyword(self, message, fuzzy=True):
        """
        Given a message, tries to parse the keyword for the form.  If it matches, then we return
        the remainder of the message, otherwise if the message doesn't start with our keyword then
        we return None
        """
        # remove leading and trailing whitespace
        message = message.strip()

        for keyword in self.keyword.split():

            # empty string case
            if message.lower() == keyword.lower():
                return ""

            # our default regex to match keywords
            regex = "^[^0-9a-z]*([0-9a-z]+)[^0-9a-z](.*)"

            # modify it if there is a keyword prefix
            # with a keyword prefix of '+', we want to match cases like:
            #       +survey,  + survey, ++survey +,survey
            if self.keyword_prefix:
                regex = "^[^0-9a-z]*" + re.escape(self.keyword_prefix) + "+[^0-9a-z]*([0-9a-z]+)[^0-9a-z](.*)"

            # run our regular expression to extract our keyword
            match = re.match(regex, message, re.IGNORECASE)
                
            # if this in a format we don't understand, return nothing
            if not match:
                continue

            # by default only match things that are exact
            target_distance = 0
            if fuzzy:
                target_distance = 1

            first_word = match.group(1)
            if dl_distance(unicode(first_word.lower()), unicode(keyword.lower())) <= target_distance:
                return match.group(2)

        return None

    def update_submission_from_dict(self, submission, values):
        """
        Sets the values for the passed in submission to the passed in dictionary.  The dict
        is expected to have keys corresponding to the commands of the fields.

        Note that the submission will set itself as no longer having any errors and trigger
        the xform_submitted signal

        TODO: I'm kind of putting all real logic in XForm as a base, but maybe this really
        belongs in XFormSubmission where I first had it.
        """
        # first update any existing values, removing them from our dict as we work
        for value in submission.values.all():
            # we have a new value, update it
            field = XFormField.objects.get(pk=value.attribute.pk)
            if field.command in values:
                new_val = values[field.command]
                
                # for binary fields, a value of None means leave the current value
                if field.xform_type() == 'binary' and new_val is None:
                    # clean up values that have null values
                    if value.value is None:
                        value.delete()
                else:
                    value.value = new_val
                    value.save()

                del values[field.command]

            # no new value, we need to remove this one
            else:
                value.delete()
                
        # now add any remaining values in our dict
        for key, value in values.items():
            # look up the field by key
            field = XFormField.objects.get(xform=self, command=key)

            if field.xform_type() != 'binary' or not value is None:
                sub_value = submission.values.create(attribute=field, value=value, entity=submission)

        # clear out our error flag if there were some
        if submission.has_errors:
            submission.has_errors = False
            submission.save()

        # trigger our signal for anybody interested in form submissions
        xform_received.send(sender=self, xform=self, submission=submission)

    def process_odk_submission(self, xml, values, binaries):
        """
        Given the raw XML content and a map of values, processes a new ODK submission, returning the newly
        created submission.

        This mostly just coerces the 4 parameter ODK geo locations to our two parameter ones.
        """
        for field in self.fields.filter(datatype=XFormField.TYPE_OBJECT):
            if field.command in values:
                typedef = XFormField.lookup_type(field.field_type)

                # binary fields (image, audio, video) will have their value set in the XML to the name
                # of the key that contains their binary content
                if typedef['xforms_type'] == 'binary':
                    binary_key = values[field.command]
                    values[field.command] = typedef['parser'](field.command, binaries[binary_key], filename=binary_key, raw=None, connection=None)

                else:
                    values[field.command] = typedef['parser'](field.command, values[field.command], raw=None, connection=None)

        # create our submission now
        submission = self.submissions.create(type='odk-www', raw=xml)
        self.update_submission_from_dict(submission, values)

        return submission

    def process_import_submission(self, raw, connection, values):
        """
        Given a dict of values and the original row, import the data
        as a submission. Validates against contraints including checking
        for required fields.  Raises ValidationError if supplied could
        not be processed.
        """
        fields = self.fields.all()
        for field in fields:
            # pass it through our cleansing filter which will
            # check for type and any constraint validation
            # this raises ValidationError if it fails
            if field.command in values:
                field.clean_submission(values[field.command], TYPE_IMPORT)
            else:
                field.clean_submission(None, TYPE_IMPORT)

        # check if the values contain extra fields not in our form
        for command, value in values.items():            
            fields = self.fields.filter(command=command)
            if len(fields) != 1 and command != "connection":
                raise ValidationError("Could not resolve field for %s" % command)

        # resolve object types to their actual objects
        for field in self.fields.filter(datatype=XFormField.TYPE_OBJECT):
            if field.command in values:
                typedef = XFormField.lookup_type(field.field_type)
                values[field.command] = typedef['parser'](field.command, values[field.command], raw=raw, connection=connection)

        # create submission and update the values
        submission = self.submissions.create(type=TYPE_IMPORT, raw=raw, connection=connection)
        self.update_submission_from_dict(submission, values)
        return submission

    def is_command(self, segment, commands):
        """
        Given a segment and commands dict, it checks if segment contains a command
        the we return that command if it's true, otherwise None
        """
        # no comamnd prefix, check whether this is a command word
        if not self.command_prefix:
            segment_word = segment
      
            if segment.find(' ') >= 0:
                # split the segment on spaces
                (segment_word, rest) = segment.split(' ', 1)
            
            if segment_word.lower() in commands:
                return segment_word

        # we do have a command prefix, and this word begins with it
        elif segment.startswith(self.command_prefix):
            return segment

        return None

    def parse_sms_submission(self, message_obj):
        """
        sms submissions can have two formats, either explicitely marking each field:
            <keyword> +field_command1 [values] +field_command2 [values]
        
        or ommitting the 'command' for all required fields:
            <keyword> [first required field] [second required field] +field_command3 [first optional field]
        
        Note that if you are using the no-delimeter form, then all string fields are 'assumed' to be
        a single word.  TODO: this could probably be made to be smarter
        """
        message = message_obj.text

        # we'll return a hash of values and/or errors
        submission = {}
        values = []
        errors = []

        submission['values'] = values
        submission['message'] = message
        submission['has_errors'] = True
        submission['errors'] = errors

        submission_type = TYPE_SMS

        # parse out our keyword
        remainder = self.parse_keyword(message)
        if remainder is None:
            errors.append(ValidationError("Incorrect keyword.  Keyword must be '%s'" % self.get_primary_keyword()))
            submission['response'] = "Incorrect keyword.  Keyword must be '%s'" % self.get_primary_keyword()
            return submission

        # separator mode means we don't split on spaces
        separator = None

        # if the separator is some non-whitespace character, and this form has only
        # one text-type field, the entire remainder can be considered its [command]-value pair
        if self.separator and self.separator.strip() and self.fields.count() == 1 and self.fields.all()[0].field_type == XFormField.TYPE_TEXT:
            segments = [remainder,]
        else:
            # figure out if we are using separators
            if self.separator and message.find(self.separator) >= 0:
                separator = self.separator

            # so first let's split on the separator passed in
            segments = remainder.split(separator)

        # remove any segments that are empty
        stripped_segments = []
        for segment in segments:
            segment = segment.strip()
            if len(segment):
                # also split any of these up by prefix, we could have a segment at this point
                # which looks like this:
                #      asdf+name emile+age 10
                # and which we want to turn it into three segments of:
                #      ['asdf', '+name emile', '+age 10']
                #
                if self.command_prefix:
                    prefix_index = segment.find(self.command_prefix)
                    while prefix_index > 0:
                        # take the left and right sides of the prefix
                        left = segment[0:prefix_index]
                        right = segment[prefix_index:]
                    
                        # append our left side to our list of stripped segments
                        stripped_segments.append(left.strip())
                    
                        # make the right side our new segment
                        segment = right.strip()
                    
                        # and see if it is worthy of stripping
                        prefix_index = segment.find(self.command_prefix)

                if len(segment):
                    stripped_segments.append(segment)

        segments = stripped_segments
        
        # build a dict of all our commands
        commands = dict()
        for field in self.fields.all():
            commands[field.command.lower()] = field

        # we first try to deal with required fields... we skip out of this processing either when
        # we reach a command prefix (+) or we have processed all required fields
        for field in self.fields.all().order_by('order', 'id'):
            # lookup our field type
            field_type = XFormField.lookup_type(field.field_type)

            # no more text in the message, break out
            if not segments:
                break

            # this field is pulled, not pushed, ignore
            if not field_type['puller'] is None:
                continue

            segment = segments.pop(0)

            # if this segment is a command
            if self.is_command(segment, commands):
                # pop it back on and break
                segments.insert(0, segment)
                break

            # ok, this looks like a valid required field value, clean it up
            try:
                cleaned = field.clean_submission(segment, submission_type, raw=message, connection=message_obj.connection)
                values.append(dict(name=field.command, value=cleaned))
            except ValidationError as err:
                errors.append(err)
                break
            
        # for any remaining segments, deal with them as command / value pairings
        while segments:
            # search for our command
            command = None
            while segments:
                segment = segments.pop(0)

                # if this segment contains a command, set the segment as our command and 
                # parse this segment
                if self.is_command(segment, commands):
                    command = segment
                    break

            # no command found?  break out
            if not command:
                break

            # if there is a prefix, strip it off
            command = command.lower()
            if self.command_prefix:
                # strip off any leading junk from the command, cases that should work:
                #    ++age, +-age, +.age +++age
                match = re.match("^([^0-9a-z]*)(.*)$", command)
                command = match.group(2)

            # if there are spaces in the command, then split on that space, commands can't have spaces
            # so everything past the space must be part of the value
            if command.find(' ') >= 0:
                (command, value) = command.split(' ', 1)
                segments.insert(0, value)

            # now read in the value, basically up to the next command, joining segments
            # with spaces
            value = None
            while segments:
                segment = segments.pop(0)
                if self.is_command(segment, commands):
                    segments.insert(0, segment)
                    break

                # this isn't a command, but rather part of the value
                if not value:
                    value = segment
                else:
                    value += ' ' + segment

            # find the corresponding field, check its value and save it if it passes
            for field in self.fields.all():
                if field.command == command:
                    found = True

                    try:
                        cleaned = field.clean_submission(value, submission_type, raw=message, connection=message_obj.connection)
                        values.append(dict(name=field.command, value=cleaned))        
                    except ValidationError as err:
                        errors.append(err)

        # now do any pull parsing
        for field in self.fields.all():
            typedef = XFormField.lookup_type(field.field_type)

            # if this is a pull field
            if typedef['puller']:
                try:
                    # try to parse it out
                    value = typedef['puller'](field.command, message_obj)
                    if not value is None:
                        cleaned = field.clean_submission(value, submission_type, raw=message, connection=message_obj.connection)
                        values.append(dict(name=field.command, value=cleaned))
                except ValidationError as err:
                    errors.append(err)

        # build a map of the number of values we have for each included field
        # TODO: for the love of god, someone show me how to do this in one beautiful Python 
        # lambda, just don't have time now
        value_count = {}
        value_dict = {}

        # NOTE: we iterate over a copy of the list since we'll be modifying our original list in the case of dupes
        for value_pair in list(values):
            name = value_pair['name']

            # if we already have a value for this field
            if name in value_count:
                # add an error and remove the duplicate
                errors.append(ValidationError("Expected one value for %s, more than one was given." % name))
                values.remove(value_pair)                
            else:
                value_count[name] = 1
                value_dict[name] = value_pair['value']

        # do basic sanity checks over all fields
        for field in self.fields.all():
            # check that all required fields had a value set
            required_const = field.constraints.all().filter(type="req_val")
            if required_const and field.command not in value_count:
                try:
                    required_const[0].validate(None, field.field_type, submission_type)
                except ValidationError as e:
                    errors.append(e)

            # check that all fields actually have values
            if field.command in value_dict and value_dict[field.command] is None:
                errors.append(ValidationError("Expected a value for %s, none given." % field.name))

        # no errors?  wahoo
        if not errors:
            submission['has_errors'] = False
            submission['response'] = self.response

        # boo!
        else:
            error = submission['errors'][0]
            if getattr(error, 'messages', None):
                submission['response'] = str(error.messages[0])
            else:
                submission['response'] = str(error)
            submission['has_errors'] = True

        return submission

    def build_template_vars(self, submission, sub_dict):
        """
        Given a submission builds the dict of values that will be available in the template.
        """
        template_vars = dict(confirmation_id=submission.confirmation_id)
        for field_value in sub_dict['values']:
            template_vars[field_value['name']] = field_value['value']

        return template_vars

    @classmethod
    def render_response(cls, response, template_vars):
        """
        Given a template string a dictionary of values, tries to compile the template and evaluate it.
        """
        # build our template
        template = Template("{% load messages %}" + response)

        context = Context(template_vars)
        return template.render(context)

    def process_sms_submission(self, message_obj):
        """
        Given an incoming SMS message, will create a new submission.  If there is an error
        we will throw with the appropriate error message.
        
        The newly created submission object will be returned.
        """
        message = message_obj.text
        connection = message_obj.connection

        # parse our submission
        sub_dict = self.parse_sms_submission(message_obj)

        # create our new submission, we'll add field values as we parse them
        submission = XFormSubmission(xform=self, type='sms', raw=message, connection=connection)
        submission.save()

        # build our template response
        template_vars = self.build_template_vars(submission, sub_dict)
        sub_dict['response'] = XForm.render_response(sub_dict['response'], template_vars)

        # save our template vars as a temporary value in our submission, this can be used by the
        # signal later on
        submission.template_vars = template_vars

        # the values we've pulled out
        values = {}

        # for each of our values create the attribute
        for field_value in sub_dict['values']:
            field = XFormField.on_site.get(xform=self, command=field_value['name'])
            submission.values.create(attribute=field, value=field_value['value'], entity=submission)

        # if we had errors
        if sub_dict['has_errors']:
            # stuff them as a transient variable in our submission, our caller may message back
            submission.errors = sub_dict['errors']
            
            # and set our db state as well
            submission.has_errors = True
            submission.save()

        # set our transient response
        submission.response = sub_dict['response']

        # do they have the right permissions?
        user = lookup_user_by_connection(connection)
        if not self.does_user_have_permission(user):
            submission.has_errors = True
            submission.save()

            submission.response = self.restrict_message

        # trigger our signal
        xform_received.send(sender=self, xform=self, submission=submission, message=message_obj)

        return submission

    def does_user_have_permission(self, user):
        """
        Does the passed in user have permission to submit / view this form?
        """
        # is there a custom permission checker?
        custom_checker = getattr(settings, 'XFORMS_AUTHENTICATION_CHECKER', 'rapidsms_xforms.models.can_user_use_form')
        if custom_checker:
            checker_func = import_from_string(custom_checker)
            return checker_func(user, self)

        return True

    def check_template(self, template):
        """
        Tries to compile and render our template to make sure it passes.
        """
        try:
            t = Template("{% load messages %}" + template)

            # we build a context that has dummy values for all required fields
            context = {}
            context['confirmation_id'] = 1
            for field in self.fields.all():
                required = field.constraints.all().filter(type="req_val")

                # we are at a field that isn't required?  pop out, these will be dealt with 
                # in the next block
                if not required:
                    continue

                if field.field_type == XFormField.TYPE_INT or field.field_type == XFormField.TYPE_FLOAT:
                    context[field.command] = "1"
                else:
                    context[field.command] = "test"

            t.render(Context(context))
        except Exception as e:
            raise ValidationError(str(e))

    def full_clean(self, exclude=None):
        self.check_template(self.response)
        return super(XForm, self).full_clean(exclude)

    def save(self, force_insert=False, force_update=False, using=None):
        """
        On saves we check to see if the keyword has changed, if so loading all our fields
        and resaving them to update their slugs.
        """
        self.check_template(self.response)

        super(XForm, self).save(force_insert, force_update, using)

        # keyword has changed, load all our fields and update their slugs
        # TODO, damnit, is this worth it?
        if self.get_primary_keyword() != self.__original_keyword:
            for field in self.fields.all():
                field.save(force_update=True, using=using)
                        
    def __unicode__(self): # pragma: no cover
        return self.name
Exemple #12
0
    self.children = qs.select_related('content_type', 'user')[:self.limit]
    if not len(self.children):
        self.pre_content = _('No recent actions.')
    self._initialized = True


SiteAdmin.fieldsets = fieldsets = ((None, {
    'fields': ['domain', 'name', 'site_cutting', 'price_field_slugs', 'obl'] +
    ['country_' + lang[0] for lang in settings.LANGUAGES] +
    ['company_' + lang[0] for lang in settings.LANGUAGES]
}), )
# add shortning to Site model. For auto selecting site-dependent information
Site.add_to_class(
    'site_cutting',
    EavSlugField(_('Site cutting'),
                 help_text=_("Short unique site identifier."),
                 max_length=14,
                 unique=True))
Site.add_to_class(
    'price_field_slugs',
    CharField(
        _('Price field attrs'),
        help_text=
        _("Attributes of price fields for this site, separated with comma-space."
          ),
        max_length=200))
Site.add_to_class(
    'company',
    CharField(
        _('Site company'),
        help_text=_(
            'Site company will show in header country-company selection'),
Exemple #13
0
class ImageSpecModel(models.Model):
    name = EavSlugField(
        max_length=30,
        help_text=
        _("Image specifications profiles are used for automatic image generation in templates."
          ),
        verbose_name=_("Profile name"))
    specs = JSONField(verbose_name=_("Options"),
                      default="{}",
                      blank=True,
                      help_text=_("Options of image convertation."))

    class Meta:
        verbose_name = _('image specification profile')
        verbose_name_plural = _('Image specification profiles')
        ordering = ['name']

    def __init__(self, *args, **kwargs):
        super(ImageSpecModel, self).__init__(*args, **kwargs)
        self.name_before_save = self.name

    def get_spec_dict(self):
        #put a dictionary of options
        #for list of options look her https://github.com/jdriscoll/django-imagekit
        #example: {
        #          "processors":"[]",
        #          "format"="JPEG",
        #          "options"={"quality":90}
        #          }
        #
        image_specs = deepcopy(self.specs)

        if 'processors' in image_specs:
            image_specs['processors'] = eval(image_specs['processors'])
        else:
            image_specs['processors'] = []

        if 'format' not in image_specs:
            image_specs['format'] = "JPEG"

        if 'options' not in image_specs:
            image_specs['options'] = {}

        return image_specs

    def get_image_spec_class(self):
        spec_dict = self.get_spec_dict()

        def spec_creater(specs):
            class Specs(ImageSpec):
                processors = specs['processors']
                format = specs['format']
                options = specs['options']

            return Specs

        return spec_creater(spec_dict)

    def save(self, *args, **kwargs):
        if self.name_before_save in generator_registry._generators:
            generator_registry.unregister(self.name_before_save)

        super(ImageSpecModel, self).save(*args, **kwargs)
        generator_registry.register(self.name, self.get_image_spec_class())
        self.name_before_save = self.name

    def __unicode__(self):
        return self.name
Exemple #14
0
class Product(models.Model):
    name_ru = models.CharField(max_length=50,
                               verbose_name=_('Name (ru)'),
                               unique=True)
    name_en = models.CharField(max_length=50,
                               verbose_name=_('Name (en)'),
                               unique=True)
    slug = EavSlugField(max_length=60,
                        verbose_name=_('Slug'),
                        help_text=_("Short unique product label."),
                        unique=True)
    product_type = models.ForeignKey(
        ProductType,
        verbose_name=_("Product type"),
        help_text=_("Product type assigns some data fields to products."),
        null=True)
    additional_fields = models.ManyToManyField(
        Attribute, verbose_name=_("Additional fields"), blank=True)
    product_tags = models.ManyToManyField(
        ProductTag,
        verbose_name=_("Product tags"),
        help_text=_("Tags are used for quick searching for products."),
        blank=True)
    active = models.BooleanField(
        default=True,
        verbose_name=_('Active'),
        help_text=_("If product is deactivated - it doesn't shown anywhere."))
    position_in_list = models.IntegerField(verbose_name=_('Position in type'))
    date_added = models.DateTimeField(auto_now_add=True,
                                      verbose_name=_('Date added'))
    last_modified = models.DateTimeField(auto_now=True,
                                         verbose_name=_('Last modified'))

    def get_prices_for_list(self):

        return self.product_set.count()

    get_prices_for_list.short_description = _("Tagged producs")

    #TODO make name unique, make slug field - python slug, mayby slug readonly

    class Meta:
        verbose_name = _('product')
        verbose_name_plural = _('Products')
        ordering = ['position_in_list']

    def save(self, *args, **kwargs):
        super(Product, self).save(*args, **kwargs)
        self.product_type.changed = datetime.now()
        self.product_type.save()
        if self.product_type.slug == "accessoires":
            try:
                for pk in self.type_accessoires:
                    try:
                        type = ProductType.objects.get(pk=pk)
                        type.changed = datetime.now()
                        type.save()
                    except:
                        pass
            except TypeError:
                pass

    def __unicode__(self):
        current_lang = get_language()
        if current_lang == 'ru':
            return self.name_ru
        else:
            return self.name_en

    def get_secondary_attributes(self):
        '''get activated product EAV attributes'''
        try:
            if self.product_type:
                attrs = Attribute.objects.filter(pk__in=list(
                    self.product_type.fields.values_list('pk', flat=True)
                ) + list(self.additional_fields.values_list('pk', flat=True)))
            else:
                attrs = self.additional_fields.all()
            attrs = attrs.order_by('importance').reverse()
        except ValueError:
            attrs = Attribute.objects.none()

        return attrs

    # if site - sites prices, no site - auto, if indexes - index price, if no - index 0, if price slug - then price slug
    def price(self, site=None, price_index=None, price_slug=None, lang=None):
        ''' get price field depending on site options and lang'''
        res = ''
        if not price_slug:
            if site:
                if price_index:
                    price_slug = Site.objects.get(
                        site_cutting=site).price_field_slugs.split(
                            ', ')[price_index]
                else:
                    price_slug = Site.objects.get(
                        site_cutting=site).price_field_slugs.split(', ')[0]
            else:
                if price_index:
                    price_slug = Site.objects.get_current(
                    ).price_field_slugs.split(', ')[price_index]
                else:
                    price_slug = Site.objects.get_current(
                    ).price_field_slugs.split(', ')[0]

        try:
            field = Attribute.objects.get(slug=price_slug)
            try:
                if 'price_field' in field.options and 'dependent' in field.options[
                        'price_field']:
                    res = getattr(
                        self, str(field.options['price_field']['dependent']))
                    try:
                        res = round(
                            float(res) /
                            float(field.options['price_field']['rate']), 2)
                        if abs(res - round(res)) < 0.001:
                            res = int(res)
                    except ValueError:
                        pass
                    res = unicode(res)

            except TypeError:
                pass

            if not res:
                res = unicode(getattr(self, price_slug))

            _(u'in developed')
            _(u'ask for price')

            if lang == None:
                return _(res)

            else:
                with translation.override(lang):
                    trans_res = _(res)
                return trans_res

        except AttributeError:
            return ''

    def get_description(self, product_type=None):
        if product_type:
            return mark_safe(
                self.descr_m.replace('{}',
                                     unicode(product_type.type_description)))
        return mark_safe(
            self.descr_m.replace('{}',
                                 unicode(self.product_type.type_description)))

    def get_description_left(self, product_type=None):
        if product_type:
            return mark_safe(
                self.descr_left_m.replace(
                    '{}', unicode(product_type.type_description_left)))
        return mark_safe(
            self.descr_left_m.replace(
                '{}', unicode(self.product_type.type_description_left)))

    #----for direct EAV attr access
    def __getattr__(self, attr):
        current_lang = get_language()
        lang_attr = attr + '_' + current_lang

        if lang_attr in self.__dict__:
            return self.__dict__[lang_attr]

        for lang in get_fallback_languages(current_lang):
            lang_attr = attr + '_' + lang
            if lang_attr in self.__dict__:
                return self.__dict__[lang_attr]

        # secondary_attrs=self.get_secondary_attributes()
        # if attr in secondary_attrs:
        #     return getattr(self.eav, attr)
        #
        # lang_attr=attr+'_'+current_lang
        # if lang_attr in secondary_attrs:
        #         return getattr(self.eav, lang_attr)
        #
        # for lang in get_fallback_languages(current_lang):
        #     lang_attr=attr+'_'+lang
        #     if lang_attr in secondary_attrs:
        #         return getattr(self.eav, lang_attr)
        if attr == '_position_in_list_cache':  #for position field
            raise AttributeError
        if attr[-2:] == '_m':
            lang_attr = attr[:-2] + '_' + current_lang
            try:
                return getattr(self.eav, lang_attr)
            except AttributeError:
                for lang in get_fallback_languages(current_lang):
                    lang_attr = attr[:-2] + '_' + lang
                    try:
                        return getattr(self.eav, lang_attr)
                    except AttributeError:
                        pass
                return ''
        else:
            return getattr(self.eav, attr)

    #----for export_import attribute import
    def set_attr(self, attr, value):
        #Todo think, rewrite
        try:
            getattr(self.__dict__['eav'], attr)
            if self.get_secondary_attributes().filter(slug=attr).count() != 0:
                if (str(value) != '-' and str(value) != ''):
                    setattr(self.__dict__['eav'], attr, value)
                elif str(value) == '-' or str(value) == '':
                    try:
                        self.additional_fields.remove(
                            Attribute.objects.get(slug=attr))
                    except:
                        pass
            else:
                if str(value) != '-' and str(value) != '':
                    self.additional_fields.add(
                        Attribute.objects.get(slug=attr))
                    setattr(self.__dict__['eav'], attr, value)

        except:
            self.__dict__[attr] = value
Exemple #15
0
class Chunk(models.Model):
    class Meta:
            verbose_name = _('Chunk for price templates')
            verbose_name_plural = _('Chunks for price templates')
    slug = EavSlugField(_("Chunk slug"), max_length=50, unique=True, help_text=_("Put this slug with ctrl-f2 in odt template as simple odt template var"))
Exemple #16
0
class Attribute(models.Model):
    """
    Putting the **A** in *EAV*. This holds the attributes, or concepts.
    Examples of possible *Attributes*: color, height, weight, number of
    children, number of patients, has fever?, etc...

    Each attribute has a name, and a description, along with a slug that must
    be unique.  If you don't provide a slug, a default slug (derived from
    name), will be created.

    The *required* field is a boolean that indicates whether this EAV attribute
    is required for entities to which it applies. It defaults to *False*.

    .. warning::
       Just like a normal model field that is required, you will not be able
       to save or create any entity object for which this attribute applies,
       without first setting this EAV attribute.

    There are 7 possible values for datatype:

        * int (TYPE_INT)
        * float (TYPE_FLOAT)
        * text (TYPE_TEXT)
        * date (TYPE_DATE)
        * bool (TYPE_BOOLEAN)
        * object (TYPE_OBJECT)
        * enum (TYPE_ENUM)
        * json (TYPE_JSON)
        * csv (TYPE_CSV)


    Examples::

        Attribute.objects.create(name='Height', datatype=Attribute.TYPE_INT)
        # = <Attribute: Height (Integer)>

        Attribute.objects.create(name='Color', datatype=Attribute.TYPE_TEXT)
        # = <Attribute: Color (Text)>

        yes = EnumValue.objects.create(value='yes')
        no = EnumValue.objects.create(value='no')
        unknown = EnumValue.objects.create(value='unknown')
        ynu = EnumGroup.objects.create(name='Yes / No / Unknown')
        ynu.values.add(yes, no, unknown)

        Attribute.objects.create(name='has fever?', datatype=Attribute.TYPE_ENUM, enum_group=ynu)
        # = <Attribute: has fever? (Multiple Choice)>

    .. warning:: Once an Attribute has been used by an entity, you can not
                 change it's datatype.
    """

    class Meta:
        ordering = ['name']

    TYPE_TEXT = 'text'
    TYPE_FLOAT = 'float'
    TYPE_INT = 'int'
    TYPE_DATE = 'date'
    TYPE_BOOLEAN = 'bool'
    TYPE_OBJECT = 'object'
    TYPE_ENUM = 'enum'
    TYPE_JSON = 'json'
    TYPE_CSV = 'csv'

    DATATYPE_CHOICES = (
        (TYPE_TEXT, _('Text')),
        (TYPE_DATE, _('Date')),
        (TYPE_FLOAT, _('Float')),
        (TYPE_INT, _('Integer')),
        (TYPE_BOOLEAN, _('True / False')),
        (TYPE_OBJECT, _('Django Object')),
        (TYPE_ENUM, _('Multiple Choice')),
        (TYPE_JSON, _('JSON Object')),
        (TYPE_CSV, _('Comma-Separated-Value')),
    )

    # Core attributes

    datatype = EavDatatypeField(
        verbose_name=_('Data Type'), choices=DATATYPE_CHOICES, max_length=6
    )

    name = models.CharField(
        verbose_name=_('Name'),
        max_length=100,
        help_text=_('User-friendly attribute name'),
    )

    """
    Main identifer for the attribute.
    Upon creation, slug is autogenerated from the name.
    (see :meth:`~eav.fields.EavSlugField.create_slug_from_name`).
    """
    slug = EavSlugField(
        verbose_name=_('Slug'),
        max_length=50,
        db_index=True,
        unique=True,
        help_text=_('Short unique attribute label'),
    )

    """
    .. warning::
        This attribute should be used with caution. Setting this to *True*
        means that *all* entities that *can* have this attribute will
        be required to have a value for it.
    """
    required = models.BooleanField(verbose_name=_('Required'), default=False)

    entity_ct = models.ManyToManyField(ContentType, blank=True)
    """
    This field allows you to specify a relationship with any number of content types.
    This would be useful, for example, if you wanted an attribute to apply only to
    a subset of entities. In that case, you could filter by content type in the
    :meth:`~eav.registry.EavConfig.get_attributes` method of that entity's config.
    """

    enum_group = models.ForeignKey(
        EnumGroup,
        verbose_name=_('Choice Group'),
        on_delete=models.PROTECT,
        blank=True,
        null=True,
    )

    description = models.CharField(
        verbose_name=_('Description'),
        max_length=256,
        blank=True,
        null=True,
        help_text=_('Short description'),
    )

    # Useful meta-information

    display_order = models.PositiveIntegerField(
        verbose_name=_('Display order'), default=1
    )

    modified = models.DateTimeField(verbose_name=_('Modified'), auto_now=True)

    created = models.DateTimeField(
        verbose_name=_('Created'), default=timezone.now, editable=False
    )

    @property
    def help_text(self):
        return self.description

    def get_validators(self):
        """
        Returns the appropriate validator function from :mod:`~eav.validators`
        as a list (of length one) for the datatype.

        .. note::
           The reason it returns it as a list, is eventually we may want this
           method to look elsewhere for additional attribute specific
           validators to return as well as the default, built-in one.
        """
        DATATYPE_VALIDATORS = {
            'text': validate_text,
            'float': validate_float,
            'int': validate_int,
            'date': validate_date,
            'bool': validate_bool,
            'object': validate_object,
            'enum': validate_enum,
            'json': validate_json,
            'csv': validate_csv,
        }

        return [DATATYPE_VALIDATORS[self.datatype]]

    def validate_value(self, value):
        """
        Check *value* against the validators returned by
        :meth:`get_validators` for this attribute.
        """
        for validator in self.get_validators():
            validator(value)

        if self.datatype == self.TYPE_ENUM:
            if isinstance(value, EnumValue):
                value = value.value
            if not self.enum_group.values.filter(value=value).exists():
                raise ValidationError(
                    _('%(val)s is not a valid choice for %(attr)s')
                    % dict(val=value, attr=self)
                )

    def save(self, *args, **kwargs):
        """
        Saves the Attribute and auto-generates a slug field
        if one wasn't provided.
        """
        if not self.slug:
            self.slug = EavSlugField.create_slug_from_name(self.name)

        self.full_clean()
        super(Attribute, self).save(*args, **kwargs)

    def clean(self):
        """
        Validates the attribute.  Will raise ``ValidationError`` if the
        attribute's datatype is *TYPE_ENUM* and enum_group is not set, or if
        the attribute is not *TYPE_ENUM* and the enum group is set.
        """
        if self.datatype == self.TYPE_ENUM and not self.enum_group:
            raise ValidationError(
                _('You must set the choice group for multiple choice attributes')
            )

        if self.datatype != self.TYPE_ENUM and self.enum_group:
            raise ValidationError(
                _('You can only assign a choice group to multiple choice attributes')
            )

    def get_choices(self):
        """
        Returns a query set of :class:`EnumValue` objects for this attribute.
        Returns None if the datatype of this attribute is not *TYPE_ENUM*.
        """
        return (
            self.enum_group.values.all()
            if self.datatype == Attribute.TYPE_ENUM
            else None
        )

    def save_value(self, entity, value):
        """
        Called with *entity*, any Django object registered with eav, and
        *value*, the :class:`Value` this attribute for *entity* should
        be set to.

        If a :class:`Value` object for this *entity* and attribute doesn't
        exist, one will be created.

        .. note::
           If *value* is None and a :class:`Value` object exists for this
           Attribute and *entity*, it will delete that :class:`Value` object.
        """
        ct = ContentType.objects.get_for_model(entity)

        entity_filter = {
            'entity_ct': ct,
            'attribute': self,
            '{0}'.format(get_entity_pk_type(entity)): entity.pk,
        }

        try:
            value_obj = self.value_set.get(**entity_filter)
        except Value.DoesNotExist:
            if value == None or value == '':
                return

            value_obj = Value.objects.create(**entity_filter)

        if value == None or value == '':
            value_obj.delete()
            return

        if value != value_obj.value:
            value_obj.value = value
            value_obj.save()

    def __str__(self):
        return '{} ({})'.format(self.name, self.get_datatype_display())