# List and Create Articles ## Example Overview This is one of the most comprehensive examples showcasing the capabilities of `django_tomselect` in a real-world scenario. The **List and Create Articles** example demonstrates how to integrate `django_tomselect` for filtering and managing articles efficiently. The "List" view enables filtering based on various attributes like edition year and word count using `django_tomselect` with iterables, while the "Create / Update" views facilitate adding or updating articles with fields powered by `django_tomselect`. We also incorporate the filter-by and exclude-by examples into the form to demonstrate the flexibility of the plugin. **Objective**: - Showcase dynamic filtering in the article list view using `TomSelectChoiceField` and `TomSelectModelChoiceField`. - Demonstrate a feature-rich forms leveraging the power of plugins and configurations in `django_tomselect`. **Visual Examples** ![Screenshot: Article List](https://raw.githubusercontent.com/OmenApps/django-tomselect/refs/heads/main/docs/images/article-list.png) ![Screenshot: Article Update 1](https://raw.githubusercontent.com/OmenApps/django-tomselect/refs/heads/main/docs/images/article-update1.png) ![Screenshot: Article Update 2](https://raw.githubusercontent.com/OmenApps/django-tomselect/refs/heads/main/docs/images/article-update2.png) --- ## Key Code Segments ### Forms #### List Articles Filters The filtering form utilizes `TomSelectChoiceField` for selecting year and word count ranges. :::{admonition} Form Definition :class: dropdown ```python class EditionYearForm(forms.Form): """Form for selecting an edition year.""" def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.fields["year"].help_text = "This field is backed by the edition_year list in models.py" year = TomSelectChoiceField( config=TomSelectConfig( url="autocomplete-edition-year", value_field="value", label_field="label", placeholder=_("Select an edition year..."), preload="focus", highlight=True, minimum_query_length=0, ), ) class WordCountForm(forms.Form): """Form for selecting a word count range.""" def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.fields["word_count"].help_text = "This field is backed by the word_count_range tuple in models.py" word_count = TomSelectChoiceField( config=TomSelectConfig( url="autocomplete-page-count", value_field="value", label_field="label", placeholder=_("Select a word count range..."), preload="focus", highlight=True, minimum_query_length=0, plugin_remove_button=PluginRemoveButton(), ), ) ``` ::: #### Create Article Form The "Create Article" form employs `TomSelectModelChoiceField` and `TomSelectModelMultipleChoiceField` to manage relationships like authors, categories, and more. :::{admonition} Form Definition :class: dropdown ```python category_header = PluginDropdownHeader( title=_("Category Selection"), show_value_field=False, extra_columns={ "parent_name": _("Parent"), "direct_articles": _("Direct Articles"), "total_articles": _("Total Articles"), }, ) author_header = PluginDropdownHeader( title=_("Author Selection"), show_value_field=False, extra_columns={ "article_count": _("Articles"), }, ) class DynamicArticleForm(forms.ModelForm): """Form for creating and editing articles with dynamic fields and dependencies.""" title = forms.CharField(max_length=200, widget=forms.TextInput(attrs={"class": "form-control"})) word_count = forms.IntegerField(widget=forms.NumberInput(attrs={"class": "form-control"}), min_value=0) status = TomSelectChoiceField( config=TomSelectConfig( url="autocomplete-article-status", value_field="value", label_field="label", placeholder=_("Select an article status..."), preload="focus", highlight=True, minimum_query_length=0, ), ) priority = TomSelectChoiceField( config=TomSelectConfig( url="autocomplete-article-priority", value_field="value", label_field="label", placeholder=_("Select an article priority..."), preload="focus", highlight=True, minimum_query_length=0, ), ) magazine = TomSelectModelChoiceField( config=TomSelectConfig( url="autocomplete-magazine", show_list=True, show_create=True, show_update=True, show_delete=True, value_field="id", label_field="name", placeholder=_("Select a magazine..."), preload="focus", highlight=True, minimum_query_length=0, plugin_dropdown_footer=PluginDropdownFooter(), ), ) primary_author = TomSelectModelChoiceField( config=TomSelectConfig( url="autocomplete-author", show_list=True, show_create=True, show_update=True, value_field="id", label_field="name", placeholder=_("Select primary author..."), highlight=True, plugin_dropdown_header=author_header, plugin_dropdown_footer=PluginDropdownFooter(), plugin_dropdown_input=PluginDropdownInput(), plugin_clear_button=PluginClearButton(title=_("Clear primary author")), ), ) contributing_authors = TomSelectModelMultipleChoiceField( config=TomSelectConfig( url="autocomplete-author", value_field="id", label_field="name", exclude_by=("primary_author", "id"), # Exclude primary author placeholder=_("Select contributing authors..."), highlight=True, max_items=None, plugin_dropdown_header=author_header, plugin_dropdown_input=PluginDropdownInput(), plugin_clear_button=PluginClearButton(title=_("Clear all contributing authors")), plugin_remove_button=PluginRemoveButton(), ), ) main_category = TomSelectModelChoiceField( config=TomSelectConfig( url="autocomplete-category", show_list=True, show_create=True, show_update=True, value_field="id", label_field="name", placeholder=_("Select main category..."), highlight=True, plugin_dropdown_header=category_header, plugin_dropdown_footer=PluginDropdownFooter(), plugin_dropdown_input=PluginDropdownInput(), plugin_clear_button=PluginClearButton(title=_("Clear main category")), ), ) subcategories = TomSelectModelMultipleChoiceField( config=TomSelectConfig( url="autocomplete-category", value_field="id", label_field="full_path", show_update=True, show_delete=True, filter_by=("main_category", "parent_id"), placeholder=_("Select subcategories..."), highlight=True, max_items=None, plugin_dropdown_header=category_header, plugin_dropdown_input=PluginDropdownInput(), plugin_clear_button=PluginClearButton(title=_("Clear all subcategories")), plugin_remove_button=PluginRemoveButton(), ), ) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # Set help text for fields self.fields["status"].help_text = "This field is backed by ArticleStatus (models.TextChoices) in models.py" self.fields["priority"].help_text = ( "This field is backed by ArticlePriority (models.IntegerChoices) in models.py" ) self.fields["magazine"].help_text = "This field is backed by the Magazine model" if "edition" in self.fields.keys(): self.fields["edition"].help_text = ( "This field is backed by the Edition model, and is filtered by the currently selected magazine" ) self.fields["primary_author"].help_text = "This field is backed by the Author model" self.fields["contributing_authors"].help_text = ( "This field is backed by the Author model, and excludes the current selection in primary_author" ) self.fields["main_category"].help_text = "This field is backed by the Category model" self.fields["subcategories"].help_text = ( "This field is backed by the Category model, and is filtered by the main_category, but it only allows " "selections when the main_category is a 'parent' category (e.g.: has no parent itself)" ) # Only try to set initial values if we have an existing instance with an id if self.instance and self.instance.pk: # Set initial values for categories if instance exists if self.instance.categories.exists(): categories = self.instance.categories.all() # Find main category (parent is None) and subcategories main_category = next((cat for cat in categories if cat.parent is None), None) subcategories = [cat for cat in categories if cat.parent is not None] if main_category: self.fields["main_category"].initial = main_category.pk if subcategories: self.fields["subcategories"].initial = [cat.pk for cat in subcategories] # Set initial values for authors if they exist if self.instance.authors.exists(): authors = self.instance.authors.all() self.fields["primary_author"].initial = authors[0].pk if authors else None if len(authors) > 1: self.fields["contributing_authors"].initial = [author.pk for author in authors[1:]] # Dynamically add edition field if magazine exists if self.instance.magazine: self.fields["edition"] = TomSelectModelChoiceField( config=TomSelectConfig( url="autocomplete-edition", show_list=True, show_create=True, show_update=True, show_delete=True, value_field="id", label_field="name", filter_by=("magazine", "magazine_id"), placeholder=_("Select an edition..."), highlight=True, plugin_dropdown_footer=PluginDropdownFooter(), ), initial=( self.instance.edition.pk if hasattr(self.instance, "edition") and hasattr(self.instance.edition, "pk") else None ), ) def clean(self): """Validate the form data.""" cleaned_data = super().clean() # Validate author selections primary_author = cleaned_data.get("primary_author") contributing_authors = cleaned_data.get("contributing_authors", []) if primary_author and primary_author in contributing_authors: self.add_error( "contributing_authors", _("Primary author cannot also be a contributing author"), ) # Validate category hierarchy main_category = cleaned_data.get("main_category") subcategories = cleaned_data.get("subcategories", []) if main_category and subcategories: invalid_subcats = [cat for cat in subcategories if cat.parent_id != main_category.id] if invalid_subcats: self.add_error( "subcategories", _("Selected subcategories must belong to the main category"), ) return cleaned_data def save(self, commit=True): """Save the form, handling the M2M relationships properly.""" article = super().save(commit=False) if commit: article.save() # Handle authors authors = [] if self.cleaned_data.get("primary_author"): authors.append(self.cleaned_data["primary_author"]) if self.cleaned_data.get("contributing_authors"): authors.extend(self.cleaned_data["contributing_authors"]) # Set the authors article.authors.set(authors) # Handle categories categories = [] if self.cleaned_data.get("main_category"): categories.append(self.cleaned_data["main_category"]) if self.cleaned_data.get("subcategories"): categories.extend(self.cleaned_data["subcategories"]) # Set the categories article.categories.set(categories) # Handle edition if it exists in cleaned_data if "edition" in self.cleaned_data and self.cleaned_data["edition"]: # Need to update this through a direct database update # since edition is not a direct field on the Article model Article.objects.filter(pk=article.pk).update(edition=self.cleaned_data["edition"]) return article class Meta: """Meta options for the model form.""" model = Article fields = [ "title", "status", "priority", "magazine", "primary_author", "contributing_authors", "main_category", "subcategories", "word_count", ] ``` ::: --- ### Templates #### List Articles The filter form and article list are rendered in the template with built-in styling and dynamic updates. :::{admonition} Template :class: dropdown ```html {% extends "example/base_with_bootstrap5.html" %} {% load static %} {% block extra_header %} {{ edition_year_form.media }} {% endblock %} {% block content %}
{% csrf_token %}
{{ edition_year_form.year }} {% if edition_year_form.year.errors %}
{{ edition_year_form.year.errors }}
{% endif %} {{ edition_year_form.year.help_text }}
{{ word_count_form.word_count }} {% if word_count_form.word_count.errors %}
{{ word_count_form.word_count.errors }}
{% endif %} {{ word_count_form.word_count.help_text }}

