Package: garpix-page
Garpix PageConvenient page structure with any context and template.
It is suitable not only for a blog, but also for large sites with a complex presentation.
Supports SEO.QuickstartInstall with pip:pipinstallgarpix_pageAdd thegarpix_pageand dependency packages to yourINSTALLED_APPS:# settings.pyINSTALLED_APPS=['modeltranslation','polymorphic_tree','polymorphic','mptt',# ... django.contrib.*'django.contrib.sites','tabbed_admin','garpix_page',# third-party and your apps]SITE_ID=1LANGUAGE_CODE='en'USE_DEFAULT_LANGUAGE_PREFIX=FalseLANGUAGES=(('en','English'),)TEMPLATES=[{'BACKEND':'django.template.backends.django.DjangoTemplates','DIRS':['templates'],'APP_DIRS':True,'OPTIONS':{'context_processors':['django.template.context_processors.debug','django.template.context_processors.request','django.contrib.auth.context_processors.auth','django.contrib.messages.context_processors.messages',],},},]MIDDLEWARE=['django.middleware.locale.LocaleMiddleware']Package not included migrations, set path to migration directory. Don't forget create this directory (app/migrations/garpix_page/) and place empty__init__.py:app/migrations/
app/migrations/__init__.py  # empty file
app/migrations/garpix_page/__init__.py  # empty fileAdd path to settings:# settings.pyMIGRATION_MODULES={'garpix_page':'app.migrations.garpix_page',}Run make migrations:pythonmanage.pymakemigrationsMigrate:pythonmanage.pymigrateAdd celery settings path tosettings.py:GARPIXCMS_CELERY_SETTINGS='app.celery.app'Now, you can create your models fromBasePageand set template and context. See example below.ImportantPage (Model Page)- model, subclass fromBasePage. You create it yourself. There must be at least 1 descendant from BasePage.Context- includesobjectandrequest. It is a function that returns a dictionary from model instance. Values from the key dictionary can be used in the template.Template- standard Django template.ExampleUrls:# app/urls.pyfromdjango.contribimportadminfromdjango.urlsimportpath,re_pathfromdjango.conf.urls.i18nimporti18n_patternsfromgarpix_page.views.pageimportPageViewfrommultiurlimportContinueResolving,multiurlfromdjango.httpimportHttp404fromdjango.confimportsettingsfromgarpix_page.views.indeximportIndexViewurlpatterns=[path('admin/',admin.site.urls),]urlpatterns+=i18n_patterns(multiurl(path('',PageView.as_view()),re_path(r'^(?P<url>.*?)$',PageView.as_view(),name='page'),re_path(r'^(?P<url>.*?)/$',PageView.as_view(),name='page'),path('',IndexView.as_view()),catch=(Http404,ContinueResolving),),prefix_default_language=settings.USE_DEFAULT_LANGUAGE_PREFIX,)Models:# app/models/page.pyfromdjango.dbimportmodelsfromgarpix_page.modelsimportBasePageclassPage(BasePage):content=models.TextField(verbose_name='Content',blank=True,default='')template='pages/default.html'classMeta:verbose_name="Page"verbose_name_plural="Pages"ordering=('-created_at',)# app/models/category.pyfromgarpix_page.modelsimportBasePageclassCategory(BasePage):template='pages/category.html'defget_context(self,request=None,*args,**kwargs):context=super().get_context(request,*args,**kwargs)posts=Post.on_site.filter(is_active=True,parent=kwargs['object'])context.update({'posts':posts})returncontextclassMeta:verbose_name="Category"verbose_name_plural="Categories"ordering=('-created_at',)# app/models/post.pyfromdjango.dbimportmodelsfromgarpix_page.modelsimportBasePageclassPost(BasePage):content=models.TextField(verbose_name='Content',blank=True,default='')template='pages/post.html'classMeta:verbose_name="Post"verbose_name_plural="Posts"ordering=('-created_at',)Admins:# app/admin/__init__.pyfrom.pageimportPageAdminfrom.categoryimportCategoryAdminfrom.postimportPostAdmin# app/admin/page.pyfrom..models.pageimportPagefromdjango.contribimportadminfromgarpix_page.adminimportBasePageAdmin@admin.register(Page)classPageAdmin(BasePageAdmin):pass# app/admin/category.pyfrom..models.categoryimportCategoryfromdjango.contribimportadminfromgarpix_page.adminimportBasePageAdmin@admin.register(Category)classCategoryAdmin(BasePageAdmin):pass# app/admin/post.pyfrom..models.postimportPostfromdjango.contribimportadminfromgarpix_page.adminimportBasePageAdmin@admin.register(Post)classPostAdmin(BasePageAdmin):passTranslations:# app/translation/__init__.pyfrom.pageimportPageTranslationOptionsfrom.categoryimportCategoryTranslationOptionsfrom.postimportPostTranslationOptions# app/translation/page.pyfrommodeltranslation.translatorimportTranslationOptions,registerfrom..modelsimportPage@register(Page)classPageTranslationOptions(TranslationOptions):fields=('content',)# app/translation/category.pyfrommodeltranslation.translatorimportTranslationOptions,registerfrom..modelsimportCategory@register(Category)classCategoryTranslationOptions(TranslationOptions):fields=[]# app/translation/post.pyfrommodeltranslation.translatorimportTranslationOptions,registerfrom..modelsimportPost@register(Post)classPostTranslationOptions(TranslationOptions):fields=('content',)Templates:# templates/base.html<!DOCTYPE html><htmllang="en"><head><metacharset="UTF-8">{% include 'garpix_page/seo.html' %}</head><body>{% include 'garpix_page/admin_toolbar.html' %}<main>{% block content %}404{% endblock %}
    {% block components %}
        {% for component in components %}
        {{ component.template }}
        {% include component.template %}
        {% endfor %}
    {% endblock %}</main></body></html># templates/pages/default.html

