📋 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
| Option | Effect |
|---|---|
table_display | The columns of the table view |
list_filter | The fields editors can filter on |
search_fields | The fields the search box searches |
sortable_by | The columns editors can sort on |
ordering | The default sort order of the list page |
list_per_page | The number of objects per page (default 20) |
list_description | A description below the model's name on the list page |
soft_delete | The field that marks objects as trashed (see List page) |
reorder_field | The field editors reorder objects by (see List page) |
allow_duplication | Whether editors can duplicate objects (see below) |
edit_width | The width of the edit page: "default" or "wide" (see Page width) |
presence | Whether the edit and list pages show who else has an object open (default True, see Presence) |
icon | The 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.
To narrow the objects an editor sees at all, override
get_queryset(request) (see Access control).
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,
and its edit_width the width of the dialog its objects open in (see
Page width).
Inline objects only exist within their parent: editors can only add them to
parents they may view.
The inline studio's permission methods decide what editors can do with the
inline's objects, as on any studio. Without add permission, the inline offers
no create button. Without change permission, an object opens read-only: its
fields are shown but cannot be saved, and it can only be deleted with delete
permission. To make an inline read-only for everyone, return False from
has_add_permission, has_change_permission and has_delete_permission:
# blog/admin.py
from content_studio.admin import InlineModelStudio
from .models import Review
class ReviewInline(InlineModelStudio):
model = Review
table_display = ["text", "stars"]
def has_add_permission(self, request, obj=None):
return False
def has_change_permission(self, request, obj=None):
return False
def has_delete_permission(self, request, obj=None):
return False
How related objects appear
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'sicon;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.
Duplicating objects
To let editors start a new object from a copy of an existing one, set
allow_duplication on the studio. List the inlines whose objects are copied
along in duplicate_inlines:
# 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]
allow_duplication = True
duplicate_inlines = [ReviewInline]
The edit page then offers Duplicate in its actions menu. It opens the create
form filled in with a copy of the object. Nothing is saved until the editor
creates the object, so the copy is validated like any new object. Once it is
saved, the objects of the inlines in duplicate_inlines are copied to it;
the other inlines start empty.
The copy takes over:
- the values of the fields on the create form, including foreign keys, media and block fields;
- the selection of many-to-many fields, which editors can change before saving.
It leaves out the primary key, read-only and excluded fields, fields that
are not editable, one-to-one fields and files. A many-to-many field with a
through model is copied only when its through model is an inline listed in
duplicate_inlines.
Unique slugs and URL paths (URLPathField) get the first free number
suffix, such as spring-sale-2. Like the
duplicate warnings on the edit page,
the suffix accounts for objects in the trash and, with
multi-tenancy, only looks within the selected tenant.
Other unique fields start empty. Inline objects are copied with the same
rules.
Duplicating needs add permission on the model and view permission on the
object. The objects of an inline are copied only when the editor has add
permission on it. Duplication is not available for singletons. The new
object's history records that it was duplicated, and the
content_changed signal reports it like
any new object.
To change what the copy starts with, override get_duplicate_initial. It
returns the values by field name: related objects as model instances and
many-to-many fields as lists of them. Only fields on the create form are
used.
# blog/admin.py
from content_studio import register
from content_studio.admin import ModelStudio
from .models import Article
@register(Article)
class ArticleStudio(ModelStudio):
allow_duplication = True
def get_duplicate_initial(self, request, obj):
initial = super().get_duplicate_initial(request, obj)
initial["title"] = f"Copy of {obj.title}"
initial["status"] = Article.Status.DRAFT
initial["author"] = request.user
return initial
System check content_studio.E021 reports duplicate_inlines entries that
are not in inlines, and content_studio.W014 options that have no effect.
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",
}
Model groups
The main menu lists the registered models under one heading, "Content".
Set model_groups on the site to arrange them in groups of your own, each a
heading with its models under it:
# mysite/studio.py
from content_studio.admin import ContentStudioSite, ModelGroup
from blog.models import Article, Category, Redirect, SiteSettings
class BlogStudioSite(ContentStudioSite):
model_groups = [
ModelGroup("content", icon="ph-bold ph-article", models=[Article, Category]),
ModelGroup(
"site",
label="Site",
icon="ph-bold ph-gear",
models=[SiteSettings, Redirect],
collapsed=True,
),
]
admin_site = BlogStudioSite()
A group starts open; collapsed=True starts it closed, which suits groups
editors rarely need. The group that holds the page you are on always
opens. Editors open and close groups by clicking the heading.