Articles

New Article
{% if page_obj.paginator.count > 0 %}
{% for article in page_obj.object_list %} {% endfor %}
Title Magazine Authors Categories Priority Status Actions
{{ article.title }}
Word Count: {{ article.word_count }}
{{ article.magazine.name }}
Edition: {{ article.edition.name }}
{% for author in article.authors.all %}
{{ author.name }} {% if forloop.first %} Primary {% endif %}
{% endfor %}
{% for category in article.categories.all %}
{% if category.parent %} {{ category.parent.name }} >> {% endif %} {{ category.name }}
{% endfor %}
{% if article.priority == 1 or article.priority == 7 or article.priority == 13 or article.priority == 17 or article.priority == 20 or article.priority == 21 or article.priority == 22 or article.priority == 29 or article.priority == 32 %} {{ article.get_priority_display }} {% elif article.priority == 2 or article.priority == 18 or article.priority == 30 %} {{ article.get_priority_display }} {% elif article.priority == 3 or article.priority == 4 or article.priority == 5 or article.priority == 6 or article.priority == 8 or article.priority == 9 or article.priority == 10 or article.priority == 11 or article.priority == 12 or article.priority == 14 or article.priority == 15 or article.priority == 16 or article.priority == 19 or article.priority == 23 or article.priority == 24 or article.priority == 25 or article.priority == 26 or article.priority == 27 or article.priority == 28 %} {{ article.get_priority_display }} {% else %} {{ article.get_priority_display }} {% endif %} {% if article.status == 'draft' %} Draft {% elif article.status == 'active' %} Active {% elif article.status == 'archived' %} Archived {% else %} {{ article.get_status_display }} {% endif %}
{% if article.status == 'draft' %}
{% csrf_token %}
{% endif %} {% if article.status == 'active' or article.status == 'canceled' or article.status == 'published' %}
{% csrf_token %}
{% endif %} {% if not article.status == 'published' and not article.status == 'canceled' %}
{% csrf_token %}
{% endif %}
{% else %}

