Introduction to the Example Project¶
The example project demonstrates how to use the django-tomselect package in a Django project. It consists of a number of basic, intermediate, and advanced implementations all using the theme of publishing articles in a magazine.
Prerequisites¶
Python 3.11+
Django 4.2+
django-tomselectpackageBasic understanding of Django forms and views
Installation¶
Clone the repository:
git clone https://github.com/OmenApps/django-tomselect.git
Install the required packages:
uv sync --extra dev
Running the Example Project¶
Note: The commands below use
uv runto execute within the project’s virtual environment. If you have activated the virtual environment manually (e.g.source .venv/bin/activate), you can omit theuv runprefix.
Apply the migrations:
uv run python manage.py migrate
Create the example data:
uv run python manage.py create_examples
This may take a couple of minutes to complete. It creates a large number of objects to demonstrate the various features of the package.
Create a superuser:
uv run python manage.py createsuperuser
Run the Django development server:
uv run python manage.py runserver 0.0.0.0:8000
Open the browser and go to
http://localhost:8000/adminto login (or directlyhttp://localhost:8000/to view as AnonymousUser - some links and bttons to list/create/update/delete will not be available).Go to
http://localhost:8000/to view the example project.
Example Project Structure¶
Basic Examples¶
Formset with filter_by - Dependent fields in formsets, including using
exclude_byto prevent circular parent-child relationships in model formsets
Intermediate Examples¶
Advanced Examples¶
Multiple Filter-By - Filter by multiple form fields with AND logic
Constant Filter-By - Filter with constant values (e.g., always show published)
Rich Author Multi-Select - Three multi-select widgets sharing one autocomplete, with different render templates and plugin combos
CRUD¶
There are also several standard CRUD views, but they are not documented here.
Models¶
Here is the simplified code for the models to aid in understanding the examples.
See the actual models in the example_project/example/models.py file.
class ArticleStatus(models.TextChoices):
"""Choices for the status field of the Article model.
Used to demonstrate the AutocompleteIterablesView.
"""
DRAFT = "draft", "Draft"
ACTIVE = "active", "Active"
ARCHIVED = "archived", "Archived"
PUBLISHED = "published", "Published"
PENDING = "pending", "Pending"
LOCKED = "locked", "Locked"
ON_HOLD = "on_hold", "On Hold"
ON_REVIEW = "on_review", "On Review"
# Additional statuses skipped for brevity
class ArticlePriority(models.IntegerChoices):
"""Choices for the priority field of the Article model.
Used to demonstrate the AutocompleteIterablesView.
"""
LOW = 1, "Low"
MEDIUM = 2, "Medium"
HIGH = 3, "High"
CRITICAL = 4, "Critical"
URGENT = 5, "Urgent"
IMMEDIATE = 6, "Immediate"
NONE = 7, "None"
# Additional priorities skipped for brevity
class EmbargoTimeframe(models.TextChoices):
"""Choices for the embargo_timeframe field of the Article model."""
PRE_RELEASE = "pre", "Pre-Release (2 weeks)"
STANDARD = "std", "Standard (1 month)"
EXTENDED = "extd", "Extended (3 months)"
EXTREME = "extr", "Extreme (6 months)"
# A list of years for the Edition model
# Used to demonstrate the AutocompleteIterablesView
edition_year = [
2020,
2021,
2022,
2023,
2024,
2025,
]
# A list of word count ranges for the Edition model
# Used to demonstrate the AutocompleteIterablesView
word_count_range = (
(0, 100),
(100, 200),
(200, 300),
(300, 400),
(400, 500),
(500, 600),
(600, 700),
# Additional ranges skipped for brevity
)
market_tier_choices = [
(1, "Tier 1"),
(2, "Tier 2"),
(3, "Tier 3"),
]
class EmbargoRegion(models.Model):
"""A model representing a region with embargo information."""
name = models.CharField(max_length=100)
market_tier = models.IntegerField(choices=market_tier_choices)
content_restrictions = models.TextField()
typical_embargo_days = models.IntegerField(validators=[MinValueValidator(1)])
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class SearchQueryset(models.QuerySet):
"""A queryset providing a search method."""
def search(self, q):
"""Return a queryset filtered by the search term."""
return self.filter(name__icontains=q)
class Edition(models.Model):
"""A model representing an edition of a magazine."""
name = models.CharField("Name", max_length=50)
year = models.CharField("Year", max_length=50)
pages = models.CharField("Pages", max_length=50)
pub_num = models.CharField("Publication Number", max_length=50)
magazine = models.ForeignKey(
"Magazine",
on_delete=models.SET_NULL,
blank=True,
null=True,
)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
objects = SearchQueryset.as_manager()
class Magazine(models.Model):
"""A model representing a magazine."""
class AcceptsNewArticles(models.TextChoices):
"""Choices for the accepts_new_articles field."""
YES = "yes", "Yes"
NO = "no", "No"
MAYBE = "maybe", "Maybe"
name = models.CharField("Name", max_length=50)
accepts_new_articles = models.CharField(
"Accepts New Articles",
max_length=10,
choices=AcceptsNewArticles.choices,
default=AcceptsNewArticles.YES,
)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class CategoryQuerySet(models.QuerySet):
"""Queryset for the Category model."""
def with_header_data(self):
"""Annotate the queryset with parent information and article counts."""
return self.annotate(
parent_name=F("parent__name"),
full_path=Concat(
"parent__name",
Value(" >> "),
"name",
),
direct_articles=Count("article"),
total_articles=Count(
"article",
filter=Q(article__categories=F("id")) | Q(article__categories__parent=F("id")),
),
).select_related("parent")
class Category(models.Model):
"""A model representing an article category."""
name = models.CharField(max_length=100)
parent = models.ForeignKey(
"self",
on_delete=models.CASCADE,
null=True,
blank=True,
related_name="children",
)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
objects = models.Manager.from_queryset(CategoryQuerySet)()
class AuthorQuerySet(models.QuerySet):
"""Queryset for the Author model."""
def with_details(self):
"""Return a queryset of authors with article count annotations."""
return self.annotate(
article_count=Count("article"),
active_articles=Count("article", filter=Q(article__status="active")),
).distinct()
class Author(models.Model):
"""A model representing an article author."""
name = models.CharField(max_length=100)
bio = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
objects = models.Manager.from_queryset(AuthorQuerySet)()
class Article(models.Model):
"""A model representing an article in a magazine."""
title = models.CharField(max_length=200)
word_count = models.PositiveSmallIntegerField()
authors = models.ManyToManyField("Author")
categories = models.ManyToManyField("Category")
magazine = models.ForeignKey("Magazine", on_delete=models.CASCADE)
edition = models.ForeignKey(
"Edition",
on_delete=models.CASCADE,
blank=True,
null=True,
)
status = models.CharField(
max_length=30,
choices=ArticleStatus.choices,
default=ArticleStatus.DRAFT,
)
priority = models.IntegerField(
choices=ArticlePriority.choices,
default=ArticlePriority.NORMAL,
)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(null=True, blank=True)
class PublishingMarket(models.Model):
"""Represents geographic markets for publishing operations.
Creates a three-level hierarchy: Region -> Country -> City/Market
"""
name = models.CharField(max_length=100)
parent = models.ForeignKey(
"self",
on_delete=models.CASCADE,
null=True,
blank=True,
related_name="children",
)
market_size = models.IntegerField(default=0)
active_publications = models.IntegerField(default=0)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class PublicationTag(models.Model):
"""Represents a tag/keyword for publications with validation rules."""
name = models.CharField(max_length=50, unique=True)
usage_count = models.IntegerField(default=0)
is_approved = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)