Browse Source

Merge pull request #245 from guewen/add-sql_view-7.0

[ADD] sql_view - create SQL views on the database
pull/359/head
Maxime Chambreuil - http://www.savoirfairelinux.com 9 years ago
parent
commit
734f6846c1
  1. 2
      .travis.yml
  2. 4
      sql_view/__init__.py
  3. 95
      sql_view/__openerp__.py
  4. 138
      sql_view/i18n/fr.po
  5. 137
      sql_view/i18n/sql_view.pot
  6. 3
      sql_view/models/__init__.py
  7. 174
      sql_view/models/sql_view.py
  8. 3
      sql_view/security/ir.model.access.csv
  9. BIN
      sql_view/static/description/icon.png
  10. 68
      sql_view/views/sql_view_views.xml
  11. 3
      sql_view/wizards/__init__.py
  12. 83
      sql_view/wizards/sql_view_csv_preview.py
  13. 37
      sql_view/wizards/sql_view_csv_preview_views.xml

2
.travis.yml

@ -25,7 +25,7 @@ install:
- git clone https://github.com/OCA/maintainer-quality-tools.git ${HOME}/maintainer-quality-tools - git clone https://github.com/OCA/maintainer-quality-tools.git ${HOME}/maintainer-quality-tools
- export PATH=${HOME}/maintainer-quality-tools/travis:${PATH} - export PATH=${HOME}/maintainer-quality-tools/travis:${PATH}
- travis_install_nightly - travis_install_nightly
- pip install python-ldap raven raven_sanitize_openerp bzr GitPython
- pip install python-ldap raven raven_sanitize_openerp bzr GitPython unicodecsv
- printf '[options]\n\nrunning_env = dev' > ${HOME}/.openerp_serverrc - printf '[options]\n\nrunning_env = dev' > ${HOME}/.openerp_serverrc
script: script:

4
sql_view/__init__.py

@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
from . import models
from . import wizards

95
sql_view/__openerp__.py

@ -0,0 +1,95 @@
# -*- coding: utf-8 -*-
#
#
# Authors: Guewen Baconnier
# Copyright 2015 Camptocamp SA
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
#
{'name': 'SQL Views',
'version': '1.0',
'author': 'Camptocamp,Odoo Community Association (OCA)',
'license': 'AGPL-3',
'category': 'Tools',
'depends': ['base'],
'description': """
=========
SQL Views
=========
This addon allows to create SQL views on the database. It also features
a simple CSV export of the views to check their result.
Usage
=====
To create new SQL views, you need to go to ``Settings > Technical >
Database Structure > SQL Views``.
Give a view a human name, a SQL name (which will be prefixed with
``sql_view_`` in the database, and the definition of the view itself
(without trailing semicolon).
Known issues / Roadmap
======================
The CSV preview can be used to read any data on the database. So this
menu **must** be accessible only by allowed admin users. By
default, the module is configured to be accessible only by users having
the ``Settings`` administration level.
Bug Tracker
===========
Bugs are tracked on `GitHub Issues
<https://github.com/OCA/server-tools/issues>`_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us smashing it by providing a detailed and
welcomed feedback
`here
<https://github.com/OCA/server-tools/issues/new?body=module:%20sql_view%0Aversion:%207.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
Credits
=======
Contributors
------------
* Guewen Baconnier <guewen.baconnier@camptocamp.com>
Maintainer
----------
.. image:: https://odoo-community.org/logo.png
:alt: Odoo Community Association
:target: https://odoo-community.org
This module is maintained by the OCA.
OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.
To contribute to this module, please visit http://odoo-community.org.
""",
'website': 'http://www.camptocamp.com',
'external_dependencies': {'python': ['unicodecsv']},
'data': ['wizards/sql_view_csv_preview_views.xml',
'views/sql_view_views.xml',
'security/ir.model.access.csv',
],
'installable': True,
}

138
sql_view/i18n/fr.po

