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
django_spire/consts.py CHANGED
@@ -1,4 +1,4 @@
1
- __VERSION__ = '0.17.11'
1
+ __VERSION__ = '0.18.0'
2
2
 
3
3
 
4
4
  AI_CHAT_WORKFLOW_SENDER_SETTINGS_NAME = 'AI_CHAT_WORKFLOW_NAME'
@@ -2,7 +2,7 @@ from abc import ABC, abstractmethod
2
2
 
3
3
  from dandy.recorder import recorder_to_html_file
4
4
  from dandy import SqliteCache
5
- from dandy import generate_cache_key
5
+ from dandy.cache.tools import generate_cache_key
6
6
 
7
7
  from django_spire.contrib.seeding.field.override import FieldOverride
8
8
  from django_spire.contrib.seeding.model.config import FieldsConfig
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import traceback
3
4
  from typing import TYPE_CHECKING
4
5
 
5
6
  import json
@@ -34,15 +35,17 @@ class EntryAutomationService(BaseDjangoModelService['Entry']):
34
35
  entry_version=entry_pk_map[file_object.object_id].current_version,
35
36
  )
36
37
  except Exception as e:
37
- errored.append({'file': file_object.name, 'error': str(e)})
38
+ errored.append({'file': file_object.name, 'error': traceback.format_exc()})
38
39
  file_object.set_deleted()
39
40
  else:
40
41
  file_object.set_deleted()
41
42
 
42
43
  message = f'Files Converted: {len(file_objects) - len(errored)}'
43
44
  if errored:
44
- raise KnowledgeBaseConversionException(
45
- f'\n{message}\nFiles Errored: {json.dumps(errored, indent=4)}'
46
- )
45
+ error_string = f'\n{message}\nFiles Errored:'
46
+ for error in errored:
47
+ error_string += f' File Name: {error["file"]}\n Error: {error["error"]}'
48
+
49
+ raise KnowledgeBaseConversionException(error_string)
47
50
 
48
51
  return message
@@ -72,9 +72,9 @@ class EntryTransformationService(BaseDjangoModelService['Entry']):
72
72
  'edit_version_url': f'''
73
73
  {site}{
74
74
  reverse(
75
- 'django_spire:knowledge:entry:version:form:update',
75
+ 'django_spire:knowledge:entry:version:page:detail',
76
76
  kwargs={'pk': current_version.pk},
77
77
  )
78
- }
78
+ }?view_mode=edit
79
79
  '''
80
80
  }
@@ -4,5 +4,4 @@ from django.db.models import TextChoices
4
4
  class BlockTypeChoices(TextChoices):
5
5
  TEXT = 'text'
6
6
  HEADING = 'heading'
