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')
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
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
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 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)
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))
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
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="(. > and . < 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 + ". >= %s" % constraint.test delim = " and " elif constraint.type == 'max_val': constraints += delim + ". <= %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
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
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'),
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
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
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"))
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())