@ -0,0 +1,138 @@
# Translation of OpenERP Server.
# This file contains the translation of the following modules:
# * sql_view
#
msgid ""
msgstr ""
"Project-Id-Version: OpenERP Server 7.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2015-09-01 07:19+0000\n"
"PO-Revision-Date: 2015-09-01 07:19+0000\n"
"Last-Translator: Guewen Baconnier <guewen.baconnier@camptocamp.com>\n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"
#. module: sql_view
#: sql_constraint:sql.view:0
msgid "Another view has the same SQL name."
msgstr "Une autre vue a le même nom SQL."
#. module: sql_view
#: field:sql.view.csv.preview,data:0
msgid "CSV"
msgstr "CSV"
#. module: sql_view
#: view:sql.view:0
msgid "CSV Preview"
msgstr "Aperçu en CSV"
#. module: sql_view
#: view:sql.view.csv.preview:0
msgid "Cancel"
msgstr "Annuler"
#. module: sql_view
#: view:sql.view.csv.preview:0
msgid "Close"
msgstr "Fermer"
#. module: sql_view
#: field:sql.view,complete_sql_name:0
msgid "Complete SQL Name"
msgstr "Nom SQL complet"
#. module: sql_view
#: view:sql.view:0
#: field:sql.view,definition:0
msgid "Definition"
msgstr "Définition"
#. module: sql_view
#: code:addons/sql_view/models/sql_view.py:127
#, python-format
msgid "Error"
msgstr "Erreur"
#. module: sql_view
#: field:sql.view.csv.preview,filename:0
msgid "File Name"
msgstr "Nom du fichier"
#. module: sql_view
#: field:sql.view.csv.preview,limit:0
msgid "Limit"
msgstr "Limite"
#. module: sql_view
#: help:sql.view,sql_name:0
msgid "Name of the view. Will be prefixed by sql_view_"
msgstr "Nom de la vue. Sera préfixé par sql_view_"
#. module: sql_view
#: help:sql.view.csv.preview,limit:0
msgid "Number of records. 0 means infinite."
msgstr "Nombre d'enregistrements. 0 signifie infini."
#. module: sql_view
#: view:sql.view.csv.preview:0
msgid "Preview"
msgstr "Aperçu"
#. module: sql_view
#: field:sql.view,sql_name:0
msgid "SQL Name"
msgstr "Nom SQL"
#. module: sql_view
#: view:sql.view:0
msgid "SQL View"
msgstr "Vue SQL"
#. module: sql_view
#: code:_description:0
#: model:ir.actions.act_window,name:sql_view.action_sql_view_csv_preview
#: view:sql.view.csv.preview:0
msgid "SQL View CSV Preview"
msgstr "Aperçu CSV de vue SQL"
#. module: sql_view
#: code:_description:0
#: model:ir.actions.act_window,name:sql_view.action_sql_view
#: model:ir.ui.menu,name:sql_view.menu_sql_view
#: view:sql.view:0
msgid "SQL Views"
msgstr "Vues SQL"
#. module: sql_view
#: constraint:sql.view:0
msgid "The SQL name is not a valid PostgreSQL identifier"
msgstr "Le nom SQL n'est pas un identifiant PostgreSQL valide."
#. module: sql_view
#: code:addons/sql_view/models/sql_view.py:128
#, python-format
msgid "The definition of the view is not correct:\n"
"\n"
"%s"
msgstr "La définition de la vue n'est pas correcte :\n"
"\n"
"%s"
#. module: sql_view
#: constraint:sql.view:0
msgid "This SQL definition is not allowed"
msgstr "Cette définition SQL n'est pas autorisée"
#. module: sql_view
#: field:sql.view,name:0
msgid "View Name"
msgstr "Nom de la vue"
#. module: sql_view
#: view:sql.view.csv.preview:0
msgid "or"
msgstr "ou"

137
sql_view/i18n/sql_view.pot