No articles yet

Get started by creating your first article

Create First Article
{% endif %}
{% endblock %} ``` ::: #### Articles Form Template The form is displayed using Bootstrap styling and `django_tomselect` integrations. Here, we manually laid out the form, but you can use `django-crispy-forms` or other form libraries for more automation. :::{admonition} Template :class: dropdown ```html {% extends "example/base_with_bootstrap5.html" %} {% load static %} {% block extra_header %} {{ form.media }} {% endblock %} {% block content %}

{% if form.instance.pk %}Edit Article{% else %}Create Article{% endif %}

{% csrf_token %}
{{ form.title }} {% if form.title.errors %}
{{ form.title.errors }}
{% endif %} {{ form.title.help_text }}
{{ form.word_count }} {% if form.word_count.errors %}
{{ form.word_count.errors }}
{% endif %} {{ form.word_count.help_text }}

Status and Priority

{{ form.priority }} {% if form.priority.errors %}
{{ form.priority.errors }}
{% endif %} {{ form.priority.help_text }}
{{ form.status }} {% if form.status.errors %}
{{ form.status.errors }}
{% endif %} {{ form.status.help_text }}

Magazine and Edition

{{ form.magazine }} {% if form.magazine.errors %}
{{ form.magazine.errors }}
{% endif %} {{ form.magazine.help_text }}
{% if form.edition %}
{{ form.edition }} {% if form.edition.errors %}
{{ form.edition.errors }}
{% endif %} {{ form.edition.help_text }}
{% endif %}

