django-spire 0.17.11__py3-none-any.whl → 0.18.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.
- django_spire/consts.py +1 -1
- django_spire/contrib/seeding/model/base.py +1 -1
- django_spire/knowledge/entry/services/automation_service.py +7 -4
- django_spire/knowledge/entry/services/transformation_services.py +2 -2
- django_spire/knowledge/entry/version/block/choices.py +1 -2
- django_spire/knowledge/entry/version/block/data/data.py +33 -0
- django_spire/knowledge/entry/version/block/data/heading_data.py +15 -0
- django_spire/knowledge/entry/version/block/data/list/choices.py +17 -0
- django_spire/knowledge/entry/version/block/data/list/data.py +102 -0
- django_spire/knowledge/entry/version/block/data/list/maps.py +14 -0
- django_spire/knowledge/entry/version/block/data/list/meta.py +15 -0
- django_spire/knowledge/entry/version/block/data/maps.py +23 -0
- django_spire/knowledge/entry/version/block/data/text_data.py +13 -0
- django_spire/knowledge/entry/version/block/models.py +22 -14
- django_spire/knowledge/entry/version/block/querysets.py +17 -7
- django_spire/knowledge/entry/version/block/seeding/constants.py +271 -188
- django_spire/knowledge/entry/version/block/services/factory_service.py +14 -47
- django_spire/knowledge/entry/version/block/services/service.py +2 -12
- django_spire/knowledge/entry/version/block/tests/factories.py +18 -2
- django_spire/knowledge/entry/version/converters/markdown_converter.py +180 -105
- django_spire/knowledge/entry/version/maps.py +1 -1
- django_spire/knowledge/entry/version/models.py +1 -0
- django_spire/knowledge/entry/version/querysets.py +11 -1
- django_spire/knowledge/entry/version/seeding/seeder.py +4 -2
- django_spire/knowledge/entry/version/services/processor_service.py +18 -0
- django_spire/knowledge/entry/version/services/tests/test_processor_service.py +0 -0
- django_spire/knowledge/entry/version/tests/test_urls/test_json_urls.py +7 -24
- django_spire/knowledge/entry/version/urls/__init__.py +0 -3
- django_spire/knowledge/entry/version/urls/json_urls.py +5 -13
- django_spire/knowledge/entry/version/views/json_views.py +8 -76
- django_spire/knowledge/entry/version/views/page_views.py +10 -5
- django_spire/knowledge/migrations/0005_entryversionblock__tunes_data_and_more.py +23 -0
- django_spire/knowledge/static/django_spire/knowledge/css/navigation_items.css +1 -1
- django_spire/knowledge/static/django_spire/knowledge/entry/version/js/editor.js +87 -0
- django_spire/knowledge/static/django_spire/knowledge/entry/version/js/null_paragraph.js +15 -0
- django_spire/knowledge/templates/django_spire/knowledge/entry/file/page/list_page.html +1 -1
- django_spire/knowledge/templates/django_spire/knowledge/entry/version/container/detail_container.html +43 -12
- django_spire/knowledge/templates/django_spire/knowledge/entry/version/page/detail_page.html +17 -1
- django_spire/knowledge/templates/django_spire/knowledge/navigation/page/full_page.html +1 -1
- {django_spire-0.17.11.dist-info → django_spire-0.18.0.dist-info}/METADATA +2 -1
- {django_spire-0.17.11.dist-info → django_spire-0.18.0.dist-info}/RECORD +47 -60
- django_spire/knowledge/entry/version/block/blocks/block.py +0 -51
- django_spire/knowledge/entry/version/block/blocks/heading_block.py +0 -14
- django_spire/knowledge/entry/version/block/blocks/list_block.py +0 -31
- django_spire/knowledge/entry/version/block/blocks/sub_heading_block.py +0 -14
- django_spire/knowledge/entry/version/block/blocks/text_block.py +0 -14
- django_spire/knowledge/entry/version/block/maps.py +0 -16
- django_spire/knowledge/entry/version/block/services/processor_service.py +0 -33
- django_spire/knowledge/entry/version/block/services/transformation_service.py +0 -35
- django_spire/knowledge/entry/version/block/tests/test_urls/test_json_urls.py +0 -28
- django_spire/knowledge/entry/version/block/urls/__init__.py +0 -13
- django_spire/knowledge/entry/version/block/urls/json_urls.py +0 -9
- django_spire/knowledge/entry/version/block/views/json_views.py +0 -30
- django_spire/knowledge/entry/version/tests/test_urls/test_form_urls.py +0 -28
- django_spire/knowledge/entry/version/urls/form_urls.py +0 -10
- django_spire/knowledge/entry/version/views/form_views.py +0 -63
- django_spire/knowledge/templates/django_spire/knowledge/entry/version/block/detail/component/heading_component.html +0 -3
- django_spire/knowledge/templates/django_spire/knowledge/entry/version/block/detail/component/list_item_component.html +0 -38
- django_spire/knowledge/templates/django_spire/knowledge/entry/version/block/detail/component/sub_heading_component.html +0 -3
- django_spire/knowledge/templates/django_spire/knowledge/entry/version/block/detail/component/text_component.html +0 -3
- django_spire/knowledge/templates/django_spire/knowledge/entry/version/block/dropdown/add_dropdown.html +0 -25
- django_spire/knowledge/templates/django_spire/knowledge/entry/version/block/update/component/heading_component.html +0 -3
- django_spire/knowledge/templates/django_spire/knowledge/entry/version/block/update/component/list_item_component.html +0 -13
- django_spire/knowledge/templates/django_spire/knowledge/entry/version/block/update/component/sub_heading_component.html +0 -3
- django_spire/knowledge/templates/django_spire/knowledge/entry/version/block/update/component/text_component.html +0 -50
- django_spire/knowledge/templates/django_spire/knowledge/entry/version/container/form_container.html +0 -129
- /django_spire/knowledge/entry/version/block/{blocks → data}/__init__.py +0 -0
- /django_spire/knowledge/entry/version/block/{tests/test_urls → data/list}/__init__.py +0 -0
- /django_spire/knowledge/entry/version/{block/views → services/tests}/__init__.py +0 -0
- {django_spire-0.17.11.dist-info → django_spire-0.18.0.dist-info}/WHEEL +0 -0
- {django_spire-0.17.11.dist-info → django_spire-0.18.0.dist-info}/licenses/LICENSE.md +0 -0
- {django_spire-0.17.11.dist-info → django_spire-0.18.0.dist-info}/top_level.txt +0 -0
|
@@ -1,16 +1,10 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from django_spire.contrib.ordering.services.service import OrderingService
|
|
4
|
-
from django_spire.contrib.service import BaseDjangoModelService
|
|
5
|
-
|
|
6
3
|
from typing import TYPE_CHECKING
|
|
7
4
|
|
|
5
|
+
from django_spire.contrib.service import BaseDjangoModelService
|
|
8
6
|
from django_spire.knowledge.entry.version.block.services.factory_service import \
|
|
9
7
|
EntryVersionBlockFactoryService
|
|
10
|
-
from django_spire.knowledge.entry.version.block.services.processor_service import \
|
|
11
|
-
EntryVersionBlockProcessorService
|
|
12
|
-
from django_spire.knowledge.entry.version.block.services.transformation_service import \
|
|
13
|
-
EntryVersionBlockTransformationService
|
|
14
8
|
|
|
15
9
|
if TYPE_CHECKING:
|
|
16
10
|
from django_spire.knowledge.entry.version.block.models import EntryVersionBlock
|
|
@@ -18,8 +12,4 @@ if TYPE_CHECKING:
|
|
|
18
12
|
|
|
19
13
|
class EntryVersionBlockService(BaseDjangoModelService['EntryVersionBlock']):
|
|
20
14
|
obj: EntryVersionBlock
|
|
21
|
-
|
|
22
|
-
factory = EntryVersionBlockFactoryService()
|
|
23
|
-
ordering = OrderingService()
|
|
24
|
-
processor = EntryVersionBlockProcessorService()
|
|
25
|
-
transformation = EntryVersionBlockTransformationService()
|
|
15
|
+
factory = EntryVersionBlockFactoryService()
|
|
@@ -2,13 +2,26 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
4
|
|
|
5
|
-
from django_spire.knowledge.entry.version.block.blocks.text_block import TextBlock
|
|
6
5
|
from django_spire.knowledge.entry.version.block.choices import BlockTypeChoices
|
|
6
|
+
from django_spire.knowledge.entry.version.block.data.text_data import \
|
|
7
|
+
TextEditorBlockData
|
|
7
8
|
from django_spire.knowledge.entry.version.block.models import EntryVersionBlock
|
|
8
9
|
from django_spire.knowledge.entry.version.tests.factories import \
|
|
9
10
|
create_test_entry_version
|
|
10
11
|
|
|
11
12
|
|
|
13
|
+
def create_test_block_form_data(**kwargs) -> dict:
|
|
14
|
+
data = {
|
|
15
|
+
'block_order': 0,
|
|
16
|
+
'block_type': BlockTypeChoices.TEXT,
|
|
17
|
+
'block_data': {'text': 'test text'},
|
|
18
|
+
'block_tunes': {}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
data.update(kwargs)
|
|
22
|
+
return data
|
|
23
|
+
|
|
24
|
+
|
|
12
25
|
def create_test_version_block(**kwargs) -> EntryVersionBlock:
|
|
13
26
|
data = {
|
|
14
27
|
'version': create_test_entry_version(),
|
|
@@ -17,9 +30,12 @@ def create_test_version_block(**kwargs) -> EntryVersionBlock:
|
|
|
17
30
|
'_block_data': json.dumps({}),
|
|
18
31
|
'_text_data': ''
|
|
19
32
|
}
|
|
33
|
+
|
|
20
34
|
data.update(kwargs)
|
|
21
35
|
version_block = EntryVersionBlock.objects.create(**data)
|
|
22
|
-
version_block.
|
|
36
|
+
version_block.editor_block_data = TextEditorBlockData(
|
|
37
|
+
text='',
|
|
38
|
+
)
|
|
23
39
|
version_block.save()
|
|
24
40
|
|
|
25
41
|
return version_block
|
|
@@ -1,19 +1,23 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
import logging
|
|
4
4
|
import re
|
|
5
|
-
|
|
6
|
-
import marko
|
|
7
|
-
|
|
5
|
+
from collections import defaultdict
|
|
8
6
|
from typing import TYPE_CHECKING
|
|
9
7
|
|
|
8
|
+
import marko
|
|
9
|
+
from bs4 import BeautifulSoup
|
|
10
10
|
from django.core.files.storage import default_storage
|
|
11
|
+
from markitdown.converters import HtmlConverter
|
|
12
|
+
from marko.block import Heading, List, ListItem, Paragraph, BlankLine
|
|
11
13
|
from marko.element import Element
|
|
12
|
-
from marko.block import Heading, List, ListItem
|
|
13
14
|
|
|
15
|
+
from django_spire.knowledge.entry.version.block import models
|
|
16
|
+
from django_spire.knowledge.entry.version.block.choices import BlockTypeChoices
|
|
17
|
+
from django_spire.knowledge.entry.version.block.data.list.choices import \
|
|
18
|
+
ListEditorBlockDataStyle
|
|
14
19
|
from django_spire.knowledge.entry.version.converters.converter import \
|
|
15
20
|
BaseConverter
|
|
16
|
-
from django_spire.knowledge.entry.version.block import models
|
|
17
21
|
|
|
18
22
|
if TYPE_CHECKING:
|
|
19
23
|
from django_spire.knowledge.entry.version.models import EntryVersion
|
|
@@ -21,12 +25,26 @@ if TYPE_CHECKING:
|
|
|
21
25
|
from marko.block import BlockElement
|
|
22
26
|
|
|
23
27
|
|
|
28
|
+
MARKO_BLOCK_TYPE_TO_BLOCK_CHOICES = defaultdict(
|
|
29
|
+
lambda:BlockTypeChoices.TEXT,
|
|
30
|
+
{
|
|
31
|
+
Paragraph: models.BlockTypeChoices.TEXT,
|
|
32
|
+
BlankLine: models.BlockTypeChoices.TEXT,
|
|
33
|
+
Heading: models.BlockTypeChoices.HEADING,
|
|
34
|
+
List: models.BlockTypeChoices.LIST,
|
|
35
|
+
}
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
24
39
|
class MarkdownConverter(BaseConverter):
|
|
25
40
|
"""Converts Markdown content to a list of EntryVersionBlocks using Marko.
|
|
26
41
|
|
|
27
42
|
For more info on Marko:
|
|
28
43
|
https://marko-py.readthedocs.io/en/latest/api.html#marko.block.BlockElement
|
|
29
44
|
"""
|
|
45
|
+
|
|
46
|
+
html_converter = HtmlConverter()
|
|
47
|
+
|
|
30
48
|
def __init__(self, entry_version: EntryVersion):
|
|
31
49
|
super().__init__(entry_version)
|
|
32
50
|
self._order = 0
|
|
@@ -35,112 +53,169 @@ class MarkdownConverter(BaseConverter):
|
|
|
35
53
|
with default_storage.open(file.file.name, 'r') as f:
|
|
36
54
|
return self.convert_markdown_to_blocks(f.read())
|
|
37
55
|
|
|
38
|
-
def
|
|
39
|
-
self,
|
|
40
|
-
marko_block: Heading,
|
|
41
|
-
order: int
|
|
42
|
-
) -> models.EntryVersionBlock:
|
|
43
|
-
heading_type = (
|
|
44
|
-
models.BlockTypeChoices.HEADING
|
|
45
|
-
if marko_block.level == 1
|
|
46
|
-
else models.BlockTypeChoices.SUB_HEADING
|
|
47
|
-
)
|
|
48
|
-
|
|
49
|
-
return models.EntryVersionBlock.services.factory.create_null_block(
|
|
50
|
-
entry_version=self.entry_version,
|
|
51
|
-
block_type=heading_type,
|
|
52
|
-
order=order,
|
|
53
|
-
value=self._get_marko_text_content(marko_block),
|
|
54
|
-
)
|
|
55
|
-
|
|
56
|
-
def _convert_list_block(
|
|
56
|
+
def convert_markdown_to_blocks(
|
|
57
57
|
self,
|
|
58
|
-
|
|
59
|
-
bullet: str,
|
|
60
|
-
indent_level: int,
|
|
61
|
-
ordered: bool,
|
|
58
|
+
markdown_content: str
|
|
62
59
|
) -> list[models.EntryVersionBlock]:
|
|
63
|
-
|
|
64
|
-
|
|
60
|
+
marko_blocks = marko.parse(markdown_content).children
|
|
61
|
+
|
|
62
|
+
return [
|
|
63
|
+
models.EntryVersionBlock.services.factory.create_validated_block(
|
|
65
64
|
entry_version=self.entry_version,
|
|
66
|
-
block_type=
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
indent_level=indent_level,
|
|
71
|
-
ordered=ordered,
|
|
72
|
-
)
|
|
73
|
-
self._order += 1
|
|
74
|
-
return [list_item_block]
|
|
75
|
-
|
|
76
|
-
if isinstance(marko_block, ListItem):
|
|
77
|
-
indent_level += 1
|
|
78
|
-
|
|
79
|
-
blocks = []
|
|
80
|
-
for child in marko_block.children:
|
|
81
|
-
blocks.extend(
|
|
82
|
-
self._convert_list_block(
|
|
83
|
-
marko_block=child,
|
|
84
|
-
bullet=bullet,
|
|
85
|
-
indent_level=indent_level,
|
|
86
|
-
ordered=ordered,
|
|
87
|
-
)
|
|
65
|
+
block_type=MARKO_BLOCK_TYPE_TO_BLOCK_CHOICES[marko_block.__class__],
|
|
66
|
+
block_data=self._marko_block_to_editor_block_data_dict(marko_block),
|
|
67
|
+
block_order=order,
|
|
68
|
+
block_tunes={}
|
|
88
69
|
)
|
|
70
|
+
for order, marko_block in enumerate(marko_blocks)
|
|
71
|
+
]
|
|
89
72
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
73
|
+
@classmethod
|
|
74
|
+
def html_to_markdown(cls, html_content: str) -> str:
|
|
75
|
+
return cls.html_converter.convert_string(html_content).markdown
|
|
93
76
|
|
|
94
|
-
|
|
77
|
+
@classmethod
|
|
78
|
+
def _remove_outer_html_tags(
|
|
79
|
+
cls,
|
|
80
|
+
html_content: str,
|
|
81
|
+
tag_name: str | None = None,
|
|
82
|
+
) -> str:
|
|
83
|
+
bs = BeautifulSoup(html_content, 'html.parser')
|
|
95
84
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
ordered=marko_block.ordered,
|
|
111
|
-
)
|
|
112
|
-
)
|
|
113
|
-
else:
|
|
114
|
-
blocks.append(
|
|
115
|
-
self._marko_block_to_version_block(
|
|
116
|
-
marko_block=marko_block,
|
|
117
|
-
order=self._order,
|
|
118
|
-
)
|
|
119
|
-
)
|
|
120
|
-
self._order += 1
|
|
121
|
-
|
|
122
|
-
return blocks
|
|
85
|
+
if tag_name:
|
|
86
|
+
# Only target specific tag given for removal
|
|
87
|
+
html_element = bs.find(tag_name)
|
|
88
|
+
|
|
89
|
+
if html_element:
|
|
90
|
+
html_content = html_element.decode_contents()
|
|
91
|
+
else:
|
|
92
|
+
# Remove any outer tag found
|
|
93
|
+
html_element = bs.find()
|
|
94
|
+
|
|
95
|
+
if html_element:
|
|
96
|
+
html_content = html_element.decode_contents()
|
|
97
|
+
|
|
98
|
+
return html_content
|
|
123
99
|
|
|
124
100
|
@staticmethod
|
|
125
|
-
def
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
101
|
+
def _try_parse_content_as_checklist_item(content: str) -> tuple[bool, bool | None, str]:
|
|
102
|
+
empty_checkbox_pattern = re.compile(r'\[ ?\]\s*')
|
|
103
|
+
checked_checkbox_pattern = re.compile(r'\[[xX]\]\s*')
|
|
104
|
+
|
|
105
|
+
is_checked = None
|
|
106
|
+
has_empty_checkbox = re.match(empty_checkbox_pattern, content)
|
|
107
|
+
|
|
108
|
+
if has_empty_checkbox:
|
|
109
|
+
content = re.sub(empty_checkbox_pattern, '', content)
|
|
110
|
+
is_checked = False
|
|
111
|
+
|
|
112
|
+
has_checked_checkbox = re.match(checked_checkbox_pattern, content)
|
|
113
|
+
if has_checked_checkbox:
|
|
114
|
+
content = re.sub(checked_checkbox_pattern, '', content)
|
|
115
|
+
is_checked = True
|
|
116
|
+
|
|
117
|
+
is_checklist_item = is_checked is not None
|
|
118
|
+
|
|
119
|
+
return (
|
|
120
|
+
is_checklist_item,
|
|
121
|
+
is_checked,
|
|
122
|
+
content
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
@classmethod
|
|
126
|
+
def _marko_list_item_block_to_editor_block_data_dict(
|
|
127
|
+
cls,
|
|
128
|
+
marko_list_item_block: ListItem,
|
|
129
|
+
parent_marko_list_block: List | None = None,
|
|
130
|
+
):
|
|
131
|
+
list_item_editor_block_data_kwargs = {
|
|
132
|
+
'items': [],
|
|
133
|
+
'content': '',
|
|
134
|
+
'meta': {},
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
# First determine if this item contains a nested list.
|
|
138
|
+
# If it does, remove the list from the item's children (so the item content can be
|
|
139
|
+
# properly rendered) and process it into editor_block_data if it does
|
|
140
|
+
for i, child in enumerate(marko_list_item_block.children):
|
|
141
|
+
if isinstance(child, List):
|
|
142
|
+
nested_marko_list_block = marko_list_item_block.children.pop(i)
|
|
143
|
+
for nested_child in nested_marko_list_block.children:
|
|
144
|
+
nested_list_item_editor_block_data_kwargs, _ = \
|
|
145
|
+
cls._marko_list_item_block_to_editor_block_data_dict(nested_child)
|
|
146
|
+
list_item_editor_block_data_kwargs['items'].append(nested_list_item_editor_block_data_kwargs)
|
|
147
|
+
|
|
148
|
+
break
|
|
149
|
+
|
|
150
|
+
# Marko wraps the text content in li when it renders a ListItem into html,
|
|
151
|
+
# so we need to remove that, plus remove any leftover tags (e.g. if the content
|
|
152
|
+
# in the li was a header)
|
|
153
|
+
content = cls._remove_outer_html_tags(
|
|
154
|
+
cls._remove_outer_html_tags(marko.render(marko_list_item_block), 'li')
|
|
146
155
|
)
|
|
156
|
+
|
|
157
|
+
is_checklist_item, is_checked, content = \
|
|
158
|
+
cls._try_parse_content_as_checklist_item(content)
|
|
159
|
+
|
|
160
|
+
if is_checklist_item:
|
|
161
|
+
# Each editor list item block data tracks its checked state through an
|
|
162
|
+
# item level meta
|
|
163
|
+
list_item_editor_block_data_kwargs['meta']['checked'] = is_checked
|
|
164
|
+
|
|
165
|
+
list_item_editor_block_data_kwargs['content'] = content
|
|
166
|
+
|
|
167
|
+
list_editor_block_data_style = None
|
|
168
|
+
if parent_marko_list_block:
|
|
169
|
+
# We determine the entire list style from the top level list items for simplicity.
|
|
170
|
+
# If parent_marko_list_block is present, it means we are processing the top level list.
|
|
171
|
+
list_editor_block_data_style = ListEditorBlockDataStyle.UNORDERED
|
|
172
|
+
if is_checklist_item:
|
|
173
|
+
# The presence of checklist items takes priority over an ordered list style
|
|
174
|
+
list_editor_block_data_style = ListEditorBlockDataStyle.CHECKLIST
|
|
175
|
+
elif parent_marko_list_block.ordered:
|
|
176
|
+
list_editor_block_data_style = ListEditorBlockDataStyle.ORDERED
|
|
177
|
+
|
|
178
|
+
return list_item_editor_block_data_kwargs, list_editor_block_data_style
|
|
179
|
+
|
|
180
|
+
@classmethod
|
|
181
|
+
def _marko_block_to_editor_block_data_dict(cls, marko_block: BlockElement | Element):
|
|
182
|
+
if isinstance(marko_block, BlankLine):
|
|
183
|
+
return { 'text': '' }
|
|
184
|
+
|
|
185
|
+
elif isinstance(marko_block, Paragraph):
|
|
186
|
+
editor_block_text_string = cls._remove_outer_html_tags(marko.render(marko_block))
|
|
187
|
+
return { 'text': editor_block_text_string }
|
|
188
|
+
|
|
189
|
+
elif isinstance(marko_block, Heading):
|
|
190
|
+
return {
|
|
191
|
+
'text': cls._remove_outer_html_tags(marko.render(marko_block)),
|
|
192
|
+
'level': marko_block.level,
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
elif isinstance(marko_block, List):
|
|
196
|
+
list_editor_block_data_dict = {
|
|
197
|
+
'items': [],
|
|
198
|
+
'style': ListEditorBlockDataStyle.UNORDERED,
|
|
199
|
+
'meta': {},
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
for child in marko_block.children:
|
|
203
|
+
list_item_editor_block_data_dict, list_editor_block_data_style = \
|
|
204
|
+
cls._marko_list_item_block_to_editor_block_data_dict(child, marko_block)
|
|
205
|
+
|
|
206
|
+
list_editor_block_data_dict['items'].append(list_item_editor_block_data_dict)
|
|
207
|
+
list_editor_block_data_dict['style'] = list_editor_block_data_style
|
|
208
|
+
|
|
209
|
+
return list_editor_block_data_dict
|
|
210
|
+
|
|
211
|
+
else:
|
|
212
|
+
logging.warning(
|
|
213
|
+
f'Unsupported marko block type: {marko_block.__class__.__name__!r}. '
|
|
214
|
+
f'Rendering content to html and adding to markdown as a basic paragraph block.'
|
|
215
|
+
)
|
|
216
|
+
return { 'text': marko.render(marko_block) }
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
@classmethod
|
|
220
|
+
def marko_block_to_markdown_string(cls, marko_block: BlockElement) -> str:
|
|
221
|
+
return cls.html_to_markdown(marko.render(marko_block))
|
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
from django.db.models import Prefetch
|
|
4
|
+
|
|
3
5
|
from django_spire.history.querysets import HistoryQuerySet
|
|
4
6
|
|
|
5
7
|
|
|
6
8
|
class EntryVersionQuerySet(HistoryQuerySet):
|
|
7
|
-
|
|
9
|
+
def prefetch_blocks(self):
|
|
10
|
+
from django_spire.knowledge.entry.version.block.models import EntryVersionBlock
|
|
11
|
+
|
|
12
|
+
return self.prefetch_related(
|
|
13
|
+
Prefetch(
|
|
14
|
+
'blocks',
|
|
15
|
+
queryset=EntryVersionBlock.objects.order_by('order')
|
|
16
|
+
)
|
|
17
|
+
)
|
|
@@ -11,6 +11,8 @@ from django_spire.knowledge.entry.models import Entry
|
|
|
11
11
|
from django_spire.knowledge.entry.version import models
|
|
12
12
|
|
|
13
13
|
from django_spire.contrib.seeding import DjangoModelSeeder
|
|
14
|
+
from django_spire.knowledge.entry.version.block.data.maps import \
|
|
15
|
+
EDITOR_BLOCK_DATA_REVERSE_MAP
|
|
14
16
|
from django_spire.knowledge.entry.version.block.models import EntryVersionBlock
|
|
15
17
|
from django_spire.knowledge.entry.version.block.seeding.constants import SAFETY_BLOCKS
|
|
16
18
|
from django_spire.knowledge.entry.version.choices import EntryVersionStatusChoices
|
|
@@ -61,10 +63,10 @@ class EntryVersionSeeder(DjangoModelSeeder):
|
|
|
61
63
|
for idx, safety_block in enumerate(safety_blocks):
|
|
62
64
|
version_block = EntryVersionBlock(
|
|
63
65
|
version=entry_version,
|
|
64
|
-
type=safety_block
|
|
66
|
+
type=EDITOR_BLOCK_DATA_REVERSE_MAP[type(safety_block)],
|
|
65
67
|
order=idx,
|
|
66
68
|
)
|
|
67
|
-
version_block.
|
|
69
|
+
version_block.editor_block_data = safety_block
|
|
68
70
|
version_blocks.append(version_block)
|
|
69
71
|
|
|
70
72
|
EntryVersionBlock.objects.bulk_create(version_blocks)
|
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from typing import TYPE_CHECKING
|
|
4
4
|
|
|
5
|
+
from django.db import transaction
|
|
5
6
|
from django.utils.timezone import localtime
|
|
6
7
|
|
|
7
8
|
from django_spire.contrib.service import BaseDjangoModelService
|
|
@@ -18,3 +19,20 @@ class EntryVersionProcessorService(BaseDjangoModelService['EntryVersion']):
|
|
|
18
19
|
self.obj.status = EntryVersionStatusChoices.PUBLISHED
|
|
19
20
|
self.obj.published_datetime = localtime()
|
|
20
21
|
self.obj.save()
|
|
22
|
+
|
|
23
|
+
def update_blocks(self, raw_block_data: list[dict]):
|
|
24
|
+
from django_spire.knowledge.entry.version.block.models import EntryVersionBlock
|
|
25
|
+
|
|
26
|
+
old_entry_blocks = self.obj.blocks.active()
|
|
27
|
+
|
|
28
|
+
incoming_entry_blocks = [
|
|
29
|
+
EntryVersionBlock.services.factory.create_validated_block(
|
|
30
|
+
entry_version=self.obj,
|
|
31
|
+
**block_data,
|
|
32
|
+
)
|
|
33
|
+
for block_data in raw_block_data
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
with transaction.atomic():
|
|
37
|
+
old_entry_blocks.delete()
|
|
38
|
+
EntryVersionBlock.objects.bulk_create(incoming_entry_blocks)
|
|
File without changes
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import json
|
|
4
|
+
|
|
3
5
|
from django.urls import reverse
|
|
4
6
|
|
|
5
7
|
from django_spire.core.tests.test_cases import BaseTestCase
|
|
6
8
|
from django_spire.knowledge.entry.tests.factories import create_test_entry
|
|
9
|
+
from django_spire.knowledge.entry.version.block.tests.factories import \
|
|
10
|
+
create_test_block_form_data
|
|
7
11
|
from django_spire.knowledge.entry.version.tests.factories import \
|
|
8
12
|
create_test_entry_version
|
|
9
13
|
|
|
@@ -17,34 +21,13 @@ class EntryVersionJsonUrlsTests(BaseTestCase):
|
|
|
17
21
|
self.test_entry.current_version = self.test_entry_version
|
|
18
22
|
self.test_entry.save()
|
|
19
23
|
|
|
20
|
-
def
|
|
21
|
-
response = self.client.post(
|
|
22
|
-
reverse(
|
|
23
|
-
'django_spire:knowledge:entry:version:json:create_blank_block',
|
|
24
|
-
kwargs={'pk': self.test_entry_version.pk}
|
|
25
|
-
),
|
|
26
|
-
content_type='application/json'
|
|
27
|
-
)
|
|
28
|
-
|
|
29
|
-
self.assertEqual(response.status_code, 200)
|
|
30
|
-
|
|
31
|
-
def test_delete_block_view_url_path(self):
|
|
32
|
-
response = self.client.post(
|
|
33
|
-
reverse(
|
|
34
|
-
'django_spire:knowledge:entry:version:json:delete_block',
|
|
35
|
-
kwargs={'pk': self.test_entry_version.pk}
|
|
36
|
-
),
|
|
37
|
-
content_type='application/json'
|
|
38
|
-
)
|
|
39
|
-
|
|
40
|
-
self.assertEqual(response.status_code, 200)
|
|
41
|
-
|
|
42
|
-
def test_reorder_view_url_path(self):
|
|
24
|
+
def test_save_version_blocks_view(self):
|
|
43
25
|
response = self.client.post(
|
|
44
26
|
reverse(
|
|
45
|
-
'django_spire:knowledge:entry:version:json:
|
|
27
|
+
'django_spire:knowledge:entry:version:json:update_blocks',
|
|
46
28
|
kwargs={'pk': self.test_entry_version.pk}
|
|
47
29
|
),
|
|
30
|
+
data=json.dumps([create_test_block_form_data()]),
|
|
48
31
|
content_type='application/json'
|
|
49
32
|
)
|
|
50
33
|
|
|
@@ -3,9 +3,6 @@ from django.urls import include, path
|
|
|
3
3
|
app_name = 'version'
|
|
4
4
|
|
|
5
5
|
urlpatterns = [
|
|
6
|
-
path('block/', include('django_spire.knowledge.entry.version.block.urls', namespace='block')),
|
|
7
|
-
|
|
8
|
-
path('form/', include('django_spire.knowledge.entry.version.urls.form_urls', namespace='form')),
|
|
9
6
|
path('json/', include('django_spire.knowledge.entry.version.urls.json_urls', namespace='json')),
|
|
10
7
|
path('page/', include('django_spire.knowledge.entry.version.urls.page_urls', namespace='page')),
|
|
11
8
|
path('redirect/', include('django_spire.knowledge.entry.version.urls.redirect_urls', namespace='redirect')),
|
|
@@ -5,20 +5,12 @@ from django_spire.knowledge.entry.version.views import json_views
|
|
|
5
5
|
|
|
6
6
|
app_name = 'json'
|
|
7
7
|
|
|
8
|
+
|
|
8
9
|
urlpatterns = [
|
|
9
10
|
path(
|
|
10
|
-
'<int:pk>/
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
path(
|
|
15
|
-
'<int:pk>/delete_block/',
|
|
16
|
-
json_views.delete_block_view,
|
|
17
|
-
name='delete_block',
|
|
18
|
-
),
|
|
19
|
-
path(
|
|
20
|
-
'<int:pk>/reorder/',
|
|
21
|
-
json_views.reorder_view,
|
|
22
|
-
name='reorder',
|
|
11
|
+
'<int:pk>/update_blocks/',
|
|
12
|
+
|
|
13
|
+
json_views.update_blocks_view,
|
|
14
|
+
name='update_blocks',
|
|
23
15
|
),
|
|
24
16
|
]
|
|
@@ -4,86 +4,18 @@ import json
|
|
|
4
4
|
|
|
5
5
|
from django.core.handlers.wsgi import WSGIRequest
|
|
6
6
|
from django.http import JsonResponse
|
|
7
|
+
from django.shortcuts import get_object_or_404
|
|
7
8
|
|
|
9
|
+
from django_spire.auth.controller.controller import AppAuthController
|
|
8
10
|
from django_spire.core.decorators import valid_ajax_request_required
|
|
9
|
-
from django_spire.core.shortcuts import get_object_or_null_obj
|
|
10
|
-
from django_spire.knowledge.entry.version.block.models import EntryVersionBlock
|
|
11
11
|
from django_spire.knowledge.entry.version.models import EntryVersion
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
@valid_ajax_request_required
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
@AppAuthController('knowledge').permission_required('can_change')
|
|
16
|
+
def update_blocks_view(request: WSGIRequest, pk: int) -> JsonResponse:
|
|
17
|
+
entry_version = get_object_or_404(EntryVersion.objects.prefetch_blocks(), pk=pk)
|
|
18
|
+
new_block_data = json.loads(request.body.decode('utf-8'))
|
|
19
|
+
entry_version.services.processor.update_blocks(new_block_data)
|
|
17
20
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
if 'order' not in body_data or 'block_type' not in body_data:
|
|
21
|
-
return JsonResponse({'type': 'error', 'message': 'Missing Required Data.'})
|
|
22
|
-
|
|
23
|
-
block_type = body_data.pop('block_type')
|
|
24
|
-
order = body_data.pop('order')
|
|
25
|
-
|
|
26
|
-
version_block = EntryVersionBlock.services.factory.create_blank_block(
|
|
27
|
-
entry_version=entry_version,
|
|
28
|
-
block_type=block_type,
|
|
29
|
-
order=order,
|
|
30
|
-
**body_data,
|
|
31
|
-
)
|
|
32
|
-
|
|
33
|
-
return JsonResponse(
|
|
34
|
-
{
|
|
35
|
-
'type': 'success',
|
|
36
|
-
'entry_version_block_json': version_block.services.transformation.to_json()
|
|
37
|
-
}
|
|
38
|
-
)
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
@valid_ajax_request_required
|
|
42
|
-
def delete_block_view(request: WSGIRequest, pk: int) -> JsonResponse:
|
|
43
|
-
entry_version = EntryVersion.objects.get(pk=pk)
|
|
44
|
-
|
|
45
|
-
body_data = json.loads(request.body.decode('utf-8'))
|
|
46
|
-
version_block_pk = body_data.get('version_block_pk')
|
|
47
|
-
|
|
48
|
-
try:
|
|
49
|
-
version_block = EntryVersionBlock.objects.get(pk=version_block_pk)
|
|
50
|
-
except EntryVersionBlock.DoesNotExist:
|
|
51
|
-
return JsonResponse({'type': 'error', 'message': 'Block Not Found.'})
|
|
52
|
-
|
|
53
|
-
version_block.services.processor.set_deleted()
|
|
54
|
-
|
|
55
|
-
return JsonResponse({'type': 'success'})
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
@valid_ajax_request_required
|
|
59
|
-
def reorder_view(request: WSGIRequest, pk: int) -> JsonResponse:
|
|
60
|
-
entry_version = get_object_or_null_obj(EntryVersion, pk=pk)
|
|
61
|
-
|
|
62
|
-
if entry_version.id is None:
|
|
63
|
-
return JsonResponse({'type': 'error', 'message': 'EntryVersion not found.'})
|
|
64
|
-
|
|
65
|
-
body_data = json.loads(request.body.decode('utf-8'))
|
|
66
|
-
|
|
67
|
-
order = body_data.get('order', None)
|
|
68
|
-
|
|
69
|
-
if order is None:
|
|
70
|
-
return JsonResponse({'type': 'error', 'message': 'Order Not Found.'})
|
|
71
|
-
|
|
72
|
-
block_id = body_data.get('block_id')
|
|
73
|
-
|
|
74
|
-
if not body_data.get('block_id'):
|
|
75
|
-
return JsonResponse({'type': 'error', 'message': 'Missing Required Data.'})
|
|
76
|
-
|
|
77
|
-
block = get_object_or_null_obj(EntryVersionBlock, pk=block_id)
|
|
78
|
-
|
|
79
|
-
if block.id is None:
|
|
80
|
-
return JsonResponse({'type': 'error', 'message': 'Block not found.'})
|
|
81
|
-
|
|
82
|
-
block.ordering_services.processor.move_to_position(
|
|
83
|
-
destination_objects=entry_version.blocks.active(),
|
|
84
|
-
position=order,
|
|
85
|
-
)
|
|
86
|
-
|
|
87
|
-
return JsonResponse({
|
|
88
|
-
'type': 'success', 'message': 'Order reordered successfully',
|
|
89
|
-
})
|
|
21
|
+
return JsonResponse({'type': 'success'})
|