Skip to main content

📋 Registering models

A model appears in the admin once you register it with a studio: a ModelStudio subclass that configures its list page and edit page. Put the registrations in your app's admin.py, where Content Studio looks for them.

# blog/admin.py
from content_studio import register
from content_studio.admin import ModelStudio

from .models import Article


@register(Article)
class ArticleStudio(ModelStudio):
table_display = ["title", "status", "published_at"]

@register also takes several models at once, which then share the studio: @register(Category, LandingPage).

Registrations go to the site set in the ADMIN_SITE setting. Content Studio keeps its own registry: models registered only with the classic Django admin do not appear in the admin. The user model is the exception. It is registered with a default studio, so editors can manage users from the start; register your own studio for it to change how it looks.

Studio options​

OptionEffect
table_displayThe columns of the table view
list_filterThe fields editors can filter on
search_fieldsThe fields the search box searches
sortable_byThe columns editors can sort on
orderingThe default sort order of the list page
list_per_pageThe number of objects per page (default 20)
list_descriptionA description below the model's name on the list page
soft_deleteThe field that marks objects as trashed (see below)
iconThe model's icon in the main menu, such as "ph-bold ph-article"

When ordering is not set, objects are sorted by primary key, so pages stay stable.

Icons come from the bold weight of Phosphor: write ph-bold ph- followed by the icon's name. The same goes for every icon option in Content Studio.

Singletons​

For a model with exactly one object, such as site settings, set is_singleton on the studio:

# blog/admin.py
from content_studio import register
from content_studio.admin import ModelStudio

from .models import SiteSettings


@register(SiteSettings)
class SiteSettingsStudio(ModelStudio):
is_singleton = True
edit_main = ["site_name", "tagline"]

A singleton has no list page: the menu opens its edit page directly. Editors can create the object only while none exists, and cannot delete it. Models built on the built-in SingletonModel are treated as singletons without the option.

Inlines​

Inlines edit related objects on the parent's edit page, such as the reviews of an article. Declare an inline studio and add it to the parent's studio:

# blog/admin.py
from content_studio import register
from content_studio.admin import InlineModelStudio, ModelStudio

from .models import Article, Review


class ReviewInline(InlineModelStudio):
model = Review
table_display = ["text", "stars"]


@register(Article)
class ArticleStudio(ModelStudio):
inlines = [ReviewInline]

When the inline model has more than one foreign key to the parent, name the one to use with fk_name. The inline's table_display sets its columns. Inline objects only exist within their parent: editors can only add them to parents they may view.

Wherever an object shows up as a related object (in a picker, in a column or as a selected value) the related model's studio decides how it looks, with get_relation_display. By default it shows the object's string representation.

# blog/admin.py
from content_studio import register
from content_studio.admin import ModelStudio, RelationDisplay

from .models import Category


@register(Category)
class CategoryStudio(ModelStudio):
def get_relation_display(self, obj, request):
return RelationDisplay(
title=obj.name,
description=f"{obj.article_set.count()} articles",
icon="ph-bold ph-tag",
)

RelationDisplay takes a title and, optionally:

  • description: a second line of text;
  • icon: a CSS icon class, like the studio's icon;
  • initials: a character on a colored circle, the color derived from the initials;
  • avatar: an image URL.

When several are set, the admin shows the avatar first, then the initials, then the icon. Models without a studio always show their string representation.

The list page​

The list page shows objects as a table, the default, or as a list of readable items. Set the views to offer with list_views; with more than one, editors switch with a toggle and the admin remembers their choice per model.

# blog/admin.py
from content_studio import register
from content_studio.admin import ModelStudio

from .models import Article


@register(Article)
class ArticleStudio(ModelStudio):
table_display = ["title", "status", "published_at"]
list_views = ["table", "list"]
list_item_display = {
"title": "title",
"description": "body",
"meta": "get_status_display",
}

Table columns​

table_display lists the table's columns: field names, or TableColumn objects for a custom header, a format or a computed value.

# blog/admin.py
from content_studio import register
from content_studio.admin import ModelStudio
from content_studio.form import TableColumn
from content_studio.formats import NumberFormat

from .models import Article


@register(Article)
class ArticleStudio(ModelStudio):
table_display = [
"title",
TableColumn(field="published_at", header="Published"),
TableColumn(field="get_word_count", header="Words", format=NumberFormat),
]

def get_word_count(self, obj, request):
return len(obj.body.split())

