Skip to content

Commit 8855ccb

Browse files
split in 2 modules, base + modeler
1 parent a50ad32 commit 8855ccb

36 files changed

+1004
-5
lines changed

elasticsearch_base/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Copyright 2017 Creu Blanca
2+
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html).
3+
4+
from . import components
5+
from . import models

elasticsearch_base/__manifest__.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Copyright 2017 Creu Blanca
2+
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html).
3+
4+
{
5+
'name': 'Elasticsearch Connector Base',
6+
'version': '11.0.1.0.1',
7+
'category': 'Elasticsearch connector',
8+
'depends': [
9+
'connector',
10+
],
11+
'author': 'Creu Blanca',
12+
'license': 'AGPL-3',
13+
'summary': 'Elasticsearch connector base',
14+
'data': [
15+
'security/ir.model.access.csv',
16+
'views/menu.xml',
17+
'views/elasticsearch_host_views.xml',
18+
'views/elasticsearch_index_views.xml',
19+
],
20+
'external_dependencies': {
21+
'python': [
22+
'elasticsearch'
23+
],
24+
},
25+
'installable': True,
26+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Copyright 2017 Creu Blanca
2+
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html).
3+
4+
from . import core
5+
from . import binder
6+
from . import exporter
7+
from . import listener
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Copyright 2017 Creu Blanca
2+
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html).
3+
4+
from odoo.addons.component.core import Component
5+
6+
7+
class ElasticsearchModelBinder(Component):
8+
_name = 'elasticsearch.binder'
9+
_inherit = ['base.binder', 'base.elasticsearch.connector']
10+
_apply_on = False