@ -0,0 +1,137 @@
# Translation of OpenERP Server.
# This file contains the translation of the following modules:
# * sql_view
#
msgid ""
msgstr ""
"Project-Id-Version: OpenERP Server 7.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2015-09-01 07:19+0000\n"
"PO-Revision-Date: 2015-09-01 07:19+0000\n"
"Last-Translator: <>\n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"
#. module: sql_view
#: sql_constraint:sql.view:0
msgid "Another view has the same SQL name."
msgstr ""
#. module: sql_view
#: field:sql.view.csv.preview,data:0
msgid "CSV"
msgstr ""
#. module: sql_view
#: view:sql.view:0
msgid "CSV Preview"
msgstr ""
#. module: sql_view
#: view:sql.view.csv.preview:0
msgid "Cancel"
msgstr ""
#. module: sql_view
#: view:sql.view.csv.preview:0
msgid "Close"
msgstr ""
#. module: sql_view
#: field:sql.view,complete_sql_name:0
msgid "Complete SQL Name"
msgstr ""
#. module: sql_view
#: view:sql.view:0
#: field:sql.view,definition:0
msgid "Definition"
msgstr ""
#. module: sql_view
#: code:addons/sql_view/models/sql_view.py:127
#, python-format
msgid "Error"
msgstr ""
#. module: sql_view
#: field:sql.view.csv.preview,filename:0
msgid "File Name"
msgstr ""
#. module: sql_view
#: field:sql.view.csv.preview,limit:0
msgid "Limit"
msgstr ""
#. module: sql_view
#: help:sql.view,sql_name:0
msgid "Name of the view. Will be prefixed by sql_view_"
msgstr ""
#. module: sql_view
#: help:sql.view.csv.preview,limit:0
msgid "Number of records. 0 means infinite."
msgstr ""
#. module: sql_view
#: view:sql.view.csv.preview:0
msgid "Preview"
msgstr ""
#. module: sql_view
#: field:sql.view,sql_name:0
msgid "SQL Name"
msgstr ""
#. module: sql_view
#: view:sql.view:0
msgid "SQL View"
msgstr ""
#. module: sql_view
#: code:_description:0
#: model:ir.actions.act_window,name:sql_view.action_sql_view_csv_preview
#: view:sql.view.csv.preview:0
msgid "SQL View CSV Preview"
msgstr ""
#. module: sql_view
#: code:_description:0
#: model:ir.actions.act_window,name:sql_view.action_sql_view
#: model:ir.ui.menu,name:sql_view.menu_sql_view
#: view:sql.view:0
#, python-format
msgid "SQL Views"
msgstr ""
#. module: sql_view
#: constraint:sql.view:0
msgid "The SQL name is not a valid PostgreSQL identifier"
msgstr ""
#. module: sql_view
#: code:addons/sql_view/models/sql_view.py:128
#, python-format
msgid "The definition of the view is not correct:\n"
"\n"
"%s"
msgstr ""
#. module: sql_view
#: constraint:sql.view:0
msgid "This SQL definition is not allowed"
msgstr ""
#. module: sql_view
#: field:sql.view,name:0
msgid "View Name"
msgstr ""
#. module: sql_view
#: view:sql.view.csv.preview:0
msgid "or"
msgstr ""

3
sql_view/models/__init__.py

@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
from . import sql_view

174
sql_view/models/sql_view.py

