Security News
Research
Data Theft Repackaged: A Case Study in Malicious Wrapper Packages on npm
The Socket Research Team breaks down a malicious wrapper package that uses obfuscation to harvest credentials and exfiltrate sensitive data.
Convenient 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.
Install with pip:
pip install garpix_page
Add the garpix_page
and dependency packages to your INSTALLED_APPS
:
# settings.py
INSTALLED_APPS = [
'modeltranslation',
'polymorphic_tree',
'polymorphic',
'mptt',
# ... django.contrib.*
'django.contrib.sites',
'tabbed_admin',
'garpix_page',
# third-party and your apps
]
SITE_ID=1
LANGUAGE_CODE = 'en'
USE_DEFAULT_LANGUAGE_PREFIX = False
LANGUAGES = (
('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 file
Add path to settings:
# settings.py
MIGRATION_MODULES = {
'garpix_page': 'app.migrations.garpix_page',
}
Run make migrations:
python manage.py makemigrations
Migrate:
python manage.py migrate
Add celery settings path to settings.py
:
GARPIXCMS_CELERY_SETTINGS = 'app.celery.app'
Now, you can create your models from BasePage
and set template and context. See example below.
Page (Model Page) - model, subclass from BasePage
. You create it yourself. There must be at least 1 descendant from BasePage.
Context - includes object
and request
. 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.
Urls:
# app/urls.py
from django.contrib import admin
from django.urls import path, re_path
from django.conf.urls.i18n import i18n_patterns
from garpix_page.views.page import PageView
from multiurl import ContinueResolving, multiurl
from django.http import Http404
from django.conf import settings
from garpix_page.views.index import IndexView
urlpatterns = [
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.py
from django.db import models
from garpix_page.models import BasePage
class Page(BasePage):
content = models.TextField(verbose_name='Content', blank=True, default='')
template = 'pages/default.html'
class Meta:
verbose_name = "Page"
verbose_name_plural = "Pages"
ordering = ('-created_at',)
# app/models/category.py
from garpix_page.models import BasePage
class Category(BasePage):
template = 'pages/category.html'
def get_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
})
return context
class Meta:
verbose_name = "Category"
verbose_name_plural = "Categories"
ordering = ('-created_at',)
# app/models/post.py
from django.db import models
from garpix_page.models import BasePage
class Post(BasePage):
content = models.TextField(verbose_name='Content', blank=True, default='')
template = 'pages/post.html'
class Meta:
verbose_name = "Post"
verbose_name_plural = "Posts"
ordering = ('-created_at',)
Admins:
# app/admin/__init__.py
from .page import PageAdmin
from .category import CategoryAdmin
from .post import PostAdmin
# app/admin/page.py
from ..models.page import Page
from django.contrib import admin
from garpix_page.admin import BasePageAdmin
@admin.register(Page)
class PageAdmin(BasePageAdmin):
pass
# app/admin/category.py
from ..models.category import Category
from django.contrib import admin
from garpix_page.admin import BasePageAdmin
@admin.register(Category)
class CategoryAdmin(BasePageAdmin):
pass
# app/admin/post.py
from ..models.post import Post
from django.contrib import admin
from garpix_page.admin import BasePageAdmin
@admin.register(Post)
class PostAdmin(BasePageAdmin):
pass
Translations:
# app/translation/__init__.py
from .page import PageTranslationOptions
from .category import CategoryTranslationOptions
from .post import PostTranslationOptions
# app/translation/page.py
from modeltranslation.translator import TranslationOptions, register
from ..models import Page
@register(Page)
class PageTranslationOptions(TranslationOptions):
fields = ('content',)
# app/translation/category.py
from modeltranslation.translator import TranslationOptions, register
from ..models import Category
@register(Category)
class CategoryTranslationOptions(TranslationOptions):
fields = []
# app/translation/post.py
from modeltranslation.translator import TranslationOptions, register
from ..models import Post
@register(Post)
class PostTranslationOptions(TranslationOptions):
fields = ('content',)
Templates:
# templates/base.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="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><a href="{{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.
If 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:
class Post(BasePage):
content = models.TextField(verbose_name='Content', blank=True, default='')
template = 'pages/post.html'
login_required = True
permissions = [IsAdminUser,]
class Meta:
verbose_name = "Post"
verbose_name_plural = "Posts"
ordering = ('-created_at',)
You can use garpix_page with SPA sites.
Add to settings API_URL parameter:
API_URL = 'api'
Add to urls.py
this:
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
]
}
}
}
Module consists of 3 reserved names for page models: Page404
, Page403
and Page401
. 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": {}
}
}
It is possible to compose a page from components. You can do this in the same way as creating pages.
Model
# app/models/components.py
from django.db import models
from garpix_page.models import BaseComponent
class TextComponent(BaseComponent):
text = models.TextField(verbose_name='Текст')
class Meta:
verbose_name = 'Текстовый компонент'
verbose_name_plural = 'Текстовые компоненты'
def get_context_data(self, request): # add overriding this method to customize component's context
context = super().get_context_data(request)
return context
Admin
# app/admin/components.py
from django.contrib import admin
from garpix_page.admin.components.base_component import BaseComponentAdmin
from app.models import TextComponent
@admin.register(TextComponent)
class TextComponentAdmin(BaseComponentAdmin):
pass
Translations:
# app/translation/components.py
from modeltranslation.translator import TranslationOptions, register
from app.models import TextComponent
@register(TextComponent)
class TextComponentTranslationOptions(TranslationOptions):
fields = ('text',)
BaseComponent has m2m field pages
to specify on which pages the component should be displayed. Through table also has view_order
field to specify the ordering of components at the page (ascending order).
You can override get_contex_data
method 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, add template
parameter to component class:
# app/models/components.py
from garpix_page.models import BaseComponent
class TextComponent(BaseComponent):
# ...
template = 'text_component.html'
# ...
In html you can use component
object:
# templates/pages/components/default.html
<h1>{{ component.title }}</h1>
You can use gx_component
tag in section with the component to add edit functionality for admin in template:
{% load gx_component %}
<section class="text-component" {% gx_component component %}>
...
</section>
You can create seo-template from admin panel.
If you set field
value to Model title
, the template will be used for pages only for those model.
In other cases the template will be used for pages with the value
of the field
.
You can also specify the sites the template will be used on.
You can add fields which will be used for template keys, using get_seo_template_keys
method and seo_template_keys_list
class method.
class Page(BasePage):
#...
def get_seo_template_keys(self):
seo_keys = super().get_seo_template_keys()
seo_keys.update({
'yourfield': self.yourfield
})
return seo_keys
@classmethod
def seo_template_keys_list(cls):
return [('yourfield', 'your field title')]
Sometimes we need to add static subpages like create
, update
etc. and it's not very convenient to create separate model/instance for each of them.
For these purposes you can use subpage url patterns.
Override url_patterns
class method of BasePage
model to add sub urls:
Method url_patterns
must 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:
class Category(BasePage):
# ...
@classmethod
def url_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'
}
}
)
return patterns
Now, if your project has Category
page with url category
, the project will also has two extra pages: category/create
and category/reports
.
If you need to use some query parameters in you urls, you can add them like any url parameters:
class Category(BasePage):
# ...
@classmethod
def url_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>'
}
}
)
return patterns
The given parameters will be stored in subpage_params
field of page model, the key of pattern will be stored in subpage_key
.
Now you can use them in get_context
to return some specific info depending on subpage_key
:
class Category(BasePage):
template = 'pages/category.html'
def get_context(self, request=None, *args, **kwargs):
context = super().get_context(request, *args, **kwargs)
if self.subpage_key == '{model_name}Create':
context.update({
'some key': 'some text'
})
return context
@classmethod
def url_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>'
}
}
)
return patterns
class Meta:
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 key permissions
to your url pattern to override permissions for subpage:
from rest_framework.permissions import IsAuthenticated
class Category(BasePage):
template = 'pages/category.html'
def get_context(self, request=None, *args, **kwargs):
context = super().get_context(request, *args, **kwargs)
if self.subpage_key == '{model_name}Create':
context.update({
'some key': 'some text'
})
return context
@classmethod
def url_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]
}
}
)
return patterns
class Meta:
verbose_name = "Категория"
verbose_name_plural = "Категория"
ordering = ('-created_at',)
Also, see this project for additional features (BaseListPage
, BaseSearchPage
, sitemap.xml
, etc).
See CHANGELOG.md.
See CONTRIBUTING.md.
FAQs
Unknown package
We found that garpix-page demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
Research
The Socket Research Team breaks down a malicious wrapper package that uses obfuscation to harvest credentials and exfiltrate sensitive data.
Research
Security News
Attackers used a malicious npm package typosquatting a popular ESLint plugin to steal sensitive data, execute commands, and exploit developer systems.
Security News
The Ultralytics' PyPI Package was compromised four times in one weekend through GitHub Actions cache poisoning and failure to rotate previously compromised API tokens.