7
- SUB_HEADING = 'sub_heading'
8
- LIST_ITEM = 'list_item'
7
+ LIST = 'list'
@@ -0,0 +1,33 @@
1
+ from __future__ import annotations
2
+
3
+ from pydantic import BaseModel
4
+
5
+
6
+ class BaseEditorBlockData(BaseModel):
7
+ """
8
+ This class serves as a foundational abstract model for EditorJS tool data objects.
9
+
10
+ Note that it does not represent the top level editor block itself,
11
+ but rather the data that is stored within it.
12
+ See https://editorjs.io/getting-started/#tools-installation for a
13
+ list of EditorJS and their associated data models.
14
+ """
15
+
16
+ def render_to_text(self) -> str:
17
+ """
18
+ Renders the content to text format.
19
+
20
+ This method should be implemented in a subclass to define how the
21
+ content will be rendered into a string. It is meant to be overridden and
22
+ raises a NotImplementedError by default.
23
+
24
+ This method is mainly used for providing knowledge base
25
+ content in a digestible format for the AI Chat's LLM connector.
26
+
27
+ Returns:
28
+ str: The rendered text output.
29
+
30
+ Raises:
31
+ NotImplementedError: If the method is not implemented in a subclass.
32
+ """
33
+ raise NotImplementedError
@@ -0,0 +1,15 @@
1
+ from __future__ import annotations
2
+
3
+ from django_spire.knowledge.entry.version.block.data.data import BaseEditorBlockData
4
+
5
+
6
+ class HeadingEditorBlockData(BaseEditorBlockData):
7
+ text: str
8
+ level: int
9
+
10
+ def render_to_text(self) -> str:
11
+ from django_spire.knowledge.entry.version.converters.markdown_converter import \
12
+ MarkdownConverter
13
+
14
+ text = MarkdownConverter.html_to_markdown(self.text)
15
+ return f'{"#" * self.level} {text}\n'
@@ -0,0 +1,17 @@
1
+ from __future__ import annotations
2
+
3
+ from django.db.models import TextChoices
4
+
5
+
6
+ class ListEditorBlockDataStyle(TextChoices):
7
+ UNORDERED = 'unordered'
8
+ ORDERED = 'ordered'
9
+ CHECKLIST = 'checklist'
10
+
11
+
12
+ class OrderedListCounterType(TextChoices):
13
+ NUMERIC = 'numeric'
14
+ UPPER_ROMAN = 'upper-roman'
15
+ LOWER_ROMAN = 'lower-roman'
16
+ UPPER_ALPHA = 'upper-alpha'
17
+ LOWER_ALPHA = 'lower-alpha'
@@ -0,0 +1,102 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Optional
4
+
5
+ from pydantic import model_validator, BaseModel
6
+
7
+ from django_spire.knowledge.entry.version.block.constants import SPACES_PER_INDENT
8
+ from django_spire.knowledge.entry.version.block.data.data import BaseEditorBlockData
9
+ from django_spire.knowledge.entry.version.block.data.list.meta import ChecklistItemMeta, \
10
+ OrderedListItemMeta
11
+ from django_spire.knowledge.entry.version.block.data.list.choices import \
12
+ ListEditorBlockDataStyle
13
+
14
+
15
+ class ListEditorBlockData(BaseEditorBlockData):
16
+ style: ListEditorBlockDataStyle | str
17
+ meta: ChecklistItemMeta | OrderedListItemMeta | None = None
18
+ items: list[ListItemEditorBlockData]
19
+
20
+ def render_to_text(self) -> str:
21
+ render_string = ''
22
+ for i, item in enumerate(self.items):
23
+ render_string += item.render_to_text(self.style, 0, i)
24
+
25
+ return render_string
26
+
27
+ @model_validator(mode='before')
28
+ @classmethod
29
+ def validate_meta(cls, values: dict) -> dict:
30
+ values['style'] = ListEditorBlockDataStyle(values.get('style'))
31
+
32
+ if values['style'] == ListEditorBlockDataStyle.ORDERED and 'meta' in values:
33
+ values['meta'] = OrderedListItemMeta(**values['meta'])
34
+
35
+ for item in values['items']:
36
+ item = ListItemEditorBlockData.style_aware_create_from_dict(item, values['style'])
37
+
38
+ return values
39
+
40
+
41
+ class ListItemEditorBlockData(BaseModel):
42
+ content: str
43
+ meta: ChecklistItemMeta | OrderedListItemMeta | dict | None = None
44
+ items: Optional[list[ListItemEditorBlockData]] = []
45
+
46
+ def get_prefix(
47
+ self,
48
+ style: ListEditorBlockDataStyle,
49
+ indent_level: int,
50
+ index = None
51
+ ):
52
+ prefix = ' ' * indent_level * SPACES_PER_INDENT
53
+
54
+ if style == ListEditorBlockDataStyle.ORDERED:
55
+ index = index or 0
56
+ start = self.meta.start or 1
57
+ prefix += f'{start + index}.'
58
+
59
+ elif style == ListEditorBlockDataStyle.CHECKLIST:
60
+ prefix += f'[{"X" if self.meta.checked else " "}]'
61
+
62
+ else:
63
+ prefix += '-'
64
+
65
+ return prefix
66
+
67
+ def render_to_text(
68
+ self,
69
+ style: ListEditorBlockDataStyle,
70
+ indent_level: int,
71
+ index: int
72
+ ) -> str:
73
+ from django_spire.knowledge.entry.version.converters.markdown_converter import \
74
+ MarkdownConverter
75
+
76
+ prefix = self.get_prefix(style, indent_level, index)
77
+ parsed_content = MarkdownConverter.html_to_markdown(self.content)
78
+ render_string = f'{prefix} {parsed_content}\n'
79
+ for i, item in enumerate(self.items):
80
+ render_string += item.render_to_text(style, indent_level + 1, i)
81
+
82
+ return render_string
83
+
84
+ @classmethod
85
+ def style_aware_create_from_dict(
86
+ cls,
87
+ values: dict,
88
+ style: ListEditorBlockDataStyle
89
+ ) -> ListItemEditorBlockData:
90
+ from django_spire.knowledge.entry.version.block.data.list.maps import \
91
+ LIST_BLOCK_DATA_META_MAP
92
+
93
+ if 'meta' in values:
94
+ meta_type = LIST_BLOCK_DATA_META_MAP[style]
95
+
96
+ if isinstance(values['meta'], dict):
97
+ values['meta'] = meta_type(**values['meta']) if meta_type else None
98
+
99
+ for item in values['items']:
100
+ item = ListItemEditorBlockData.style_aware_create_from_dict(item, style)
101
+
102
+ return ListItemEditorBlockData(**values)
@@ -0,0 +1,14 @@
1
+ from __future__ import annotations
2
+
3
+ from django_spire.knowledge.entry.version.block.data.list import choices, meta
4
+
5
+ LIST_BLOCK_DATA_META_MAP = {
6
+ choices.ListEditorBlockDataStyle.ORDERED: meta.OrderedListItemMeta,
7
+ choices.ListEditorBlockDataStyle.CHECKLIST: meta.ChecklistItemMeta,
8
+ choices.ListEditorBlockDataStyle.UNORDERED: None,
9
+ }
10
+ LIST_BLOCK_DATA_REVERSE_META_MAP = {
11
+ meta.OrderedListItemMeta: choices.ListEditorBlockDataStyle.ORDERED,
12
+ meta.ChecklistItemMeta: choices.ListEditorBlockDataStyle.CHECKLIST,
13
+ None: choices.ListEditorBlockDataStyle.UNORDERED,
14
+ }
@@ -0,0 +1,15 @@
1
+ from __future__ import annotations
2
+
3
+ from pydantic import BaseModel
4
+
5
+ from django_spire.knowledge.entry.version.block.data.list.choices import \
6
+ OrderedListCounterType
7
+
8
+
9
+ class ChecklistItemMeta(BaseModel):
10
+ checked: bool = False
11
+
12
+
13
+ class OrderedListItemMeta(BaseModel):
14
+ start: int | None = None
15
+ counterType: OrderedListCounterType | str | None = None
@@ -0,0 +1,23 @@
1
+ from __future__ import annotations
2
+
3
+ from django_spire.knowledge.entry.version.block.choices import BlockTypeChoices
4
+ from django_spire.knowledge.entry.version.block.data.heading_data import \
5
+ HeadingEditorBlockData
6
+ from django_spire.knowledge.entry.version.block.data.list.data import \
7
+ ListEditorBlockData
8
+ from django_spire.knowledge.entry.version.block.data.text_data import \
9
+ TextEditorBlockData
10
+
11
+
12
+ EDITOR_BLOCK_DATA_MAP = {
13
+ BlockTypeChoices.TEXT: TextEditorBlockData,
14
+ BlockTypeChoices.HEADING: HeadingEditorBlockData,
15
+ BlockTypeChoices.LIST: ListEditorBlockData,
16
+ }
17
+
18
+
19
+ EDITOR_BLOCK_DATA_REVERSE_MAP = {
20
+ TextEditorBlockData: BlockTypeChoices.TEXT,
21
+ HeadingEditorBlockData: BlockTypeChoices.HEADING,
22
+ ListEditorBlockData: BlockTypeChoices.LIST
23
+ }
@@ -0,0 +1,13 @@
1
+ from __future__ import annotations
2
+
3
+ from django_spire.knowledge.entry.version.block.data.data import BaseEditorBlockData
4
+
5
+
6
+ class TextEditorBlockData(BaseEditorBlockData):
7
+ text: str
8
+
9
+ def render_to_text(self) -> str:
10
+ from django_spire.knowledge.entry.version.converters.markdown_converter import \
11
+ MarkdownConverter
12
+
13
+ return f'{MarkdownConverter.html_to_markdown(self.text)}\n'
@@ -1,17 +1,17 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from django.db import models
4
- from django.forms import model_to_dict
5
- from django.template.loader import render_to_string
6
4
 
