djangocms-render-context 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ __version__ = "1.0.0"
@@ -0,0 +1,2 @@
1
+ def get_cache_key(identifier: int) -> str:
2
+ return f"djangocms_render_context_url_{identifier}"
@@ -0,0 +1,50 @@
1
+ from cms.models.placeholdermodel import Placeholder
2
+ from cms.plugin_base import CMSPluginBase
3
+ from cms.plugin_pool import plugin_pool
4
+ from django.template import Template
5
+ from django.template.backends.django import Template as DjangoTemplate
6
+ from django.template.loader import get_template
7
+ from django.utils.translation import gettext_lazy as _
8
+
9
+ from .forms import RenderContextForm
10
+ from .models import RenderContext
11
+ from .utils import create_html
12
+
13
+
14
+ @plugin_pool.register_plugin
15
+ class RenderContextPlugin(CMSPluginBase):
16
+ model = RenderContext
17
+ form = RenderContextForm
18
+ name = _("Render Context Plugin")
19
+ fieldsets = [
20
+ (
21
+ _("Sources"),
22
+ {
23
+ "fields": (
24
+ "context",
25
+ "file",
26
+ (
27
+ "source",
28
+ "cached",
29
+ ),
30
+ ),
31
+ },
32
+ ),
33
+ (
34
+ _("Templates"),
35
+ {
36
+ "classes": ["collapse"],
37
+ "fields": (
38
+ "template",
39
+ "template_list",
40
+ ),
41
+ },
42
+ ),
43
+ ]
44
+
45
+ def get_render_template(self, context: dict, instance: RenderContext, placeholder: Placeholder) -> DjangoTemplate:
46
+ context["data"] = instance.get_data()
47
+ if instance.template_list and not instance.template:
48
+ return get_template(instance.template_list)
49
+ tmpl = Template(instance.template if instance.template else create_html(context["data"]))
50
+ return DjangoTemplate(tmpl, tmpl)
@@ -0,0 +1,8 @@
1
+ from django.core.serializers.json import DjangoJSONEncoder
2
+
3
+
4
+ class PrettyJsonEncoder(DjangoJSONEncoder):
5
+ def __init__(self, *args, **kwargs):
6
+ kwargs["indent"] = 2
7
+ kwargs["sort_keys"] = False
8
+ super().__init__(*args, **kwargs)
@@ -0,0 +1,2 @@
1
+ class SourceParseFailure(Exception):
2
+ """Source parse failure."""
@@ -0,0 +1,95 @@
1
+ from collections.abc import Callable
2
+ from typing import Union
3
+
4
+ from django import forms
5
+ from django.conf import settings
6
+ from django.core.cache import cache
7
+ from django.core.exceptions import ValidationError
8
+ from django.forms import ALL_FIELDS, ModelForm
9
+ from django.template import Context, Template, TemplateDoesNotExist
10
+ from django.template.exceptions import TemplateSyntaxError
11
+ from django.template.loader import get_template
12
+ from django.utils.translation import gettext_lazy as _
13
+ from filer.models.filemodels import File
14
+
15
+ from .cache import get_cache_key
16
+ from .exceptions import SourceParseFailure
17
+ from .loaders import SUPPORTED_FILE_TYPES, get_data_from_file, get_data_from_url
18
+
19
+ TEMPLATES = (("", ""),)
20
+
21
+ sourceType = Union[str, File] # noqa: UP007
22
+
23
+
24
+ class RenderContextForm(ModelForm):
25
+ """Render Context Form."""
26
+
27
+ template_list = forms.ChoiceField(
28
+ label=_("Template list"),
29
+ required=False,
30
+ choices=getattr(settings, "DJANGOCMS_RENDER_CONTEXT_TEMPLATES", TEMPLATES),
31
+ help_text=_("List of templates. If Template is specified, the template set in the list will not be used."),
32
+ )
33
+
34
+ def __init__(self, *args, **kwargs):
35
+ super().__init__(*args, **kwargs)
36
+ self.fields["file"].help_text += " " + _("Supported formats are:") + " " + ", ".join(SUPPORTED_FILE_TYPES)
37
+
38
+ def clean_template_list(self) -> None:
39
+ if self.cleaned_data["template_list"]:
40
+ try:
41
+ get_template(self.cleaned_data["template_list"])
42
+ except TemplateSyntaxError as error:
43
+ raise ValidationError(error) from error
44
+ except TemplateDoesNotExist as error:
45
+ raise ValidationError(f'Template "{error}" does not exist.') from error
46
+ return self.cleaned_data["template_list"]
47
+
48
+ def check_value(self, name: str, value: sourceType, loader: Callable) -> Context:
49
+ try:
50
+ return Context(loader(value))
51
+ except SourceParseFailure as error:
52
+ self.add_error(name, error)
53
+
54
+ def check_context(self, cleaned_data: dict) -> Context:
55
+ context = Context({})
56
+ if cleaned_data["context"]:
57
+ context = Context(cleaned_data["context"])
58
+ elif cleaned_data["file"]:
59
+ context = self.check_value("file", cleaned_data["file"], get_data_from_file)
60
+ elif cleaned_data["source"]:
61
+ context = self.check_value("source", cleaned_data["source"], get_data_from_url)
62
+ if context is None:
63
+ self.add_error(ALL_FIELDS, "Failed to set context.")
64
+ context = Context({})
65
+ return context
66
+
67
+ def check_template(self, cleaned_data: dict, context: Context) -> None:
68
+ template = None
69
+ field_name = ALL_FIELDS
70
+ if cleaned_data["template"]:
71
+ field_name = "template"
72
+ try:
73
+ template = Template(cleaned_data["template"])
74
+ except TemplateSyntaxError as error:
75
+ self.add_error("template", error)
76
+ elif cleaned_data["template_list"]:
77
+ field_name = "template_list"
78
+ template = get_template(cleaned_data["template_list"]).template
79
+ if template:
80
+ try:
81
+ template.render(context)
82
+ except (TemplateDoesNotExist, TemplateSyntaxError) as error:
83
+ self.add_error(field_name, error)
84
+
85
+ def clean(self) -> None:
86
+ """Clean form."""
87
+ cleaned_data = super().clean()
88
+ if self.is_valid():
89
+ context = self.check_context(cleaned_data)
90
+ self.check_template(cleaned_data, context)
91
+
92
+ def save(self, *args, **kwargs):
93
+ if self.instance.pk:
94
+ cache.delete(get_cache_key(self.instance.pk))
95
+ return super().save(*args, **kwargs)
@@ -0,0 +1,214 @@
1
+ import csv
2
+ import json
3
+ import xml.etree.ElementTree as ET
4
+ import zipfile
5
+ from io import BytesIO, StringIO
6
+ from mimetypes import guess_type
7
+ from typing import Union, cast
8
+
9
+ import requests
10
+ import yaml
11
+ from django.conf import settings
12
+ from django.core.cache import cache
13
+ from django.utils.translation import gettext_lazy as _
14
+ from filer.models.filemodels import File
15
+
16
+ # Use the C (faster) implementation if possible
17
+ try:
18
+ from yaml import CSafeLoader as SafeLoader
19
+ except ImportError: # pragma: no cover
20
+ from yaml import SafeLoader # type: ignore
21
+
22
+ from .cache import get_cache_key
23
+ from .exceptions import SourceParseFailure
24
+
25
+ dataType = Union[dict, list] # noqa: UP007
26
+ valueType = Union[str, list] # noqa: UP007
27
+ cellType = Union[dict, str, list] # noqa: UP007
28
+ nodeTextType = Union[str, None] # noqa: UP007
29
+ payloadType = Union[str, dict] # noqa: UP007
30
+
31
+
32
+ def get_mime_type_message(mime_type: nodeTextType) -> str:
33
+ return (
34
+ _("Unsupported file mime type: ")
35
+ + str(mime_type)
36
+ + ". "
37
+ + _("Only allowed are:")
38
+ + " "
39
+ + ", ".join(SUPPORTED_FILE_TYPES)
40
+ + "."
41
+ )
42
+
43
+
44
+ def get_data_from_file(source: File) -> dataType:
45
+ if source.mime_type not in LOADERS:
46
+ raise SourceParseFailure(get_mime_type_message(source.mime_type))
47
+ return LOADERS[source.mime_type](source.file.read())
48
+
49
+
50
+ def load_json(content: bytes) -> dataType:
51
+ try:
52
+ return json.load(BytesIO(content))
53
+ except json.decoder.JSONDecodeError as err:
54
+ raise SourceParseFailure(err) from err
55
+
56
+
57
+ def load_yaml(content: bytes) -> dataType:
58
+ try:
59
+ return yaml.load(content, Loader=SafeLoader)
60
+ except yaml.YAMLError as err:
61
+ raise SourceParseFailure(err) from err
62
+
63
+
64
+ def load_csv(content: bytes) -> list:
65
+ try:
66
+ body = content.decode("utf8")
67
+ except UnicodeDecodeError as err:
68
+ raise SourceParseFailure(err) from err
69
+ reader = csv.reader(StringIO(body))
70
+ try:
71
+ return list(reader)
72
+ except csv.Error as err:
73
+ raise SourceParseFailure(err) from err
74
+
75
+
76
+ def load_xml(content: bytes) -> dataType:
77
+ data = []
78
+ try:
79
+ doc = ET.parse(BytesIO(content))
80
+ except ET.ParseError as err:
81
+ raise SourceParseFailure(err) from err
82
+ root = doc.getroot()
83
+ for row in root:
84
+ data.append([column.text for column in row])
85
+ return data
86
+
87
+
88
+ def load_spreadsheet(content: bytes) -> dataType:
89
+ handle = None
90
+ try:
91
+ handle = zipfile.ZipFile(BytesIO(content))
92
+ payload = handle.read("content.xml")
93
+ except (zipfile.BadZipFile, KeyError) as err:
94
+ raise SourceParseFailure(err) from err
95
+ finally:
96
+ if handle is None:
97
+ pass
98
+ else:
99
+ handle.close()
100
+ try:
101
+ doc = ET.parse(BytesIO(payload))
102
+ except ET.ParseError as err:
103
+ raise SourceParseFailure(err) from err
104
+ return pase_data(cast(ET.ElementTree, doc))
105
+
106
+
107
+ def pase_data(doc: ET.ElementTree) -> dataType:
108
+ data: list[list[cellType]] = []
109
+ ns = {
110
+ "table": "urn:oasis:names:tc:opendocument:xmlns:table:1.0",
111
+ "text": "urn:oasis:names:tc:opendocument:xmlns:text:1.0",
112
+ "xlink": "http://www.w3.org/1999/xlink",
113
+ }
114
+ max_gap = getattr(settings, "DJANGOCMS_RENDER_CONTEXT_ODS_MAX_GAP", 51)
115
+ root = doc.getroot()
116
+ if root is None:
117
+ return data
118
+ table = root.find(".//table:table", ns)
119
+ if table is not None:
120
+ for row in table.findall("table:table-row", ns):
121
+ repeated = row.get(qname(ns, "table:number-rows-repeated"))
122
+ if repeated is not None:
123
+ offset = int(repeated) - 1
124
+ if offset < max_gap:
125
+ data.extend([] * offset)
126
+ data.append(parse_cells(ns, max_gap, row))
127
+ while data and data[-1] == [""]:
128
+ data.pop()
129
+ return data
130
+
131
+
132
+ def parse_cells(ns: dict[str, str], max_gap: int, row: ET.Element) -> list[cellType]:
133
+ line: list[cellType] = []
134
+ for cell in row.findall("table:table-cell", ns):
135
+ repeated = cell.get(qname(ns, "table:number-columns-repeated"))
136
+ if repeated is not None:
137
+ offset = int(repeated) - 1
138
+ if offset < max_gap:
139
+ line.extend([""] * offset)
140
+ payload: list[payloadType] = []
141
+ for text in cell.findall("text:p", ns):
142
+ if text.text is not None:
143
+ payload.append({"p": text.text})
144
+ for link in text.findall("text:a", ns):
145
+ href = link.get(qname(ns, "xlink:href"))
146
+ payload.append({"href": href, "text": none_to_str(link.text)})
147
+ if len(payload) == 1:
148
+ payload = payload[0] # type: ignore
149
+ if "p" in payload:
150
+ payload = payload["p"] # type: ignore
151
+ if payload == []:
152
+ payload = "" # type: ignore
153
+ line.append(payload)
154
+ return line
155
+
156
+
157
+ def none_to_str(value: nodeTextType) -> str:
158
+ return "" if value is None else str(value)
159
+
160
+
161
+ def qname(ns: dict[str, str], prefix_and_name: str) -> str:
162
+ """Create qualified xml element name."""
163
+ parts = prefix_and_name.split(":", 1)
164
+ prefix, name = parts
165
+ return str(ET.QName(ns[prefix], name))
166
+
167
+
168
+ def load_source(url: str):
169
+ """Load data from the source."""
170
+ timeout = getattr(settings, "DJANGOCMS_RENDER_CONTEXT_LOAD_TIMEOUT", 6)
171
+ verify = getattr(settings, "DJANGOCMS_RENDER_CONTEXT_VERIFY", True)
172
+ response = requests.get(url, timeout=timeout, verify=verify)
173
+ response.raise_for_status()
174
+ return response
175
+
176
+
177
+ def get_data_from_url(source: str) -> dataType:
178
+ data: dataType = []
179
+ try:
180
+ response = load_source(source)
181
+ except requests.RequestException as err:
182
+ raise SourceParseFailure(err) from err
183
+ content_type = response.headers.get("Content-Type")
184
+ if content_type not in LOADERS:
185
+ content_type = guess_type(source)[0]
186
+ if content_type not in LOADERS:
187
+ raise SourceParseFailure(get_mime_type_message(content_type))
188
+ try:
189
+ data = LOADERS[content_type](response.content)
190
+ except SourceParseFailure as err:
191
+ raise SourceParseFailure(_("Resource parsing failed.")) from err
192
+ return data
193
+
194
+
195
+ def get_cached_data_from_url(identifier: int, source: str, timeout: int) -> dataType:
196
+ key = get_cache_key(identifier)
197
+ if timeout:
198
+ data = cache.get(key)
199
+ if data is None:
200
+ data = get_data_from_url(source)
201
+ cache.set(key, data, timeout * 60)
202
+ else:
203
+ data = get_data_from_url(source)
204
+ return data
205
+
206
+
207
+ SUPPORTED_FILE_TYPES = ("csv", "json", "yaml", "xml", "ods")
208
+ LOADERS = {
209
+ "text/csv": load_csv,
210
+ "application/json": load_json,
211
+ "application/yaml": load_yaml,
212
+ "application/xml": load_xml,
213
+ "application/vnd.oasis.opendocument.spreadsheet": load_spreadsheet,
214
+ }
@@ -0,0 +1,107 @@
1
+ # SOME DESCRIPTIVE TITLE.
2
+ # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
3
+ # This file is distributed under the same license as the PACKAGE package.
4
+ # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
5
+ #
6
+ msgid ""
7
+ msgstr ""
8
+ "Project-Id-Version: PACKAGE VERSION\n"
9
+ "Report-Msgid-Bugs-To: \n"
10
+ "POT-Creation-Date: 2025-12-11 13:17+0100\n"
11
+ "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
12
+ "Last-Translator: Zdeněk Böhm <zdenek.bohm@nic.cz>\n"
13
+ "Language-Team: LANGUAGE <LL@li.org>\n"
14
+ "Language: cs\n"
15
+ "MIME-Version: 1.0\n"
16
+ "Content-Type: text/plain; charset=UTF-8\n"
17
+ "Content-Transfer-Encoding: 8bit\n"
18
+ "Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n"
19
+
20
+ msgid ""
21
+ "Context data file. If Data is specified for the context, the source file is "
22
+ "not used even though it is specified."
23
+ msgstr ""
24
+ "Soubor se zdrojovými daty. Pokud jsou zadána Data pro kontext, tak se "
25
+ "zdrojový soubor nepoužije i přesto, že je zadán."
26
+
27
+ msgid ""
28
+ "Context data in JSON format. They take precedence over the source file and "
29
+ "source URL."
30
+ msgstr ""
31
+ "Data pro kontext ve formátu JSON. Mají přednost před zdrojovým souborem a "
32
+ "URL zdroje."
33
+
34
+ msgid "Data for context"
35
+ msgstr "Data pro kontext"
36
+
37
+ msgid "Data not entered."
38
+ msgstr "Data nejsou zadána."
39
+
40
+ msgid ""
41
+ "Django template for the context. It takes precedence over the template "
42
+ "selected from the list."
43
+ msgstr "Django šablona. Má přednost přes šablonou vybranou ze seznamu."
44
+
45
+ msgid "Download period from source URL"
46
+ msgstr "Perioda stahování ze URL zdroje"
47
+
48
+ msgid ""
49
+ "List of templates. If Template is specified, the template set in the list "
50
+ "will not be used."
51
+ msgstr ""
52
+ "Seznam šablon. Pokud je zadána Šablona, tak se šablona nastavená v seznamu "
53
+ "nepoužije."
54
+
55
+ msgid "No template."
56
+ msgstr "Žádná šablona."
57
+
58
+ msgid "Only allowed are:"
59
+ msgstr "Povolené jsou jen:"
60
+
61
+ msgid "Render Context Plugin"
62
+ msgstr "Vykreslování kontextu"
63
+
64
+ msgid "Resource parsing failed."
65
+ msgstr "Parsování zdroje selhalo."
66
+
67
+ msgid "Source URL"
68
+ msgstr "URL zdroje"
69
+
70
+ msgid "Source file"
71
+ msgstr "Zdrojový soubor."
72
+
73
+ msgid "Sources"
74
+ msgstr "Zdroje"
75
+
76
+ msgid "Supported formats are:"
77
+ msgstr "Podporované formáty jsou:"
78
+
79
+ msgid "Template"
80
+ msgstr "Šablona"
81
+
82
+ msgid "Template list"
83
+ msgstr "Seznam šablon"
84
+
85
+ msgid "Templates"
86
+ msgstr "Šablony"
87
+
88
+ msgid ""
89
+ "The URL of the source from which the data will be downloaded. If Data for "
90
+ "Context or Source File is specified, the source URL is not used even though "
91
+ "it is specified."
92
+ msgstr ""
93
+ "URL zdroje, ze kterého se data budou stahovat. Pokud jsou zadána Data pro "
94
+ "kontext nebo Zdrojový soubor, tak se URL zdroje nepoužije i přesto, že je "
95
+ "zadáno."
96
+
97
+ msgid ""
98
+ "The time in minutes during which data will not be retrieved from the Source "
99
+ "URL, but will remain in the cache. If set to zero, data from the source URL "
100
+ "is not cached, but is loaded every time."
101
+ msgstr ""
102
+ "Doba v minutách, po kterou se data nebudou načítat z URL zdroje, ale "
103
+ "zůstanou ponechána v cache. Je-li nastavena nula, tak se data z URL zdroje "
104
+ "do cache neukládají, ale načítají se pokaždé."
105
+
106
+ msgid "Unsupported file mime type: "
107
+ msgstr "Nepodporovaný mime typ souboru: "
@@ -0,0 +1,96 @@
1
+ # Generated by Django 5.2.8 on 2025-12-10 09:02
2
+
3
+ import django.db.models.deletion
4
+ import djangocms_render_context.encoders
5
+ import filer.fields.file
6
+ from django.db import migrations, models
7
+
8
+
9
+ class Migration(migrations.Migration):
10
+
11
+ initial = True
12
+
13
+ dependencies = [
14
+ ("cms", "0034_remove_pagecontent_placeholders"),
15
+ ("filer", "0017_image__transparent"),
16
+ ]
17
+
18
+ operations = [
19
+ migrations.CreateModel(
20
+ name="RenderContext",
21
+ fields=[
22
+ (
23
+ "cmsplugin_ptr",
24
+ models.OneToOneField(
25
+ auto_created=True,
26
+ on_delete=django.db.models.deletion.CASCADE,
27
+ parent_link=True,
28
+ primary_key=True,
29
+ related_name="%(app_label)s_%(class)s",
30
+ serialize=False,
31
+ to="cms.cmsplugin",
32
+ ),
33
+ ),
34
+ (
35
+ "context",
36
+ models.JSONField(
37
+ blank=True,
38
+ encoder=djangocms_render_context.encoders.PrettyJsonEncoder,
39
+ help_text="Context data in JSON format. They take precedence over the source file and source URL.",
40
+ null=True,
41
+ verbose_name="Data for context",
42
+ ),
43
+ ),
44
+ (
45
+ "source",
46
+ models.URLField(
47
+ blank=True,
48
+ help_text="The URL of the source from which the data will be downloaded. If Data for Context or Source File is specified, "
49
+ "the source URL is not used even though it is specified.",
50
+ null=True,
51
+ verbose_name="Source URL",
52
+ ),
53
+ ),
54
+ (
55
+ "cached",
56
+ models.PositiveSmallIntegerField(
57
+ default=5,
58
+ help_text="The time in minutes during which data will not be retrieved from the Source URL, but will remain in the cache. "
59
+ "If set to zero, data from the source URL is not cached, but is loaded every time.",
60
+ verbose_name="Download period from source URL",
61
+ ),
62
+ ),
63
+ (
64
+ "template",
65
+ models.TextField(
66
+ blank=True,
67
+ help_text="Django template for the context. It takes precedence over the template selected from the list.",
68
+ null=True,
69
+ verbose_name="Template",
70
+ ),
71
+ ),
72
+ (
73
+ "template_list",
74
+ models.CharField(
75
+ blank=True,
76
+ help_text="List of templates. If Template is specified, the template set in the list will not be used.",
77
+ max_length=255,
78
+ null=True,
79
+ verbose_name="Template list",
80
+ ),
81
+ ),
82
+ (
83
+ "file",
84
+ filer.fields.file.FilerFileField(
85
+ blank=True,
86
+ help_text="Context data file. If Data is specified for the context, the source file is not used even though it is specified.",
87
+ null=True,
88
+ on_delete=django.db.models.deletion.SET_NULL,
89
+ to="filer.file",
90
+ verbose_name="Source file",
91
+ ),
92
+ ),
93
+ ],
94
+ bases=("cms.cmsplugin",),
95
+ ),
96
+ ]
File without changes
@@ -0,0 +1,96 @@
1
+ import logging
2
+
3
+ from cms.models.pluginmodel import CMSPlugin
4
+ from django.db import models
5
+ from django.utils.translation import gettext_lazy as _
6
+ from filer.fields.file import FilerFileField
7
+
8
+ from .encoders import PrettyJsonEncoder
9
+ from .exceptions import SourceParseFailure
10
+ from .loaders import get_cached_data_from_url, get_data_from_file
11
+
12
+ LOGGER = logging.getLogger(__name__)
13
+
14
+
15
+ class RenderContext(CMSPlugin):
16
+ context = models.JSONField(
17
+ verbose_name=_("Data for context"),
18
+ null=True,
19
+ blank=True,
20
+ encoder=PrettyJsonEncoder,
21
+ help_text=_("Context data in JSON format. They take precedence over the source file and source URL."),
22
+ )
23
+ file = FilerFileField(
24
+ verbose_name=_("Source file"),
25
+ null=True,
26
+ blank=True,
27
+ on_delete=models.SET_NULL,
28
+ help_text=_(
29
+ "Context data file. If Data is specified for the context, the source file is not used even "
30
+ "though it is specified."
31
+ ),
32
+ )
33
+ source = models.URLField(
34
+ verbose_name=_("Source URL"),
35
+ null=True,
36
+ blank=True,
37
+ help_text=_(
38
+ "The URL of the source from which the data will be downloaded. If Data for Context or Source File"
39
+ " is specified, the source URL is not used even though it is specified."
40
+ ),
41
+ )
42
+ cached = models.PositiveSmallIntegerField(
43
+ verbose_name=_("Download period from source URL"),
44
+ default=5,
45
+ help_text=_(
46
+ "The time in minutes during which data will not be retrieved from the Source URL, but will remain "
47
+ "in the cache. If set to zero, data from the source URL is not cached, but is loaded every time."
48
+ ),
49
+ )
50
+
51
+ template = models.TextField(
52
+ verbose_name=_("Template"),
53
+ null=True,
54
+ blank=True,
55
+ help_text=_("Django template for the context. It takes precedence over the template selected from the list."),
56
+ )
57
+ template_list = models.CharField(
58
+ verbose_name=_("Template list"),
59
+ null=True,
60
+ blank=True,
61
+ max_length=255,
62
+ help_text=_("List of templates. If Template is specified, the template set in the list will not be used."),
63
+ )
64
+
65
+ def __str__(self):
66
+ text = []
67
+ if self.context:
68
+ text.append(self._meta.get_field("context").verbose_name)
69
+ elif self.file:
70
+ text.append(self._meta.get_field("file").verbose_name)
71
+ elif self.source:
72
+ text.append(self._meta.get_field("source").verbose_name)
73
+ else:
74
+ text.append(_("Data not entered."))
75
+ if self.template:
76
+ text.append(self._meta.get_field("template").verbose_name)
77
+ elif self.template_list:
78
+ text.append(self._meta.get_field("template_list").verbose_name)
79
+ else:
80
+ text.append(_("No template."))
81
+ return " + ".join([str(t) for t in text])
82
+
83
+ def get_data(self):
84
+ if self.context:
85
+ return self.context
86
+ elif self.file:
87
+ try:
88
+ return get_data_from_file(self.file)
89
+ except SourceParseFailure as err:
90
+ LOGGER.error(err)
91
+ elif self.source:
92
+ try:
93
+ return get_cached_data_from_url(self.pk, self.source, self.cached)
94
+ except SourceParseFailure as err:
95
+ LOGGER.error(err)
96
+ return None
@@ -0,0 +1,46 @@
1
+ from collections.abc import ValuesView
2
+ from datetime import date, datetime
3
+ from typing import Union
4
+
5
+ from django.utils.formats import localize
6
+
7
+ dataType = Union[str, dict, list, ValuesView, None] # noqa: UP007
8
+
9
+
10
+ def get_data(data: dataType, level: int = 0) -> str:
11
+ wrapper, separator = "{}", "\n"
12
+ if level == 0:
13
+ separator = "</tr>\n<tr>\n"
14
+ if level == 1:
15
+ wrapper = "<td>{}</td>"
16
+ if data is None:
17
+ content = wrapper.format("")
18
+ elif isinstance(data, (date, datetime)):
19
+ content = wrapper.format(localize(data))
20
+ elif not isinstance(data, (tuple, list, dict, ValuesView)):
21
+ content = wrapper.format(data)
22
+ else:
23
+ content = collect_data(data, wrapper, separator, level)
24
+ return content
25
+
26
+
27
+ def collect_data(data: dataType, wrapper: str, separator: str, level: int) -> str:
28
+ content = []
29
+ if isinstance(data, (tuple, list, ValuesView)):
30
+ for item in data:
31
+ content.append(wrapper.format(get_data(item, level + 1)))
32
+ elif isinstance(data, dict):
33
+ if "href" in data and "text" in data:
34
+ href, text = data.pop("href"), data.pop("text")
35
+ content.append(f"""<a href="{href}">{text}</a>""")
36
+ if "p" in data:
37
+ para = data.pop("p")
38
+ content.append(f"""<p>{para}</p>""")
39
+ content.append(wrapper.format(get_data(data.values(), level + 1)))
40
+ else:
41
+ content.append(wrapper.format(get_data(data, level + 1)))
42
+ return separator.join(content)
43
+
44
+
45
+ def create_html(data: dataType) -> str:
46
+ return f"""<table class="rc-data"><tbody><tr>{get_data(data)}</tr></tbody></table>"""
@@ -0,0 +1,93 @@
1
+ Metadata-Version: 2.4
2
+ Name: djangocms-render-context
3
+ Version: 1.0.0
4
+ Summary: Render context into template.
5
+ Author-email: Zdeněk Böhm <zdenek.bohm@nic.cz>
6
+ License-Expression: BSD-3-Clause
7
+ Project-URL: Homepage, https://gitlab.nic.cz/djangocms-apps/djangocms-render-context
8
+ Project-URL: Repository, https://gitlab.nic.cz/djangocms-apps/djangocms-render-context.git
9
+ Project-URL: Changelog, https://gitlab.nic.cz/djangocms-apps/djangocms-render-context/-/blob/main/CHANGELOG.md
10
+ Classifier: Development Status :: 5 - Production/Stable
11
+ Classifier: Framework :: Django
12
+ Classifier: Framework :: Django :: 4.2
13
+ Classifier: Framework :: Django :: 5.0
14
+ Classifier: Framework :: Django :: 5.1
15
+ Classifier: Framework :: Django :: 5.2
16
+ Classifier: Framework :: Django CMS
17
+ Classifier: Framework :: Django CMS :: 4.1
18
+ Classifier: Framework :: Django CMS :: 5.0
19
+ Classifier: Programming Language :: Python :: 3 :: Only
20
+ Classifier: Programming Language :: Python :: 3.9
21
+ Classifier: Programming Language :: Python :: 3.10
22
+ Classifier: Programming Language :: Python :: 3.11
23
+ Classifier: Programming Language :: Python :: 3.12
24
+ Classifier: Programming Language :: Python :: 3.13
25
+ Requires-Python: >=3.9
26
+ Description-Content-Type: text/markdown
27
+ Requires-Dist: django-cms<6,>=4.0
28
+ Requires-Dist: django-filer~=3.4
29
+ Requires-Dist: pyyaml
30
+ Requires-Dist: requests
31
+ Provides-Extra: quality
32
+ Requires-Dist: ruff; extra == "quality"
33
+ Requires-Dist: mypy; extra == "quality"
34
+ Requires-Dist: types-PyYAML; extra == "quality"
35
+ Requires-Dist: types-requests; extra == "quality"
36
+ Provides-Extra: test
37
+ Requires-Dist: testfixtures; extra == "test"
38
+ Requires-Dist: responses; extra == "test"
39
+
40
+ # DjangoCMS Render Context
41
+
42
+ The plugin for the [Django CMS](https://www.django-cms.org/) content management system.
43
+ The plugin displays the context in the template. The context can be specified directly in JSON format.
44
+ Or the context can be used as a media file. Or the context can be loaded from a URL.
45
+ The template can be entered directly or selected from a list defined in the settings in the ``DJANGOCMS_RENDER_CONTEXT_TEMPLATES`` constant.
46
+
47
+ Supported source data formats (mimetype):
48
+
49
+ - csv (text/csv)
50
+ - json (application/json)
51
+ - yaml (application/yaml)
52
+ - xml (application/xml)
53
+ - ods (application/vnd.oasis.opendocument.spreadsheet)
54
+
55
+ ## Install
56
+
57
+ Install the package from pypi.org.
58
+
59
+ ```
60
+ pip install djangocms-render-context
61
+ ```
62
+
63
+ Add into `INSTALLED_APPS` in your site `settings.py`:
64
+
65
+ ```python
66
+ INSTALLED_APPS = [
67
+ ...
68
+ "easy_thumbnails",
69
+ "filer",
70
+ "djangocms_render_context",
71
+ ]
72
+ ```
73
+
74
+ ### Extra settings
75
+
76
+ This value can be defined in settings.
77
+
78
+ - `DJANGOCMS_RENDER_CONTEXT_TEMPLATES` - List of templates that the plugin can use.
79
+
80
+ For example:
81
+
82
+ ```python
83
+ DJANGOCMS_RENDER_CONTEXT_TEMPLATES = (
84
+ ("", ""),
85
+ ("plugin.html", "Plugin"),
86
+ ("schedule.html", "Schedule"),
87
+ )
88
+ ```
89
+
90
+
91
+ ## License
92
+
93
+ BSD-3-Clause
@@ -0,0 +1,17 @@
1
+ djangocms_render_context/__init__.py,sha256=J-j-u0itpEFT6irdmWmixQqYMadNl1X91TxUmoiLHMI,22
2
+ djangocms_render_context/cache.py,sha256=PwagqDTqh3I83e7Ru52ei4P05juZ6Ob_pILukGGtsTo,99
3
+ djangocms_render_context/cms_plugins.py,sha256=YBNH5Rh79xdC2puO3HOLZ0R6miUDBelGd-rmPAQ2Epo,1591
4
+ djangocms_render_context/encoders.py,sha256=TIIcj_6vgXlUH5D0oeozCA7ixN4WNnfrnk15bAmfpQ8,253
5
+ djangocms_render_context/exceptions.py,sha256=r5L-U90T_60CPSagcwWmu_r6DkbHAghg04uS8OvzTPo,69
6
+ djangocms_render_context/forms.py,sha256=bpfoWoHUZXzhobH_fekH5txXRLQIKjMsxl8n6Yi-6fs,3754
7
+ djangocms_render_context/loaders.py,sha256=JUFOwXCPbRG-4J79nrIT1AiRgq_je7km5VGhnO22Uxs,6774
8
+ djangocms_render_context/models.py,sha256=BmdQQi47vZjkf1vaQr7KyfeSw9tAmY0atnoNfA2AgxI,3404
9
+ djangocms_render_context/utils.py,sha256=HmkARbCxCJ6-SNRuH7zI4BY3BBf87c9cQuwBUNId53I,1632
10
+ djangocms_render_context/locale/cs/LC_MESSAGES/django.mo,sha256=sxF9h5d4-av6CQoVXCSIKuIw_KMyCzmKJ7N74rD0hus,2903
11
+ djangocms_render_context/locale/cs/LC_MESSAGES/django.po,sha256=bQMJnXI6CkCbMfbJ56SvirX1_rJOVwFLxJjFe2tsQeE,3157
12
+ djangocms_render_context/migrations/0001_initial.py,sha256=bdIs0Jl8M8JcC-i03vxp612Lim_mUJ-BOaq2D3YI6uw,3854
13
+ djangocms_render_context/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
+ djangocms_render_context-1.0.0.dist-info/METADATA,sha256=k1wb9eo0N7fBlLk8Xm4nv5zWddbQSJRjrcsQxYwEmSA,2865
15
+ djangocms_render_context-1.0.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
16
+ djangocms_render_context-1.0.0.dist-info/top_level.txt,sha256=J82yBJeJTHoDzwzFIU3i4jFHxn3nAjgn-Fn5JSjVKQ8,25
17
+ djangocms_render_context-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ djangocms_render_context