elasticsearch_base/components/core.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Copyright 2017 Creu Blanca
2+
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html).
3+
4+
from odoo.addons.component.core import AbstractComponent
5+
6+
7+
class BaseElasticsearchConnectorComponent(AbstractComponent):
8+
""" Base elasticsearch Connector Component
9+
All components of this connector should inherit from it.
10+
"""
11+
12+
_name = 'base.elasticsearch.connector'
13+
_inherit = 'base.connector'
14+
_collection = 'elasticsearch.index'
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
# Copyright 2017 Creu Blanca
2+
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html).
3+
4+
import logging
5+
import psycopg2
6+
import json
7+
from datetime import datetime
8+
9+
import odoo
10+
from odoo.addons.component.core import Component
11+
from odoo.addons.connector.exception import RetryableJobError
12+
13+
_logger = logging.getLogger(__name__)
14+
try:
15+
import elasticsearch
16+
except (ImportError, IOError) as err:
17+
_logger.debug(err)
18+
19+
ISO_FORMAT = '%Y-%m-%dT%H:%M:%S.%f'
20+
21+
22+
class ElasticsearchBaseExporter(Component):
23+
""" Base exporter for the Elasticsearch """
24+
25+
_name = 'elasticsearch.document.exporter'
26+
_inherit = ['base.exporter', 'base.elasticsearch.connector']
27+
_usage = 'record.exporter'
28+
_apply_on = ['elasticsearch.document']
29+
_exporter_failure_timeout = 2
30+
31+
def _lock(self, document):
32+
""" Lock the binding record.
33+
Lock the binding record so we are sure that only one export
34+
job is running for this record if concurrent jobs have to export the
35+
same record.
36+
When concurrent jobs try to export the same record, the first one
37+
will lock and proceed, the others will fail to lock and will be
38+
retried later.
39+
This behavior works also when the export becomes multilevel
40+
with :meth:`_export_dependencies`. Each level will set its own lock
41+
on the binding record it has to export.
42+
"""
43+
sql = ("SELECT id FROM %s WHERE ID = %%s FOR UPDATE NOWAIT" %
44+
self.model._table)
45+
try:
46+
self.env.cr.execute(sql, (document.id,),
47+
log_exceptions=False)
48+
except psycopg2.OperationalError:
49+
_logger.info('A concurrent job is already exporting the same '
50+
'record (%s with id %s). Job delayed later.',
51+
self.model._name, document.id)
52+
raise RetryableJobError(
53+
'A concurrent job is already exporting the same record '
54+
'(%s with id %s). The job will be retried later.' %
55+
(self.model._name, document.id),
56+
seconds=self._exporter_failure_timeout)
57+
58+
def es_create(self, document, *args, **kwargs):
59+
self._lock(document)
60+
index = document.index_id.index
61+
es = elasticsearch.Elasticsearch(
62+
hosts=document.index_id.get_hosts())
63+
data = json.dumps(kwargs['data'])
64+
es.index(index, '_doc', id=document.id, body=data)
65+
document.with_context(no_elasticserach_sync=True).write({
66+
'sync_date': kwargs['sync_date']
67+
})
68+
if not odoo.tools.config['test_enable']:
69+
self.env.cr.commit() # pylint: disable=E8102
70+
self._after_export()
71+
return True
72+
73+
def es_unlink(self, id, index, *args, **kwargs):
74+
""" Run the synchronization
75+
:param binding: binding record to export
76+
"""
77+
es = elasticsearch.Elasticsearch(hosts=index.get_hosts())
78+
es.delete(index.index, '_doc', id)
79+
self._after_export()
80+
return True
81+
82+
def es_write(self, document, *args, **kwargs):
83+
""" Run the synchronization
84+
:param binding: binding record to export
85+
"""
86+
self._lock(document)
87+
sync_date = datetime.strptime(kwargs['sync_date'], ISO_FORMAT)
88+
if (
89+
not document.sync_date or
90+
sync_date >= datetime.strptime(document.sync_date, ISO_FORMAT)
91+
):
92+
index = document.index_id.index
93+
es = elasticsearch.Elasticsearch(
94+
hosts=document.index_id.get_hosts())
95+
data = json.dumps(kwargs['data'])
96+
es.index(index, '_doc', id=document.id, body=data)
97+
document.with_context(no_elasticserach_sync=True).write({
98+
'sync_date': kwargs['sync_date']
99+
})
100+
else:
101+
_logger.info(
102+
'Record from %s with id %s has already been sended (%s), so it'
103+
' is deprecated ' % (
104+
self.model._name, document.id, kwargs['sync_date']
105+
)
106+
)
107+
# Commit so we keep the external ID when there are several
108+
# exports (due to dependencies) and one of them fails.
109+
# The commit will also release the lock acquired on the binding
110+
# record
111+
if not odoo.tools.config['test_enable']:
112+
self.env.cr.commit() # pylint: disable=E8102
113+
self._after_export()
114+
return True
115+
116+
def _after_export(self):
117+
""" Can do several actions after exporting a record"""
118+
pass
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# Copyright 2017 Creu Blanca
2+
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html).
3+
4+
from odoo.addons.component.core import Component
5+
from datetime import datetime
6+
7+
8+
class ElasticsearchDocumentListener(Component):
9+
_name = 'elasticsearch.document.listener'
10+
_inherit = 'base.event.listener'
11+
_apply_on = 'elasticsearch.document'
12+
13+
def on_record_create(self, record, fields):
14+
if self.env.context.get('no_elasticserach_sync', False):
15+
return
16+
for rec in record:
17+
rec.with_delay().export_create(datetime.now().isoformat())
18+
19+
def on_record_write(self, record, fields):
20+
if self.env.context.get('no_elasticserach_sync', False):
21+
return
22+
for rec in record:
23+
rec.with_delay().export_update(datetime.now().isoformat())
24+
25+
def on_record_unlink(self, record):
26+
if self.env.context.get('no_elasticserach_sync', False):
27+
return
28+
if self.env.context.get('no_elasticsearch_delay', False):
29+
for rec in record:
30+
rec.export_delete(rec.id, rec.index_id)
31+
for rec in record:
32+
rec.with_delay()