7
5
  from django_spire.contrib.ordering.mixins import OrderingModelMixin
8
6
  from django_spire.history.mixins import HistoryModelMixin
9
7
  from django_spire.knowledge.entry.version.block.choices import BlockTypeChoices
10
- from django_spire.knowledge.entry.version.block.maps import ENTRY_BLOCK_MAP
11
- from django_spire.knowledge.entry.version.block.services.service import EntryVersionBlockService
12
- from django_spire.knowledge.entry.version.block.blocks.block import BaseBlock
8
+ from django_spire.knowledge.entry.version.block.data.data import BaseEditorBlockData
9
+ from django_spire.knowledge.entry.version.block.data.maps import EDITOR_BLOCK_DATA_MAP
10
+ from django_spire.knowledge.entry.version.block.querysets import \
11
+ EntryVersionBlockQuerySet
12
+ from django_spire.knowledge.entry.version.block.services.service import \
13
+ EntryVersionBlockService
13
14
  from django_spire.knowledge.entry.version.models import EntryVersion
14
- from django_spire.knowledge.entry.version.block.querysets import EntryVersionBlockQuerySet
15
15
 
16
16
 
17
17
  class EntryVersionBlock(HistoryModelMixin, OrderingModelMixin):
@@ -21,29 +21,37 @@ class EntryVersionBlock(HistoryModelMixin, OrderingModelMixin):
21
21
  related_name='blocks',
