summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--UPGRADE.md7
-rw-r--r--archaeological_context_records/forms.py1
-rw-r--r--archaeological_context_records/migrations/0010_auto_20171011_1644.py26
-rw-r--r--archaeological_context_records/migrations/0011_auto_20171012_1316.py25
-rw-r--r--archaeological_context_records/models.py9
-rw-r--r--archaeological_context_records/tests.py60
-rw-r--r--archaeological_files/migrations/0008_auto_20171011_1644.py26
-rw-r--r--archaeological_files/migrations/0009_auto_20171012_1316.py25
-rw-r--r--archaeological_files/models.py3
-rw-r--r--archaeological_finds/forms.py1
-rw-r--r--archaeological_finds/migrations/0010_auto_20171011_1644.py61
-rw-r--r--archaeological_finds/migrations/0011_auto_20171012_1316.py65
-rw-r--r--archaeological_finds/models_finds.py18
-rw-r--r--archaeological_finds/models_treatments.py6
-rw-r--r--archaeological_operations/forms.py1
-rw-r--r--archaeological_operations/migrations/0009_auto_20171011_1644.py51
-rw-r--r--archaeological_operations/migrations/0010_auto_20171012_1316.py25
-rw-r--r--archaeological_operations/models.py7
-rw-r--r--archaeological_operations/tests.py51
-rw-r--r--archaeological_warehouse/migrations/0008_auto_20171011_1644.py36
-rw-r--r--archaeological_warehouse/migrations/0009_auto_20171012_1316.py25
-rw-r--r--archaeological_warehouse/models.py4
-rw-r--r--example_project/settings.py1
-rw-r--r--ishtar_common/admin.py25
-rw-r--r--ishtar_common/management/commands/update_search_vectors.py24
-rw-r--r--ishtar_common/migrations/0015_auto_20171011_1644.py36
-rw-r--r--ishtar_common/migrations/0016_auto_20171016_1104.py30
-rw-r--r--ishtar_common/migrations/0017_auto_20171016_1320.py29
-rw-r--r--ishtar_common/models.py114
-rw-r--r--ishtar_common/tests.py9
-rw-r--r--ishtar_common/utils.py44
-rw-r--r--ishtar_common/views.py26
32 files changed, 852 insertions, 19 deletions
diff --git a/UPGRADE.md b/UPGRADE.md
index 34d1560db..de01ca78b 100644
--- a/UPGRADE.md
+++ b/UPGRADE.md
@@ -35,3 +35,10 @@ cd <application-path>
./manage.py migrate --fake archaeological_warehouse 0002_auto_20170414_2123
./manage.py migrate
```
+
+Finally create indexes the new full text search engine
+
+```
+cd <application-path>
+./manage.py update_search_vectors
+```
diff --git a/archaeological_context_records/forms.py b/archaeological_context_records/forms.py
index e5c244fde..c310e98fa 100644
--- a/archaeological_context_records/forms.py
+++ b/archaeological_context_records/forms.py
@@ -56,6 +56,7 @@ class OperationFormSelection(forms.Form):
class RecordSelect(TableSelect):
+ search_vector = forms.CharField(label=_(u"Full text search"))
label = forms.CharField(label=_(u"ID"), max_length=100)
parcel__town = get_town_field()
if settings.COUNTRY == 'fr':
diff --git a/archaeological_context_records/migrations/0010_auto_20171011_1644.py b/archaeological_context_records/migrations/0010_auto_20171011_1644.py
new file mode 100644
index 000000000..379110e44
--- /dev/null
+++ b/archaeological_context_records/migrations/0010_auto_20171011_1644.py
@@ -0,0 +1,26 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11 on 2017-10-11 16:44
+from __future__ import unicode_literals
+
+import django.contrib.postgres.search
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('archaeological_context_records', '0009_auto_20170829_1639'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='contextrecord',
+ name='search_vector',
+ field=django.contrib.postgres.search.SearchVectorField(blank=True, help_text='Auto filled at save', null=True, verbose_name='Search vector'),
+ ),
+ migrations.AddField(
+ model_name='historicalcontextrecord',
+ name='search_vector',
+ field=django.contrib.postgres.search.SearchVectorField(blank=True, help_text='Auto filled at save', null=True, verbose_name='Search vector'),
+ ),
+ ]
diff --git a/archaeological_context_records/migrations/0011_auto_20171012_1316.py b/archaeological_context_records/migrations/0011_auto_20171012_1316.py
new file mode 100644
index 000000000..95b042c43
--- /dev/null
+++ b/archaeological_context_records/migrations/0011_auto_20171012_1316.py
@@ -0,0 +1,25 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11 on 2017-10-12 13:16
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('archaeological_context_records', '0010_auto_20171011_1644'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='contextrecord',
+ name='cached_label',
+ field=models.TextField(blank=True, db_index=True, null=True, verbose_name='Cached name'),
+ ),
+ migrations.AlterField(
+ model_name='historicalcontextrecord',
+ name='cached_label',
+ field=models.TextField(blank=True, db_index=True, null=True, verbose_name='Cached name'),
+ ),
+ ]
diff --git a/archaeological_context_records/models.py b/archaeological_context_records/models.py
index 2f02ed9df..a8517ed26 100644
--- a/archaeological_context_records/models.py
+++ b/archaeological_context_records/models.py
@@ -302,7 +302,14 @@ class ContextRecord(BulkUpdatedItem, BaseHistorizedItem,
point_2d = models.PointField(_(u"Point (2D)"), blank=True, null=True)
point = models.PointField(_(u"Point (3D)"), blank=True, null=True, dim=3)
polygon = models.PolygonField(_(u"Polygon"), blank=True, null=True)
- cached_label = models.TextField(_(u"Cached name"), null=True, blank=True)
+ cached_label = models.TextField(_(u"Cached name"), null=True, blank=True,
+ db_index=True)
+ PARENT_SEARCH_VECTORS = ['operation']
+ BASE_SEARCH_VECTORS = ["cached_label", "label", "location",
+ "interpretation", "filling", "datings_comment",
+ "identification__label", "activity__label",
+ "excavation_technic__label"]
+ M2M_SEARCH_VECTORS = ["datings__period__label"]
history = HistoricalRecords()
class Meta:
diff --git a/archaeological_context_records/tests.py b/archaeological_context_records/tests.py
index 89b15fbbf..b0f4b8f9e 100644
--- a/archaeological_context_records/tests.py
+++ b/archaeological_context_records/tests.py
@@ -273,6 +273,20 @@ class ContextRecordTest(ContextRecordInit, TestCase):
cr.operation
)
+ def test_search_vector_update(self):
+ cr = self.create_context_record(force=True)[0]
+ cr = models.ContextRecord.objects.get(pk=cr.pk)
+ cr.label = "Label label"
+ cr.location = "I am heeere"
+ cr.save()
+ for key in ('label', 'heeer'):
+ self.assertIn(key, cr.search_vector)
+ cr.operation.code_patriarche = "PATRIARCHE"
+ cr.operation.save()
+ cr = models.ContextRecord.objects.get(pk=cr.pk)
+ self.assertIn(settings.ISHTAR_OPE_PREFIX.lower() + "patriarch",
+ cr.search_vector)
+
def test_upstream_cache_update(self):
cr = self.create_context_record()[0]
cr_pk = cr.pk
@@ -399,6 +413,44 @@ class ContextRecordSearchTest(ContextRecordInit, TestCase):
models.RecordRelations.objects.create(
left_record=cr_1, right_record=cr_2, relation_type=sym_rel_type)
+ def test_town_search(self):
+ c = Client()
+ c.login(username=self.username, password=self.password)
+
+ data = {'numero_insee': '98989', 'name': 'base_town'}
+ base_town = self.create_towns(datas=data)[-1]
+
+ parcel = self.create_parcel(data={'town': base_town,
+ 'section': 'A', 'parcel_number': '1'})[-1]
+ self.context_records[0].parcel = parcel
+ self.context_records[0].save()
+
+ data = {'numero_insee': '56789', 'name': 'parent_town'}
+ parent_town = self.create_towns(datas=data)[-1]
+ parent_town.children.add(base_town)
+
+ data = {'numero_insee': '01234', 'name': 'child_town'}
+ child_town = self.create_towns(datas=data)[-1]
+ base_town.children.add(child_town)
+
+ # simple search
+ search = {'parcel__town': base_town.pk}
+ response = c.get(reverse('get-contextrecord'), search)
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(json.loads(response.content)['total'], 1)
+
+ # parent search
+ search = {'parcel__town': parent_town.pk}
+ response = c.get(reverse('get-contextrecord'), search)
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(json.loads(response.content)['total'], 1)
+
+ # child search
+ search = {'parcel__town': child_town.pk}
+ response = c.get(reverse('get-contextrecord'), search)
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(json.loads(response.content)['total'], 1)
+
def testSearchExport(self):
c = Client()
response = c.get(reverse('get-contextrecord'))
@@ -416,6 +468,14 @@ class ContextRecordSearchTest(ContextRecordInit, TestCase):
{'label': 'cr 1',
'cr_relation_types_0': self.cr_rel_type.pk})
self.assertEqual(json.loads(response.content)['total'], 2)
+ # test search vector
+ response = c.get(reverse('get-contextrecord'),
+ {'search_vector': 'CR'})
+ self.assertEqual(json.loads(response.content)['total'], 2)
+ # the 2 context records have the same operation
+ response = c.get(reverse('get-contextrecord'),
+ {'search_vector': 'op2010'})
+ self.assertEqual(json.loads(response.content)['total'], 2)
# test search between related operations
first_ope = self.operations[0]
first_ope.year = 2010
diff --git a/archaeological_files/migrations/0008_auto_20171011_1644.py b/archaeological_files/migrations/0008_auto_20171011_1644.py
new file mode 100644
index 000000000..33dfbf59e
--- /dev/null
+++ b/archaeological_files/migrations/0008_auto_20171011_1644.py
@@ -0,0 +1,26 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11 on 2017-10-11 16:44
+from __future__ import unicode_literals
+
+import django.contrib.postgres.search
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('archaeological_files', '0007_auto_20170826_1152'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='file',
+ name='search_vector',
+ field=django.contrib.postgres.search.SearchVectorField(blank=True, help_text='Auto filled at save', null=True, verbose_name='Search vector'),
+ ),
+ migrations.AddField(
+ model_name='historicalfile',
+ name='search_vector',
+ field=django.contrib.postgres.search.SearchVectorField(blank=True, help_text='Auto filled at save', null=True, verbose_name='Search vector'),
+ ),
+ ]
diff --git a/archaeological_files/migrations/0009_auto_20171012_1316.py b/archaeological_files/migrations/0009_auto_20171012_1316.py
new file mode 100644
index 000000000..cd33d8243
--- /dev/null
+++ b/archaeological_files/migrations/0009_auto_20171012_1316.py
@@ -0,0 +1,25 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11 on 2017-10-12 13:16
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('archaeological_files', '0008_auto_20171011_1644'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='file',
+ name='cached_label',
+ field=models.TextField(blank=True, db_index=True, null=True, verbose_name='Cached name'),
+ ),
+ migrations.AlterField(
+ model_name='historicalfile',
+ name='cached_label',
+ field=models.TextField(blank=True, db_index=True, null=True, verbose_name='Cached name'),
+ ),
+ ]
diff --git a/archaeological_files/models.py b/archaeological_files/models.py
index 0d5b4b3e8..b0f53f11c 100644
--- a/archaeological_files/models.py
+++ b/archaeological_files/models.py
@@ -204,7 +204,8 @@ class File(ClosedItem, BaseHistorizedItem, OwnPerms, ValueGetter,
mh_listing = models.NullBooleanField(
u"Sur Monument Historique inscrit", blank=True, null=True)
# <-- research archaeology
- cached_label = models.TextField(_(u"Cached name"), null=True, blank=True)
+ cached_label = models.TextField(_(u"Cached name"), null=True, blank=True,
+ db_index=True)
imported_line = models.TextField(_(u"Imported line"), null=True,
blank=True)
history = HistoricalRecords()
diff --git a/archaeological_finds/forms.py b/archaeological_finds/forms.py
index aa0ae4621..1f81cf52f 100644
--- a/archaeological_finds/forms.py
+++ b/archaeological_finds/forms.py
@@ -366,6 +366,7 @@ DatingFormSet.form_label = _("Dating")
class FindSelect(TableSelect):
+ search_vector = forms.CharField(label=_(u"Full text search"))
base_finds__cache_short_id = forms.CharField(label=_(u"Short ID"))
base_finds__cache_complete_id = forms.CharField(label=_(u"Complete ID"))
label = forms.CharField(label=_(u"Free ID"))
diff --git a/archaeological_finds/migrations/0010_auto_20171011_1644.py b/archaeological_finds/migrations/0010_auto_20171011_1644.py
new file mode 100644
index 000000000..ce892e96d
--- /dev/null
+++ b/archaeological_finds/migrations/0010_auto_20171011_1644.py
@@ -0,0 +1,61 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11 on 2017-10-11 16:44
+from __future__ import unicode_literals
+
+import django.contrib.postgres.search
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('archaeological_finds', '0009_auto_20171010_1644'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='basefind',
+ name='search_vector',
+ field=django.contrib.postgres.search.SearchVectorField(blank=True, help_text='Auto filled at save', null=True, verbose_name='Search vector'),
+ ),
+ migrations.AddField(
+ model_name='find',
+ name='search_vector',
+ field=django.contrib.postgres.search.SearchVectorField(blank=True, help_text='Auto filled at save', null=True, verbose_name='Search vector'),
+ ),
+ migrations.AddField(
+ model_name='historicalbasefind',
+ name='search_vector',
+ field=django.contrib.postgres.search.SearchVectorField(blank=True, help_text='Auto filled at save', null=True, verbose_name='Search vector'),
+ ),
+ migrations.AddField(
+ model_name='historicalfind',
+ name='search_vector',
+ field=django.contrib.postgres.search.SearchVectorField(blank=True, help_text='Auto filled at save', null=True, verbose_name='Search vector'),
+ ),
+ migrations.AddField(
+ model_name='historicaltreatment',
+ name='search_vector',
+ field=django.contrib.postgres.search.SearchVectorField(blank=True, help_text='Auto filled at save', null=True, verbose_name='Search vector'),
+ ),
+ migrations.AddField(
+ model_name='historicaltreatmentfile',
+ name='search_vector',
+ field=django.contrib.postgres.search.SearchVectorField(blank=True, help_text='Auto filled at save', null=True, verbose_name='Search vector'),
+ ),
+ migrations.AddField(
+ model_name='property',
+ name='search_vector',
+ field=django.contrib.postgres.search.SearchVectorField(blank=True, help_text='Auto filled at save', null=True, verbose_name='Search vector'),
+ ),
+ migrations.AddField(
+ model_name='treatment',
+ name='search_vector',
+ field=django.contrib.postgres.search.SearchVectorField(blank=True, help_text='Auto filled at save', null=True, verbose_name='Search vector'),
+ ),
+ migrations.AddField(
+ model_name='treatmentfile',
+ name='search_vector',
+ field=django.contrib.postgres.search.SearchVectorField(blank=True, help_text='Auto filled at save', null=True, verbose_name='Search vector'),
+ ),
+ ]
diff --git a/archaeological_finds/migrations/0011_auto_20171012_1316.py b/archaeological_finds/migrations/0011_auto_20171012_1316.py
new file mode 100644
index 000000000..6fabd578f
--- /dev/null
+++ b/archaeological_finds/migrations/0011_auto_20171012_1316.py
@@ -0,0 +1,65 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11 on 2017-10-12 13:16
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('archaeological_finds', '0010_auto_20171011_1644'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='basefind',
+ name='cache_complete_id',
+ field=models.TextField(blank=True, db_index=True, help_text='Cached value - do not edit', null=True, verbose_name='Complete ID'),
+ ),
+ migrations.AlterField(
+ model_name='basefind',
+ name='cache_short_id',
+ field=models.TextField(blank=True, db_index=True, help_text='Cached value - do not edit', null=True, verbose_name='Short ID'),
+ ),
+ migrations.AlterField(
+ model_name='find',
+ name='cached_label',
+ field=models.TextField(blank=True, db_index=True, null=True, verbose_name='Cached name'),
+ ),
+ migrations.AlterField(
+ model_name='historicalbasefind',
+ name='cache_complete_id',
+ field=models.TextField(blank=True, db_index=True, help_text='Cached value - do not edit', null=True, verbose_name='Complete ID'),
+ ),
+ migrations.AlterField(
+ model_name='historicalbasefind',
+ name='cache_short_id',
+ field=models.TextField(blank=True, db_index=True, help_text='Cached value - do not edit', null=True, verbose_name='Short ID'),
+ ),
+ migrations.AlterField(
+ model_name='historicalfind',
+ name='cached_label',
+ field=models.TextField(blank=True, db_index=True, null=True, verbose_name='Cached name'),
+ ),
+ migrations.AlterField(
+ model_name='historicaltreatment',
+ name='cached_label',
+ field=models.TextField(blank=True, db_index=True, null=True, verbose_name='Cached name'),
+ ),
+ migrations.AlterField(
+ model_name='historicaltreatmentfile',
+ name='cached_label',
+ field=models.TextField(blank=True, db_index=True, null=True, verbose_name='Cached name'),
+ ),
+ migrations.AlterField(
+ model_name='treatment',
+ name='cached_label',
+ field=models.TextField(blank=True, db_index=True, null=True, verbose_name='Cached name'),
+ ),
+ migrations.AlterField(
+ model_name='treatmentfile',
+ name='cached_label',
+ field=models.TextField(blank=True, db_index=True, null=True, verbose_name='Cached name'),
+ ),
+ ]
diff --git a/archaeological_finds/models_finds.py b/archaeological_finds/models_finds.py
index e58d14f7e..8052601bf 100644
--- a/archaeological_finds/models_finds.py
+++ b/archaeological_finds/models_finds.py
@@ -231,14 +231,17 @@ class BaseFind(BulkUpdatedItem, BaseHistorizedItem, OwnPerms):
line = models.LineStringField(_(u"Line"), blank=True, null=True)
polygon = models.PolygonField(_(u"Polygon"), blank=True, null=True)
cache_short_id = models.TextField(
- _(u"Short ID"), blank=True, null=True,
+ _(u"Short ID"), blank=True, null=True, db_index=True,
help_text=_(u"Cached value - do not edit"))
cache_complete_id = models.TextField(
- _(u"Complete ID"), blank=True, null=True,
+ _(u"Complete ID"), blank=True, null=True, db_index=True,
help_text=_(u"Cached value - do not edit"))
history = HistoricalRecords()
RELATED_POST_PROCESS = ['find']
CACHED_LABELS = ['cache_short_id', 'cache_complete_id']
+ PARENT_SEARCH_VECTORS = ['context_record']
+ BASE_SEARCH_VECTORS = ["label", "description", "comment", "cache_short_id",
+ "cache_complete_id"]
class Meta:
verbose_name = _(u"Base find")
@@ -748,9 +751,18 @@ class Find(BulkUpdatedItem, ValueGetter, BaseHistorizedItem, ImageModel,
appraisal_date = models.DateField(_(u"Appraisal date"), blank=True,
null=True)
- cached_label = models.TextField(_(u"Cached name"), null=True, blank=True)
+ cached_label = models.TextField(_(u"Cached name"), null=True, blank=True,
+ db_index=True)
history = HistoricalRecords()
BASKET_MODEL = FindBasket
+ PARENT_SEARCH_VECTORS = ['base_finds']
+ BASE_SEARCH_VECTORS = [
+ "cached_label", "label", "description", "container__location__name",
+ "container__reference", "mark", "comment", "dating_comment",
+ "previous_id"]
+ M2M_SEARCH_VECTORS = [
+ "datings__period__label", "object_types__label", "integrities__label",
+ "remarkabilities__label", "material_types__label"]
class Meta:
verbose_name = _(u"Find")
diff --git a/archaeological_finds/models_treatments.py b/archaeological_finds/models_treatments.py
index 0ffcd87fa..b4d98528b 100644
--- a/archaeological_finds/models_treatments.py
+++ b/archaeological_finds/models_treatments.py
@@ -115,7 +115,8 @@ class Treatment(DashboardFormItem, ValueGetter, BaseHistorizedItem,
blank=True, null=True)
target_is_basket = models.BooleanField(_(u"Target a basket"),
default=False)
- cached_label = models.TextField(_(u"Cached name"), null=True, blank=True)
+ cached_label = models.TextField(_(u"Cached name"), null=True, blank=True,
+ db_index=True)
history = HistoricalRecords()
class Meta:
@@ -506,7 +507,8 @@ class TreatmentFile(DashboardFormItem, ClosedItem, BaseHistorizedItem,
reception_date = models.DateField(_(u'Reception date'), blank=True,
null=True)
comment = models.TextField(_(u"Comment"), null=True, blank=True)
- cached_label = models.TextField(_(u"Cached name"), null=True, blank=True)
+ cached_label = models.TextField(_(u"Cached name"), null=True, blank=True,
+ db_index=True)
history = HistoricalRecords()
class Meta:
diff --git a/archaeological_operations/forms.py b/archaeological_operations/forms.py
index 651cd740f..841131da6 100644
--- a/archaeological_operations/forms.py
+++ b/archaeological_operations/forms.py
@@ -480,6 +480,7 @@ RecordRelationsFormSet.form_label = _(u"Relations")
class OperationSelect(TableSelect):
+ search_vector = forms.CharField(label=_(u"Full text search"))
year = forms.IntegerField(label=_("Year"))
operation_code = forms.IntegerField(label=_(u"Numeric reference"))
if settings.COUNTRY == 'fr':
diff --git a/archaeological_operations/migrations/0009_auto_20171011_1644.py b/archaeological_operations/migrations/0009_auto_20171011_1644.py
new file mode 100644
index 000000000..18a284a21
--- /dev/null
+++ b/archaeological_operations/migrations/0009_auto_20171011_1644.py
@@ -0,0 +1,51 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11 on 2017-10-11 16:44
+from __future__ import unicode_literals
+
+import django.contrib.postgres.search
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('archaeological_operations', '0008_auto_20170829_1639'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='administrativeact',
+ name='search_vector',
+ field=django.contrib.postgres.search.SearchVectorField(blank=True, help_text='Auto filled at save', null=True, verbose_name='Search vector'),
+ ),
+ migrations.AddField(
+ model_name='archaeologicalsite',
+ name='search_vector',
+ field=django.contrib.postgres.search.SearchVectorField(blank=True, help_text='Auto filled at save', null=True, verbose_name='Search vector'),
+ ),
+ migrations.AddField(
+ model_name='historicaladministrativeact',
+ name='search_vector',
+ field=django.contrib.postgres.search.SearchVectorField(blank=True, help_text='Auto filled at save', null=True, verbose_name='Search vector'),
+ ),
+ migrations.AddField(
+ model_name='historicaloperation',
+ name='search_vector',
+ field=django.contrib.postgres.search.SearchVectorField(blank=True, help_text='Auto filled at save', null=True, verbose_name='Search vector'),
+ ),
+ migrations.AddField(
+ model_name='operation',
+ name='search_vector',
+ field=django.contrib.postgres.search.SearchVectorField(blank=True, help_text='Auto filled at save', null=True, verbose_name='Search vector'),
+ ),
+ migrations.AddField(
+ model_name='parcel',
+ name='search_vector',
+ field=django.contrib.postgres.search.SearchVectorField(blank=True, help_text='Auto filled at save', null=True, verbose_name='Search vector'),
+ ),
+ migrations.AddField(
+ model_name='parcelowner',
+ name='search_vector',
+ field=django.contrib.postgres.search.SearchVectorField(blank=True, help_text='Auto filled at save', null=True, verbose_name='Search vector'),
+ ),
+ ]
diff --git a/archaeological_operations/migrations/0010_auto_20171012_1316.py b/archaeological_operations/migrations/0010_auto_20171012_1316.py
new file mode 100644
index 000000000..3a847a803
--- /dev/null
+++ b/archaeological_operations/migrations/0010_auto_20171012_1316.py
@@ -0,0 +1,25 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11 on 2017-10-12 13:16
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('archaeological_operations', '0009_auto_20171011_1644'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='historicaloperation',
+ name='cached_label',
+ field=models.CharField(blank=True, db_index=True, max_length=500, null=True, verbose_name='Cached name'),
+ ),
+ migrations.AlterField(
+ model_name='operation',
+ name='cached_label',
+ field=models.CharField(blank=True, db_index=True, max_length=500, null=True, verbose_name='Cached name'),
+ ),
+ ]
diff --git a/archaeological_operations/models.py b/archaeological_operations/models.py
index 54ed96cec..dd3e65534 100644
--- a/archaeological_operations/models.py
+++ b/archaeological_operations/models.py
@@ -248,6 +248,10 @@ class Operation(ClosedItem, BaseHistorizedItem, ImageModel, OwnPerms,
'archaeological_sites__reference': _(u"Archaeological sites ("
u"reference)"),
}
+ BASE_SEARCH_VECTORS = ["scientist__raw_name", "cached_label",
+ "common_name", "comment", "address", "old_code"]
+ INT_SEARCH_VECTORS = ["year"]
+ M2M_SEARCH_VECTORS = ["towns__name"]
# fields definition
creation_date = models.DateField(_(u"Creation date"),
@@ -309,6 +313,7 @@ class Operation(ClosedItem, BaseHistorizedItem, ImageModel, OwnPerms,
code_patriarche = models.TextField(u"Code PATRIARCHE", null=True,
blank=True, unique=True)
TABLE_COLS = ['full_code_patriarche'] + TABLE_COLS
+ BASE_SEARCH_VECTORS = ['code_patriarche'] + BASE_SEARCH_VECTORS
# preventive
fnap_financing = models.FloatField(u"Financement FNAP (%)",
blank=True, null=True)
@@ -340,7 +345,7 @@ class Operation(ClosedItem, BaseHistorizedItem, ImageModel, OwnPerms,
scientific_documentation_comment = models.TextField(
_(u"Comment about scientific documentation"), null=True, blank=True)
cached_label = models.CharField(_(u"Cached name"), max_length=500,
- null=True, blank=True)
+ null=True, blank=True, db_index=True)
archaeological_sites = models.ManyToManyField(
ArchaeologicalSite, verbose_name=_(u"Archaeological sites"),
blank=True, related_name='operations')
diff --git a/archaeological_operations/tests.py b/archaeological_operations/tests.py
index 0d6908374..9d99ed67d 100644
--- a/archaeological_operations/tests.py
+++ b/archaeological_operations/tests.py
@@ -895,6 +895,21 @@ class OperationTest(TestCase, OperationInitTest):
self.assertEqual(ope_id, 'OP2011-1')
self.assertEqual(town, self.towns[0].name)
+ def test_search_vector_update(self):
+ operation = self.operations[0]
+ town = self.create_towns({'numero_insee': '12346', 'name': 'Daisy'})[-1]
+ operation.towns.add(town)
+ town = self.create_towns(
+ {'numero_insee': '12347', 'name': 'Dirty old'})[-1]
+ operation.towns.add(town)
+ operation = models.Operation.objects.get(pk=operation.pk)
+ operation.comment = u"Zardoz"
+ operation.code_patriarche = u"HUIAAA5"
+ operation.save()
+ for key in ('old', 'op2010', 'dirty', 'daisy', "'2010'", "zardoz",
+ "huiaaa5"):
+ self.assertIn(key, operation.search_vector)
+
def test_cache_bulk_update(self):
if settings.USE_SPATIALITE_FOR_TESTS:
# using views - can only be tested with postgresql
@@ -1104,6 +1119,42 @@ class OperationSearchTest(TestCase, OperationInitTest):
self.assertEqual(response.status_code, 200)
self.assertEqual(json.loads(response.content)['total'], 1)
+ def test_town_search(self):
+ c = Client()
+ c.login(username=self.username, password=self.password)
+
+ data = {'numero_insee': '98989', 'name': 'base_town'}
+ base_town = self.create_towns(datas=data)[-1]
+
+ data = {'numero_insee': '56789', 'name': 'parent_town'}
+ parent_town = self.create_towns(datas=data)[-1]
+ parent_town.children.add(base_town)
+
+ data = {'numero_insee': '01234', 'name': 'child_town'}
+ child_town = self.create_towns(datas=data)[-1]
+ base_town.children.add(child_town)
+
+ ope = self.operations[1]
+ ope.towns.add(base_town)
+
+ # simple search
+ search = {'towns': base_town.pk}
+ response = c.get(reverse('get-operation'), search)
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(json.loads(response.content)['total'], 1)
+
+ # parent search
+ search = {'towns': parent_town.pk}
+ response = c.get(reverse('get-operation'), search)
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(json.loads(response.content)['total'], 1)
+
+ # child search
+ search = {'towns': child_town.pk}
+ response = c.get(reverse('get-operation'), search)
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(json.loads(response.content)['total'], 1)
+
def testOwnSearch(self):
c = Client()
response = c.get(reverse('get-operation'), {'year': '2010'})
diff --git a/archaeological_warehouse/migrations/0008_auto_20171011_1644.py b/archaeological_warehouse/migrations/0008_auto_20171011_1644.py
new file mode 100644
index 000000000..82245647d
--- /dev/null
+++ b/archaeological_warehouse/migrations/0008_auto_20171011_1644.py
@@ -0,0 +1,36 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11 on 2017-10-11 16:44
+from __future__ import unicode_literals
+
+import django.contrib.postgres.search
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('archaeological_warehouse', '0007_auto_20171004_1125'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='collection',
+ name='search_vector',
+ field=django.contrib.postgres.search.SearchVectorField(blank=True, help_text='Auto filled at save', null=True, verbose_name='Search vector'),
+ ),
+ migrations.AddField(
+ model_name='container',
+ name='search_vector',
+ field=django.contrib.postgres.search.SearchVectorField(blank=True, help_text='Auto filled at save', null=True, verbose_name='Search vector'),
+ ),
+ migrations.AddField(
+ model_name='warehouse',
+ name='search_vector',
+ field=django.contrib.postgres.search.SearchVectorField(blank=True, help_text='Auto filled at save', null=True, verbose_name='Search vector'),
+ ),
+ migrations.AlterField(
+ model_name='container',
+ name='index',
+ field=models.IntegerField(default=0, verbose_name='Container ID'),
+ ),
+ ]
diff --git a/archaeological_warehouse/migrations/0009_auto_20171012_1316.py b/archaeological_warehouse/migrations/0009_auto_20171012_1316.py
new file mode 100644
index 000000000..a25a2d2f2
--- /dev/null
+++ b/archaeological_warehouse/migrations/0009_auto_20171012_1316.py
@@ -0,0 +1,25 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11 on 2017-10-12 13:16
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('archaeological_warehouse', '0008_auto_20171011_1644'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='container',
+ name='cached_label',
+ field=models.CharField(blank=True, db_index=True, max_length=500, null=True, verbose_name='Localisation'),
+ ),
+ migrations.AlterField(
+ model_name='container',
+ name='cached_location',
+ field=models.CharField(blank=True, db_index=True, max_length=500, null=True, verbose_name='Cached location'),
+ ),
+ ]
diff --git a/archaeological_warehouse/models.py b/archaeological_warehouse/models.py
index 41891b341..71f31981a 100644
--- a/archaeological_warehouse/models.py
+++ b/archaeological_warehouse/models.py
@@ -283,9 +283,9 @@ class Container(LightHistorizedItem, ImageModel):
reference = models.CharField(_(u"Container ref."), max_length=40)
comment = models.TextField(_(u"Comment"), null=True, blank=True)
cached_label = models.CharField(_(u"Localisation"), max_length=500,
- null=True, blank=True)
+ null=True, blank=True, db_index=True)
cached_location = models.CharField(_(u"Cached location"), max_length=500,
- null=True, blank=True)
+ null=True, blank=True, db_index=True)
index = models.IntegerField(u"Container ID", default=0)
external_id = models.TextField(_(u"External ID"), blank=True, null=True)
auto_external_id = models.BooleanField(
diff --git a/example_project/settings.py b/example_project/settings.py
index ea50daffb..6ca8cb5fc 100644
--- a/example_project/settings.py
+++ b/example_project/settings.py
@@ -240,6 +240,7 @@ ISHTAR_PERIODS = {}
ISHTAR_PERMIT_TYPES = {}
ISHTAR_DOC_TYPES = {u"undefined": u"Undefined"}
+ISHTAR_SEARCH_LANGUAGE = "french"
ISHTAR_DPTS = []
diff --git a/ishtar_common/admin.py b/ishtar_common/admin.py
index cec61a51e..fbd9f4d29 100644
--- a/ishtar_common/admin.py
+++ b/ishtar_common/admin.py
@@ -20,6 +20,8 @@
import csv
from ajax_select import make_ajax_form
+from ajax_select.fields import AutoCompleteSelectField, \
+ AutoCompleteSelectMultipleField
from django.conf import settings
from django.contrib import admin
@@ -171,12 +173,31 @@ class PersonAdmin(HistorizedObjectAdmin):
admin_site.register(models.Person, PersonAdmin)
+class AdminRelatedTownForm(forms.ModelForm):
+ class Meta:
+ model = models.Town.children.through
+ exclude = []
+ from_town = AutoCompleteSelectField(
+ 'town', required=True, label=_(u"Parent"))
+
+
class AdminTownForm(forms.ModelForm):
class Meta:
model = models.Town
exclude = []
center = PointField(label=_(u"center"), required=False,
widget=OSMWidget)
+ children = AutoCompleteSelectMultipleField('town', required=False,
+ label=_(u"Town children"))
+
+
+class TownParentInline(admin.TabularInline):
+ model = models.Town.children.through
+ fk_name = 'to_town'
+ form = AdminRelatedTownForm
+ verbose_name = _(u"Parent")
+ verbose_name_plural = _(u"Parents")
+ extra = 1
class TownAdmin(admin.ModelAdmin):
@@ -186,9 +207,11 @@ class TownAdmin(admin.ModelAdmin):
list_display += ['numero_insee', 'departement', ]
search_fields += ['numero_insee', 'departement__label', ]
list_filter = ("departement",)
- readonly_fields = ['imports']
+ readonly_fields = ['cached_label', 'imports']
model = models.Town
form = AdminTownForm
+ inlines = [TownParentInline]
+
admin_site.register(models.Town, TownAdmin)
diff --git a/ishtar_common/management/commands/update_search_vectors.py b/ishtar_common/management/commands/update_search_vectors.py
new file mode 100644
index 000000000..c73a6e88e
--- /dev/null
+++ b/ishtar_common/management/commands/update_search_vectors.py
@@ -0,0 +1,24 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+import sys
+
+from django.core.management.base import BaseCommand
+import django.apps
+
+
+class Command(BaseCommand):
+ help = "./manage.py update_search_vectors\n\n"\
+ "Update full texte search vectors."
+
+ def handle(self, *args, **options):
+ for model in django.apps.apps.get_models():
+ if hasattr(model, "update_search_vector") and \
+ getattr(model, "BASE_SEARCH_VECTORS", None):
+ self.stdout.write("\n* update {}".format(model))
+ total = model.objects.count()
+ for idx, item in enumerate(model.objects.all()):
+ sys.stdout.write("\r{}/{} ".format(idx, total))
+ sys.stdout.flush()
+ item.update_search_vector()
+ self.stdout.write("\n")
diff --git a/ishtar_common/migrations/0015_auto_20171011_1644.py b/ishtar_common/migrations/0015_auto_20171011_1644.py
new file mode 100644
index 000000000..a9f4499c2
--- /dev/null
+++ b/ishtar_common/migrations/0015_auto_20171011_1644.py
@@ -0,0 +1,36 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11 on 2017-10-11 16:44
+from __future__ import unicode_literals
+
+import django.contrib.postgres.search
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('ishtar_common', '0014_ishtarsiteprofile_preservation'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='historicalorganization',
+ name='search_vector',
+ field=django.contrib.postgres.search.SearchVectorField(blank=True, help_text='Auto filled at save', null=True, verbose_name='Search vector'),
+ ),
+ migrations.AddField(
+ model_name='historicalperson',
+ name='search_vector',
+ field=django.contrib.postgres.search.SearchVectorField(blank=True, help_text='Auto filled at save', null=True, verbose_name='Search vector'),
+ ),
+ migrations.AddField(
+ model_name='organization',
+ name='search_vector',
+ field=django.contrib.postgres.search.SearchVectorField(blank=True, help_text='Auto filled at save', null=True, verbose_name='Search vector'),
+ ),
+ migrations.AddField(
+ model_name='person',
+ name='search_vector',
+ field=django.contrib.postgres.search.SearchVectorField(blank=True, help_text='Auto filled at save', null=True, verbose_name='Search vector'),
+ ),
+ ]
diff --git a/ishtar_common/migrations/0016_auto_20171016_1104.py b/ishtar_common/migrations/0016_auto_20171016_1104.py
new file mode 100644
index 000000000..1d9209bdd
--- /dev/null
+++ b/ishtar_common/migrations/0016_auto_20171016_1104.py
@@ -0,0 +1,30 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11 on 2017-10-16 11:04
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('ishtar_common', '0015_auto_20171011_1644'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='town',
+ name='cached_label',
+ field=models.CharField(blank=True, db_index=True, max_length=500, null=True, verbose_name='Cached name'),
+ ),
+ migrations.AddField(
+ model_name='town',
+ name='children',
+ field=models.ManyToManyField(blank=True, related_name='parents', to='ishtar_common.Town', verbose_name='Town children'),
+ ),
+ migrations.AddField(
+ model_name='town',
+ name='year',
+ field=models.IntegerField(blank=True, help_text='If not filled considered as the older town known.', null=True, verbose_name='Year of creation'),
+ ),
+ ]
diff --git a/ishtar_common/migrations/0017_auto_20171016_1320.py b/ishtar_common/migrations/0017_auto_20171016_1320.py
new file mode 100644
index 000000000..a48b36ce7
--- /dev/null
+++ b/ishtar_common/migrations/0017_auto_20171016_1320.py
@@ -0,0 +1,29 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11 on 2017-10-16 13:20
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('ishtar_common', '0016_auto_20171016_1104'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='town',
+ name='numero_insee',
+ field=models.CharField(max_length=6, verbose_name='Num\xe9ro INSEE'),
+ ),
+ migrations.AlterField(
+ model_name='town',
+ name='year',
+ field=models.IntegerField(blank=True, help_text='Filling this field is relevant to distinguish old towns to new towns.', null=True, verbose_name='Year of creation'),
+ ),
+ migrations.AlterUniqueTogether(
+ name='town',
+ unique_together=set([('numero_insee', 'year')]),
+ ),
+ ]
diff --git a/ishtar_common/models.py b/ishtar_common/models.py
index 28a24115b..2904e51cd 100644
--- a/ishtar_common/models.py
+++ b/ishtar_common/models.py
@@ -35,6 +35,7 @@ import tempfile
import time
from django.conf import settings
+from django.contrib.postgres.search import SearchVectorField, SearchVector
from django.core.cache import cache
from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.core.files.uploadedfile import SimpleUploadedFile
@@ -58,7 +59,7 @@ from simple_history.models import HistoricalRecords as BaseHistoricalRecords
from ishtar_common.model_merging import merge_model_objects
from ishtar_common.utils import get_cache, disable_for_loaddata, create_slug,\
- get_all_field_names
+ get_all_field_names, merge_tsvectors, cached_label_changed
from ishtar_common.models_imports import ImporterModel, ImporterType, \
ImporterDefault, ImporterDefaultValues, ImporterColumn, \
@@ -917,9 +918,85 @@ class Imported(models.Model):
abstract = True
-class BaseHistorizedItem(Imported):
+class FullSearch(models.Model):
+ search_vector = SearchVectorField(_("Search vector"), blank=True, null=True,
+ help_text=_("Auto filled at save"))
+ BASE_SEARCH_VECTORS = []
+ INT_SEARCH_VECTORS = []
+ M2M_SEARCH_VECTORS = []
+ PARENT_SEARCH_VECTORS = []
+
+ class Meta:
+ abstract = True
+
+ def update_search_vector(self, save=True):
+ """
+ Update the search vector
+ :param save: True if you want to save the object immediately
+ :return: True if modified
+ """
+ if not self.BASE_SEARCH_VECTORS and not self.M2M_SEARCH_VECTORS:
+ logger.warning("No search_vectors defined for {}".format(
+ self.__class__))
+ return
+ if getattr(self, '_search_updated', None):
+ return
+ self._search_updated = True
+
+ old_search = ""
+ if self.search_vector:
+ old_search = self.search_vector[:]
+ search_vectors = []
+ base_q = self.__class__.objects.filter(pk=self.pk)
+
+ # many to many have to be queried one by one otherwise only one is fetch
+ for M2M_SEARCH_VECTOR in self.M2M_SEARCH_VECTORS:
+ key = M2M_SEARCH_VECTOR.split('__')[0]
+ rel_key = getattr(self, key)
+ for item in rel_key.values('pk').all():
+ query_dct = {key + "__pk": item['pk']}
+ q = copy.copy(base_q).filter(**query_dct)
+ q = q.annotate(
+ search=SearchVector(
+ M2M_SEARCH_VECTOR,
+ config=settings.ISHTAR_SEARCH_LANGUAGE)
+ ).values('search')
+ search_vectors.append(q.all()[0]['search'])
+
+ # int/float are not well managed by the SearchVector
+ for INT_SEARCH_VECTOR in self.INT_SEARCH_VECTORS:
+ q = base_q.values(INT_SEARCH_VECTOR)
+ search_vectors.append(
+ "'{}':1".format(q.all()[0][INT_SEARCH_VECTOR]))
+
+ # copy parent vector fields
+ for PARENT_SEARCH_VECTOR in self.PARENT_SEARCH_VECTORS:
+ parent = getattr(self, PARENT_SEARCH_VECTOR)
+ if hasattr(parent, 'all'): # m2m
+ for p in parent.all():
+ search_vectors.append(p.search_vector)
+ else:
+ search_vectors.append(parent.search_vector)
+
+ # query "simple" fields
+ q = base_q.annotate(
+ search=SearchVector(
+ *self.BASE_SEARCH_VECTORS,
+ config=settings.ISHTAR_SEARCH_LANGUAGE
+ )).values('search')
+ search_vectors.append(q.all()[0]['search'])
+ self.search_vector = merge_tsvectors(search_vectors)
+ changed = old_search != self.search_vector
+ if save and changed:
+ self.skip_history_when_saving = True
+ self.save()
+ return changed
+
+
+class BaseHistorizedItem(FullSearch, Imported):
"""
- Historized item with external ID management
+ Historized item with external ID management.
+ All historized items are searcheable
"""
IS_BASKET = False
EXTERNAL_ID_KEY = ''
@@ -1187,6 +1264,7 @@ class LightHistorizedItem(BaseHistorizedItem):
super(LightHistorizedItem, self).save(*args, **kwargs)
return True
+
PARSE_FORMULA = re.compile("{([^}]*)}")
FORMULA_FILTERS = {
@@ -1409,6 +1487,7 @@ def get_current_profile(force=False):
def cached_site_changed(sender, **kwargs):
get_current_profile(force=True)
+
post_save.connect(cached_site_changed, sender=IshtarSiteProfile)
post_delete.connect(cached_site_changed, sender=IshtarSiteProfile)
@@ -2490,12 +2569,20 @@ class Town(Imported, models.Model):
center = models.PointField(_(u"Localisation"), srid=settings.SRID,
blank=True, null=True)
if settings.COUNTRY == 'fr':
- numero_insee = models.CharField(u"Numéro INSEE", max_length=6,
- unique=True)
+ numero_insee = models.CharField(u"Numéro INSEE", max_length=6)
departement = models.ForeignKey(
Department, verbose_name=u"Département", null=True, blank=True)
canton = models.ForeignKey(Canton, verbose_name=u"Canton", null=True,
blank=True)
+ year = models.IntegerField(
+ _("Year of creation"), null=True, blank=True,
+ help_text=_(u"Filling this field is relevant to distinguish old towns "
+ u"to new towns."))
+ children = models.ManyToManyField(
+ 'Town', verbose_name=_(u"Town children"), blank=True,
+ related_name='parents')
+ cached_label = models.CharField(_(u"Cached name"), max_length=500,
+ null=True, blank=True, db_index=True)
objects = models.GeoManager()
class Meta:
@@ -2503,11 +2590,24 @@ class Town(Imported, models.Model):
verbose_name_plural = _(u"Towns")
if settings.COUNTRY == 'fr':
ordering = ['numero_insee']
+ unique_together = (('numero_insee', 'year'),)
def __unicode__(self):
+ if self.cached_label:
+ return self.cached_label
+ self.save()
+ return self.cached_label
+
+ def _generate_cached_label(self):
+ cached_label = self.name
if settings.COUNTRY == "fr":
- return u"%s (%s)" % (self.name, self.numero_insee[:2])
- return self.name
+ cached_label = u"%s - %s" % (self.name, self.numero_insee[:2])
+ if self.year:
+ cached_label += " ({})".format(self.year)
+ return cached_label
+
+
+post_save.connect(cached_label_changed, sender=Town)
class OperationType(GeneralType):
diff --git a/ishtar_common/tests.py b/ishtar_common/tests.py
index 349408465..8a951b29a 100644
--- a/ishtar_common/tests.py
+++ b/ishtar_common/tests.py
@@ -1046,6 +1046,15 @@ class IshtarBasicTest(TestCase):
self.assertEqual(response.status_code, 200)
self.assertIn('class="sheet"', response.content)
+ def test_town_cache(self):
+ models.Town.objects.create(name="Sin City", numero_insee="99999")
+ town = models.Town.objects.get(numero_insee="99999")
+ self.assertEqual(town.cached_label, "Sin City - 99")
+ town.year = 2050
+ town.save()
+ town = models.Town.objects.get(numero_insee="99999")
+ self.assertEqual(town.cached_label, "Sin City - 99 (2050)")
+
class GeomaticTest(TestCase):
def test_post_save_point(self):
diff --git a/ishtar_common/utils.py b/ishtar_common/utils.py
index c6a4032f0..5d9e85c60 100644
--- a/ishtar_common/utils.py
+++ b/ishtar_common/utils.py
@@ -104,9 +104,12 @@ def cached_label_changed(sender, **kwargs):
setattr(instance, cached_label, lbl)
changed = True
if changed:
+ instance._search_updated = False
if hasattr(instance, '_cascade_change') and instance._cascade_change:
instance.skip_history_when_saving = True
instance.save()
+ if hasattr(instance, 'update_search_vector'):
+ instance.update_search_vector()
updated = False
if hasattr(instance, '_cached_labels_bulk_update'):
updated = instance._cached_labels_bulk_update()
@@ -117,6 +120,7 @@ def cached_label_changed(sender, **kwargs):
item.test_obj = instance.test_obj
cached_label_changed(item.__class__, instance=item)
+
SHORTIFY_STR = ugettext(" (...)")
@@ -289,3 +293,43 @@ def get_all_related_objects(model):
and f.auto_created and not f.concrete
]
+
+def merge_tsvectors(vectors):
+ """
+ Parse tsvector to merge them in one string
+ :param vectors: list of tsvector string
+ :return: merged tsvector
+ """
+ result_dict = {}
+ for vector in vectors:
+ if not vector:
+ continue
+
+ current_position = 0
+ if result_dict:
+ for key in result_dict:
+ max_position = max(result_dict[key])
+ if max_position > current_position:
+ current_position = max_position
+
+ for dct_member in vector.split(" "):
+ splitted = dct_member.split(':')
+ key = ":".join(splitted[:-1])
+ positions = splitted[-1]
+ key = key[1:-1] # remove quotes
+ positions = [int(pos) + current_position
+ for pos in positions.split(',')]
+ if key in result_dict:
+ result_dict[key] += positions
+ else:
+ result_dict[key] = positions
+
+ # {'lamelie': [1, 42, 5]} => {'lamelie': "1,42,5"}
+ result_dict = {k: ",".join([str(val) for val in result_dict[k]])
+ for k in result_dict}
+ # {'lamelie': "1,5", "hagarde": "2", "regarde": "4"} =>
+ # "'lamelie':1,5 'hagarde':2 'regarde':4"
+ result = " ".join(["'{}':{}".format(k, result_dict[k])
+ for k in result_dict])
+
+ return result
diff --git a/ishtar_common/views.py b/ishtar_common/views.py
index 997acd7df..b8350c62a 100644
--- a/ishtar_common/views.py
+++ b/ishtar_common/views.py
@@ -41,6 +41,7 @@ from extra_views import ModelFormSetView
from django.conf import settings
from django.contrib.auth import logout
from django.contrib.auth.decorators import login_required
+from django.contrib.postgres.search import SearchQuery
from django.core.exceptions import ObjectDoesNotExist
from django.core.urlresolvers import reverse, NoReverseMatch
from django.db.models import Q, ImageField
@@ -814,6 +815,23 @@ def get_item(model, func_name, default_name, extra_request_keys=[],
dct.pop(k)
# manage hierarchic conditions
for req in dct.copy():
+ if req.endswith('town__pk') or req.endswith('towns__pk'):
+ val = dct.pop(req)
+ reqs = Q(**{req: val})
+ base_req = req[:-2] + '__'
+ req = base_req[:]
+ for idx in range(HIERARCHIC_LEVELS):
+ req = req[:-2] + 'parents__pk'
+ q = Q(**{req: val})
+ reqs |= q
+ req = base_req[:]
+ for idx in range(HIERARCHIC_LEVELS):
+ req = req[:-2] + 'children__pk'
+ q = Q(**{req: val})
+ reqs |= q
+ and_reqs.append(reqs)
+ continue
+
for k_hr in HIERARCHIC_FIELDS:
if type(req) in (list, tuple):
val = dct.pop(req)
@@ -829,12 +847,15 @@ def get_item(model, func_name, default_name, extra_request_keys=[],
val = dct.pop(req)
reqs = Q(**{req: val})
req = req[:-2] + '__'
- for idx in xrange(HIERARCHIC_LEVELS):
+ for idx in range(HIERARCHIC_LEVELS):
req = req[:-2] + 'parent__pk'
q = Q(**{req: val})
reqs |= q
and_reqs.append(reqs)
break
+ if 'search_vector' in dct:
+ dct['search_vector'] = SearchQuery(
+ dct['search_vector'], config=settings.ISHTAR_SEARCH_LANGUAGE)
query = Q(**dct)
for k, or_req in or_reqs:
alt_dct = dct.copy()
@@ -908,6 +929,9 @@ def get_item(model, func_name, default_name, extra_request_keys=[],
items = model.objects.filter(query).distinct()
# print(items.query)
+ if 'search_vector' in dct: # for serialization
+ dct['search_vector'] = dct['search_vector'].value
+
# table cols
if own_table_cols:
table_cols = own_table_cols