{% extends 'base.html' %}

{% block content %}<h1>{{object.title}}</h1><div>{{object.content|safe}}</div>{% endblock %}



# templates/pages/category.html

{% extends 'base.html' %}

{% block content %}<h1>{{object.title}}</h1>{% for post in posts %}<div><h3><ahref="{{post.get_absolute_url}}">{{post.title}}</a></h3></div>{% endfor %}

{% endblock %}



# templates/pages/post.html

{% extends 'base.html' %}

{% block content %}<h1>{{object.title}}</h1><div>{{object.content|safe}}</div>{% endblock %}Now you can auth in admin panel and starting add pages.If you need to use a serializer whose model is this page, use the get_serializer() method to avoid circular imports.Page permissionsIf you need to add login access to your model pages, add login_required static field to your model.To add some user permissions to page, add permissions  static field to your page model:classPost(BasePage):content=models.TextField(verbose_name='Content',blank=True,default='')template='pages/post.html'login_required=Truepermissions=[IsAdminUser,]classMeta:verbose_name="Post"verbose_name_plural="Posts"ordering=('-created_at',)APIYou can use garpix_page with SPA sites.Add to settings API_URL parameter:API_URL='api'Add tourls.pythis:urlpatterns+=[re_path(r'{}/page_models_list/$'.format(settings.API_URL),PageApiListView.as_view()),re_path(r'{}/page/(?P<slugs>.*)$'.format(settings.API_URL),PageApiView.as_view()),]And you can test it:http://localhost:8000/api/page/- home page (empty slug)http://localhost:8000/api/page/another_page- another page (slug)http://localhost:8000/api/page/kategoriya/post-1- sub page (slug)Example answer:{"page_model":"Post","init_state":{"object":{"id":4,"title":"post 1","title_en":"post 1","is_active":true,"display_on_sitemap":true,"slug":"post-1","created_at":"2021-06-21T19:39:49.749460Z","updated_at":"2021-06-21T19:39:49.749488Z","seo_title":"","seo_title_en":null,"seo_keywords":"","seo_keywords_en":null,"seo_description":"","seo_description_en":"","seo_author":"","seo_author_en":null,"seo_og_type":"website","seo_image":null,"lft":2,"rght":3,"tree_id":3,"level":1,"content":"example","content_en":"example","polymorphic_ctype":11,"parent":3,"sites":[1]}}}Error contextsModule consists of 3 reserved names for page models:Page404,Page403andPage401. These names are used for responses when corresponding error statuses are caught.
Example answer on not found error:{"page_model":"Page404","init_state":{"object":null,"global":{}}}ComponentsIt is possible to compose a page from components. You can do this in the same way as creating pages.Model# app/models/components.pyfromdjango.dbimportmodelsfromgarpix_page.modelsimportBaseComponentclassTextComponent(BaseComponent):text=models.TextField(verbose_name='Текст')classMeta:verbose_name='Текстовый компонент'verbose_name_plural='Текстовые компоненты'defget_context_data(self,request):# add overriding this method to customize component's contextcontext=super().get_context_data(request)returncontextAdmin# app/admin/components.pyfromdjango.contribimportadminfromgarpix_page.admin.components.base_componentimportBaseComponentAdminfromapp.modelsimportTextComponent@admin.register(TextComponent)classTextComponentAdmin(BaseComponentAdmin):passTranslations:# app/translation/components.pyfrommodeltranslation.translatorimportTranslationOptions,registerfromapp.modelsimportTextComponent@register(TextComponent)classTextComponentTranslationOptions(TranslationOptions):fields=('text',)BaseComponent has m2m fieldpagesto specify on which pages the component should be displayed. Through table also hasview_orderfield to specify the ordering of components at the page (ascending order).
You can overrideget_contex_datamethod to add some info to component context.Example answer with some components:{"page_model":"Page","init_state":{"object":{"id":1,"title":"page","title_en":"page","is_active":true,"display_on_sitemap":true,"slug":"page","created_at":"2022-02-28T15:33:26.083166Z","updated_at":"2022-04-12T07:45:34.695803Z","seo_title":"","seo_title_en":null,"seo_keywords":"","seo_keywords_en":null,"seo_description":"","seo_description_en":"","seo_author":"","seo_author_en":null,"seo_og_type":"website","seo_image":null,"lft":1,"rght":2,"tree_id":1,"level":0,"content":"","content_en":"","polymorphic_ctype":10,"parent":null,"sites":[1],"components":[{"component_model":"TextComponent","object":{"id":1,"title":"Текстовый блок","title_en":"Text block","created_at":"2022-04-11T15:35:24.829579Z","updated_at":"2022-04-11T15:37:09.898287Z","text_title":"","text":"Текст","text_en":"Text","polymorphic_ctype":22,"pages":[1]}},{"component_model":"TextDescriptionComponent","object":{"id":2,"title":"Описание рубрики","created_at":"2022-04-12T07:45:15.341862Z","updated_at":"2022-04-12T07:45:15.341886Z","text_title":"","text":"Текст","description":"Описание","polymorphic_ctype":21,"pages":[1]}}]},"global":{}}}Templates:If you want to override the base component template, addtemplateparameter to component class:# app/models/components.pyfromgarpix_page.modelsimportBaseComponentclassTextComponent(BaseComponent):# ...template='text_component.html'# ...In html you can usecomponentobject:# templates/pages/components/default.html<h1>{{ component.title }}</h1>You can usegx_componenttag in section with the component to add edit functionality for admin in template:{% load gx_component %}<sectionclass="text-component"{%gx_componentcomponent%}>...</section>Seo-templatesYou can create seo-template from admin panel.
If you setfieldvalue toModel title, the template will be used for pages only for those model.
In other cases the template will be used for pages with thevalueof thefield.You can also specify the sites the template will be used on.You can add fields which will be used for template keys, usingget_seo_template_keysmethod andseo_template_keys_listclass method.classPage(BasePage):#...defget_seo_template_keys(self):seo_keys=super().get_seo_template_keys()seo_keys.update({'yourfield':self.yourfield})returnseo_keys@classmethoddefseo_template_keys_list(cls):return[('yourfield','your field title')]Subpage url patternsSometimes we need to add static subpages likecreate,updateetc. and it's not very convenient to create separate model/instance for each of them.
For these purposes you can use subpage url patterns.
Overrideurl_patternsclass method ofBasePagemodel to add sub urls:
Methodurl_patternsmust return dict, which keys are names for models, which will be sent to api result; values are dicts with two keys:verbose_name- humanize model name,pattern- url pattern.Example:classCategory(BasePage):# ...@classmethoddefurl_patterns(cls):patterns=super().url_patterns()patterns.update({'{model_name}Create':{'verbose_name':'Создание{model_title}','pattern':'/create'},'{model_name}Reports':{'verbose_name':'Отчеты для{model_title}','pattern':'/reports'}})returnpatternsNow, if your project hasCategorypage with urlcategory, the project will also has two extra pages:category/createandcategory/reports.If you need to use some query parameters in you urls, you can add them like any url parameters:classCategory(BasePage):# ...@classmethoddefurl_patterns(cls):patterns=super().url_patterns()patterns.update({'{model_name}Create':{'verbose_name':'Создание{model_title}','pattern':'/create'},'{model_name}Reports':{'verbose_name':'Отчеты для{model_title}','pattern':'/reports'},'{model_name}Update':{'verbose_name':'Редактирование{model_title}','pattern':'/update/<id>'}})returnpatternsThe given parameters will be stored insubpage_paramsfield of page model, the key of pattern will be stored insubpage_key.
Now you can use them inget_contextto return some specific info depending onsubpage_key:classCategory(BasePage):template='pages/category.html'defget_context(self,request=None,*args,**kwargs):context=super().get_context(request,*args,**kwargs)ifself.subpage_key=='{model_name}Create':context.update({'some key':'some text'})returncontext@classmethoddefurl_patterns(cls):patterns=super().url_patterns()patterns.update({'{model_name}Create':{'verbose_name':'Создание{model_title}','pattern':'/create'},'{model_name}Update':{'verbose_name':'Редактирование{model_title}','pattern':'/update/<id>'}})returnpatternsclassMeta:verbose_name="Категория"verbose_name_plural="Категория"ordering=('-created_at',)Api result:{"page_model":"CategoryCreate","init_state":{"object":{"id":16,"seo_title":"title-1","seo_keywords":"","seo_description":"","seo_author":"","seo_og_type":"website","title":"title-1","is_active":true,"display_on_sitemap":true,"slug":"title","created_at":"2022-10-11T14:13:31.214166Z","updated_at":"2023-02-07T06:07:43.179306Z","seo_image":null},"components":[],"some key":"some text","global":{}}}You also can add extra keypermissionsto your url pattern to override permissions for subpage:fromrest_framework.permissionsimportIsAuthenticatedclassCategory(BasePage):template='pages/category.html'defget_context(self,request=None,*args,**kwargs):context=super().get_context(request,*args,**kwargs)ifself.subpage_key=='{model_name}Create':context.update({'some key':'some text'})returncontext@classmethoddefurl_patterns(cls):patterns=super().url_patterns()patterns.update({'{model_name}Create':{'verbose_name':'Создание{model_title}','pattern':'/create'},'{model_name}Update':{'verbose_name':'Редактирование{model_title}','pattern':'/update/<id>','permissions':[IsAuthenticated]}})returnpatternsclassMeta:verbose_name="Категория"verbose_name_plural="Категория"ordering=('-created_at',)Important!Also, see this project for additional features (BaseListPage,BaseSearchPage,sitemap.xml, etc).ChangelogSeeCHANGELOG.md.ContributingSeeCONTRIBUTING.md.LicenseMIT