@ -0,0 +1,174 @@
# -*- coding: utf-8 -*-
#
#
# Authors: Guewen Baconnier
# Copyright 2015 Camptocamp SA
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
#
import re
import psycopg2
from openerp.osv import orm, fields
from openerp.tools.translate import _
# views are created with a prefix to prevent conflicts
SQL_VIEW_PREFIX = u'sql_view_'
# 63 chars is the length of a postgres identifier
USER_NAME_SIZE = 63 - len(SQL_VIEW_PREFIX)
PG_NAME_RE = re.compile(r'^[a-z_][a-z0-9_$]*$', re.I)
class SQLView(orm.Model):
_name = 'sql.view'
_description = 'SQL Views'
def _complete_from_sql_name(self, cr, uid, sql_name, context=None):
return SQL_VIEW_PREFIX + (sql_name or '')
def _compute_complete_sql_name(self, cr, uid, ids, name, args,
context=None):
res = {}
for sql_view in self.browse(cr, uid, ids, context=context):
res[sql_view.id] = self._complete_from_sql_name(cr, uid,
sql_view.sql_name,
context=context)
return res
_columns = {
'name': fields.char(string='View Name', required=True),
'sql_name': fields.char(string='SQL Name', required=True,
size=USER_NAME_SIZE,
help="Name of the view. Will be prefixed "
"by %s" % (SQL_VIEW_PREFIX,)),
'complete_sql_name': fields.function(_compute_complete_sql_name,
string='Complete SQL Name',
readonly=True,
type='char'),
'definition': fields.text(string='Definition', required=True),
}
def _check_sql_name(self, cr, uid, ids, context=None):
records = self.browse(cr, uid, ids, context=context)
return all(PG_NAME_RE.match(record.sql_name) for record in records)
def _check_definition(self, cr, uid, ids, context=None):
""" Forbid a SQL definition with unbalanced parenthesis.
The reason is that the view's definition will be enclosed in:
CREATE VIEW {view_name} AS ({definition})
The parenthesis around the definition prevent users to inject
and execute arbitrary queries after the SELECT part (by using a
semicolon). However, it would still be possible to craft a
definition like the following which would close the parenthesis
and start a new query:
SELECT * FROM res_users); DELETE FROM res_users WHERE id IN (1
This is no longer possible if we ensure that we don't have an
unbalanced closing parenthesis.
"""
for record in self.browse(cr, uid, ids, context=context):
balanced = 0
for char in record.definition:
if char == '(':
balanced += 1
elif char == ')':
balanced -= 1
if balanced == -1:
return False
return True
_constraints = [
(_check_sql_name,
'The SQL name is not a valid PostgreSQL identifier',
['sql_name']),
(_check_definition,
'This SQL definition is not allowed',
['definition']),
]
_sql_constraints = [
('sql_name_uniq', 'unique (sql_name)',
'Another view has the same SQL name.')
]
def _sql_view_comment(self, cr, uid, sql_view, context=None):
return "%s (created by the module sql_view)" % sql_view.name
def _create_or_replace_sql_view(self, cr, uid, sql_view, context=None):
view_name = sql_view.complete_sql_name
try:
# The parenthesis around the definition are important:
# they prevent to insert a semicolon and another query after
# the view declaration
cr.execute(
"CREATE VIEW {view_name} AS ({definition})".format(
view_name=view_name,
definition=sql_view.definition)
)
except psycopg2.ProgrammingError as err:
raise orm.except_orm(
_('Error'),
_('The definition of the view is not correct:\n\n%s') % (err,)
)
comment = self._sql_view_comment(cr, uid, sql_view, context=context)
cr.execute(
"COMMENT ON VIEW {view_name} IS %s".format(view_name=view_name),
(comment,)
)
def _delete_sql_view(self, cr, uid, sql_view, context=None):
view_name = sql_view.complete_sql_name
cr.execute("DROP view IF EXISTS %s" % (view_name,))
def create(self, cr, uid, vals, context=None):
record_id = super(SQLView, self).create(cr, uid, vals,
context=context)
record = self.browse(cr, uid, record_id, context=context)
self._create_or_replace_sql_view(cr, uid, record, context=context)
return record_id
def write(self, cr, uid, ids, vals, context=None):
# If the name has been changed, we have to drop the view,
# it will be created with the new name.
# If the definition have been changed, we better have to delete
# it and create it again otherwise we can have 'cannot drop
# columns from view' errors.
for record in self.browse(cr, uid, ids, context=context):
self._delete_sql_view(cr, uid, record, context=context)
result = super(SQLView, self).write(cr, uid, ids, vals,
context=context)
for record in self.browse(cr, uid, ids, context=context):
self._create_or_replace_sql_view(cr, uid, record,
context=context)
return result
def unlink(self, cr, uid, ids, context=None):
for record in self.browse(cr, uid, ids, context=context):
self._delete_sql_view(cr, uid, record, context=context)
result = super(SQLView, self).unlink(cr, uid, ids, context=context)
return result
def onchange_sql_name(self, cr, uid, ids, sql_name, context=None):
complete_name = self._complete_from_sql_name(cr, uid, sql_name,
context=context)
return {'value': {'complete_sql_name': complete_name}}

3
sql_view/security/ir.model.access.csv

@ -0,0 +1,3 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_sql_view_admin,sql_view admin,model_sql_view,base.group_system,1,1,1,1

BIN
sql_view/static/description/icon.png

After

Width: 128  |  Height: 128  |  Size: 9.2 KiB

68
sql_view/views/sql_view_views.xml