Authors

{{ form.primary_author }} {% if form.primary_author.errors %}
{{ form.primary_author.errors }}
{% endif %} {{ form.primary_author.help_text }}
{{ form.contributing_authors }} {% if form.contributing_authors.errors %}
{{ form.contributing_authors.errors }}
{% endif %} {{ form.contributing_authors.help_text }}

Categories

{{ form.main_category }} {% if form.main_category.errors %}
{{ form.main_category.errors }}
{% endif %} {{ form.main_category.help_text }}
{{ form.subcategories }} {% if form.subcategories.errors %}
{{ form.subcategories.errors }}
{% endif %} {{ form.subcategories.help_text }}
{% if form.non_field_errors %}
{{ form.non_field_errors }}
{% endif %}
Cancel
{% endblock %} ``` ::: --- ### Autocomplete Views Multiple autocomplete views are used to populate the dropdowns with dynamic data, such as categories, authors, and article statuses. Please see the example app's [Autocomplete Views Code](https://github.com/OmenApps/django-tomselect/blob/main/example_project/example/autocompletes.py) for more details. --- ### Views #### List Articles Dynamic filters retrieve data using autocomplete endpoints. :::{admonition} View :class: dropdown ```python def article_list_view(request: HttpRequest, page: int = 1) -> HttpResponse: """View for the article list page.""" template = "example/advanced_demos/article_list.html" context = {} edition_year = request.GET.get("year") word_count = request.GET.get("word_count") articles = Article.objects.all().prefetch_related("authors", "categories").select_related("magazine") # Filter articles by edition year if provided if edition_year: articles = articles.filter(edition__year=edition_year) # Filter articles by word count range if provided if word_count: try: # Convert string representation of tuple back to range values range_str = word_count.strip("()").split(",") min_count = int(range_str[0]) max_count = int(range_str[1]) articles = articles.filter(word_count__gte=min_count, word_count__lte=max_count) except (ValueError, IndexError): pass paginator = Paginator(articles, 8) try: page_obj = paginator.get_page(page) except PageNotAnInteger: page_obj = paginator.get_page(1) except EmptyPage: page_obj = paginator.get_page(paginator.num_pages) context["page_obj"] = page_obj context["edition_year_form"] = EditionYearForm(initial={"year": edition_year}) context["word_count_form"] = WordCountForm(initial={"word_count": word_count}) return TemplateResponse(request, template, context) ``` ::: #### Create / Update Article The form is rendered with dynamic fields based on the article instance. :::{admonition} View :class: dropdown ```python def article_create_view(request: HttpRequest) -> HttpResponse: """View for the article create page.""" template = "example/advanced_demos/article_form.html" context = {} form = DynamicArticleForm(request.POST or None) if request.POST: if form.is_valid(): form.save() messages.success(request, f'Article "{form.cleaned_data["title"]}" has been created.') else: messages.error(request, "Please correct the errors below.") return HttpResponseRedirect(reverse("article-list")) context["form"] = form return TemplateResponse(request, template, context) def article_update_view(request: HttpRequest, pk: int) -> HttpResponse: """View for the article update page.""" template = "example/advanced_demos/article_form.html" context = {} article = get_object_or_404(Article, pk=pk) form = DynamicArticleForm(request.POST or None, instance=article) if request.POST: if form.is_valid(): form.save() messages.success(request, f'Article "{form.cleaned_data["title"]}" has been updated.') return HttpResponseRedirect(reverse("article-list")) else: messages.error(request, "Please correct the errors below.") context["form"] = form return TemplateResponse(request, template, context) ``` ::: --- ## Design and Implementation Notes ### Key Features - **Dynamic Filtering**: Use `django_tomselect` for real-time filtering in the article list. - **Relationship Management**: Leverage multi-select dropdowns for managing complex relationships like authors and categories. ### Design Decisions - Using `plugin_dropdown_header` enhances the dropdown with contextual information (e.g., article count for authors). - `plugin_dropdown_footer` allows quick navigation to related views, like creating new magazines.