django-spire 0.19.4__py3-none-any.whl → 0.20.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/core/migrations/0001_initial.py +26 -0
- django_spire/core/migrations/__init__.py +0 -0
- django_spire/core/static/django_spire/css/app-background.css +6 -0
- django_spire/core/static/django_spire/css/app-import.css +1 -0
- django_spire/core/static/django_spire/css/app-override.css +0 -0
- django_spire/core/static/django_spire/css/themes/ayu/app-dark.css +2 -0
- django_spire/core/static/django_spire/css/themes/ayu/app-light.css +2 -0
- django_spire/core/static/django_spire/css/themes/catppuccin/app-dark.css +2 -0
- django_spire/core/static/django_spire/css/themes/catppuccin/app-light.css +2 -0
- django_spire/core/static/django_spire/css/themes/default/app-dark.css +2 -0
- django_spire/core/static/django_spire/css/themes/default/app-light.css +2 -0
- django_spire/core/static/django_spire/css/themes/gruvbox/app-dark.css +2 -0
- django_spire/core/static/django_spire/css/themes/gruvbox/app-light.css +2 -0
- django_spire/core/static/django_spire/css/themes/material/app-dark.css +2 -0
- django_spire/core/static/django_spire/css/themes/material/app-light.css +2 -0
- django_spire/core/static/django_spire/css/themes/nord/app-dark.css +2 -0
- django_spire/core/static/django_spire/css/themes/nord/app-light.css +2 -0
- django_spire/core/static/django_spire/css/themes/one-dark/app-dark.css +2 -0
- django_spire/core/static/django_spire/css/themes/one-dark/app-light.css +2 -0
- django_spire/core/static/django_spire/css/themes/palenight/app-dark.css +2 -0
- django_spire/core/static/django_spire/css/themes/palenight/app-light.css +2 -0
- django_spire/core/static/django_spire/css/themes/rose-pine/app-dark.css +2 -0
- django_spire/core/static/django_spire/css/themes/rose-pine/app-light.css +2 -0
- django_spire/core/static/django_spire/css/themes/tokyo-night/app-dark.css +2 -0
- django_spire/core/static/django_spire/css/themes/tokyo-night/app-light.css +2 -0
- django_spire/core/tags/__init__.py +0 -0
- django_spire/core/tags/intelligence/__init__.py +0 -0
- django_spire/core/tags/intelligence/tag_set_bot.py +41 -0
- django_spire/core/tags/mixins.py +61 -0
- django_spire/core/tags/models.py +38 -0
- django_spire/core/tags/querysets.py +9 -0
- django_spire/core/tags/tests/__init__.py +0 -0
- django_spire/core/tags/tests/test_intelligence.py +28 -0
- django_spire/core/tags/tests/test_tags.py +102 -0
- django_spire/core/tags/tools.py +20 -0
- django_spire/knowledge/collection/admin.py +20 -1
- django_spire/knowledge/collection/models.py +6 -1
- django_spire/knowledge/collection/services/service.py +2 -0
- django_spire/knowledge/collection/services/tag_service.py +52 -0
- django_spire/knowledge/collection/views/form_views.py +2 -0
- django_spire/knowledge/entry/admin.py +18 -2
- django_spire/knowledge/entry/models.py +6 -1
- django_spire/knowledge/entry/services/service.py +2 -0
- django_spire/knowledge/entry/services/tag_service.py +34 -0
- django_spire/knowledge/entry/version/urls/json_urls.py +5 -1
- django_spire/knowledge/entry/version/views/json_views.py +9 -0
- django_spire/knowledge/intelligence/decoders/collection_decoder.py +4 -4
- django_spire/knowledge/intelligence/decoders/entry_decoder.py +2 -2
- django_spire/knowledge/intelligence/workflows/knowledge_workflow.py +9 -3
- django_spire/knowledge/migrations/0008_collection_tags_entry_tags.py +24 -0
- django_spire/knowledge/templates/django_spire/knowledge/collection/page/display_page.html +16 -0
- django_spire/knowledge/templates/django_spire/knowledge/entry/version/container/{detail_container.html → editor_container.html} +6 -0
- django_spire/knowledge/templates/django_spire/knowledge/entry/version/page/editor_page.html +27 -10
- django_spire/theme/enums.py +0 -3
- django_spire/theme/models.py +0 -3
- django_spire/theme/tests/test_context_processor.py +6 -6
- django_spire/theme/tests/test_enums.py +0 -3
- django_spire/theme/tests/test_integration.py +2 -2
- django_spire/theme/tests/test_model.py +23 -24
- django_spire/theme/tests/test_views/test_json_views.py +4 -4
- {django_spire-0.19.4.dist-info → django_spire-0.20.0.dist-info}/METADATA +2 -2
- {django_spire-0.19.4.dist-info → django_spire-0.20.0.dist-info}/RECORD +66 -56
- django_spire/core/static/django_spire/css/themes/dracula/app-dark.css +0 -71
- django_spire/core/static/django_spire/css/themes/dracula/app-light.css +0 -66
- django_spire/core/static/django_spire/css/themes/oceanic-next/app-dark.css +0 -71
- django_spire/core/static/django_spire/css/themes/oceanic-next/app-light.css +0 -66
- django_spire/core/static/django_spire/css/themes/synthwave/app-dark.css +0 -71
- django_spire/core/static/django_spire/css/themes/synthwave/app-light.css +0 -66
- {django_spire-0.19.4.dist-info → django_spire-0.20.0.dist-info}/WHEEL +0 -0
- {django_spire-0.19.4.dist-info → django_spire-0.20.0.dist-info}/licenses/LICENSE.md +0 -0
- {django_spire-0.19.4.dist-info → django_spire-0.20.0.dist-info}/top_level.txt +0 -0
django_spire/consts.py
CHANGED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Generated by Django 5.2.7 on 2025-11-09 05:02
|
|
2
|
+
|
|
3
|
+
from django.db import migrations, models
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Migration(migrations.Migration):
|
|
7
|
+
|
|
8
|
+
initial = True
|
|
9
|
+
|
|
10
|
+
dependencies = [
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
operations = [
|
|
14
|
+
migrations.CreateModel(
|
|
15
|
+
name='Tag',
|
|
16
|
+
fields=[
|
|
17
|
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
18
|
+
('name', models.SlugField(max_length=128, unique=True)),
|
|
19
|
+
],
|
|
20
|
+
options={
|
|
21
|
+
'verbose_name': 'Tag',
|
|
22
|
+
'verbose_name_plural': 'Tags',
|
|
23
|
+
'db_table': 'django_spire_core_tag',
|
|
24
|
+
},
|
|
25
|
+
),
|
|
26
|
+
]
|
|
File without changes
|
|
@@ -33,6 +33,8 @@
|
|
|
33
33
|
--app-bg-alt-layer-three: var(--app-alt-layer-three);
|
|
34
34
|
--app-bg-alt-layer-four: var(--app-alt-layer-four);
|
|
35
35
|
|
|
36
|
+
--app-bg-card-header: var(--app-card-header);
|
|
37
|
+
|
|
36
38
|
--app-bg-side-navigation: var(--app-side-navigation-bg-color);
|
|
37
39
|
|
|
38
40
|
--app-bg-top-navigation: var(--app-top-navigation-bg-color);
|
|
@@ -151,6 +153,10 @@
|
|
|
151
153
|
background-color: var(--app-bg-alt-layer-four) !important;
|
|
152
154
|
}
|
|
153
155
|
|
|
156
|
+
.bg-app-card-header {
|
|
157
|
+
background-color: var(--app-bg-card-header) !important;
|
|
158
|
+
}
|
|
159
|
+
|
|
154
160
|
.bg-app-side-navigation, .bg-app-side-navigation-hover:hover {
|
|
155
161
|
background-color: var(--app-bg-side-navigation) !important;
|
|
156
162
|
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from dandy import Bot, Prompt, BaseListIntel
|
|
2
|
+
from django.utils.text import slugify
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class TagsIntel(BaseListIntel[list[str]]):
|
|
6
|
+
tags: list[str]
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TagSetBot(Bot):
|
|
10
|
+
llm_role = 'Tag Identifier'
|
|
11
|
+
llm_task = 'Read through the provided content and return a list of tags.'
|
|
12
|
+
llm_guidelines = (
|
|
13
|
+
Prompt()
|
|
14
|
+
.list([
|
|
15
|
+
'Make sure to have enough tags to properly cover all the provided content.',
|
|
16
|
+
'Focus on tagging the words in the content',
|
|
17
|
+
'Only add additional words that are very relevant to the content.'
|
|
18
|
+
'Use spaces to separate words in tags',
|
|
19
|
+
'Include known acronyms along with the full tags.',
|
|
20
|
+
])
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
def process(self, content: Prompt | str) -> set[str]:
|
|
24
|
+
tags_intel = self.llm.prompt_to_intel(
|
|
25
|
+
prompt=(
|
|
26
|
+
Prompt()
|
|
27
|
+
.heading('Content to be Tagged:')
|
|
28
|
+
.line_break()
|
|
29
|
+
.text(content)
|
|
30
|
+
),
|
|
31
|
+
intel_class=TagsIntel
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
for tag in tags_intel:
|
|
35
|
+
word_segments = tag.split(" ")
|
|
36
|
+
|
|
37
|
+
if len(word_segments) > 1:
|
|
38
|
+
for word_segment in word_segments:
|
|
39
|
+
tags_intel.append(word_segment)
|
|
40
|
+
|
|
41
|
+
return {slugify(tag) for tag in tags_intel}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from django.db import models
|
|
2
|
+
from django.db.models import QuerySet
|
|
3
|
+
|
|
4
|
+
from django_spire.core.tags.models import Tag
|
|
5
|
+
from django_spire.core.tags import tools
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TagsModelMixin(models.Model):
|
|
9
|
+
tags = models.ManyToManyField(Tag, related_name='+', null=True, blank=True, editable=False)
|
|
10
|
+
|
|
11
|
+
class Meta:
|
|
12
|
+
abstract = True
|
|
13
|
+
|
|
14
|
+
@property
|
|
15
|
+
def tag_set(self) -> set[str]:
|
|
16
|
+
return set(self.tags.values_list('name', flat=True))
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
def tag_set_simplified(self) -> set[str]:
|
|
20
|
+
simplified_tag_words = []
|
|
21
|
+
|
|
22
|
+
for tag in self.tag_set:
|
|
23
|
+
tag_words = tag.split('-')
|
|
24
|
+
simplified_tag_words.extend(tag_words)
|
|
25
|
+
|
|
26
|
+
return set(simplified_tag_words)
|
|
27
|
+
|
|
28
|
+
def add_tags_from_tag_set(self, tag_set: set[str]):
|
|
29
|
+
self._update_global_tags_from_set(tag_set)
|
|
30
|
+
|
|
31
|
+
self.tags.add(*Tag.objects.in_tag_set(tag_set))
|
|
32
|
+
|
|
33
|
+
def clear_tags(self):
|
|
34
|
+
self.tags.clear()
|
|
35
|
+
|
|
36
|
+
def get_matching_tags_from_tag_set(self, tag_set: set[str]) -> QuerySet:
|
|
37
|
+
return self.tags.in_tag_set(tag_set)
|
|
38
|
+
|
|
39
|
+
def get_matching_percentage_of_tag_set(self, tag_set: set[str]) -> float:
|
|
40
|
+
return tools.get_matching_a_percentage_from_tag_sets(tag_set, self.tag_set)
|
|
41
|
+
|
|
42
|
+
def get_matching_percentage_of_model_tags_from_tag_set(self, tag_set: set[str]) -> float:
|
|
43
|
+
return tools.get_matching_b_percentage_from_tag_sets(tag_set, self.tag_set)
|
|
44
|
+
|
|
45
|
+
def has_tags_in_tag_set(self, tag_set: set[str]) -> bool:
|
|
46
|
+
return self.tag_set.issuperset(tag_set)
|
|
47
|
+
|
|
48
|
+
def remove_tags_by_tag_set(self, tag_set: set[str]):
|
|
49
|
+
tag_objects = Tag.objects.in_tag_set(tag_set)
|
|
50
|
+
|
|
51
|
+
self.tags.remove(*tag_objects)
|
|
52
|
+
|
|
53
|
+
def set_tags_from_tag_set(self, tag_set: set[str]):
|
|
54
|
+
self._update_global_tags_from_set(tag_set)
|
|
55
|
+
|
|
56
|
+
tag_objects = Tag.objects.in_tag_set(tag_set)
|
|
57
|
+
self.tags.set(tag_objects)
|
|
58
|
+
|
|
59
|
+
@staticmethod
|
|
60
|
+
def _update_global_tags_from_set(tag_set: set[str]):
|
|
61
|
+
Tag.add_tags([Tag(name=tag) for tag in tag_set])
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from typing import Sequence, Self
|
|
2
|
+
|
|
3
|
+
from django.db import models
|
|
4
|
+
from django.utils.text import slugify
|
|
5
|
+
|
|
6
|
+
from django_spire.core.tags.querysets import TagQuerySet
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Tag(models.Model):
|
|
10
|
+
name = models.SlugField(
|
|
11
|
+
max_length=128,
|
|
12
|
+
unique=True
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
objects = TagQuerySet.as_manager()
|
|
16
|
+
|
|
17
|
+
def __init__(self, *args, **kwargs):
|
|
18
|
+
name = kwargs.get('name')
|
|
19
|
+
if name:
|
|
20
|
+
kwargs['name'] = slugify(name)
|
|
21
|
+
|
|
22
|
+
super().__init__(*args, **kwargs)
|
|
23
|
+
|
|
24
|
+
def __str__(self):
|
|
25
|
+
return self.name
|
|
26
|
+
|
|
27
|
+
@classmethod
|
|
28
|
+
def add_tags(cls, tags: Sequence[Self]):
|
|
29
|
+
cls.objects.bulk_create(
|
|
30
|
+
tags,
|
|
31
|
+
ignore_conflicts=True
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
class Meta:
|
|
35
|
+
db_table = 'django_spire_core_tag'
|
|
36
|
+
verbose_name = 'Tag'
|
|
37
|
+
verbose_name_plural = 'Tags'
|
|
38
|
+
|
|
File without changes
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from unittest import TestCase
|
|
2
|
+
|
|
3
|
+
from django_spire.core.tags.intelligence.tag_set_bot import TagSetBot
|
|
4
|
+
|
|
5
|
+
TEST_INPUT = """
|
|
6
|
+
I love reading books about science fiction, fantasy, and adventure, especially ones
|
|
7
|
+
that feature artificial intelligence, machine learning, and natural language processing.
|
|
8
|
+
Some of my favorite authors include Isaac Asimov, J.R.R. Tolkien, and Neil Gaiman.
|
|
9
|
+
I'm also interested in learning more about data science, programming languages like Python
|
|
10
|
+
and Java, and emerging technologies like blockchain and the Internet of Things (IoT).
|
|
11
|
+
Can you recommend some books or resources that align with these interests?
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TestTagIntelligence(TestCase):
|
|
16
|
+
def setUp(self):
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
def test_tag_set_bot(self):
|
|
20
|
+
tag_set = TagSetBot().process(TEST_INPUT)
|
|
21
|
+
|
|
22
|
+
self.assertIn('science', tag_set)
|
|
23
|
+
self.assertIn('artificial', tag_set)
|
|
24
|
+
self.assertIn('fantasy', tag_set)
|
|
25
|
+
|
|
26
|
+
self.assertNotIn('camping', tag_set)
|
|
27
|
+
self.assertNotIn('art', tag_set)
|
|
28
|
+
self.assertNotIn('hate', tag_set)
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
from django.test import TestCase
|
|
2
|
+
|
|
3
|
+
from django_spire.core.tags.models import Tag
|
|
4
|
+
from django_spire.knowledge.collection.models import Collection
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class TestTags(TestCase):
|
|
8
|
+
def setUp(self):
|
|
9
|
+
self.collection = Collection.objects.create(
|
|
10
|
+
name='Test Collection', description='Test Collection Description'
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
self.tag_set_a = {'apple', 'banana', 'orange'}
|
|
14
|
+
self.tag_set_b = {'grape', 'kiwi', 'pear'}
|
|
15
|
+
self.tag_set_c = {'lemon', 'lime', 'mango'}
|
|
16
|
+
|
|
17
|
+
self.mixed_tag_set = {'apple', 'grape', 'lemon'}
|
|
18
|
+
|
|
19
|
+
self.dirty_tag_set = {'HaT~', 'four* tacos', '123 hoop!!'}
|
|
20
|
+
|
|
21
|
+
self.collection.add_tags_from_tag_set(self.tag_set_a)
|
|
22
|
+
self.collection.add_tags_from_tag_set(self.tag_set_b)
|
|
23
|
+
self.collection.add_tags_from_tag_set(self.tag_set_c)
|
|
24
|
+
|
|
25
|
+
self.collection.add_tags_from_tag_set(self.mixed_tag_set)
|
|
26
|
+
|
|
27
|
+
self.collection.add_tags_from_tag_set(self.dirty_tag_set)
|
|
28
|
+
|
|
29
|
+
def test_tag_removal(self):
|
|
30
|
+
self.assertIn('apple', self.collection.tag_set)
|
|
31
|
+
|
|
32
|
+
self.collection.remove_tags_by_tag_set(self.mixed_tag_set)
|
|
33
|
+
|
|
34
|
+
self.assertNotIn('lemon', self.collection.tag_set)
|
|
35
|
+
|
|
36
|
+
self.assertIn('kiwi', self.collection.tag_set)
|
|
37
|
+
|
|
38
|
+
def test_tag_add_and_set(self):
|
|
39
|
+
self.collection.set_tags_from_tag_set(self.mixed_tag_set)
|
|
40
|
+
|
|
41
|
+
self.assertIn('lemon', self.collection.tag_set)
|
|
42
|
+
|
|
43
|
+
self.assertNotIn('banana', self.collection.tag_set)
|
|
44
|
+
|
|
45
|
+
def test_tag_count(self):
|
|
46
|
+
self.assertEqual(Tag.objects.count(), 12)
|
|
47
|
+
self.assertEqual(self.collection.tags.count(), 12)
|
|
48
|
+
|
|
49
|
+
def test_tag_removal(self):
|
|
50
|
+
self.collection.remove_tags_by_tag_set(self.tag_set_a)
|
|
51
|
+
|
|
52
|
+
self.assertEqual(self.collection.tags.count(), 9)
|
|
53
|
+
|
|
54
|
+
def test_tag_bool_methods(self):
|
|
55
|
+
self.collection.remove_tags_by_tag_set(self.tag_set_b)
|
|
56
|
+
|
|
57
|
+
self.assertTrue(self.collection.has_tags_in_tag_set(self.tag_set_c))
|
|
58
|
+
self.assertFalse(self.collection.has_tags_in_tag_set(self.mixed_tag_set))
|
|
59
|
+
|
|
60
|
+
def test_tag_matching_methods(self):
|
|
61
|
+
self.assertEqual(len(self.collection.get_matching_tags_from_tag_set(self.tag_set_a)), 3)
|
|
62
|
+
|
|
63
|
+
self.collection.remove_tags_by_tag_set(self.tag_set_a)
|
|
64
|
+
|
|
65
|
+
self.assertEqual(len(self.collection.get_matching_tags_from_tag_set(self.tag_set_a)), 0)
|
|
66
|
+
self.assertEqual(len(self.collection.get_matching_tags_from_tag_set(self.tag_set_b)), 3)
|
|
67
|
+
self.assertEqual(len(self.collection.get_matching_tags_from_tag_set(self.mixed_tag_set)), 2)
|
|
68
|
+
|
|
69
|
+
self.collection.remove_tags_by_tag_set(self.tag_set_b)
|
|
70
|
+
|
|
71
|
+
self.assertEqual(len(self.collection.get_matching_tags_from_tag_set(self.tag_set_b)), 0)
|
|
72
|
+
|
|
73
|
+
self.assertEqual(len(self.collection.get_matching_tags_from_tag_set(self.mixed_tag_set)), 1)
|
|
74
|
+
|
|
75
|
+
def test_tag_metric_methods(self):
|
|
76
|
+
self.assertAlmostEqual(
|
|
77
|
+
self.collection.get_matching_percentage_of_tag_set(self.tag_set_a),
|
|
78
|
+
1.00,
|
|
79
|
+
2
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
self.assertAlmostEqual(
|
|
83
|
+
self.collection.get_matching_percentage_of_model_tags_from_tag_set(self.tag_set_b),
|
|
84
|
+
0.25,
|
|
85
|
+
2
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
self.collection.remove_tags_by_tag_set(self.mixed_tag_set)
|
|
89
|
+
|
|
90
|
+
self.assertAlmostEqual(
|
|
91
|
+
self.collection.get_matching_percentage_of_tag_set(self.tag_set_a),
|
|
92
|
+
0.666,
|
|
93
|
+
2
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
self.assertAlmostEqual(
|
|
98
|
+
self.collection.get_matching_percentage_of_model_tags_from_tag_set(self.tag_set_b),
|
|
99
|
+
0.222,
|
|
100
|
+
2
|
|
101
|
+
)
|
|
102
|
+
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
def get_matching_tag_set_from_tag_sets(tag_set_a: set[str], tag_set_b: set[str]) -> set[str]:
|
|
2
|
+
return tag_set_a.intersection(tag_set_b)
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def get_matching_count_from_tag_sets(tag_set_a: set[str], tag_set_b: set[str]) -> int:
|
|
6
|
+
return len(get_matching_tag_set_from_tag_sets(tag_set_a, tag_set_b))
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_matching_a_percentage_from_tag_sets(tag_set_a: set[str], tag_set_b: set[str]) -> float:
|
|
10
|
+
try:
|
|
11
|
+
return get_matching_count_from_tag_sets(tag_set_a, tag_set_b) / len(tag_set_a)
|
|
12
|
+
except ZeroDivisionError:
|
|
13
|
+
return 0.0
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def get_matching_b_percentage_from_tag_sets(tag_set_a: set[str], tag_set_b: set[str]) -> float:
|
|
17
|
+
try:
|
|
18
|
+
return get_matching_count_from_tag_sets(tag_set_a, tag_set_b) / len(tag_set_b)
|
|
19
|
+
except ZeroDivisionError:
|
|
20
|
+
return 0.0
|
|
@@ -1,18 +1,35 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from django.contrib import admin
|
|
4
|
+
from django.db.models import QuerySet
|
|
5
|
+
from django.contrib import messages
|
|
4
6
|
|
|
5
7
|
from .models import Collection, CollectionGroup
|
|
6
8
|
|
|
7
9
|
|
|
8
10
|
@admin.register(Collection)
|
|
9
11
|
class CollectionAdmin(admin.ModelAdmin):
|
|
10
|
-
list_display = ['id', 'name', 'parent', 'is_deleted']
|
|
12
|
+
list_display = ['id', 'name', 'parent', 'is_deleted', 'tag_count']
|
|
11
13
|
list_select_related = ['parent']
|
|
12
14
|
list_filter = ['is_deleted', 'is_active']
|
|
13
15
|
search_fields = ['id', 'name', 'description', 'parent__name']
|
|
14
16
|
ordering = ['name']
|
|
15
17
|
autocomplete_fields = ['parent']
|
|
18
|
+
actions = ['set_tags_for_collections']
|
|
19
|
+
|
|
20
|
+
@admin.action(description="Set Tags for Collections (Allow 5 Seconds Per)")
|
|
21
|
+
def set_tags_for_collections(self, request, queryset: QuerySet[Collection]):
|
|
22
|
+
processed = 0
|
|
23
|
+
for collection in queryset:
|
|
24
|
+
collection.services.tag.process_and_set_tags()
|
|
25
|
+
processed += 1
|
|
26
|
+
|
|
27
|
+
messages.success(request, f'Successfully processed {processed} collections.')
|
|
28
|
+
|
|
29
|
+
def tag_count(self, collection: Collection):
|
|
30
|
+
return collection.tags.count()
|
|
31
|
+
|
|
32
|
+
tag_count.short_description = 'Tags'
|
|
16
33
|
|
|
17
34
|
|
|
18
35
|
@admin.register(CollectionGroup)
|
|
@@ -20,3 +37,5 @@ class CollectionGroupAdmin(admin.ModelAdmin):
|
|
|
20
37
|
list_display = ['id', 'collection', 'auth_group']
|
|
21
38
|
list_select_related = ['collection', 'auth_group']
|
|
22
39
|
search_fields = ['id', 'collection__name', 'auth_group__name']
|
|
40
|
+
|
|
41
|
+
|
|
@@ -5,6 +5,7 @@ from django.db import models
|
|
|
5
5
|
from django_spire.auth.group.models import AuthGroup
|
|
6
6
|
from django_spire.contrib import Breadcrumbs
|
|
7
7
|
from django_spire.contrib.ordering.mixins import OrderingModelMixin
|
|
8
|
+
from django_spire.core.tags.mixins import TagsModelMixin
|
|
8
9
|
from django_spire.contrib.utils import truncate_string
|
|
9
10
|
from django_spire.history.mixins import HistoryModelMixin
|
|
10
11
|
from django_spire.knowledge.collection.querysets import CollectionQuerySet
|
|
@@ -12,7 +13,11 @@ from django_spire.knowledge.collection.services.service import CollectionGroupSe
|
|
|
12
13
|
CollectionService
|
|
13
14
|
|
|
14
15
|
|
|
15
|
-
class Collection(
|
|
16
|
+
class Collection(
|
|
17
|
+
HistoryModelMixin,
|
|
18
|
+
OrderingModelMixin,
|
|
19
|
+
TagsModelMixin,
|
|
20
|
+
):
|
|
16
21
|
parent = models.ForeignKey(
|
|
17
22
|
'self',
|
|
18
23
|
on_delete=models.CASCADE,
|
|
@@ -10,6 +10,7 @@ from django_spire.knowledge.collection.services.ordering_service import \
|
|
|
10
10
|
CollectionOrderingService
|
|
11
11
|
from django_spire.knowledge.collection.services.processor_service import \
|
|
12
12
|
CollectionProcessorService
|
|
13
|
+
from django_spire.knowledge.collection.services.tag_service import CollectionTagService
|
|
13
14
|
from django_spire.knowledge.collection.services.transformation_service import \
|
|
14
15
|
CollectionTransformationService
|
|
15
16
|
|
|
@@ -22,6 +23,7 @@ class CollectionService(BaseDjangoModelService['Collection']):
|
|
|
22
23
|
|
|
23
24
|
ordering = CollectionOrderingService()
|
|
24
25
|
processor = CollectionProcessorService()
|
|
26
|
+
tag = CollectionTagService()
|
|
25
27
|
transformation = CollectionTransformationService()
|
|
26
28
|
|
|
27
29
|
def save_model_obj(self, **field_data) -> tuple[Collection, bool]:
|