@ -0,0 +1,68 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data noupdate="0">
<record id="view_sql_view_form" model="ir.ui.view">
<field name="name">sql.view.form</field>
<field name="model">sql.view</field>
<field name="arch" type="xml">
<form string="SQL View" version="7.0">
<header>
<button name="%(action_sql_view_csv_preview)d"
type="action"
class="oe_highlight"
string="CSV Preview"/>
</header>
<sheet string="SQL View">
<group>
<field name="name"/>
<field name="sql_name" on_change="onchange_sql_name(sql_name)"/>
<field name="complete_sql_name"/>
</group>
<group string="Definition">
<field name="definition" nolabel="1"/>
</group>
</sheet>
</form>
</field>
</record>
<record id="view_sql_view_tree" model="ir.ui.view">
<field name="name">sql.view.tree</field>
<field name="model">sql.view</field>
<field name="arch" type="xml">
<tree string="SQL Views">
<field name="name"/>
<field name="complete_sql_name"/>
</tree>
</field>
</record>
<record id="view_sql_view_search" model="ir.ui.view">
<field name="name">sql.view.filter</field>
<field name="model">sql.view</field>
<field name="arch" type="xml">
<search string="SQL Views">
<field name="name"/>
<field name="complete_sql_name"/>
</search>
</field>
</record>
<record id="action_sql_view" model="ir.actions.act_window">
<field name="name">SQL Views</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">sql.view</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="search_view_id" ref="view_sql_view_search"/>
</record>
<menuitem id="menu_sql_view"
action="action_sql_view"
sequence="20"
parent="base.next_id_9"
string="SQL views"/>
</data>
</openerp>

3
sql_view/wizards/__init__.py

@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
from . import sql_view_csv_preview

83
sql_view/wizards/sql_view_csv_preview.py

@ -0,0 +1,83 @@
# -*- coding: utf-8 -*-
#
#
# Authors: Guewen Baconnier
# Copyright 2015 Camptocamp SA
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
#
import base64
from StringIO import StringIO
import unicodecsv
from openerp.osv import orm, fields
class SQLViewCSVPreview(orm.TransientModel):
_name = 'sql.view.csv.preview'
_description = 'SQL View CSV Preview'
_columns = {
'limit': fields.integer(string='Limit',
help='Number of records. 0 means infinite.'),
'data': fields.binary('CSV', readonly=True),
'filename': fields.char('File Name', readonly=True),
}
_defaults = {
'filename': 'csv-preview.csv',
'limit': 100,
}
def _query(self, cr, uid, form, sql_view, context=None):
view_name = sql_view.complete_sql_name
query = "SELECT * FROM {view_name} "
if form.limit:
query += "LIMIT {limit}"
return query.format(view_name=view_name, limit=form.limit)
def export_csv(self, cr, uid, ids, context=None):
if context is None:
return
sql_view_ids = context.get('active_ids', [])
assert len(ids) == 1, "1 wizard ID expected"
assert len(sql_view_ids) == 1, "1 active ID expected"
form = self.browse(cr, uid, ids[0], context=context)
sql_view = self.pool['sql.view'].browse(cr, uid, sql_view_ids[0],
context=context)
query = self._query(cr, uid, form, sql_view, context=context)
cr.execute(query)
headers = [desc[0] for desc in cr.description]
records = cr.fetchall()
filedata = StringIO()
try:
writer = unicodecsv.writer(filedata, encoding='utf-8')
writer.writerow(headers)
writer.writerows(records)
form.write({'data': base64.encodestring(filedata.getvalue())})
finally:
filedata.close()
return {
'type': 'ir.actions.act_window',
'res_model': self._name,
'view_mode': 'form',
'view_type': 'form',
'res_id': ids[0],
'views': [(False, 'form')],
'target': 'new',
}

37
sql_view/wizards/sql_view_csv_preview_views.xml

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data noupdate="0">
<record id="view_sql_view_csv_preview_form" model="ir.ui.view">
<field name="name">sql.view.csv.preview.form</field>
<field name="model">sql.view.csv.preview</field>
<field name="arch" type="xml">
<form string="SQL View CSV Preview" version="7.0">
<group>
<field name="limit" attrs="{'invisible': [('data', '!=', False)]}"/>
<field name="filename" invisible="1"/>
<field name="data" attrs="{'invisible': [('data', '=', False)]}"
filename="filename"/>
</group>
<footer attrs="{'invisible': [('data', '!=', False)]}">
<button string="Preview" name="export_csv" type="object" class="oe_highlight"/>
or
<button string="Cancel" class="oe_link" special="cancel"/>
</footer>
<footer attrs="{'invisible': [('data', '=', False)]}">
<button special="cancel" string="Close" type="object"/>
</footer>
</form>
</field>
</record>
<record id="action_sql_view_csv_preview" model="ir.actions.act_window">
<field name="name">SQL View CSV Preview</field>
<field name="res_model">sql.view.csv.preview</field>
<field name="view_type">form</field>
<field name="view_mode">form</field>
<field name="view_id" ref="view_sql_view_csv_preview_form"/>
<field name="target">new</field>
</record>
</data>
</openerp>
Loading…
Cancel
Save