22
22
  related_query_name='block'
23
23
  )
24
+
24
25
  type = models.CharField(
25
26
  max_length=32,
26
27
  choices=BlockTypeChoices,
27
28
  default=BlockTypeChoices.TEXT
28
29
  )
30
+
29
31
  _block_data = models.JSONField()
30
32
  _text_data = models.TextField()
31
33
 
34
+ # contains data related to EditorJS tunes,
35
+ # which are additional modifications to blocks (e.g. footnotes, etc.)
36
+ # https://editorjs.io/block-tunes-api/
37
+ _tunes_data = models.JSONField(null=True, blank=True)
38
+
32
39
  objects = EntryVersionBlockQuerySet.as_manager()
33
40
  services = EntryVersionBlockService()
34
41
 
35
42
  @property
36
- def block(self) -> BaseBlock:
37
- return ENTRY_BLOCK_MAP[self.type](**self._block_data)
38
-
39
- @block.setter
40
- def block(self, value: BaseBlock):
41
- self.type = value.type
42
- self._block_data = value.model_dump()
43
+ def editor_block_data(self) -> BaseEditorBlockData:
44
+ return EDITOR_BLOCK_DATA_MAP[self.type](**self._block_data)
45
+
46
+ @editor_block_data.setter
47
+ def editor_block_data(self, value: BaseEditorBlockData):
48
+ # exclude_none=True ensures that block meta objects aren't autofilled with keys,
49
+ # which can mess up editor rendering
50
+ self._block_data = value.model_dump(exclude_none=True)
43
51
  self._text_data = value.render_to_text()
44
52
 
45
53
  def render_to_text(self) -> str:
46
- return self.block.render_to_text()
54
+ return self.editor_block_data.render_to_text()
47
55
 
48
56
  class Meta:
49
57
  verbose_name = 'Block'
@@ -1,15 +1,25 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import TYPE_CHECKING
3
+ from django.db.models.functions import Coalesce
4
+ from django.db.models import JSONField
4
5
 
5
6
  from django_spire.contrib.ordering.querysets import OrderingQuerySetMixin
6
7
  from django_spire.history.querysets import HistoryQuerySet
7
-
8
- if TYPE_CHECKING:
9
- from django.db.models import QuerySet
10
- from django_spire.knowledge.entry.version.block.models import EntryVersionBlock
8
+ from django.db.models import Value
11
9
 
12
10
 
13
11
  class EntryVersionBlockQuerySet(HistoryQuerySet, OrderingQuerySetMixin):
14
- def by_version_id(self, entry_version_id: int) -> QuerySet[EntryVersionBlock]:
15
- return self.filter(version_id=entry_version_id)
12
+ def format_for_editor(self):
13
+ coalesce_json_field = lambda field_name: Coalesce(
14
+ field_name,
15
+ Value({}, output_field=JSONField())
16
+ )
17
+
18
+ return (
19
+ self.annotate(
20
+ data=coalesce_json_field('_block_data'),
21
+ tunes=coalesce_json_field('_tunes_data'),
22
+ )
23
+ .order_by('order')
24
+ .values('id', 'type', 'data', 'tunes')
25
+ )