elasticsearch_base/models/__init__.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Copyright 2017 Creu Blanca
2+
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html).
3+
4+
from . import elasticsearch_document
5+
from . import elasticsearch_host
6+
from . import elasticsearch_index
7+
from . import elasticsearch_model
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# Copyright 2017 Creu Blanca
2+
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html).
3+
4+
from datetime import datetime
5+
6+
from odoo import api, models, fields
7+
from odoo.addons.queue_job.job import job
8+
9+
10+
class ElasticsearchDocument(models.Model):
11+
_name = 'elasticsearch.document'
12+
_inherit = 'external.binding'
13+
_description = 'Elasticsearch abstract Document'
14+
15+
index_id = fields.Many2one(
16+
comodel_name='elasticsearch.index',
17+
string='Index',
18+
required=True,
19+
ondelete='restrict',
20+
)
21+
sync_date = fields.Char()
22+
23+
@job(default_channel='root.elasticsearch')
24+
@api.multi
25+
def export_create(self, date=datetime.now().isoformat()):
26+
self.ensure_one()
27+
with self.index_id.work_on(self._name) as work:
28+
exporter = work.component(usage='record.exporter')
29+
return exporter.es_create(
30+
self,
31+
data=self.get_document_values(),
32+
sync_date=date)
33+
34+
@job(default_channel='root.elasticsearch')
35+
@api.multi
36+
def export_update(self, date=datetime.now().isoformat()):
37+
self.ensure_one()
38+
with self.index_id.work_on(self._name) as work:
39+
exporter = work.component(usage='record.exporter')
40+
return exporter.es_write(
41+
self,
42+
data=self.get_document_values(),
43+
sync_date=date)
44+
45+
@job(default_channel='root.elasticsearch')
46+
@api.multi
47+
def export_delete(self, id, index):
48+
with index.work_on(self._name) as work:
49+
exporter = work.component(usage='record.exporter')
50+
return exporter.es_unlink(id, index)
51+
52+
def get_document_values(self):
53+
return {}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Copyright 2018 Creu Blanca
2+
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html).
3+
4+
from odoo import models, fields
5+
6+
7+
class ElasticsearchHost(models.Model):
8+
_name = 'elasticsearch.host'
9+
_description = 'ElasticSearch Host'
10+
11+
host = fields.Char(
12+
required=True
13+
)
14+
port = fields.Integer(
15+
required=True,
16+
default=9200,
17+
)
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
# Copyright 2017 Creu Blanca
2+
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html).
3+
import logging
4+
from odoo import api, fields, models
5+
from odoo.tools import safe_eval
6+
import json
7+
8+
_logger = logging.getLogger(__name__)
9+
try:
10+
import elasticsearch
11+
except (ImportError, IOError) as err:
12+
_logger.debug(err)
13+
14+
15+
class ElasticsearchIndex(models.Model):
16+
_name = 'elasticsearch.index'
17+
_description = 'Elasticsearch Index'
18+
_inherit = 'connector.backend'
19+
20+
name = fields.Char(
21+
required=True,
22+
readonly=True,
23+
states={'draft': [('readonly', False)]},
24+
)
25+
state = fields.Selection([
26+
('draft', 'Draft'),
27+
('posted', 'Posted'),
28+
('cancelled', 'Cancelled')
29+
], required=True, default='draft', readonly=True)
30+
index = fields.Char(
31+
required=True,
32+
readonly=True,
33+
states={'draft': [('readonly', False)]},
34+
)
35+
host_ids = fields.Many2many(
36+
'elasticsearch.host',
37+
required=True,
38+
readonly=True,
39+
states={'draft': [('readonly', False)]},
40+
)
41+
document_ids = fields.One2many(
42+
'elasticsearch.document',
43+
inverse_name='index_id',
44+
readonly=True
45+
)
46+
type = fields.Selection(
47+
[],
48+
required=True,
49+
readonly=True,
50+
states={'draft': [('readonly', False)]},
51+
)
52+
53+
def _post_values(self):
54+
return {'state': 'posted'}
55+
56+
def _get_index_template(self):
57+
return {
58+
"settings": {"index.mapping.ignore_malformed": True}
59+
}
60+
61+
def _post(self):
62+
es = elasticsearch.Elasticsearch(hosts=self.get_hosts())
63+
es.indices.create(
64+
self.index,
65+
body=json.dumps(self._get_index_template()))
66+
67+
@api.multi
68+
def post(self):
69+
self.ensure_one()
70+
self._post()
71+
self.write(self._post_values())
72+
73+
def _reset_index(self):
74+
self.ensure_one()
75+
es = elasticsearch.Elasticsearch(hosts=self.get_hosts())
76+
self.document_ids.with_context(no_elasticserach_sync=True).unlink()
77+
es.indices.delete(index=self.index, ignore=[400, 404])
78+
self.state = 'cancelled'
79+
80+
def _draft_values(self):
81+
return {'state': 'draft'}
82+
83+
@api.multi
84+
def restore(self):
85+
self.write(self._draft_values())
86+
87+
def _cancel_values(self):
88+
return {'state': 'cancelled'}
89+
90+
@api.multi
91+
def cancel(self):
92+
self.ensure_one()
93+
self._reset_index()
94+
self.write(self._cancel_values())
95+
96+
def get_hosts(self):
97+
return [{"host": r.host, "port": r.port} for r in self.host_ids]

elasticsearch_base/models/elasticsearch_model.py

Whitespace-only changes.

0 commit comments

Comments
 (0)