def clean(self, value, single=False): """ Clean the form value If optional argument ``single`` is True, the returned value will be a string containing the tag name. If ``single`` is False (default), the returned value will be a list of strings containing tag names. """ value = super(BaseTagField, self).clean(value) if self.tag_options.force_lowercase: value = value.lower() if single: return value try: return parse_tags( value, self.tag_options.max_count, self.tag_options.space_delimiter, ) except ValueError as e: raise forms.ValidationError(_('%s' % e))
def test_quotes_comma_delim_spaces_ignored(self): tags = tag_utils.parse_tags( '"adam, one" , "brian, two" , "chris, three"') self.assertEqual(len(tags), 3) self.assertEqual(tags[0], "adam, one") self.assertEqual(tags[1], "brian, two") self.assertEqual(tags[2], "chris, three")
def test_quotes_dont_close(self): tags = tag_utils.parse_tags('"adam,one","brian,two","chris, dave') self.assertEqual(len(tags), 3) # Will be sorted, " comes first self.assertEqual(tags[0], '"chris, dave') self.assertEqual(tags[1], "adam,one") self.assertEqual(tags[2], "brian,two")
def test_quotes_escaped_late(self): """ Tests quotes when delimiter switches to comma """ tags = tag_utils.parse_tags('""adam"" brian"", chris') self.assertEqual(len(tags), 2) self.assertEqual(tags[0], '"adam" brian"') self.assertEqual(tags[1], "chris")
def test_parse_renders_tags(self): tagstr = "adam, brian, chris" tags = tag_utils.parse_tags(tagstr) tagstr2 = tag_utils.render_tags(tags) self.assertEqual(len(tags), 3) self.assertEqual(tags[0], "adam") self.assertEqual(tags[1], "brian") self.assertEqual(tags[2], "chris") self.assertEqual(tagstr, tagstr2)
def test_parse_renders_tags_complex(self): tagstr = '"""adam brian"", ""chris, dave", "ed, frank", gary' tags = tag_utils.parse_tags(tagstr) tagstr2 = tag_utils.render_tags(tags) self.assertEqual(len(tags), 3) self.assertEqual(tags[0], '"adam brian", "chris, dave') self.assertEqual(tags[1], "ed, frank") self.assertEqual(tags[2], "gary") self.assertEqual(tagstr, tagstr2)
def test_quotes_escaped(self): """ Tests quotes when delimiter is already comma """ tags = tag_utils.parse_tags('adam, br""ian, ""chris, dave""') self.assertEqual(len(tags), 4) self.assertEqual(tags[0], '"chris') self.assertEqual(tags[1], "adam") self.assertEqual(tags[2], 'br"ian') self.assertEqual(tags[3], 'dave"')
def set_tag_string(self, tag_string): """ Sets the tags for this instance, given a tag edit string """ # Get all tag names tag_names = parse_tags( tag_string, space_delimiter=self.tag_options.space_delimiter) # Pass on to set_tag_list return self.set_tag_list(tag_names)
def set_tag_string(self, tag_string): """ Sets the tags for this instance, given a tag edit string """ # Get all tag names tag_names = parse_tags( tag_string, space_delimiter=self.tag_options.space_delimiter, ) # Pass on to set_tag_list return self.set_tag_list(tag_names)
def __eq__(self, other): """ Compare a tag string or iterable of tags to the tags on this manager """ # If not case sensitive, or lowercase forced, compare on lowercase lower = False if self.tag_options.force_lowercase or not self.tag_options.case_sensitive: lower = True # Prep other argument we're comparing against if isinstance(other, BaseTagRelatedManager): other = other.tags if isinstance(other, six.string_types): other_str = six.text_type(other) # Enforce case non-sensitivity or lowercase if lower: other_str = other_str.lower() # Parse other_str into list of tags other_tags = parse_tags( other_str, space_delimiter=self.tag_options.space_delimiter, ) else: # Assume it's an iterable other_tags = other if lower: other_tags = [six.text_type(tag).lower() for tag in other] # Get list of set tags self_tags = self.get_tag_list() # Compare tag count if len(other_tags) != len(self_tags): return False # Compare tags for tag in self_tags: # If lowercase or not case sensitive, lower for comparison if lower: tag = tag.lower() # Check tag in other tags if tag not in other_tags: return False # Same number of tags, and all self tags present in other tags # It's a match return True
def _prep_merge_tags(self, tags): """ Ensure tags argument for merge_tags is an iterable of tag objects, excluding self """ # Ensure tags is a list of tag instances if isinstance(tags, six.string_types): tags = utils.parse_tags( tags, space_delimiter=self.tag_options.space_delimiter) if not isinstance(tags, models.query.QuerySet): tags = self.tag_model.objects.filter(name__in=tags) # Make sure self isn't in tags return tags.exclude(pk=self.pk)
def _prep_merge_tags(self, tags): """ Ensure tags argument for merge_tags is an iterable of tag objects, excluding self """ # Ensure tags is a list of tag instances if isinstance(tags, six.string_types): tags = utils.parse_tags( tags, space_delimiter=self.tag_options.space_delimiter, ) if not isinstance(tags, models.query.QuerySet): tags = self.tag_model.objects.filter(name__in=tags) # Make sure self isn't in tags return tags.exclude(pk=self.pk)
def __setattr__(self, name, value): """ Only allow an option to be set if it's valid """ if name == 'initial': # Store as a list of strings, with the tag string available on # initial_string for migrations if value is None: self.__dict__['initial_string'] = '' self.__dict__['initial'] = [] elif isinstance(value, six.string_types): self.__dict__['initial_string'] = value self.__dict__['initial'] = parse_tags(value) else: self.__dict__['initial_string'] = render_tags(value) self.__dict__['initial'] = value elif name in constants.OPTION_DEFAULTS: self.__dict__[name] = value else: raise AttributeError(name)
def test_spaces_false_mixed(self): tags = tag_utils.parse_tags("adam,brian chris", space_delimiter=False) self.assertEqual(len(tags), 2) self.assertEqual(tags[0], "adam") self.assertEqual(tags[1], "brian chris")
def test_spaces_false_spaces(self): tags = tag_utils.parse_tags("adam brian chris", space_delimiter=False) self.assertEqual(len(tags), 1) self.assertEqual(tags[0], "adam brian chris")
def test_spaces_false_commas(self): tags = tag_utils.parse_tags("adam,brian,chris", space_delimiter=False) self.assertEqual(len(tags), 3) self.assertEqual(tags[0], "adam") self.assertEqual(tags[1], "brian") self.assertEqual(tags[2], "chris")
def _filter_or_exclude(self, negate, *args, **kwargs): """ Custom lookups for tag fields """ # TODO: When minimum supported Django 1.7+, this can be replaced with # custom lookups, which would work much better anyway. safe_fields, singletag_fields, tag_fields, field_lookup = _split_kwargs( self.model, kwargs, lookups=True, with_fields=True ) # Look up string values for SingleTagFields by name for field_name, val in singletag_fields.items(): query_field_name = field_name if isinstance(val, six.string_types): query_field_name += '__name' if not field_lookup[field_name].tag_options.case_sensitive: query_field_name += '__iexact' safe_fields[query_field_name] = val # Query as normal qs = super(TaggedQuerySet, self)._filter_or_exclude( negate, *args, **safe_fields ) # Look up TagFields by string name # # Each of these comparisons will be done with a subquery; for # A filter can chain, ie .filter(tags__name=..).filter(tags__name=..), # but exclude won't work that way; has to be done with a subquery for field_name, val in tag_fields.items(): val, lookup = val tag_options = field_lookup[field_name].tag_options # Only perform custom lookup if value is a string if not isinstance(val, six.string_types): qs = super(TaggedQuerySet, self)._filter_or_exclude( negate, **{field_name: val} ) continue # Parse the tag string tags = utils.parse_tags( val, space_delimiter=tag_options.space_delimiter, ) # Prep the subquery subqs = qs if negate: subqs = self.__class__(model=self.model, using=self._db) # To get an exact match, filter this queryset to only include # items with a tag count that matches the number of specified tags if lookup == 'exact': count_name = '_tagulous_count_%s' % field_name subqs = subqs.annotate( **{count_name: models.Count(field_name)} ).filter(**{count_name: len(tags)}) # Prep the field name query_field_name = field_name + '__name' if not tag_options.case_sensitive: query_field_name += '__iexact' # Now chain the filters for each tag # # Have to do it this way to create new inner joins for each tag; # ANDing Q objects will do it all on a single inner join, which # will match nothing for tag in tags: subqs = subqs.filter(**{query_field_name: tag}) # Fold subquery back into main query if negate: # Exclude on matched ID qs = qs.exclude(pk__in=subqs.values('pk')) else: # A filter op can just replace the main query qs = subqs return qs
def test_order(self): tags = tag_utils.parse_tags("chris, adam, brian") self.assertEqual(len(tags), 3) self.assertEqual(tags[0], "adam") self.assertEqual(tags[1], "brian") self.assertEqual(tags[2], "chris")
def test_commas_and_spaces(self): tags = tag_utils.parse_tags("adam, brian chris") self.assertEqual(len(tags), 2) self.assertEqual(tags[0], "adam") self.assertEqual(tags[1], "brian chris")
def test_spaces(self): tags = tag_utils.parse_tags("adam brian chris") self.assertEqual(len(tags), 3) self.assertEqual(tags[0], "adam") self.assertEqual(tags[1], "brian") self.assertEqual(tags[2], "chris")
def test_quotes_and_unquoted(self): tags = tag_utils.parse_tags('adam , "brian, chris" , dave') self.assertEqual(len(tags), 3) self.assertEqual(tags[0], "adam") self.assertEqual(tags[1], "brian, chris") self.assertEqual(tags[2], "dave")
def _filter_or_exclude(self, negate, *args, **kwargs): """ Custom lookups for tag fields """ # TODO: When minimum supported Django 1.7+, this can be replaced with # custom lookups, which would work much better anyway. safe_fields, singletag_fields, tag_fields, field_lookup = _split_kwargs( self.model, kwargs, lookups=True, with_fields=True) # Look up string values for SingleTagFields by name for field_name, val in singletag_fields.items(): query_field_name = field_name if isinstance(val, six.string_types): query_field_name += "__name" if not field_lookup[field_name].tag_options.case_sensitive: query_field_name += "__iexact" safe_fields[query_field_name] = val # Query as normal qs = super(TaggedQuerySet, self)._filter_or_exclude(negate, *args, **safe_fields) # Look up TagFields by string name # # Each of these comparisons will be done with a subquery; for # A filter can chain, ie .filter(tags__name=..).filter(tags__name=..), # but exclude won't work that way; has to be done with a subquery for field_name, val in tag_fields.items(): val, lookup = val tag_options = field_lookup[field_name].tag_options # Only perform custom lookup if value is a string if not isinstance(val, six.string_types): qs = super(TaggedQuerySet, self)._filter_or_exclude(negate, **{field_name: val}) continue # Parse the tag string tags = utils.parse_tags( val, space_delimiter=tag_options.space_delimiter) # Prep the subquery subqs = qs if negate: subqs = self.__class__(model=self.model, using=self._db) # To get an exact match, filter this queryset to only include # items with a tag count that matches the number of specified tags if lookup == "exact": count_name = "_tagulous_count_%s" % field_name subqs = subqs.annotate(**{ count_name: models.Count(field_name) }).filter(**{count_name: len(tags)}) # Prep the field name query_field_name = field_name + "__name" if not tag_options.case_sensitive: query_field_name += "__iexact" # Now chain the filters for each tag # # Have to do it this way to create new inner joins for each tag; # ANDing Q objects will do it all on a single inner join, which # will match nothing for tag in tags: subqs = subqs.filter(**{query_field_name: tag}) # Fold subquery back into main query if negate: # Exclude on matched ID qs = qs.exclude(pk__in=subqs.values("pk")) else: # A filter op can just replace the main query qs = subqs return qs
def test_quotes_dont_delimit(self): tags = tag_utils.parse_tags('adam"brian,chris dave') self.assertEqual(len(tags), 2) self.assertEqual(tags[0], 'adam"brian') self.assertEqual(tags[1], "chris dave")
def test_quotes_comma_delim_late_wins_unescaped_quotes(self): "When delimiter changes, return insignificant unescaped quotes" tags = tag_utils.parse_tags('adam "one", brian two') self.assertEqual(len(tags), 2) self.assertEqual(tags[0], 'adam "one"') self.assertEqual(tags[1], "brian two")
def test_quotes_comma_delim_late_wins(self): tags = tag_utils.parse_tags('"adam one" "brian two","chris three"') self.assertEqual(len(tags), 2) self.assertEqual(tags[0], 'adam one" "brian two') self.assertEqual(tags[1], "chris three")
class Thesis(models.Model): """ This class defines the field that a thesis can have. The following fields are optional: additional, pdf A thesis can be promoted for either BSc, MSc, BEd, MEd, or as being part of a project. Each thesis is referenced with the chair that is providing it via a ForeignKey field. """ THESIS_CHOICES = ( ('BSC', 'Bachelor of Science'), ('MSC', 'Master of Science'), ('BED', 'Bachelor of Education'), ('MED', 'Master of Education'), ('PRO', 'Forschungsprojekt'), ('ETC', 'nach Absprache') ) title = models.CharField('Titel der Arbeit', blank=False, max_length=200) description = models.TextField('Beschreibung', blank=False) date_added = models.DateTimeField('Erstellungsdatum', default=timezone.now, editable=False) additional = models.TextField('weitere Beschreibung', blank=True, max_length=1000) contact = models.EmailField('E-Mail der Kontaktperson:', blank=False) chair = models.ForeignKey(Chair, on_delete=models.CASCADE, related_name="provided_by", verbose_name='angeboten durch Lehrstuhl') start_date = models.DateField('frühester Beginn', blank=False, default=timezone.now) is_active = models.BooleanField('aktiv', default=True) pdf = models.FileField('PDF mit Ausschreibung', validators=[FileExtensionValidator(allowed_extensions=['pdf'])], blank=True) type = MultiSelectField('Art der Arbeit', choices=THESIS_CHOICES, blank=False) tags = tagulous.models.TagField(get_absolute_url=lambda tag: reverse( 'by_tag', args=parse_tags(tag.slug))) user = models.ForeignKey(AAIUser, null=True, on_delete=models.DO_NOTHING, related_name="uploaded_by", verbose_name="hochgeladen von") def __str__(self): return self.title def get_absolute_url(self): return "/%i/" % self.id class Meta: verbose_name = "Abschlussarbeit" verbose_name_plural = "Abschlussarbeiten"
def test_empty_tag(self): tags = tag_utils.parse_tags('"adam" , , brian , ') self.assertEqual(len(tags), 2) self.assertEqual(tags[0], "adam") self.assertEqual(tags[1], "brian")
def test_trailing_space(self): tags = tag_utils.parse_tags("adam brian ") self.assertEqual(len(tags), 2) self.assertEqual(tags[0], "adam") self.assertEqual(tags[1], "brian")
def test_limit(self): with self.assertRaises(ValueError) as cm: tag_utils.parse_tags("adam,brian,chris", 1) e = cm.exception self.assertEqual(str(e), "This field can only have 1 argument")
def test_commas_over_spaces(self): tags = tag_utils.parse_tags("adam brian , chris") self.assertEqual(len(tags), 2) self.assertEqual(tags[0], "adam brian") self.assertEqual(tags[1], "chris")
def test_limit_quotes(self): with self.assertRaises(ValueError) as cm: tag_utils.parse_tags('"adam","brian",chris', 2) e = cm.exception self.assertEqual(str(e), "This field can only have 2 arguments")
def test_quotes(self): tags = tag_utils.parse_tags('"adam, one"') self.assertEqual(len(tags), 1) self.assertEqual(tags[0], "adam, one")
def test_quotes_space_delim(self): tags = tag_utils.parse_tags('"adam one" "brian two" "chris three"') self.assertEqual(len(tags), 3) self.assertEqual(tags[0], "adam one") self.assertEqual(tags[1], "brian two") self.assertEqual(tags[2], "chris three")