# Tagging Publications ## Example Overview This example implements tagging in a Django form with `django_tomselect`. Users can autocomplete existing tags or create new ones on the fly with the `create` feature, and the tags are validated and saved to the database on submit. Reach for this pattern in content management systems or anywhere users generate metadata to categorize entities. **Visual Examples** ![Screenshot: Tagging](https://raw.githubusercontent.com/OmenApps/django-tomselect/refs/heads/main/docs/images/tagging1.png) ![Screenshot: Tagging (invalid tag cleaning)](https://raw.githubusercontent.com/OmenApps/django-tomselect/refs/heads/main/docs/images/tagging2.png) ## Key Code Segments ### Forms The form uses a custom `DynamicTagField`, a subclass of `TomSelectMultipleChoiceField`, to allow dynamic creation and selection of tags. The most complex part of the form is the `clean_tags` method, which validates and saves tags to the database. :::{admonition} Form Definition :class: dropdown ```python class DynamicTagField(TomSelectMultipleChoiceField): """A custom field that allows new values to be created on the fly.""" def clean(self, value): """Override to allow values that aren't in the autocomplete results yet.""" if not value: if self.required: raise ValidationError(self.error_messages["required"], code="required") return [] # Convert all values to strings str_values = [str(v) for v in value] return str_values class TaggingForm(forms.Form): """Form for managing publication tags with validation.""" tags = DynamicTagField( config=TomSelectConfig( url="autocomplete-publication-tag", value_field="value", label_field="label", placeholder="Enter tags...", highlight=True, create=True, minimum_query_length=2, max_items=10, plugin_dropdown_header=PluginDropdownHeader( title="Publication Tags", extra_columns={"usage_count": "Usage Count", "created_at": "Created"}, ), plugin_remove_button=PluginRemoveButton(title="Remove tag", label="×", class_name="remove-tag"), ), help_text=( "Enter or select tags. Tags must be 2-50 characters long, " "contain only letters, numbers, hyphens, and underscores, " "and start/end with letters or numbers." ), ) def clean_tags(self): """Validate tags and create/update them in the database.""" tag_names = self.cleaned_data.get("tags", []) if len(tag_names) < 1: raise ValidationError("Please add at least one tag") if len(tag_names) > 10: raise ValidationError("Maximum 10 tags allowed") # Check for duplicates (case-insensitive) lower_names = [name.lower() for name in tag_names] if len(lower_names) != len(set(lower_names)): raise ValidationError("Duplicate tags are not allowed") # Process each tag tags = [] for name in tag_names: # Clean the tag name name = name.lower().strip() # Validate the tag format if len(name) < 2: raise ValidationError(f"Tag {name!r} is too short") if not all(c.isalnum() or c in "-_" for c in name): raise ValidationError(f"Tag {name!r} contains invalid characters") if "--" in name or "__" in name: raise ValidationError(f"Tag {name!r} contains consecutive special characters") if not name[0].isalnum() or not name[-1].isalnum(): raise ValidationError(f"Tag {name!r} must start and end with a letter or number") # Try to get existing tag or create new one tag, _ = PublicationTag.objects.get_or_create( name=name, defaults={"is_approved": True}, ) tags.append(tag) return tags ``` ::: **Explanation**: - The `DynamicTagField` allows users to add new tags on the fly. - The `clean_tags` method ensures all tags are validated and saved to the database. ### Templates The form is rendered in the `tagging_publication.html` template, providing an intuitive interface for managing tags. :::{admonition} Main Template :class: dropdown ```html {% extends 'example/base_with_bootstrap5.html' %} {% block extra_header %} {{ form.media.css }} {{ form.media.js }} {% endblock %} {% block content %}

Publication Tags

This example is a sort of hybrid, backed by AutocompleteModelView, but using a TomSelectMultipleChoiceField subclass to handle the tag input. The form is rendered with the TomSelect widget, and the tags are saved to the database.


Add or select tags for your publication. Valid tag examples:

  • machine-learning
  • python3_tutorial
  • react17
{% csrf_token %}
{{ form.tags }} {% if form.tags.errors %}
{{ form.tags.errors }}
{% endif %}
{% if existing_tags %}

Existing Tags

{% for tag in existing_tags %} {% endfor %}
Tag Usage Count Created Status
{{ tag.name }} {{ tag.usage_count }} uses {{ tag.created_at|date:"M j, Y" }} {% if tag.is_approved %} Approved {% else %} Pending {% endif %}
{% endif %}
{% endblock %} ``` ::: Upon form submission, the `clean_tags` method is called to validate and save the tags to the database. If successful, the user is shown a success page; otherwise, the form is re-rendered with error messages. :::{admonition} Success Template :class: dropdown ```html {% extends 'example/base_with_bootstrap5.html' %} {% block extra_header %} {% endblock %} {% block content %}

Tags Saved Successfully

Selected Tags ({{ tags|length }})

{% for tag in tags %} {{ tag.name }} {% endfor %}
{% endblock %} ``` ::: ### Autocomplete Views The `autocomplete-publication-tag` endpoint provides tag suggestions based on user input. In this example the form field (`DynamicTagField`, built on `TomSelectMultipleChoiceField`) drives the iterables widget, while `PublicationTagAutocompleteView` is a model-based view. To bridge the two, we override `get_iterable` so the model view also emits the `{value, label}` iterable shape the `TomSelectIterablesWidget` expects. :::{admonition} Autocomplete View :class: dropdown ```python class PublicationTagAutocompleteView(AutocompleteModelView): """Autocomplete view for publication tags with iterable support.""" model = PublicationTag search_lookups = ["name__icontains"] ordering = ["-usage_count", "name"] value_fields = ["id", "name", "usage_count", "created_at"] skip_authorization = True iterable = [] def get_queryset(self): """Return queryset with usage statistics.""" return super().get_queryset().filter(is_approved=True) def get_iterable(self): """Provide iterable interface for TomSelectIterablesWidget compatibility.""" results = self.get_queryset() return [ { "value": tag.name, # Value used for selection "label": tag.name, # Label shown in dropdown "usage_count": tag.usage_count, "created_at": tag.created_at.strftime("%Y-%m-%d"), } for tag in results ] def prepare_results(self, results): """Prepare results with formatted value/label pairs.""" return [ { "value": tag.name, # Value used for selection "label": tag.name, # Label shown in dropdown "usage_count": tag.usage_count, "created_at": tag.created_at.strftime("%Y-%m-%d"), } for tag in results ] ``` ::: ## Views The `tagging` view processes the form data and displays a success message with the selected tags. :::{admonition} View Code :class: dropdown ```python def tagging_view(request): """View for managing publication tags.""" template = "example/intermediate_demos/tagging_publication.html" context = {} if request.method == "POST": form = TaggingForm(request.POST) if form.is_valid(): tags = form.cleaned_data["tags"] # Update usage counts for tag in tags: tag.usage_count += 1 tag.save() template = "example/intermediate_demos/tagging_success.html" context["tags"] = tags return TemplateResponse(request, template, context) else: form = TaggingForm() existing_tags = PublicationTag.objects.all().order_by("-usage_count", "name") context["form"] = form context["existing_tags"] = existing_tags return TemplateResponse(request, template, context) ``` :::