apis-data-projection 0.1.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.
- apis_data_projection/__init__.py +0 -0
- apis_data_projection/admin.py +43 -0
- apis_data_projection/apps.py +7 -0
- apis_data_projection/checks.py +47 -0
- apis_data_projection/config.py +4 -0
- apis_data_projection/management/commands/rebuild_projections.py +16 -0
- apis_data_projection/migrations/0001_initial.py +311 -0
- apis_data_projection/migrations/__init__.py +0 -0
- apis_data_projection/models.py +184 -0
- apis_data_projection/projection_sync.py +867 -0
- apis_data_projection/templates/admin/apis_data_projection/change_list.html +10 -0
- apis_data_projection-0.1.0.dist-info/METADATA +27 -0
- apis_data_projection-0.1.0.dist-info/RECORD +15 -0
- apis_data_projection-0.1.0.dist-info/WHEEL +4 -0
- apis_data_projection-0.1.0.dist-info/licenses/LICENSE +21 -0
|
File without changes
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from apis_data_projection.models import RelationProjection, EntityProjection, EntityFacet
|
|
2
|
+
from django.contrib import admin, messages
|
|
3
|
+
from django.http import HttpResponseRedirect
|
|
4
|
+
from django.urls import path
|
|
5
|
+
from django.template.response import TemplateResponse
|
|
6
|
+
|
|
7
|
+
from .models import EntityProjection
|
|
8
|
+
from .projection_sync import ProjectionSyncService
|
|
9
|
+
from apis_core.entities.abc import Entity
|
|
10
|
+
from apis_core.relations.models import Relation
|
|
11
|
+
|
|
12
|
+
@admin.register(RelationProjection)
|
|
13
|
+
class RelationProjectionAdmin(admin.ModelAdmin):
|
|
14
|
+
list_display = tuple(field.name for field in RelationProjection._meta.fields)
|
|
15
|
+
|
|
16
|
+
@admin.register(EntityFacet)
|
|
17
|
+
class EntityFacetAdmin(admin.ModelAdmin):
|
|
18
|
+
list_display = tuple(field.name for field in EntityFacet._meta.fields)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@admin.register(EntityProjection)
|
|
22
|
+
class EntityProjectionAdmin(admin.ModelAdmin):
|
|
23
|
+
list_display = [field.name for field in EntityProjection._meta.fields]
|
|
24
|
+
|
|
25
|
+
def get_urls(self):
|
|
26
|
+
urls = super().get_urls()
|
|
27
|
+
custom_urls = [
|
|
28
|
+
path(
|
|
29
|
+
"rebuild-projections/",
|
|
30
|
+
self.admin_site.admin_view(self.rebuild_projections_view),
|
|
31
|
+
name="apis_data_projection_rebuild_projections",
|
|
32
|
+
),
|
|
33
|
+
]
|
|
34
|
+
return custom_urls + urls
|
|
35
|
+
|
|
36
|
+
def rebuild_projections_view(self, request):
|
|
37
|
+
service = ProjectionSyncService(
|
|
38
|
+
abstract_entity_model=Entity,
|
|
39
|
+
relation_model=Relation,
|
|
40
|
+
)
|
|
41
|
+
service.sync_all()
|
|
42
|
+
self.message_user(request, "Projection rebuild completed.", level=messages.SUCCESS)
|
|
43
|
+
return HttpResponseRedirect("../")
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from django.core.checks import Error, Warning, register
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _has_attr(model, attr_name: str) -> bool:
|
|
8
|
+
try:
|
|
9
|
+
model._meta.get_field(attr_name)
|
|
10
|
+
return True
|
|
11
|
+
except Exception:
|
|
12
|
+
return hasattr(model, attr_name)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@register()
|
|
16
|
+
def apis_data_projection_checks(app_configs, **kwargs):
|
|
17
|
+
errors = []
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
from apis_core.entities.abc import Entity
|
|
21
|
+
except Exception as exc:
|
|
22
|
+
errors.append(
|
|
23
|
+
Error(
|
|
24
|
+
"apis_core.entities.abc.Entity could not be imported.",
|
|
25
|
+
hint="Install and configure apis_core before using apis_data_projection.",
|
|
26
|
+
id="apis_data_projection.E001",
|
|
27
|
+
obj=exc,
|
|
28
|
+
)
|
|
29
|
+
)
|
|
30
|
+
Entity = None
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
from apis_core.relations import Relation
|
|
34
|
+
except Exception as exc:
|
|
35
|
+
errors.append(
|
|
36
|
+
Error(
|
|
37
|
+
"apis_core.relations.Relation could not be imported.",
|
|
38
|
+
hint="Install and configure apis_core before using apis_data_projection.",
|
|
39
|
+
id="apis_data_projection.E002",
|
|
40
|
+
obj=exc,
|
|
41
|
+
)
|
|
42
|
+
)
|
|
43
|
+
Relation = None
|
|
44
|
+
|
|
45
|
+
if Entity is None or Relation is None:
|
|
46
|
+
return errors
|
|
47
|
+
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from django.core.management.base import BaseCommand
|
|
2
|
+
|
|
3
|
+
from apis_data_projection.projection_sync import ProjectionSyncService
|
|
4
|
+
from apis_core.entities.abc import Entity
|
|
5
|
+
from apis_core.relations.models import Relation
|
|
6
|
+
|
|
7
|
+
class Command(BaseCommand):
|
|
8
|
+
help = "Rebuild entity and relation projections."
|
|
9
|
+
|
|
10
|
+
def handle(self, *args, **options):
|
|
11
|
+
service = ProjectionSyncService(
|
|
12
|
+
abstract_entity_model=Entity,
|
|
13
|
+
relation_model=Relation,
|
|
14
|
+
)
|
|
15
|
+
service.sync_all()
|
|
16
|
+
self.stdout.write(self.style.SUCCESS("Projection rebuild completed."))
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
# Generated by Django 6.0.6 on 2026-06-19 09:38
|
|
2
|
+
|
|
3
|
+
import django.db.models.deletion
|
|
4
|
+
from django.db import migrations, models
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Migration(migrations.Migration):
|
|
8
|
+
|
|
9
|
+
initial = True
|
|
10
|
+
|
|
11
|
+
dependencies = [
|
|
12
|
+
("contenttypes", "0002_remove_content_type_name"),
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
operations = [
|
|
16
|
+
migrations.CreateModel(
|
|
17
|
+
name="EntityProjection",
|
|
18
|
+
fields=[
|
|
19
|
+
(
|
|
20
|
+
"id",
|
|
21
|
+
models.BigAutoField(
|
|
22
|
+
auto_created=True,
|
|
23
|
+
primary_key=True,
|
|
24
|
+
serialize=False,
|
|
25
|
+
verbose_name="ID",
|
|
26
|
+
),
|
|
27
|
+
),
|
|
28
|
+
("source_object_id", models.PositiveIntegerField()),
|
|
29
|
+
(
|
|
30
|
+
"entity_type",
|
|
31
|
+
models.CharField(
|
|
32
|
+
db_index=True,
|
|
33
|
+
help_text="Entity class name, e.g. Person, Place, Group.",
|
|
34
|
+
max_length=100,
|
|
35
|
+
),
|
|
36
|
+
),
|
|
37
|
+
("label", models.CharField(db_index=True, max_length=255)),
|
|
38
|
+
("uri", models.CharField(blank=True, max_length=500)),
|
|
39
|
+
("search_text", models.TextField(blank=True)),
|
|
40
|
+
("start_date", models.DateField(blank=True, null=True)),
|
|
41
|
+
("end_date", models.DateField(blank=True, null=True)),
|
|
42
|
+
(
|
|
43
|
+
"properties_json",
|
|
44
|
+
models.JSONField(
|
|
45
|
+
blank=True,
|
|
46
|
+
default=dict,
|
|
47
|
+
help_text="dict representation of the entity's properties.",
|
|
48
|
+
),
|
|
49
|
+
),
|
|
50
|
+
(
|
|
51
|
+
"relations_json",
|
|
52
|
+
models.JSONField(
|
|
53
|
+
blank=True,
|
|
54
|
+
default=dict,
|
|
55
|
+
help_text="dict representation of the entity's relations.",
|
|
56
|
+
),
|
|
57
|
+
),
|
|
58
|
+
("created_at", models.DateTimeField(auto_now_add=True)),
|
|
59
|
+
("updated_at", models.DateTimeField(auto_now=True)),
|
|
60
|
+
(
|
|
61
|
+
"source_content_type",
|
|
62
|
+
models.ForeignKey(
|
|
63
|
+
on_delete=django.db.models.deletion.CASCADE,
|
|
64
|
+
related_name="apis_data_projection_entity_projections",
|
|
65
|
+
to="contenttypes.contenttype",
|
|
66
|
+
),
|
|
67
|
+
),
|
|
68
|
+
],
|
|
69
|
+
options={
|
|
70
|
+
"ordering": ["label"],
|
|
71
|
+
},
|
|
72
|
+
),
|
|
73
|
+
migrations.CreateModel(
|
|
74
|
+
name="EntityFacet",
|
|
75
|
+
fields=[
|
|
76
|
+
(
|
|
77
|
+
"id",
|
|
78
|
+
models.BigAutoField(
|
|
79
|
+
auto_created=True,
|
|
80
|
+
primary_key=True,
|
|
81
|
+
serialize=False,
|
|
82
|
+
verbose_name="ID",
|
|
83
|
+
),
|
|
84
|
+
),
|
|
85
|
+
(
|
|
86
|
+
"name",
|
|
87
|
+
models.CharField(
|
|
88
|
+
db_index=True,
|
|
89
|
+
help_text="Facet key, e.g. profession, gender, works in, place of residence.",
|
|
90
|
+
max_length=100,
|
|
91
|
+
),
|
|
92
|
+
),
|
|
93
|
+
(
|
|
94
|
+
"value",
|
|
95
|
+
models.CharField(
|
|
96
|
+
db_index=True,
|
|
97
|
+
help_text="Facet display value, e.g. Wizard, male, Ankh Morpork.",
|
|
98
|
+
max_length=255,
|
|
99
|
+
),
|
|
100
|
+
),
|
|
101
|
+
(
|
|
102
|
+
"source",
|
|
103
|
+
models.CharField(
|
|
104
|
+
blank=True,
|
|
105
|
+
help_text="Optional origin, e.g. profession, gender, relation:works in.",
|
|
106
|
+
max_length=255,
|
|
107
|
+
),
|
|
108
|
+
),
|
|
109
|
+
(
|
|
110
|
+
"kind",
|
|
111
|
+
models.CharField(
|
|
112
|
+
blank=True,
|
|
113
|
+
help_text="Optional classifier, e.g. scalar, fk, m2m, relation-derived.",
|
|
114
|
+
max_length=30,
|
|
115
|
+
),
|
|
116
|
+
),
|
|
117
|
+
(
|
|
118
|
+
"weight",
|
|
119
|
+
models.DecimalField(
|
|
120
|
+
blank=True, decimal_places=2, max_digits=6, null=True
|
|
121
|
+
),
|
|
122
|
+
),
|
|
123
|
+
(
|
|
124
|
+
"extra",
|
|
125
|
+
models.JSONField(
|
|
126
|
+
blank=True,
|
|
127
|
+
default=dict,
|
|
128
|
+
help_text="Optional extra facet metadata",
|
|
129
|
+
),
|
|
130
|
+
),
|
|
131
|
+
("created_at", models.DateTimeField(auto_now_add=True)),
|
|
132
|
+
("updated_at", models.DateTimeField(auto_now=True)),
|
|
133
|
+
(
|
|
134
|
+
"entity_projection_id",
|
|
135
|
+
models.ForeignKey(
|
|
136
|
+
on_delete=django.db.models.deletion.CASCADE,
|
|
137
|
+
related_name="facets",
|
|
138
|
+
to="apis_data_projection.entityprojection",
|
|
139
|
+
),
|
|
140
|
+
),
|
|
141
|
+
],
|
|
142
|
+
options={
|
|
143
|
+
"ordering": ["name", "value"],
|
|
144
|
+
},
|
|
145
|
+
),
|
|
146
|
+
migrations.CreateModel(
|
|
147
|
+
name="RelationProjection",
|
|
148
|
+
fields=[
|
|
149
|
+
(
|
|
150
|
+
"id",
|
|
151
|
+
models.BigAutoField(
|
|
152
|
+
auto_created=True,
|
|
153
|
+
primary_key=True,
|
|
154
|
+
serialize=False,
|
|
155
|
+
verbose_name="ID",
|
|
156
|
+
),
|
|
157
|
+
),
|
|
158
|
+
("source_relation_object_id", models.PositiveIntegerField()),
|
|
159
|
+
("subj_object_id", models.PositiveIntegerField()),
|
|
160
|
+
("obj_object_id", models.PositiveIntegerField()),
|
|
161
|
+
("subj_label", models.CharField(blank=True, max_length=255)),
|
|
162
|
+
("obj_label", models.CharField(blank=True, max_length=255)),
|
|
163
|
+
("subj_entity_type", models.CharField(db_index=True, max_length=100)),
|
|
164
|
+
("obj_entity_type", models.CharField(db_index=True, max_length=100)),
|
|
165
|
+
(
|
|
166
|
+
"relation_type",
|
|
167
|
+
models.CharField(
|
|
168
|
+
db_index=True, help_text="Relation class name", max_length=100
|
|
169
|
+
),
|
|
170
|
+
),
|
|
171
|
+
("forward_label", models.CharField(db_index=True, max_length=255)),
|
|
172
|
+
(
|
|
173
|
+
"reverse_label",
|
|
174
|
+
models.CharField(blank=True, db_index=True, max_length=255),
|
|
175
|
+
),
|
|
176
|
+
("search_text", models.TextField(blank=True)),
|
|
177
|
+
("start_date", models.DateField(blank=True, null=True)),
|
|
178
|
+
("end_date", models.DateField(blank=True, null=True)),
|
|
179
|
+
(
|
|
180
|
+
"properties_json",
|
|
181
|
+
models.JSONField(
|
|
182
|
+
blank=True,
|
|
183
|
+
default=dict,
|
|
184
|
+
help_text="Optional extra relation metadata that is not a first-class column.",
|
|
185
|
+
),
|
|
186
|
+
),
|
|
187
|
+
("created_at", models.DateTimeField(auto_now_add=True)),
|
|
188
|
+
("updated_at", models.DateTimeField(auto_now=True)),
|
|
189
|
+
(
|
|
190
|
+
"obj_content_type",
|
|
191
|
+
models.ForeignKey(
|
|
192
|
+
on_delete=django.db.models.deletion.CASCADE,
|
|
193
|
+
related_name="apis_data_projection_relation_obj",
|
|
194
|
+
to="contenttypes.contenttype",
|
|
195
|
+
),
|
|
196
|
+
),
|
|
197
|
+
(
|
|
198
|
+
"source_relation_content_type",
|
|
199
|
+
models.ForeignKey(
|
|
200
|
+
on_delete=django.db.models.deletion.CASCADE,
|
|
201
|
+
related_name="apis_data_projection_relation_projections",
|
|
202
|
+
to="contenttypes.contenttype",
|
|
203
|
+
),
|
|
204
|
+
),
|
|
205
|
+
(
|
|
206
|
+
"subj_content_type",
|
|
207
|
+
models.ForeignKey(
|
|
208
|
+
on_delete=django.db.models.deletion.CASCADE,
|
|
209
|
+
related_name="apis_data_projection_relation_subj",
|
|
210
|
+
to="contenttypes.contenttype",
|
|
211
|
+
),
|
|
212
|
+
),
|
|
213
|
+
],
|
|
214
|
+
options={
|
|
215
|
+
"ordering": ["relation_type", "subj_label", "obj_label"],
|
|
216
|
+
},
|
|
217
|
+
),
|
|
218
|
+
migrations.AddIndex(
|
|
219
|
+
model_name="entityprojection",
|
|
220
|
+
index=models.Index(
|
|
221
|
+
fields=["source_content_type", "source_object_id"],
|
|
222
|
+
name="apis_data_p_source__ae8955_idx",
|
|
223
|
+
),
|
|
224
|
+
),
|
|
225
|
+
migrations.AddIndex(
|
|
226
|
+
model_name="entityprojection",
|
|
227
|
+
index=models.Index(
|
|
228
|
+
fields=["entity_type"], name="apis_data_p_entity__a87a6d_idx"
|
|
229
|
+
),
|
|
230
|
+
),
|
|
231
|
+
migrations.AddIndex(
|
|
232
|
+
model_name="entityprojection",
|
|
233
|
+
index=models.Index(fields=["label"], name="apis_data_p_label_493c50_idx"),
|
|
234
|
+
),
|
|
235
|
+
migrations.AddConstraint(
|
|
236
|
+
model_name="entityprojection",
|
|
237
|
+
constraint=models.UniqueConstraint(
|
|
238
|
+
fields=("source_content_type", "source_object_id"),
|
|
239
|
+
name="uniq_entity_projection_source",
|
|
240
|
+
),
|
|
241
|
+
),
|
|
242
|
+
migrations.AddIndex(
|
|
243
|
+
model_name="entityfacet",
|
|
244
|
+
index=models.Index(
|
|
245
|
+
fields=["entity_projection_id"], name="apis_data_p_entity__3eec92_idx"
|
|
246
|
+
),
|
|
247
|
+
),
|
|
248
|
+
migrations.AddIndex(
|
|
249
|
+
model_name="entityfacet",
|
|
250
|
+
index=models.Index(
|
|
251
|
+
fields=["name", "value"], name="apis_data_p_name_a349f0_idx"
|
|
252
|
+
),
|
|
253
|
+
),
|
|
254
|
+
migrations.AddIndex(
|
|
255
|
+
model_name="entityfacet",
|
|
256
|
+
index=models.Index(fields=["name"], name="apis_data_p_name_bd2449_idx"),
|
|
257
|
+
),
|
|
258
|
+
migrations.AddConstraint(
|
|
259
|
+
model_name="entityfacet",
|
|
260
|
+
constraint=models.UniqueConstraint(
|
|
261
|
+
fields=("entity_projection_id", "name", "value"),
|
|
262
|
+
name="uniq_entity_facet_per_projection",
|
|
263
|
+
),
|
|
264
|
+
),
|
|
265
|
+
migrations.AddIndex(
|
|
266
|
+
model_name="relationprojection",
|
|
267
|
+
index=models.Index(
|
|
268
|
+
fields=["source_relation_content_type", "source_relation_object_id"],
|
|
269
|
+
name="apis_data_p_source__2a9bae_idx",
|
|
270
|
+
),
|
|
271
|
+
),
|
|
272
|
+
migrations.AddIndex(
|
|
273
|
+
model_name="relationprojection",
|
|
274
|
+
index=models.Index(
|
|
275
|
+
fields=["subj_content_type", "subj_object_id"],
|
|
276
|
+
name="apis_data_p_subj_co_0bd7df_idx",
|
|
277
|
+
),
|
|
278
|
+
),
|
|
279
|
+
migrations.AddIndex(
|
|
280
|
+
model_name="relationprojection",
|
|
281
|
+
index=models.Index(
|
|
282
|
+
fields=["obj_content_type", "obj_object_id"],
|
|
283
|
+
name="apis_data_p_obj_con_255e1e_idx",
|
|
284
|
+
),
|
|
285
|
+
),
|
|
286
|
+
migrations.AddIndex(
|
|
287
|
+
model_name="relationprojection",
|
|
288
|
+
index=models.Index(
|
|
289
|
+
fields=["relation_type"], name="apis_data_p_relatio_ede626_idx"
|
|
290
|
+
),
|
|
291
|
+
),
|
|
292
|
+
migrations.AddIndex(
|
|
293
|
+
model_name="relationprojection",
|
|
294
|
+
index=models.Index(
|
|
295
|
+
fields=["forward_label"], name="apis_data_p_forward_384d6d_idx"
|
|
296
|
+
),
|
|
297
|
+
),
|
|
298
|
+
migrations.AddIndex(
|
|
299
|
+
model_name="relationprojection",
|
|
300
|
+
index=models.Index(
|
|
301
|
+
fields=["reverse_label"], name="apis_data_p_reverse_5ef6f2_idx"
|
|
302
|
+
),
|
|
303
|
+
),
|
|
304
|
+
migrations.AddConstraint(
|
|
305
|
+
model_name="relationprojection",
|
|
306
|
+
constraint=models.UniqueConstraint(
|
|
307
|
+
fields=("source_relation_content_type", "source_relation_object_id"),
|
|
308
|
+
name="uniq_relation_projection_source",
|
|
309
|
+
),
|
|
310
|
+
),
|
|
311
|
+
]
|
|
File without changes
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
from django.contrib.contenttypes.models import ContentType
|
|
2
|
+
from django.db import models
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class EntityProjection(models.Model):
|
|
6
|
+
source_content_type = models.ForeignKey(
|
|
7
|
+
ContentType,
|
|
8
|
+
on_delete=models.CASCADE,
|
|
9
|
+
related_name="apis_data_projection_entity_projections",
|
|
10
|
+
)
|
|
11
|
+
source_object_id = models.PositiveIntegerField()
|
|
12
|
+
|
|
13
|
+
entity_type = models.CharField(
|
|
14
|
+
max_length=100,
|
|
15
|
+
db_index=True,
|
|
16
|
+
help_text="Entity class name, e.g. Person, Place, Group.",
|
|
17
|
+
)
|
|
18
|
+
label = models.CharField(max_length=255, db_index=True)
|
|
19
|
+
uri = models.CharField(max_length=500, blank=True)
|
|
20
|
+
|
|
21
|
+
search_text = models.TextField(blank=True)
|
|
22
|
+
|
|
23
|
+
start_date = models.DateField(null=True, blank=True)
|
|
24
|
+
end_date = models.DateField(null=True, blank=True)
|
|
25
|
+
|
|
26
|
+
properties_json = models.JSONField(
|
|
27
|
+
default=dict,
|
|
28
|
+
blank=True,
|
|
29
|
+
help_text="dict representation of the entity's properties.",
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
relations_json = models.JSONField(
|
|
33
|
+
default=dict,
|
|
34
|
+
blank=True,
|
|
35
|
+
help_text="dict representation of the entity's relations.",
|
|
36
|
+
)
|
|
37
|
+
created_at = models.DateTimeField(auto_now_add=True)
|
|
38
|
+
updated_at = models.DateTimeField(auto_now=True)
|
|
39
|
+
|
|
40
|
+
class Meta:
|
|
41
|
+
indexes = [
|
|
42
|
+
models.Index(fields=["source_content_type", "source_object_id"]),
|
|
43
|
+
models.Index(fields=["entity_type"]),
|
|
44
|
+
models.Index(fields=["label"]),
|
|
45
|
+
]
|
|
46
|
+
constraints = [
|
|
47
|
+
models.UniqueConstraint(
|
|
48
|
+
fields=["source_content_type", "source_object_id"],
|
|
49
|
+
name="uniq_entity_projection_source",
|
|
50
|
+
)
|
|
51
|
+
]
|
|
52
|
+
ordering = ["label"]
|
|
53
|
+
|
|
54
|
+
def __str__(self):
|
|
55
|
+
return f"{self.entity_type}: {self.label}"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class RelationProjection(models.Model):
|
|
59
|
+
source_relation_content_type = models.ForeignKey(
|
|
60
|
+
ContentType,
|
|
61
|
+
on_delete=models.CASCADE,
|
|
62
|
+
related_name="apis_data_projection_relation_projections",
|
|
63
|
+
)
|
|
64
|
+
source_relation_object_id = models.PositiveIntegerField()
|
|
65
|
+
|
|
66
|
+
subj_content_type = models.ForeignKey(
|
|
67
|
+
ContentType,
|
|
68
|
+
on_delete=models.CASCADE,
|
|
69
|
+
related_name="apis_data_projection_relation_subj",
|
|
70
|
+
)
|
|
71
|
+
subj_object_id = models.PositiveIntegerField()
|
|
72
|
+
|
|
73
|
+
obj_content_type = models.ForeignKey(
|
|
74
|
+
ContentType,
|
|
75
|
+
on_delete=models.CASCADE,
|
|
76
|
+
related_name="apis_data_projection_relation_obj",
|
|
77
|
+
)
|
|
78
|
+
obj_object_id = models.PositiveIntegerField()
|
|
79
|
+
|
|
80
|
+
subj_label = models.CharField(max_length=255, blank=True)
|
|
81
|
+
obj_label = models.CharField(max_length=255, blank=True)
|
|
82
|
+
|
|
83
|
+
subj_entity_type = models.CharField(max_length=100, db_index=True)
|
|
84
|
+
obj_entity_type = models.CharField(max_length=100, db_index=True)
|
|
85
|
+
|
|
86
|
+
relation_type = models.CharField(
|
|
87
|
+
max_length=100,
|
|
88
|
+
db_index=True,
|
|
89
|
+
help_text="Relation class name",
|
|
90
|
+
)
|
|
91
|
+
forward_label = models.CharField(max_length=255, db_index=True)
|
|
92
|
+
reverse_label = models.CharField(max_length=255, blank=True, db_index=True)
|
|
93
|
+
|
|
94
|
+
search_text = models.TextField(blank=True)
|
|
95
|
+
|
|
96
|
+
start_date = models.DateField(null=True, blank=True)
|
|
97
|
+
end_date = models.DateField(null=True, blank=True)
|
|
98
|
+
|
|
99
|
+
properties_json = models.JSONField(
|
|
100
|
+
default=dict,
|
|
101
|
+
blank=True,
|
|
102
|
+
help_text="Optional extra relation metadata that is not a first-class column.",
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
created_at = models.DateTimeField(auto_now_add=True)
|
|
106
|
+
updated_at = models.DateTimeField(auto_now=True)
|
|
107
|
+
|
|
108
|
+
class Meta:
|
|
109
|
+
indexes = [
|
|
110
|
+
models.Index(fields=["source_relation_content_type", "source_relation_object_id"]),
|
|
111
|
+
models.Index(fields=["subj_content_type", "subj_object_id"]),
|
|
112
|
+
models.Index(fields=["obj_content_type", "obj_object_id"]),
|
|
113
|
+
models.Index(fields=["relation_type"]),
|
|
114
|
+
models.Index(fields=["forward_label"]),
|
|
115
|
+
models.Index(fields=["reverse_label"]),
|
|
116
|
+
]
|
|
117
|
+
constraints = [
|
|
118
|
+
models.UniqueConstraint(
|
|
119
|
+
fields=["source_relation_content_type", "source_relation_object_id"],
|
|
120
|
+
name="uniq_relation_projection_source",
|
|
121
|
+
)
|
|
122
|
+
]
|
|
123
|
+
ordering = ["relation_type", "subj_label", "obj_label"]
|
|
124
|
+
|
|
125
|
+
def __str__(self):
|
|
126
|
+
return f"{self.subj_label} -[{self.forward_label}]-> {self.obj_label}"
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class EntityFacet(models.Model):
|
|
130
|
+
entity_projection_id = models.ForeignKey(
|
|
131
|
+
EntityProjection,
|
|
132
|
+
on_delete=models.CASCADE,
|
|
133
|
+
related_name="facets",
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
name = models.CharField(
|
|
137
|
+
max_length=100,
|
|
138
|
+
db_index=True,
|
|
139
|
+
help_text="Facet key, e.g. profession, gender, works in, place of residence.",
|
|
140
|
+
)
|
|
141
|
+
value = models.CharField(
|
|
142
|
+
max_length=255,
|
|
143
|
+
db_index=True,
|
|
144
|
+
help_text="Facet display value, e.g. Wizard, male, Ankh Morpork.",
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
source = models.CharField(
|
|
148
|
+
max_length=255,
|
|
149
|
+
blank=True,
|
|
150
|
+
help_text="Optional origin, e.g. profession, gender, relation:works in.",
|
|
151
|
+
)
|
|
152
|
+
kind = models.CharField(
|
|
153
|
+
max_length=30,
|
|
154
|
+
blank=True,
|
|
155
|
+
help_text="Optional classifier, e.g. scalar, fk, m2m, relation-derived.",
|
|
156
|
+
)
|
|
157
|
+
weight = models.DecimalField(
|
|
158
|
+
max_digits=6,
|
|
159
|
+
decimal_places=2,
|
|
160
|
+
null=True,
|
|
161
|
+
blank=True,
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
extra = models.JSONField(default=dict, blank=True, help_text="Optional extra facet metadata")
|
|
165
|
+
|
|
166
|
+
created_at = models.DateTimeField(auto_now_add=True)
|
|
167
|
+
updated_at = models.DateTimeField(auto_now=True)
|
|
168
|
+
|
|
169
|
+
class Meta:
|
|
170
|
+
indexes = [
|
|
171
|
+
models.Index(fields=["entity_projection_id"]),
|
|
172
|
+
models.Index(fields=["name", "value"]),
|
|
173
|
+
models.Index(fields=["name"]),
|
|
174
|
+
]
|
|
175
|
+
constraints = [
|
|
176
|
+
models.UniqueConstraint(
|
|
177
|
+
fields=["entity_projection_id", "name", "value"],
|
|
178
|
+
name="uniq_entity_facet_per_projection",
|
|
179
|
+
)
|
|
180
|
+
]
|
|
181
|
+
ordering = ["name", "value"]
|
|
182
|
+
|
|
183
|
+
def __str__(self):
|
|
184
|
+
return f"{self.entity_projection_id}: {self.name}={self.value}"
|