A column's field is a model field or a method on the studio. A method takes (obj, request) and returns a value its format can show: text for text formats, a number for NumberFormat, an ISO 8601 string for the date and time formats. Without a format, a method column shows text and a field column uses its field type's format.

A column without a header uses the field's verbose name; a method column without one has no header. Editors can only sort on columns backed by a model field.

list_display, the classic Django admin's name for the table columns, also works, so a studio ported from the classic Django admin keeps its columns. System check content_studio.W010 asks you to rename it to table_display; when both are set, table_display applies.

List items​

list_item_display maps the parts of a list item to field or method names: a prominent title, a muted description and a meta label on the right. For items computed in code, override get_list_item_display instead:

# blog/admin.py
from content_studio import register
from content_studio.admin import ListItemDisplay, ModelStudio

from .models import Article


@register(Article)
class ArticleStudio(ModelStudio):
list_views = ["table", "list"]

def get_list_item_display(self, obj, request):
return ListItemDisplay(
title=obj.title, description=obj.author.get_full_name(), meta=obj.status
)

Bulk actions​

In the table view, editors select objects (the header checkbox selects the whole page) and run an action from the bar that appears. A selection belongs to one page: paging, filtering or sorting clears it.

The built-in delete action is available to editors with delete permission. Add your own actions to the studio with @bulk_action; like the actions of the classic Django admin, they receive the request and the selected objects as a queryset:

# blog/admin.py
from content_studio import register
from content_studio.actions import bulk_action
from content_studio.admin import ModelStudio

from .models import Article


@register(Article)
class ArticleStudio(ModelStudio):
@bulk_action(
label="Publish",
icon="ph-bold ph-paper-plane-tilt",
confirm="Publish the selected articles?",
allowed_permissions=["change"],
)
def publish(self, request, queryset):
count = queryset.update(status=Article.Status.PUBLISHED)
return f"Published {count} articles."
  • label: the action's name; by default, the method name.
  • icon: an icon class, like the studio's icon.
  • confirm: a question to confirm before the action runs.
  • allowed_permissions: the model permissions that allow the action; any one of them is enough. Without it, everyone who can see the list can run the action.

The return value is the message editors see: a number reports how many objects changed, a string is shown as it is.

The studio's actions attribute chooses which actions are offered:

actions = None # the default: delete and every @bulk_action
actions = [] # no bulk actions
actions = ["publish", "delete"] # exactly these, in this order

System check content_studio.E007 reports unknown or duplicate names and an action that would replace the built-in delete.

Soft delete and the trash​

By default, deleting an object removes it from the database. To move objects to a trash instead, set soft_delete to a nullable date or date and time field on the model that marks an object as trashed:

# blog/admin.py
from content_studio import register
from content_studio.admin import ModelStudio

from .models import Article


@register(Article)
class ArticleStudio(ModelStudio):
soft_delete = "removed_at"

With the option set:

  • deleting fills in the field instead of removing the object, and the object disappears from the list page;
  • the list page links to the model's trash, where editors restore objects (with change permission) or delete them for good (with delete permission);
  • restoring an object is recorded in its history.

Any model with such a field works; the built-in SoftDeletableModel has one called removed_at. System check content_studio.E008 reports a field that is missing or of the wrong type.

Object history​

The ActivityLog component shows an object's history on its edit page: every addition, change and deletion, newest first. Add it to the sidebar or to any form set:

# blog/admin.py
from content_studio import register
from content_studio.admin import ModelStudio
from content_studio.form import ActivityLog, FormSet

from .models import Article


@register(Article)
class ArticleStudio(ModelStudio):
edit_sidebar = [
FormSet(fields=["published_at", "author"]),
FormSet(fields=[ActivityLog(title="History")]),
]

The title is "Activity" unless you pass one. The history appears on the edit page of saved objects only, for editors who may view the object. Each FormSet in the sidebar is its own card, so the history above gets a card of its own.

Use your own site​

The default site, content_studio.admin.admin_site, is enough for many projects. To add a dashboard, model groups or extensions, subclass ContentStudioSite in a module of its own, so your admin.py modules can import it:

# mysite/studio.py
from content_studio.admin import ContentStudioSite


class BlogStudioSite(ContentStudioSite):
pass


admin_site = BlogStudioSite()
# mysite/settings.py
CONTENT_STUDIO = {
"ADMIN_SITE": "mysite.studio.admin_site",
}