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.
Files changed (72) hide show
  1. django_spire/consts.py +1 -1
  2. django_spire/contrib/seeding/model/base.py +1 -1
  3. django_spire/knowledge/entry/services/automation_service.py +7 -4
  4. django_spire/knowledge/entry/services/transformation_services.py +2 -2
  5. django_spire/knowledge/entry/version/block/choices.py +1 -2
  6. django_spire/knowledge/entry/version/block/data/data.py +33 -0
  7. django_spire/knowledge/entry/version/block/data/heading_data.py +15 -0
  8. django_spire/knowledge/entry/version/block/data/list/choices.py +17 -0
  9. django_spire/knowledge/entry/version/block/data/list/data.py +102 -0
  10. django_spire/knowledge/entry/version/block/data/list/maps.py +14 -0
  11. django_spire/knowledge/entry/version/block/data/list/meta.py +15 -0
  12. django_spire/knowledge/entry/version/block/data/maps.py +23 -0
  13. django_spire/knowledge/entry/version/block/data/text_data.py +13 -0
  14. django_spire/knowledge/entry/version/block/models.py +22 -14
  15. django_spire/knowledge/entry/version/block/querysets.py +17 -7
  16. django_spire/knowledge/entry/version/block/seeding/constants.py +271 -188
  17. django_spire/knowledge/entry/version/block/services/factory_service.py +14 -47
  18. django_spire/knowledge/entry/version/block/services/service.py +2 -12
  19. django_spire/knowledge/entry/version/block/tests/factories.py +18 -2
  20. django_spire/knowledge/entry/version/converters/markdown_converter.py +180 -105
  21. django_spire/knowledge/entry/version/maps.py +1 -1
  22. django_spire/knowledge/entry/version/models.py +1 -0
  23. django_spire/knowledge/entry/version/querysets.py +11 -1
  24. django_spire/knowledge/entry/version/seeding/seeder.py +4 -2
  25. django_spire/knowledge/entry/version/services/processor_service.py +18 -0
  26. django_spire/knowledge/entry/version/services/tests/test_processor_service.py +0 -0
  27. django_spire/knowledge/entry/version/tests/test_urls/test_json_urls.py +7 -24
  28. django_spire/knowledge/entry/version/urls/__init__.py +0 -3
  29. django_spire/knowledge/entry/version/urls/json_urls.py +5 -13
  30. django_spire/knowledge/entry/version/views/json_views.py +8 -76
  31. django_spire/knowledge/entry/version/views/page_views.py +10 -5
  32. django_spire/knowledge/migrations/0005_entryversionblock__tunes_data_and_more.py +23 -0
  33. django_spire/knowledge/static/django_spire/knowledge/css/navigation_items.css +1 -1
  34. django_spire/knowledge/static/django_spire/knowledge/entry/version/js/editor.js +87 -0
  35. django_spire/knowledge/static/django_spire/knowledge/entry/version/js/null_paragraph.js +15 -0
  36. django_spire/knowledge/templates/django_spire/knowledge/entry/file/page/list_page.html +1 -1
  37. django_spire/knowledge/templates/django_spire/knowledge/entry/version/container/detail_container.html +43 -12
  38. django_spire/knowledge/templates/django_spire/knowledge/entry/version/page/detail_page.html +17 -1
  39. django_spire/knowledge/templates/django_spire/knowledge/navigation/page/full_page.html +1 -1
  40. {django_spire-0.17.11.dist-info → django_spire-0.18.0.dist-info}/METADATA +2 -1
  41. {django_spire-0.17.11.dist-info → django_spire-0.18.0.dist-info}/RECORD +47 -60
  42. django_spire/knowledge/entry/version/block/blocks/block.py +0 -51
  43. django_spire/knowledge/entry/version/block/blocks/heading_block.py +0 -14
  44. django_spire/knowledge/entry/version/block/blocks/list_block.py +0 -31
  45. django_spire/knowledge/entry/version/block/blocks/sub_heading_block.py +0 -14
  46. django_spire/knowledge/entry/version/block/blocks/text_block.py +0 -14
  47. django_spire/knowledge/entry/version/block/maps.py +0 -16
  48. django_spire/knowledge/entry/version/block/services/processor_service.py +0 -33
  49. django_spire/knowledge/entry/version/block/services/transformation_service.py +0 -35
  50. django_spire/knowledge/entry/version/block/tests/test_urls/test_json_urls.py +0 -28
  51. django_spire/knowledge/entry/version/block/urls/__init__.py +0 -13
  52. django_spire/knowledge/entry/version/block/urls/json_urls.py +0 -9
  53. django_spire/knowledge/entry/version/block/views/json_views.py +0 -30
  54. django_spire/knowledge/entry/version/tests/test_urls/test_form_urls.py +0 -28
  55. django_spire/knowledge/entry/version/urls/form_urls.py +0 -10
  56. django_spire/knowledge/entry/version/views/form_views.py +0 -63
  57. django_spire/knowledge/templates/django_spire/knowledge/entry/version/block/detail/component/heading_component.html +0 -3
  58. django_spire/knowledge/templates/django_spire/knowledge/entry/version/block/detail/component/list_item_component.html +0 -38
  59. django_spire/knowledge/templates/django_spire/knowledge/entry/version/block/detail/component/sub_heading_component.html +0 -3
  60. django_spire/knowledge/templates/django_spire/knowledge/entry/version/block/detail/component/text_component.html +0 -3
  61. django_spire/knowledge/templates/django_spire/knowledge/entry/version/block/dropdown/add_dropdown.html +0 -25
  62. django_spire/knowledge/templates/django_spire/knowledge/entry/version/block/update/component/heading_component.html +0 -3
  63. django_spire/knowledge/templates/django_spire/knowledge/entry/version/block/update/component/list_item_component.html +0 -13
  64. django_spire/knowledge/templates/django_spire/knowledge/entry/version/block/update/component/sub_heading_component.html +0 -3
  65. django_spire/knowledge/templates/django_spire/knowledge/entry/version/block/update/component/text_component.html +0 -50
  66. django_spire/knowledge/templates/django_spire/knowledge/entry/version/container/form_container.html +0 -129
  67. /django_spire/knowledge/entry/version/block/{blocks → data}/__init__.py +0 -0
  68. /django_spire/knowledge/entry/version/block/{tests/test_urls → data/list}/__init__.py +0 -0
  69. /django_spire/knowledge/entry/version/{block/views → services/tests}/__init__.py +0 -0
  70. {django_spire-0.17.11.dist-info → django_spire-0.18.0.dist-info}/WHEEL +0 -0
  71. {django_spire-0.17.11.dist-info → django_spire-0.18.0.dist-info}/licenses/LICENSE.md +0 -0
  72. {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.block = TextBlock(value='', type=BlockTypeChoices.TEXT)
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 html
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 _convert_heading_block(
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
- marko_block: List | ListItem | Element,
59
- bullet: str,
60
- indent_level: int,
61
- ordered: bool,
58
+ markdown_content: str
62
59
  ) -> list[models.EntryVersionBlock]:
63
- if isinstance(marko_block.children, str):
64
- list_item_block = models.EntryVersionBlock.services.factory.create_null_block(
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=models.BlockTypeChoices.LIST_ITEM,
67
- order=self._order,
68
- value=self._get_marko_text_content(marko_block),
69
- bullet=bullet,
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
- if isinstance(child, ListItem) and bullet.endswith('.'):
91
- bullet = bullet.rstrip('.')
92
- bullet = str(int(bullet) + 1) + '.'
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
- return blocks
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
- def convert_markdown_to_blocks(
97
- self,
98
- markdown_content: str
99
- ) -> list[models.EntryVersionBlock]:
100
- syntax_tree = marko.parse(markdown_content)
101
-
102
- blocks = []
103
- for marko_block in syntax_tree.children:
104
- if isinstance(marko_block, List):
105
- blocks.extend(
106
- self._convert_list_block(
107
- marko_block=marko_block,
108
- bullet=marko_block.bullet,
109
- indent_level=-1,
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 _get_marko_text_content(marko_block: BlockElement) -> str:
126
- html_text = marko.render(marko_block)
127
- bolded_text = re.sub(r'<strong>(.*?)</strong>', r'**\1**', html_text)
128
- italicized_text = re.sub(r'<em>(.*?)</em>', r'*\1*', bolded_text)
129
- strikethrough_text = re.sub(r'<del>(.*?)</del>', r'~~*\1*~~', italicized_text)
130
- text = re.sub(r'<[^>]+>', '', strikethrough_text)
131
- return html.unescape(text)
132
-
133
- def _marko_block_to_version_block(
134
- self,
135
- marko_block: BlockElement | Element,
136
- order: int
137
- ) -> models.EntryVersionBlock:
138
- if isinstance(marko_block, Heading):
139
- return self._convert_heading_block(marko_block=marko_block, order=order)
140
-
141
- return models.EntryVersionBlock.services.factory.create_null_block(
142
- entry_version=self.entry_version,
143
- block_type=models.BlockTypeChoices.TEXT,
144
- order=order,
145
- value=self._get_marko_text_content(marko_block)
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))
@@ -8,4 +8,4 @@ from django_spire.knowledge.entry.version.converters.markdown_converter import \
8
8
  FILE_TYPE_CONVERTER_MAP = {
9
9
  'md': MarkdownConverter,
10
10
  'docx': DocxConverter
11
- }
11
+ }
@@ -42,3 +42,4 @@ class EntryVersion(HistoryModelMixin):
42
42
  verbose_name = 'Entry Version'
43
43
  verbose_name_plural = 'Entry Versions'
44
44
  db_table = 'django_spire_knowledge_entry_version'
45
+
@@ -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
- pass
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.type,
66
+ type=EDITOR_BLOCK_DATA_REVERSE_MAP[type(safety_block)],
65
67
  order=idx,
66
68
  )
67
- version_block.block = safety_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)
@@ -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 test_create_blank_block_view_url_path(self):
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:reorder',
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>/create_blank_block/',
11
- json_views.create_blank_block_view,
12
- name='create_blank_block',
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
- def create_blank_block_view(request: WSGIRequest, pk: int) -> JsonResponse:
16
- entry_version = EntryVersion.objects.get(pk=pk)
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
- body_data = json.loads(request.body.decode('utf-8'))
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'})