diff --git a/adhoc_modules/__init__.py b/adhoc_modules/__init__.py new file mode 100644 index 0000000..b8f0d04 --- /dev/null +++ b/adhoc_modules/__init__.py @@ -0,0 +1,4 @@ +# -*- encoding: utf-8 -*- +from . import models +from . import wizard +# from . import controllers diff --git a/adhoc_modules/__openerp__.py b/adhoc_modules/__openerp__.py new file mode 100644 index 0000000..121f9f8 --- /dev/null +++ b/adhoc_modules/__openerp__.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Copyright (C) 2015 ADHOC SA (http://www.adhoc.com.ar) +# All Rights Reserved. +# +# 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 . +# +############################################################################## +{ + "name": "ADHOC Modules", + "version": "8.0.0.0.0", + 'author': 'ADHOC SA', + 'website': 'www.adhoc.com.ar', + 'license': 'AGPL-3', + 'depends': [ + # module to fetch modules info + 'web_support_client', + 'database_tools', + ], + 'external_dependencies': { + }, + 'data': [ + 'views/adhoc_module_category_view.xml', + 'views/adhoc_module_view.xml', + 'views/support_view.xml', + 'views/db_configuration_view.xml', + 'wizard/module_upgrade_view.xml', + 'security/ir.model.access.csv', + ], + 'demo': [], + 'test': [], + 'installable': True, + 'active': False, + 'auto_install': True +} +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/adhoc_modules/models/__init__.py b/adhoc_modules/models/__init__.py new file mode 100644 index 0000000..cc18067 --- /dev/null +++ b/adhoc_modules/models/__init__.py @@ -0,0 +1,6 @@ +# -*- encoding: utf-8 -*- +from . import adhoc_module_category +from . import ir_module +from . import support +from . import db_configuration +from . import module_dependency diff --git a/adhoc_modules/models/adhoc_module_category.py b/adhoc_modules/models/adhoc_module_category.py new file mode 100644 index 0000000..8773f10 --- /dev/null +++ b/adhoc_modules/models/adhoc_module_category.py @@ -0,0 +1,289 @@ +# -*- coding: utf-8 -*- +############################################################################## +# For copyright and license notices, see __openerp__.py file in module root +# directory +############################################################################## +from openerp import models, fields, api +import logging +import string +_logger = logging.getLogger(__name__) + + +class AdhocModuleCategory(models.Model): + _name = 'adhoc.module.category' + # we add parent order so we can fetch in right order to upload data + # correctly + _order = 'sequence' + # _order = "parent_left" + _parent_store = True + _parent_order = "sequence" + # _rec_name = 'display_name' + + visibility = fields.Selection([ + ('normal', 'Normal'), + ('product_required', 'Product Required'), + ], + required=True, + readonly=True, + default='normal', + ) + contracted_product = fields.Char( + readonly=True, + ) + name = fields.Char( + readonly=True, + required=True, + ) + code = fields.Char( + readonly=True, + # required=True, + # readonly=True, + # default='/', + ) + count_modules = fields.Integer( + string='# Modules', + compute='get_count_modules', + ) + count_pending_modules = fields.Integer( + string='# Revised Modules', + compute='get_count_modules', + ) + count_revised_modules = fields.Integer( + string='# Revised Modules', + compute='get_count_modules', + ) + # count_subcategories_modules = fields.Integer( + # string='# Subcategories Modules', + # compute='get_count_subcategories_modules', + # ) + # count_suggested_subcategories_modules = fields.Integer( + # string='# Suggested Subcategories Modules', + # compute='get_count_subcategories_modules', + # ) + count_subcategories = fields.Integer( + string='# Subcategories', + compute='get_count_subcategories', + ) + count_revised_subcategories = fields.Integer( + string='# Revised Subcategories', + compute='get_count_subcategories', + ) + color = fields.Integer( + string='Color Index', + compute='get_color', + ) + parent_id = fields.Many2one( + 'adhoc.module.category', + 'Parent Category', + select=True, + ondelete='restrict', + readonly=True, + ) + parent_left = fields.Integer( + 'Parent Left', + select=1 + ) + parent_right = fields.Integer( + 'Parent Right', + select=1 + ) + child_ids = fields.One2many( + 'adhoc.module.category', + 'parent_id', + 'Child Categories', + readonly=True, + ) + module_ids = fields.One2many( + 'ir.module.module', + 'adhoc_category_id', + 'Modules', + domain=[('visible', '=', True)], + readonly=True, + ) + description = fields.Text( + readonly=True, + ) + sequence = fields.Integer( + 'Sequence', + default=10, + readonly=True, + ) + to_revise = fields.Boolean( + compute='get_to_revise', + search='search_to_revise', + ) + display_name = fields.Char( + compute='get_display_name', + # store=True + ) + + _sql_constraints = [ + ('code_uniq', 'unique(code)', + 'Category name must be unique'), + ] + + @api.one + @api.depends() + def get_to_revise(self): + if 'uninstalled' in self.module_ids.mapped('state'): + self.to_revise = True + elif True in self.child_ids.mapped('to_revise'): + self.to_revise = True + else: + self.to_revise = False + + @api.model + def search_to_revise(self, operator, value): + """Se tiene que revisar si hay modulos o categorías a revisar""" + # TODO mejorar, en teoria esta soportando solo dos niveles de + # anidamiento con esta form + # intente child_ids.to_revise = True pero dio max recursion + # una alternativa es buscar todos los modulos uninstalled y visible + # agruparlos por categoria y buscar las categorías padres de esas + return [ + '|', ('module_ids.state', 'in', ['uninstalled']), + ('child_ids.module_ids.state', 'in', ['uninstalled']), + ] + + @api.one + @api.constrains('child_ids', 'name', 'parent_id') + def set_code(self): + # if not self.code: + code = self.display_name + valid_chars = "-_.() %s%s" % (string.ascii_letters, string.digits) + code = ''.join(c for c in code if c in valid_chars) + code = code.replace(' ', '').replace('.', '').lower() + self.code = code + + @api.multi + @api.depends('child_ids', 'name', 'parent_id') + def get_display_name(self): + def get_names(cat): + """ Return the list [cat.name, cat.parent_id.name, ...] """ + res = [] + while cat: + res.append(cat.name) + cat = cat.parent_id + return res + for cat in self: + cat.display_name = " / ".join(reversed(get_names(cat))) + + @api.multi + def name_get(self): + result = [] + for record in self: + result.append((record.id, record.display_name)) + return result + + @api.one + @api.depends('child_ids') + def get_count_subcategories(self): + self.count_subcategories = len(self.child_ids) + self.count_revised_subcategories = len(self.child_ids.filtered( + lambda x: not x.to_revise)) + + @api.multi + def get_subcategories_modules(self): + self.ensure_one() + return self.module_ids.search([ + ('adhoc_category_id', 'child_of', self.id)]) + + @api.multi + def get_suggested_subcategories_modules(self): + self.ensure_one() + return self.module_ids.search([ + ('adhoc_category_id', 'child_of', self.id), + ('state', '=', 'uninstalled'), + ]) + + # @api.model + # def search_count_subcategories_modules(self, operator, value): + # sub_modules = self.get_suggested_subcategories_modules() + # if operator == 'like': + # operator = 'ilike' + # return [('name', operator, value)] + + # @api.one + # def get_count_subcategories_modules(self): + # self.count_suggested_subcategories_modules = len( + # self.get_suggested_subcategories_modules()) + # self.count_subcategories_modules = len( + # self.get_subcategories_modules()) + + @api.one + @api.depends('module_ids') + def get_count_modules(self): + count_modules = len(self.module_ids) + count_pending_modules = len(self.module_ids.filtered( + lambda x: x.state == 'uninstalled')) + self.count_modules = count_modules + self.count_pending_modules = count_pending_modules + self.count_revised_modules = count_modules - count_pending_modules + + # @api.depends('state') + @api.one + def get_color(self): + color = 4 + # TODO implementar color de las no contratadas + # if self.count_pending_modules: + if self.visibility != 'normal' and not self.contracted_product: + color = 1 + elif self.to_revise: + color = 7 + # elif self.state == 'cancel': + # color = 1 + # elif self.state == 'inactive': + # color = 3 + # if self.overall_state != 'ok': + # color = 2 + self.color = color + + @api.multi + def action_subcategories(self): + self.ensure_one() + action = self.env['ir.model.data'].xmlid_to_object( + 'adhoc_modules.action_adhoc_module_category') + + if not action: + return False + res = action.read()[0] + res['context'] = { + 'search_default_parent_id': self.id, + 'search_default_to_revise': 1, + 'search_default_not_contracted': 1 + } + return res + + @api.multi + def action_modules(self): + self.ensure_one() + action = self.env['ir.model.data'].xmlid_to_object( + 'adhoc_modules.action_adhoc_ir_module_module') + + if not action: + return False + res = action.read()[0] + res['domain'] = [('adhoc_category_id', '=', self.id)] + res['context'] = { + # 'search_default_not_ignored': 1, + 'search_default_state': 'uninstalled', + } + return res + + # @api.multi + # def action_subcategories_modules(self): + # self.ensure_one() + # action = self.env['ir.model.data'].xmlid_to_object( + # 'adhoc_modules.action_adhoc_ir_module_module') + + # if not action: + # return False + # res = action.read()[0] + # modules = self.get_subcategories_modules() + # res['domain'] = [('id', 'in', modules.ids)] + # res['context'] = { + # 'search_default_not_ignored': 1, + # 'search_default_state': 'uninstalled', + # 'search_default_group_by_adhoc_category': 1 + # } + # return res diff --git a/adhoc_modules/models/db_configuration.py b/adhoc_modules/models/db_configuration.py new file mode 100644 index 0000000..42faace --- /dev/null +++ b/adhoc_modules/models/db_configuration.py @@ -0,0 +1,57 @@ +# -*- coding: utf-8 -*- +############################################################################## +# For copyright and license notices, see __openerp__.py file in module root +# directory +############################################################################## +from openerp import models, fields, api +from openerp.addons.adhoc_modules.models.ir_module import uninstallables +# from openerp.exceptions import Warning +# from datetime import datetime +# from datetime import date +# from dateutil.relativedelta import relativedelta + + +class database_tools_configuration(models.TransientModel): + _inherit = 'db.configuration' + + # @api.model + # def _get_adhoc_modules_state(self): + # return self.env[ + # 'ir.module.module'].get_overall_adhoc_modules_state()['state'] + + @api.one + # dummy depends son computed field is computed + @api.depends('backups_state') + def get_adhoc_modules_data(self): + uninstalled_modules_names = self.env['ir.module.module'].search([ + ('state', 'not in', ['installed', 'to install'])]).mapped('name') + auto_install_modules = self.env['ir.module.module'].search([ + # '|', ('dependencies_id', '=', False), + ('dependencies_id.name', 'not in', uninstalled_modules_names), + ('conf_visibility', '=', 'auto_install'), + ('state', '=', 'uninstalled'), + ]) + self.adhoc_modules_to_install = auto_install_modules + self.adhoc_modules_to_uninstall = self.env['ir.module.module'].search([ + ('conf_visibility', 'in', uninstallables), + # ('conf_visibility', 'in', []), + ('state', '=', 'installed'), + ]) + + adhoc_modules_to_uninstall = fields.Many2many( + 'ir.module.module', + compute='get_adhoc_modules_data', + ) + adhoc_modules_to_install = fields.Many2many( + 'ir.module.module', + compute='get_adhoc_modules_data', + ) + # adhoc_modules_state = fields.Selection([ + # ('should_not_be_installed', 'Should Not be Installed'), + # ('installation_required', 'Installation Required'), + # ('ok', 'Ok'), + # ], + # 'Update Status', + # readonly=True, + # default=_get_adhoc_modules_state, + # ) \ No newline at end of file diff --git a/adhoc_modules/models/ir_module.py b/adhoc_modules/models/ir_module.py new file mode 100644 index 0000000..221c639 --- /dev/null +++ b/adhoc_modules/models/ir_module.py @@ -0,0 +1,196 @@ +# -*- coding: utf-8 -*- +############################################################################## +# For copyright and license notices, see __openerp__.py file in module root +# directory +############################################################################## +from openerp import models, fields, api, _ +from openerp.exceptions import Warning +import logging + +_logger = logging.getLogger(__name__) +uninstallables = ['to_review', 'future_versions', 'unusable'] + + +class AdhocModuleModule(models.Model): + _inherit = 'ir.module.module' + + adhoc_category_id = fields.Many2one( + 'adhoc.module.category', + 'ADHOC Category', + auto_join=True, + readonly=True, + ) + computed_summary = fields.Char( + compute='get_computed_summary', + inverse='set_adhoc_summary', + readonly=True, + ) + adhoc_summary = fields.Char( + readonly=True, + ) + adhoc_description_html = fields.Html( + readonly=True, + ) + support_type = fields.Selection([ + ('supported', 'Soportado'), + ('unsupported', 'No Soportado'), + # ('unsupport_all', 'No Soporta BD'), + ], + string='Support Type', + readonly=True, + ) + review = fields.Selection([ + ('0', 'Not Recommended'), + ('1', 'Only If Necessary'), + ('2', 'Neutral'), + ('3', 'Recomendado'), + ('4', 'Muy Recomendado'), + ], 'Opinion', + select=True, + readonly=True, + ) + conf_visibility = fields.Selection([ + # instalables + ('normal', 'Normal'), + ('only_if_depends', 'Solo si dependencias'), + ('auto_install', 'Auto Install'), + # los auto install por defecto los estamos filtrando y no categorizando + # ('auto_install_by_module', 'Auto Install by Module'), + ('installed_by_others', 'Instalado por Otro'), + ('on_config_wizard', 'En asistente de configuración'), + # no instalable + ('to_review', 'A Revisar'), + ('future_versions', 'Versiones Futuras'), + ('unusable', 'No Usable'), + ], + 'Visibility', + required=True, + readonly=True, + default='normal', + help="Módulos que se pueden instalar:\n" + "* Normal: visible para ser instalado\n" + "* Solo si dependencias: se muestra solo si dependencias instaladas\n" + "* Auto Instalar: auto instalar si se cumplen dependencias\n" + "* Auto Instalado Por Módulo: se instala si se cumplen dependencias\n" + "* Instalado por Otro: algún otro módulo dispara la instalación\n" + "* En asistente de configuración: este módulo esta presente en el " + "asistente de configuración\n" + "\nMódulos en los que se bloquea la instalación:\n" + "* A Revisar: hay que analizar como lo vamos a utilizar\n" + "* Versiones Futuras: se va a incorporar más adelante\n" + "* No Usable: no se usa ni se va a sugerir uso en versiones futuras\n" + ) + visibility_obs = fields.Char( + 'Visibility Observation', + readonly=True, + ) + visible = fields.Boolean( + compute='get_visible', + search='search_visible', + ) + state = fields.Selection( + selection_add=[('ignored', 'Ignored')] + ) + + @api.one + @api.constrains('state') + def check_module_is_installable(self): + if ( + self.state == 'to install' and + self.conf_visibility in uninstallables): + raise Warning(_( + 'You can not install module %s as is %s') % ( + self.name, self.conf_visibility)) + + @api.one + @api.depends('adhoc_category_id', 'conf_visibility') + def get_visible(self): + visible = True + # si esta en estos estados, no importa el resto, queremos verlo + if self.state in ['installed', 'to install']: + visible = True + elif not self.adhoc_category_id: + visible = False + elif ( + self.adhoc_category_id.visibility == 'product_required' and + not self.adhoc_category_id.contracted_product + ): + visible = False + elif self.conf_visibility == 'only_if_depends': + uninstalled_dependencies = self.dependencies_id.mapped( + 'depend_id').filtered( + lambda x: x.state not in ['installed', 'to install']) + if uninstalled_dependencies: + visible = False + elif self.conf_visibility != 'normal': + visible = False + self.visible = visible + + @api.model + def search_visible(self, operator, value): + installed_modules_names = self.search([ + ('state', 'in', ['installed', 'to install'])]).mapped('name') + return [ + '|', ('state', 'in', ['installed', 'to install']), + '&', ('adhoc_category_id', '!=', False), + '&', '|', ('adhoc_category_id.visibility', '=', 'normal'), + '&', ('adhoc_category_id.visibility', '=', 'product_required'), + ('adhoc_category_id.contracted_product', '!=', False), + '|', ('conf_visibility', '=', 'normal'), + '&', ('conf_visibility', '=', 'only_if_depends'), + # puede llegar a ser necesario si no tiene dependencias pero + # no tendria sentido + # '|', ('dependencies_id', '=', False), + ('dependencies_id.name', 'in', installed_modules_names), + ] + + @api.model + def set_adhoc_summary(self): + self.adhoc_summary = self.computed_summary + + @api.one + def get_computed_summary(self): + self.computed_summary = self.adhoc_summary or self.summary + + @api.multi + def button_un_ignore(self): + return self.write({'state': 'uninstalled'}) + + @api.multi + def button_ignore(self): + return self.write({'state': 'ignored'}) + # return self.write({'ignored': True}) + + @api.multi + def button_set_to_install(self): + """ + Casi igual a "button_install" pero no devuelve ninguna acción, queda + seteado unicamente + """ + # Mark the given modules to be installed. + self.state_update('to install', ['uninstalled']) + + # Mark (recursively) the newly satisfied modules to also be installed + + # Select all auto-installable (but not yet installed) modules. + domain = [('state', '=', 'uninstalled'), ('auto_install', '=', True)] + uninstalled_modules = self.search(domain) + + # Keep those with: + # - all dependencies satisfied (installed or to be installed), + # - at least one dependency being 'to install' + satisfied_states = frozenset(('installed', 'to install', 'to upgrade')) + + def all_depencies_satisfied(m): + states = set(d.state for d in m.dependencies_id) + return states.issubset( + satisfied_states) and ('to install' in states) + to_install_modules = filter( + all_depencies_satisfied, uninstalled_modules) + to_install_ids = map(lambda m: m.id, to_install_modules) + + # Mark them to be installed. + if to_install_ids: + self.browse(to_install_ids).button_install() + + return True diff --git a/adhoc_modules/models/module_dependency.py b/adhoc_modules/models/module_dependency.py new file mode 100644 index 0000000..8f669f8 --- /dev/null +++ b/adhoc_modules/models/module_dependency.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +############################################################################## +# For copyright and license notices, see __openerp__.py file in module root +# directory +############################################################################## +from openerp import models, fields +import logging + +_logger = logging.getLogger(__name__) + + +class module_dependency(models.Model): + _inherit = "ir.module.module.dependency" + + state = fields.Selection( + selection_add=[('ignored', 'Ignored')] + ) diff --git a/adhoc_modules/models/support.py b/adhoc_modules/models/support.py new file mode 100644 index 0000000..4fc7cb0 --- /dev/null +++ b/adhoc_modules/models/support.py @@ -0,0 +1,120 @@ +# -*- coding: utf-8 -*- +############################################################################## +# For copyright and license notices, see __openerp__.py file in module root +# directory +############################################################################## +from openerp import models, api +from openerp.exceptions import Warning +import logging +_logger = logging.getLogger(__name__) + + +class Contract(models.Model): + _inherit = 'support.contract' + + @api.model + def _cron_update_adhoc_modules(self): + contract = self.get_active_contract() + try: + contract.get_adhoc_modules_data() + except: + _logger.error( + "Error Updating ADHOC Modules Data For Contract %s" % ( + contract.name)) + + @api.multi + def get_adhoc_modules_data(self): + # we send contract_id so it can be used in other functions + self = self.with_context( + contract_id=self.contract_id) + self.ensure_one() + _logger.info( + "Updating Updating ADHOC Modules Data For Contract %s" % self.name) + adhoc_server_module = 'adhoc_modules_server' + if not self.check_modules_installed([adhoc_server_module]): + raise Warning(( + 'Can not sync modules data because module "%s" is not ' + 'installed on support provider database')) + client = self.get_connection() + self.update_adhoc_categories(client) + self.update_adhoc_modules(client) + + @api.model + def update_adhoc_categories(self, client): + contract_id = self._context.get('contract_id') + fields = [ + 'name', + 'code', + 'parent_id', + 'visibility', + 'description', + 'sequence', + ] + updated_records = local_model = self.env['adhoc.module.category'] + remote_model = client.model('adhoc.module.category.server') + + remote_datas = remote_model.search_read( + [], fields, 0, None, 'parent_left') + for remote_data in remote_datas: + # we dont wont or need id + category_id = remote_data.pop('id') + parent_data = remote_data.pop('parent_id') + if parent_data: + parent_code = remote_model.search_read( + [('id', '=', parent_data[0])], ['code'])[0]['code'] + parent = local_model.search([ + ('code', '=', parent_code)], limit=1) + remote_data['parent_id'] = parent.id + local_record = local_model.search([ + ('name', '=', remote_data.get('name'))], limit=1) + if remote_data['visibility'] == 'product_required': + remote_data['contracted_product'] = ( + remote_model.get_related_contracted_product( + category_id, contract_id)) + if local_record: + local_record.write(remote_data) + else: + local_record = local_record.create(remote_data) + updated_records += local_record + # remove records that has not been updated (they dont exist anymore) + (local_model.search([]) - updated_records).unlink() + + @api.model + def update_adhoc_modules(self, client): + fields = [ + 'name', + 'adhoc_category_id', + 'adhoc_summary', + 'adhoc_description_html', + 'support_type', + 'review', + 'conf_visibility', + 'visibility_obs', + ] + local_model = self.env['ir.module.module'] + remote_model = client.model('adhoc.module.module') + + remote_datas = remote_model.search_read( + [], fields) + for remote_data in remote_datas: + # we dont wont or need id + remote_data.pop('id') + category_data = remote_data.pop('adhoc_category_id') + if category_data: + category_code = client.model( + 'adhoc.module.category.server').search_read( + [('id', '=', category_data[0])], ['code'])[0]['code'] + adhoc_category = self.env['adhoc.module.category'].search([ + ('code', '=', category_code)], limit=1) + remote_data['adhoc_category_id'] = ( + adhoc_category and adhoc_category.id or False) + local_record = local_model.search([ + ('name', '=', remote_data.get('name'))], limit=1) + if local_record: + local_record.write(remote_data) + else: + _logger.warning( + 'Module %s not found on database, you can try updating db' + ' list' % remote_data.get('name')) + +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/adhoc_modules/security/ir.model.access.csv b/adhoc_modules/security/ir.model.access.csv new file mode 100644 index 0000000..da0d9a0 --- /dev/null +++ b/adhoc_modules/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_adhoc_module_category_config,access_adhoc_module_category_config,model_adhoc_module_category,,1,0,0,0 diff --git a/adhoc_modules/views/adhoc_module_category_view.xml b/adhoc_modules/views/adhoc_module_category_view.xml new file mode 100644 index 0000000..b7f7ea8 --- /dev/null +++ b/adhoc_modules/views/adhoc_module_category_view.xml @@ -0,0 +1,126 @@ + + + + + + + adhoc.module.category.kanban + adhoc.module.category + + + + + + + + + + + +
+

+ + +
+

Contactar a ADHOC
para contratar

+
+ +
+
+
+
+
+
+ + + + adhoc.module.category.search + adhoc.module.category + + + + + + + + + + + + + + + adhoc.module.category.tree + adhoc.module.category + + + + + + + + + + adhoc.module.category.form + adhoc.module.category + +
+
+
+ + + + + + + + + + + + + + + + +
+
+
+ + + ADHOC Categories + adhoc.module.category + form + kanban + {'search_default_root_categories': 1, 'search_default_to_revise': 1, 'search_default_not_contracted': 1} + + + + +
+
\ No newline at end of file diff --git a/adhoc_modules/views/adhoc_module_view.xml b/adhoc_modules/views/adhoc_module_view.xml new file mode 100644 index 0000000..ea592ee --- /dev/null +++ b/adhoc_modules/views/adhoc_module_view.xml @@ -0,0 +1,145 @@ + + + + + + + adhoc.ir.module.module.search + ir.module.module + + + + + + + + + + + + + + + + + adhoc.ir.module.module.kanban + ir.module.module + + primary + + + + + + + + + + + + + + + + adhoc.ir.module.module.tree + ir.module.module + + primary + + + + 1 + + + 1 + + + + + + + + + + + + + + + + + + ADHOC Modules + ir.module.module + form + kanban,tree,form + [('visible', '=', True)] + {'search_default_state': 'uninstalled'} + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/adhoc_modules/views/db_configuration_view.xml b/adhoc_modules/views/db_configuration_view.xml new file mode 100644 index 0000000..cba7e2b --- /dev/null +++ b/adhoc_modules/views/db_configuration_view.xml @@ -0,0 +1,28 @@ + + + + + + + db.configuration + db.configuration + + + + + + + diff --git a/adhoc_modules/views/support_view.xml b/adhoc_modules/views/support_view.xml new file mode 100644 index 0000000..3a27d79 --- /dev/null +++ b/adhoc_modules/views/support_view.xml @@ -0,0 +1,17 @@ + + + + + + + support.contract.form + support.contract + + +
+
+
+
+
+
diff --git a/adhoc_modules/wizard/__init__.py b/adhoc_modules/wizard/__init__.py new file mode 100644 index 0000000..fbea60a --- /dev/null +++ b/adhoc_modules/wizard/__init__.py @@ -0,0 +1,2 @@ +# -*- encoding: utf-8 -*- +from . import module_upgrade diff --git a/adhoc_modules/wizard/module_upgrade.py b/adhoc_modules/wizard/module_upgrade.py new file mode 100644 index 0000000..0a834fd --- /dev/null +++ b/adhoc_modules/wizard/module_upgrade.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +############################################################################## +# For copyright and license notices, see __openerp__.py file in module root +# directory +############################################################################## +from openerp import models, api, fields, _ +from openerp.exceptions import Warning + + +class BaseModulePreUpgrade(models.TransientModel): + """ Module Pre Upgrade """ + + _inherit = 'base.module.upgrade' + + recent_backup = fields.Boolean( + readonly=True, + ) + low_review_module_ids = fields.Many2many( + 'ir.module.module', + compute='get_low_review_modules', + string='Low Review Modules', + ) + + @api.one + @api.depends('module_info') + def get_low_review_modules(self): + low_review_modules = self.env['ir.module.module'].search([ + ('state', '=', 'to install'), + ('review', 'in', ['0', '1']), + ]) + self.low_review_module_ids = low_review_modules + + @api.multi + def backup_now(self): + db = self.env['db.database'].search([('type', '=', 'self')], limit=1) + if not db: + raise Warning(_( + 'No Database "Self" found on Database Tools Databses')) + db.action_database_backup() + self.recent_backup = True + return { + 'type': 'ir.actions.act_window', + 'res_model': self._name, + 'res_id': self.id, + 'view_mode': 'form', + 'target': 'new', + } diff --git a/adhoc_modules/wizard/module_upgrade_view.xml b/adhoc_modules/wizard/module_upgrade_view.xml new file mode 100644 index 0000000..65d4882 --- /dev/null +++ b/adhoc_modules/wizard/module_upgrade_view.xml @@ -0,0 +1,28 @@ + + + + + + base.module.upgrade + base.module.upgrade + + + + + + + + + + diff --git a/adhoc_modules_server/README.rst b/adhoc_modules_server/README.rst new file mode 100644 index 0000000..2d28a16 --- /dev/null +++ b/adhoc_modules_server/README.rst @@ -0,0 +1,27 @@ +TODO +==== + +En clientes: + Agregamos en categorías un campo booleano "Contratado" + +En nuestra bd: + los productos tienen un link a "categoría de módulos" + Entonces al agregar un producto a una bd, se actualiza la bd del cliente seteando que esa categoría está comprada. + + +* Evaluar si es mejor entrar a la kanban de categorías con un default_group_by="parent_id" para que permita drag and drop y de un solo vistazo vez todas las categorías, tipo dashboard. +* implementar suggested subcategories +* Agregar modules required para categorías? y que solo aparezcan si dichos modulos estan instalados) +* Agregamos atributos a los módulos tipo sugerido, normal, skipped. Luego en las kanban de categorías, si no hay ninguno a revisar (todos skipped o instalados) lo mostramos de un color, como para saber que terminaste una configuración +* De hecho solo mostramos de manera predeterminada (por filtro) categorías recomendadas y módulos a revisae +* Implementar sacar descripciones de readme o index, tampoco es tan necesario +* agregar vista particular para configuracion que muestre los desconfigurados +* mejorar button_install_cancel para que desmarque los padres +* Llevar todo lo que podamos al modulo de clientes, y luego que este dependa de aquel + +* Vincular documentos o temas a un módulo para que luego de instalarlo al client lo lleve a la documentación correspondiente +* Traer icono, aunque es renigue sin sentido tal vez +* Agregar version requerida en los modulos o algo por el estilo para que se actualice automáticamente +* Cron para actualizar repos (solo los auto_update) +* Mostrar los que faltan asignar +* Sacar warning de "InsecurePlatformWarning: A true SSLContext object is not available." diff --git a/adhoc_modules_server/__init__.py b/adhoc_modules_server/__init__.py new file mode 100644 index 0000000..c6a3ab6 --- /dev/null +++ b/adhoc_modules_server/__init__.py @@ -0,0 +1,4 @@ +# -*- encoding: utf-8 -*- +from . import models +# from . import controllers +# from . import wizard diff --git a/adhoc_modules_server/__openerp__.py b/adhoc_modules_server/__openerp__.py new file mode 100644 index 0000000..48c5b20 --- /dev/null +++ b/adhoc_modules_server/__openerp__.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Copyright (C) 2015 ADHOC SA (http://www.adhoc.com.ar) +# All Rights Reserved. +# +# 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 . +# +############################################################################## +{ + "name": "ADHOC Modules", + "version": "8.0.0.0.0", + 'author': 'ADHOC SA', + 'website': 'www.adhoc.com.ar', + 'license': 'AGPL-3', + 'depends': [ + 'adhoc_modules', + 'mass_editing', + 'web_support_server', + ], + # 'external_dependencies': { + # 'python': ['octuhub'] + # }, + 'data': [ + 'views/adhoc_module_repository_view.xml', + 'views/adhoc_module_category_view.xml', + 'views/adhoc_module_view.xml', + 'security/ir.model.access.csv', + 'data/mass_editting_data.xml', + ], + 'demo': [], + 'test': [], + 'installable': True, + 'active': False, + 'auto_install': True +} +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/adhoc_modules_server/data/mass_editting_data.xml b/adhoc_modules_server/data/mass_editting_data.xml new file mode 100644 index 0000000..f3e24d1 --- /dev/null +++ b/adhoc_modules_server/data/mass_editting_data.xml @@ -0,0 +1,13 @@ + + + + + Asignar Categoría + + + + + + + + diff --git a/adhoc_modules_server/models/__init__.py b/adhoc_modules_server/models/__init__.py new file mode 100644 index 0000000..4c84557 --- /dev/null +++ b/adhoc_modules_server/models/__init__.py @@ -0,0 +1,6 @@ +# -*- encoding: utf-8 -*- +from . import adhoc_module_repository +from . import adhoc_module_dependency +from . import adhoc_module_category +from . import adhoc_module +from . import product_template diff --git a/adhoc_modules_server/models/adhoc_module.py b/adhoc_modules_server/models/adhoc_module.py new file mode 100644 index 0000000..d4fc3c4 --- /dev/null +++ b/adhoc_modules_server/models/adhoc_module.py @@ -0,0 +1,115 @@ +# -*- coding: utf-8 -*- +############################################################################## +# For copyright and license notices, see __openerp__.py file in module root +# directory +############################################################################## +from openerp import models, fields, api, _ +# from openerp.exceptions import Warning +import logging + +_logger = logging.getLogger(__name__) + + +class AdhocModuleModule(models.Model): + _inherit = 'ir.module.module' + _name = 'adhoc.module.module' + + adhoc_category_id = fields.Many2one( + 'adhoc.module.category.server', + 'ADHOC Category', + auto_join=True, + readonly=False, + ) + repository_id = fields.Many2one( + 'adhoc.module.repository', + 'Repository', + ondelete='cascade', + required=True, + auto_join=True, + readonly=True, + ) + dependencies_id = fields.One2many( + 'adhoc.module.dependency', + 'module_id', + 'Dependencies', + readonly=True, + ) + computed_summary = fields.Char( + readonly=False, + ) + adhoc_summary = fields.Char( + readonly=False, + ) + adhoc_description_html = fields.Html( + readonly=False, + ) + support_type = fields.Selection( + readonly=False, + ) + review = fields.Selection( + readonly=False, + ) + conf_visibility = fields.Selection( + readonly=False, + ) + visibility_obs = fields.Char( + readonly=False, + ) + + @api.model + def create(self, vals): + # ir module modifies create, we need default one + create_original = models.BaseModel.create + module = create_original(self, vals) + module_metadata = { + 'name': 'module_%s_%s' % ( + vals['name'], + module.repository_id.branch.replace('.', '_')), + 'model': self._name, + 'module': 'adhoc_module_server', + 'res_id': module.id, + 'noupdate': True, + } + self.env['ir.model.data'].create(module_metadata) + return module + + @api.multi + def _update_dependencies(self, depends=None): + self.ensure_one() + if depends is None: + depends = [] + existing = set(x.name for x in self.dependencies_id) + needed = set(depends) + for dep in (needed - existing): + self._cr.execute( + 'INSERT INTO adhoc_module_dependency (module_id, name) ' + 'values (%s, %s)', (self.id, dep)) + for dep in (existing - needed): + self._cr.execute( + 'DELETE FROM adhoc_module_dependency WHERE module_id = %s ' + 'and name = %s', (self.id, dep)) + self.invalidate_cache(['dependencies_id']) + + @api.multi + def open_module(self): + self.ensure_one() + module_form = self.env.ref( + 'adhoc_modules_server.view_adhoc_module_module_form', False) + if not module_form: + return False + return { + 'name': _('Module Description'), + 'type': 'ir.actions.act_window', + 'view_type': 'form', + 'view_mode': 'form', + 'res_model': 'adhoc.module.module', + 'views': [(module_form.id, 'form')], + 'view_id': module_form.id, + 'res_id': self.id, + 'target': 'current', + 'target': 'new', + 'context': self._context, + # top open in editable form + 'flags': { + 'form': {'action_buttons': True, 'options': {'mode': 'edit'}}} + } diff --git a/adhoc_modules_server/models/adhoc_module_category.py b/adhoc_modules_server/models/adhoc_module_category.py new file mode 100644 index 0000000..b660300 --- /dev/null +++ b/adhoc_modules_server/models/adhoc_module_category.py @@ -0,0 +1,102 @@ +# -*- coding: utf-8 -*- +############################################################################## +# For copyright and license notices, see __openerp__.py file in module root +# directory +############################################################################## +from openerp import models, fields, api +import logging +_logger = logging.getLogger(__name__) + + +class AdhocModuleCategory(models.Model): + _inherit = 'adhoc.module.category' + _name = 'adhoc.module.category.server' + + product_tmpl_ids = fields.Many2many( + 'product.template', + 'adhoc_module_category_product_rel', + 'adhoca_category_id', 'product_tmpl_id', + 'Products', + ) + module_ids = fields.One2many( + 'adhoc.module.module', + 'adhoc_category_id', + 'Modules', + # domain=[('visible', '=', True)], + readonly=False, + ) + parent_id = fields.Many2one( + 'adhoc.module.category.server', + 'Parent Category', + select=True, + ondelete='restrict', + readonly=False, + ) + child_ids = fields.One2many( + 'adhoc.module.category.server', + 'parent_id', + 'Child Categories', + readonly=False, + ) + visibility = fields.Selection( + readonly=False, + ) + contracted_product = fields.Char( + readonly=False, + ) + name = fields.Char( + readonly=False, + ) + code = fields.Char( + readonly=False, + ) + description = fields.Text( + readonly=False, + ) + sequence = fields.Integer( + readonly=False, + ) + + @api.multi + def get_related_contracted_product(self, contract_id): + self.ensure_one() + analytic_lines = self.env['account.analytic.invoice.line'].search([ + ('analytic_account_id.id', '=', contract_id), + ('product_id.product_tmpl_id', 'in', self.product_tmpl_ids.ids), + ]) + if analytic_lines: + return analytic_lines.mapped('product_id.name') + else: + return False + + @api.multi + def action_subcategories(self): + self.ensure_one() + action = self.env['ir.model.data'].xmlid_to_object( + 'adhoc_modules_server.action_adhoc_module_category') + + if not action: + return False + res = action.read()[0] + res['context'] = { + 'search_default_parent_id': self.id, + # 'search_default_to_revise': 1, + # 'search_default_not_contracted': 1 + } + return res + + @api.multi + def action_modules(self): + self.ensure_one() + action = self.env['ir.model.data'].xmlid_to_object( + 'adhoc_modules_server.action_adhoc_module_module') + + if not action: + return False + res = action.read()[0] + res['domain'] = [('adhoc_category_id', '=', self.id)] + res['context'] = { + # 'search_default_not_ignored': 1, + # 'search_default_state': 'uninstalled', + } + return res diff --git a/adhoc_modules_server/models/adhoc_module_dependency.py b/adhoc_modules_server/models/adhoc_module_dependency.py new file mode 100644 index 0000000..c1f5df5 --- /dev/null +++ b/adhoc_modules_server/models/adhoc_module_dependency.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- +############################################################################## +# For copyright and license notices, see __openerp__.py file in module root +# directory +############################################################################## +from openerp import models, fields, api + +DEP_STATES = [ + ('uninstallable', 'Uninstallable'), + ('uninstalled', 'Not Installed'), + ('installed', 'Installed'), + ('to upgrade', 'To be upgraded'), + ('to remove', 'To be removed'), + ('to install', 'To be installed'), + ('unknown', 'Unknown'), +] + + +class AdhocModuleDependency(models.Model): + _name = "adhoc.module.dependency" + _description = "Module dependency" + + # the dependency name + name = fields.Char( + index=True + ) + # the module that depends on it + module_id = fields.Many2one( + 'adhoc.module.module', + 'Module', + ondelete='cascade', + auto_join=True, + ) + # the module corresponding to the dependency, and its status + depend_id = fields.Many2one( + 'adhoc.module.module', + 'Dependency', + compute='_compute_depend' + ) + state = fields.Selection( + DEP_STATES, + string='Status', + compute='_compute_depend' + ) + + @api.one + @api.depends('name') + def _compute_depend(self): + mod = self.env['adhoc.module.module'].search([ + ('name', '=', self.name), + ('repository_id.branch', '=', self.module_id.repository_id.branch), + ], limit=1) + self.depend_id = mod + self.state = self.depend_id.state or 'unknown' diff --git a/adhoc_modules_server/models/adhoc_module_repository.py b/adhoc_modules_server/models/adhoc_module_repository.py new file mode 100644 index 0000000..76816ca --- /dev/null +++ b/adhoc_modules_server/models/adhoc_module_repository.py @@ -0,0 +1,238 @@ +# -*- coding: utf-8 -*- +############################################################################## +# For copyright and license notices, see __openerp__.py file in module root +# directory +############################################################################## +from openerp import models, fields, api, modules, tools +from openerp.modules.module import adapt_version +from openerp.exceptions import Warning +from openerp.addons.adhoc_modules_server.octohub.connection import Connection +# from octohub.connection import Connection +from openerp.tools.parse_version import parse_version +import base64 +import logging +import itertools +import os + +_logger = logging.getLogger(__name__) +MANIFEST = '__openerp__.py' +README = ['README.rst', 'README.md', 'README.txt'] + + +def load_information_from_contents( + manifest_content, readme_content=False, index_content=False): + """ + :param module: The name of the module (sale, purchase, ...) + :param mod_path: Physical path of module, if not providedThe name of the + module (sale, purchase, ...) + """ + + # default values for descriptor + info = { + 'application': False, + 'author': '', + 'auto_install': False, + 'category': 'Uncategorized', + 'depends': [], + 'description': '', + # 'icon': get_module_icon(module), + 'installable': True, + 'license': 'AGPL-3', + 'post_load': None, + 'version': '1.0', + 'web': False, + 'website': '', + 'sequence': 100, + 'summary': '', + } + info.update(itertools.izip( + 'depends data demo test init_xml update_xml demo_xml'.split(), + iter(list, None))) + try: + info.update(eval(manifest_content)) + except: + _logger.warning('could not ') + return {} + # if not info.get('description'): + # readme_path = [opj(mod_path, x) for x in README + # if os.path.isfile(opj(mod_path, x))] + # if readme_path: + # readme_text = tools.file_open(readme_path[0]).read() + # info['description'] = readme_text + + if 'active' in info: + # 'active' has been renamed 'auto_install' + info['auto_install'] = info['active'] + + info['version'] = adapt_version(info['version']) + return info + + +class AdhocModuleRepository(models.Model): + _name = 'adhoc.module.repository' + + user = fields.Char( + 'User or Organization', + required=True, + help='eg. "ingadhoc"', + ) + subdirectory = fields.Char( + 'Subdirectory', + help='For eg. "addons"', + ) + name = fields.Char( + 'Repository Name', + required=True, + help='eg. "product"', + ) + branch = fields.Selection( + [('8.0', '8.0'), ('9.0', '9.0')], + 'Branch / Odoo Version', + required=True, + ) + module_ids = fields.One2many( + 'adhoc.module.module', + 'repository_id', + 'Modules', + ) + token = fields.Char( + help='If no token configured, we will try to use a general one setted ' + 'as "github.token" parameter, if none configured, we try connecting ' + 'without token' + ) + auto_update = fields.Boolean( + default=True, + ) + sequence = fields.Integer( + string='Sequence', + default=10, + ) + + @api.multi + def get_token(self): + self.ensure_one() + token = self.token + if not token: + token = self.env['ir.config_parameter'].get_param( + 'github.token') or '' + return token + + @api.multi + def get_connection(self): + self.ensure_one() + token = self.get_token() + return Connection(token) + + @api.multi + def get_modules_paths(self): + """return name of remote modules""" + response = self.read_remote_path(self.subdirectory or '') + paths = [x['path'] for x in response.parsed if x['type'] == 'dir'] + _logger.info('Readed paths %s' % paths) + return paths + + @api.multi + def read_remote_path(self, path=False): + _logger.info('Reading data from remote path %s' % path) + conn = self.get_connection() + # obtener directorios + uri = "/repos/%s/%s/contents/%s" % ( + self.user, self.name, path or '') + try: + response = conn.send( + 'GET', uri, params={'ref': self.branch}) + except Exception, ResponseError: + raise Warning( + 'Could not get modules for:\n' + '* Repository: %s\n' + '* URI: %s\n' + '* Branch: %s\n' + '* Token: %s\n\n' + 'This is what we get:%s' % ( + self.name, uri, self.branch, + self.get_token(), ResponseError)) + return response + + @api.model + def get_module_info(self, name): + info = {} + try: + response = self.read_remote_path("%s/__openerp__.py" % name) + encoded_content = response.parsed['content'] + info = load_information_from_contents( + base64.b64decode(encoded_content)) + except Exception: + _logger.debug('Error when trying to fetch informations for ' + 'module %s', name, exc_info=True) + return info + + @api.multi + def get_module_vals(self, info): + self.ensure_one() + return { + 'description': info.get('description', ''), + 'shortdesc': info.get('name', ''), + 'author': info.get('author', 'Unknown'), + 'maintainer': info.get('maintainer', False), + 'contributors': ', '.join(info.get('contributors', [])) or False, + 'website': info.get('website', ''), + 'license': info.get('license', 'AGPL-3'), + 'sequence': info.get('sequence', 100), + 'application': info.get('application', False), + 'auto_install': info.get('auto_install', False), + 'icon': info.get('icon', False), + 'summary': info.get('summary', ''), + } + + @api.multi + def scan_repository(self): + self.ensure_one() + res = [0, 0] # [update, add] + + default_version = modules.adapt_version('1.0') + + # iterate through detected modules and update/create them in db + for module_path in self.get_modules_paths(): + # sacamos la ultima parte del path como nombre del modulo + mod_name = os.path.basename(module_path) + # search for modules of same name an odoo version + mod = self.env['adhoc.module.module'].search([ + ('name', '=', mod_name), + ('repository_id.branch', '=', self.branch)], limit=1) + module_info = self.get_module_info(module_path) + values = self.get_module_vals(module_info) + + if mod: + _logger.info('Updating data for module %s' % mod_name) + if mod.repository_id.id != self.id: + raise Warning( + 'Module already exist in other repository') + updated_values = {} + for key in values: + old = getattr(mod, key) + updated = isinstance( + values[key], basestring) and tools.ustr( + values[key]) or values[key] + if (old or updated) and updated != old: + updated_values[key] = values[key] + if module_info.get( + 'installable', True) and mod.state == 'uninstallable': + updated_values['state'] = 'uninstalled' + if parse_version(module_info.get( + 'version', default_version)) > parse_version( + mod.latest_version or default_version): + res[0] += 1 + if updated_values: + mod.write(updated_values) + else: + _logger.info('Creating new module %s' % mod_name) + # if not installable, we dont upload + if not module_info or not module_info.get( + 'installable', True): + continue + mod = mod.create(dict( + name=mod_name, state='uninstalled', + repository_id=self.id, **values)) + res[1] += 1 + mod._update_dependencies(module_info.get('depends', [])) + return res diff --git a/adhoc_modules_server/models/product_template.py b/adhoc_modules_server/models/product_template.py new file mode 100644 index 0000000..773854b --- /dev/null +++ b/adhoc_modules_server/models/product_template.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +############################################################################## +# For copyright and license notices, see __openerp__.py file in module root +# directory +############################################################################## +# from openerp import models, fields, api +# import logging +# _logger = logging.getLogger(__name__) + + +# class ProductTempalte(models.Model): +# _inherit = 'product.template' + +# adhoc_category_ids = fields.Many2many( +# 'product.template', +# 'adhoc_module_category_product_rel', +# 'adhoca_category_id', 'product_tmpl_id', +# 'Products', +# required=True, +# ) diff --git a/adhoc_modules_server/octohub/__init__.py b/adhoc_modules_server/octohub/__init__.py new file mode 100644 index 0000000..4ac03a0 --- /dev/null +++ b/adhoc_modules_server/octohub/__init__.py @@ -0,0 +1,12 @@ +# Copyright (c) 2013 Alon Swartz +# +# This file is part of OctoHub. +# +# OctoHub is free software; you can redistribute it and/or modify it under the +# terms of the GNU General Public License as published by the Free Software +# Foundation; either version 3 of the License, or (at your option) any later +# version. + +__version__ = '0.1' +__useragent__ = 'octohub/%s' % __version__ + diff --git a/adhoc_modules_server/octohub/connection.py b/adhoc_modules_server/octohub/connection.py new file mode 100644 index 0000000..12db2ad --- /dev/null +++ b/adhoc_modules_server/octohub/connection.py @@ -0,0 +1,75 @@ +# Copyright (c) 2013 Alon Swartz +# +# This file is part of OctoHub. +# +# OctoHub is free software; you can redistribute it and/or modify it under the +# terms of the GNU General Public License as published by the Free Software +# Foundation; either version 3 of the License, or (at your option) any later +# version. + +import requests + +from . import __useragent__ +from .response import parse_response + +class Pager(object): + def __init__(self, conn, uri, params, max_pages=0): + """Iterator object handling pagination of Connection.send (method: GET) + conn (octohub.Connection): Connection object + uri (str): Request URI (e.g., /user/issues) + params (dict): Parameters to include in request + max_pages (int): Maximum amount of pages to get (0 for all) + """ + self.conn = conn + self.uri = uri + self.params = params + self.max_pages = max_pages + self.count = 0 + + def __iter__(self): + while True: + self.count += 1 + response = self.conn.send('GET', self.uri, self.params) + yield response + + if self.count == self.max_pages: + break + + if not 'next' in response.parsed_link.keys(): + break + + # Parsed link is absolute. Connection wants a relative link, + # so remove protocol and GitHub endpoint for the pagination URI. + m = re.match(self.conn.endpoint + '(.*)', response.parsed_link.next.uri) + self.uri = m.groups()[0] + self.params = response.parsed_link.next.params + +class Connection(object): + def __init__(self, token=None): + """OctoHub connection + token (str): GitHub Token (anonymous if not provided) + """ + self.endpoint = 'https://api.github.com' + self.headers = {'User-Agent': __useragent__} + + if token: + self.headers['Authorization'] = 'token %s' % token + + def send(self, method, uri, params={}, data=None): + """Prepare and send request + method (str): Request HTTP method (e.g., GET, POST, DELETE, ...) + uri (str): Request URI (e.g., /user/issues) + params (dict): Parameters to include in request + data (str | file type object): data to include in request + + returns: requests.Response object, including: + response.parsed (AttrDict): parsed response when applicable + response.parsed_link (AttrDict): parsed header link when applicable + http://docs.python-requests.org/en/latest/api/#requests.Response + """ + url = self.endpoint + uri + kwargs = {'headers': self.headers, 'params': params, 'data': data} + response = requests.request(method, url, **kwargs) + + return parse_response(response) + diff --git a/adhoc_modules_server/octohub/exceptions.py b/adhoc_modules_server/octohub/exceptions.py new file mode 100644 index 0000000..ccb4f95 --- /dev/null +++ b/adhoc_modules_server/octohub/exceptions.py @@ -0,0 +1,26 @@ +# Copyright (c) 2013 Alon Swartz +# +# This file is part of OctoHub. +# +# OctoHub is free software; you can redistribute it and/or modify it under the +# terms of the GNU General Public License as published by the Free Software +# Foundation; either version 3 of the License, or (at your option) any later +# version. + +import simplejson as json + +class ResponseError(Exception): + """Accessible attributes: error + error (AttrDict): Parsed error response + """ + def __init__(self, error): + Exception.__init__(self, error) + self.error = error + + def __str__(self): + return json.dumps(self.error, indent=1) + + +class OctoHubError(Exception): + pass + diff --git a/adhoc_modules_server/octohub/response.py b/adhoc_modules_server/octohub/response.py new file mode 100644 index 0000000..bdc34c0 --- /dev/null +++ b/adhoc_modules_server/octohub/response.py @@ -0,0 +1,103 @@ +# Copyright (c) 2013 Alon Swartz +# +# This file is part of OctoHub. +# +# OctoHub is free software; you can redistribute it and/or modify it under the +# terms of the GNU General Public License as published by the Free Software +# Foundation; either version 3 of the License, or (at your option) any later +# version. + +import re + +from .utils import AttrDict, get_logger +from .exceptions import ResponseError, OctoHubError + +log = get_logger('response') + +def _get_content_type(response): + """Parse response and return content-type""" + try: + content_type = response.headers['Content-Type'] + content_type = content_type.split(';', 1)[0] + except KeyError: + content_type = None + + return content_type + +def _parse_link(header_link): + """Parse header link and return AttrDict[rel].uri|params""" + links = AttrDict() + for s in header_link.split(','): + link = AttrDict() + + m = re.match('<(.*)\?(.*)>', s.split(';')[0].strip()) + link.uri = m.groups()[0] + link.params = {} + for kv in m.groups()[1].split('&'): + key, value = kv.split('=') + link.params[key] = value + + m = re.match('rel="(.*)"', s.split(';')[1].strip()) + rel = m.groups()[0] + + links[rel] = link + log.debug('link-%s-page: %s' % (rel, link.params['page'])) + + return links + +def parse_element(el): + """Parse el recursively, replacing dicts with AttrDicts representation""" + if type(el) == dict: + el_dict = AttrDict() + for key, val in el.items(): + el_dict[key] = parse_element(val) + + return el_dict + + elif type(el) == list: + el_list = [] + for l in el: + el_list.append(parse_element(l)) + + return el_list + + else: + return el + +def parse_response(response): + """Parse request response object and raise exception on response error code + response (requests.Response object): + + returns: requests.Response object, including: + response.parsed (AttrDict) + response.parsed_link (AttrDict) + http://docs.python-requests.org/en/latest/api/#requests.Response + """ + response.parsed = AttrDict() + response.parsed_link = AttrDict() + + if 'link' in response.headers.keys(): + response.parsed_link = _parse_link(response.headers['link']) + + headers = ['status', 'x-ratelimit-limit', 'x-ratelimit-remaining'] + for header in headers: + if header in response.headers.keys(): + log.info('%s: %s' % (header, response.headers[header])) + + content_type = _get_content_type(response) + + if content_type == 'application/json': + json = response.json + if callable(json): + json = json() + response.parsed = parse_element(json) + else: + if not response.status_code == 204: + raise OctoHubError('unhandled content_type: %s' % content_type) + + if not response.status_code in (200, 201, 204): + raise ResponseError(response.parsed) + + return response + + diff --git a/adhoc_modules_server/octohub/utils.py b/adhoc_modules_server/octohub/utils.py new file mode 100644 index 0000000..e168472 --- /dev/null +++ b/adhoc_modules_server/octohub/utils.py @@ -0,0 +1,40 @@ +# Copyright (c) 2013 Alon Swartz +# +# This file is part of OctoHub. +# +# OctoHub is free software; you can redistribute it and/or modify it under the +# terms of the GNU General Public License as published by the Free Software +# Foundation; either version 3 of the License, or (at your option) any later +# version. + +import os +import logging + +class AttrDict(dict): + """Attribute Dictionary (set and access attributes 'pythonically')""" + def __getattr__(self, name): + if name in self: + return self[name] + raise AttributeError('no such attribute: %s' % name) + + def __setattr__(self, name, val): + self[name] = val + +def get_logger(name, level=None): + """Returns logging handler based on name and level (stderr) + name (str): name of logging handler + level (str): see logging.LEVEL + """ + logger = logging.getLogger(name) + + if not logger.handlers: + stderr = logging.StreamHandler() + stderr.setFormatter(logging.Formatter( + '%(levelname)s [%(name)s]: %(message)s')) + logger.addHandler(stderr) + + level = level if level else os.environ.get('OCTOHUB_LOGLEVEL', 'CRITICAL') + logger.setLevel(getattr(logging, level)) + + return logger + diff --git a/adhoc_modules_server/security/ir.model.access.csv b/adhoc_modules_server/security/ir.model.access.csv new file mode 100644 index 0000000..00e0f44 --- /dev/null +++ b/adhoc_modules_server/security/ir.model.access.csv @@ -0,0 +1,9 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_adhoc_module_module_all,access_adhoc_module_module_all,model_adhoc_module_module,,1,0,0,0 +access_adhoc_module_module_config,access_adhoc_module_module_config,model_adhoc_module_module,base.group_system,1,1,1,1 +access_adhoc_module_category_server_config,access_adhoc_module_category_server_config,model_adhoc_module_category_server,base.group_system,1,1,1,1 +access_adhoc_module_category_server_all,access_adhoc_module_category_server_all,model_adhoc_module_category_server,,1,0,0,0 +access_adhoc_module_repository_all,access_adhoc_module_repository_all,model_adhoc_module_repository,,1,0,0,0 +access_adhoc_module_repository_config,access_adhoc_module_repository_config,model_adhoc_module_repository,base.group_system,1,1,1,1 +access_adhoc_module_dependency_all,access_adhoc_module_dependency_all,model_adhoc_module_dependency,,1,0,0,0 +access_adhoc_module_dependency_config,access_adhoc_module_dependency_config,model_adhoc_module_dependency,base.group_system,1,1,1,1 diff --git a/adhoc_modules_server/views/adhoc_module_category_view.xml b/adhoc_modules_server/views/adhoc_module_category_view.xml new file mode 100644 index 0000000..6424714 --- /dev/null +++ b/adhoc_modules_server/views/adhoc_module_category_view.xml @@ -0,0 +1,126 @@ + + + + + + + + adhoc.module.category.server.kanban + adhoc.module.category.server + + + + + + + + + + + + + + + + + + + + + + adhoc.module.category.search + adhoc.module.category.server + + primary + + + + + + + + + adhoc.module.category.tree + adhoc.module.category.server + + primary + + + + + + true + + + + + + + adhoc.module.category.form + adhoc.module.category.server + + primary + + + + + + +
+ true + true + true +
+
+
+ + + ADHOC Categories + adhoc.module.category.server + form + + kanban,tree,form + {'search_default_root_categories': 1} + + + + +
+
diff --git a/adhoc_modules_server/views/adhoc_module_repository_view.xml b/adhoc_modules_server/views/adhoc_module_repository_view.xml new file mode 100644 index 0000000..07183d4 --- /dev/null +++ b/adhoc_modules_server/views/adhoc_module_repository_view.xml @@ -0,0 +1,71 @@ + + + + + + + adhoc.module.repository.form + adhoc.module.repository + +
+
+
+ + + + + + + + + + + +
+
+
+ + + + adhoc.module.repository.search + adhoc.module.repository + + + + + + + + + + + + + + + + adhoc.module.repository.tree + adhoc.module.repository + + + + + + + + + + + ADHOC Repositories + adhoc.module.repository + form + tree,form + + + + + + +
+
diff --git a/adhoc_modules_server/views/adhoc_module_view.xml b/adhoc_modules_server/views/adhoc_module_view.xml new file mode 100644 index 0000000..3e256c0 --- /dev/null +++ b/adhoc_modules_server/views/adhoc_module_view.xml @@ -0,0 +1,133 @@ + + + + + + + adhoc.module.module.search + adhoc.module.module + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + adhoc.module.module.kanban + adhoc.module.module + + primary + + + + + + + + + + + + + + + adhoc.module.module.tree + adhoc.module.module + + primary + + + top + + + + + + 1 + + + 1 + + + + + + + + + + + + + + + + ADHOC Modules + adhoc.module.module + form + tree,kanban,form + {'search_default_un_categorized': 1, 'search_default_conf_visibility': 'normal', 'search_default_not_auto_install': 1, 'search_default_installable': 1} + + + + + + diff --git a/database_tools/models/database.py b/database_tools/models/database.py index 6490067..837e6f7 100644 --- a/database_tools/models/database.py +++ b/database_tools/models/database.py @@ -26,23 +26,23 @@ def _get_preserve_rules(self): not_self_name = fields.Char( 'Database', default=_get_default_name, - ) + ) name = fields.Char( 'Database', compute='_get_name', - ) + ) type = fields.Selection( [('self', 'Self'), ('other', 'Local')], string='Type', required=True, default='self', - ) + ) syncked_backup_path = fields.Char( string='Sincked Backup Path', default='/var/odoo/backups/syncked/', help='If defined, after each backup, a copy backup with database name ' 'as file name, will be saved on this folder' - ) + ) backups_path = fields.Char( string='Backups Path', required=True, @@ -50,7 +50,7 @@ def _get_preserve_rules(self): help='User running this odoo intance must have CRUD access rights on ' 'this folder' # TODO agregar boton para probar que se tiene permisos - ) + ) backup_next_date = fields.Datetime( string='Date of Next Backup', default=fields.Datetime.now, @@ -59,32 +59,32 @@ def _get_preserve_rules(self): # datetime.strftime(datetime.today()+timedelta(days=1), # '%Y-%m-%d 05:%M:%S') required=True, - ) + ) backup_rule_type = fields.Selection([ ('hourly', 'Hour(s)'), ('daily', 'Day(s)'), ('weekly', 'Week(s)'), ('monthly', 'Month(s)'), ('yearly', 'Year(s)'), - ], + ], 'Recurrency', help="Backup automatically repeat at specified interval", default='daily', required=True, - ) + ) backup_format = fields.Selection([ ('zip', 'zip (With Filestore)'), ('pg_dump', 'pg_dump (Without Filestore)')], 'Backup Format', default='zip', required=True, - ) + ) backup_interval = fields.Integer( string='Repeat Every', default=1, required=True, help="Repeat every (Days/Week/Month/Year)" - ) + ) backup_preserve_rule_ids = fields.Many2many( 'db.database.backup.preserve_rule', 'db_backup_preserve_rule_rel', @@ -92,24 +92,24 @@ def _get_preserve_rules(self): 'Preservation Rules', required=True, default=_get_preserve_rules, - ) + ) backup_ids = fields.One2many( 'db.database.backup', 'database_id', string='Backups', readonly=True, - ) + ) backup_count = fields.Integer( string='# Backups', compute='_get_backups' - ) + ) @api.model def get_overall_backups_state(self): res = { 'state': 'ok', 'detail': False, - } + } backups_state = self.search([]).get_backups_state() if backups_state: res['state'] = 'error' @@ -142,7 +142,7 @@ def get_backups_state(self): ('database_id', '=', database.id), ('date', '>=', fields.Datetime.to_string(from_date)), ('type', '=', 'automatic'), - ]) + ]) if not backups: res[database.id] = ( @@ -198,7 +198,7 @@ def drop_con(self): # Por si no anda... # db = sql_db.db_connect('postgres') # with closing(db.cursor()) as pg_cr: - # pg_cr.autocommit(True) # avoid transaction block + # pg_cr.autocommit(True) # avoid transaction block # db_ws._drop_conn(pg_cr, self.name) return True @@ -244,7 +244,7 @@ def cron_database_backup(self): # get databases databases = self.search([ ('backup_next_date', '<=', current_date), - ]) + ]) # make bakcup # we make a loop to commit after each backup @@ -265,13 +265,13 @@ def cron_database_backup(self): @api.model def relative_delta(self, from_date, interval, rule_type): if rule_type == 'hourly': - next_date = from_date+relativedelta(hours=+interval) + next_date = from_date + relativedelta(hours=+interval) elif rule_type == 'daily': - next_date = from_date+relativedelta(days=+interval) + next_date = from_date + relativedelta(days=+interval) elif rule_type == 'weekly': - next_date = from_date+relativedelta(weeks=+interval) + next_date = from_date + relativedelta(weeks=+interval) elif rule_type == 'monthly': - next_date = from_date+relativedelta(months=+interval) + next_date = from_date + relativedelta(months=+interval) else: raise Warning('Type must be one of "days, weekly or monthly"') return next_date @@ -300,7 +300,7 @@ def database_manual_backup_clean(self): domain = [ ('database_id', 'in', self.ids), ('keep_till_date', '<=', fields.Datetime.now()), - ] + ] to_delete_backups = self.env['db.database.backup'].search( domain) to_delete_backups.unlink() @@ -330,7 +330,7 @@ def database_auto_backup_clean(self): ('date', '<=', fields.Datetime.to_string( interval_to_date)), ('type', '=', 'automatic'), - ] + ] backup = self.env['db.database.backup'].search( domain, order='date', limit=1) if backup: @@ -344,7 +344,7 @@ def database_auto_backup_clean(self): ('id', 'not in', preserve_backups_ids), ('type', '=', 'automatic'), ('database_id', '=', self.id), - ]) + ]) _logger.info('Backups to delete ids %s', to_delete_backups.ids) to_delete_backups.unlink() return True @@ -427,7 +427,7 @@ def database_backup( 'date': now, 'type': bu_type, 'keep_till_date': keep_till_date, - } + } self.backup_ids.create(backup_vals) _logger.info('Backup %s Created' % backup_name) diff --git a/oca_dependencies.txt b/oca_dependencies.txt index 5601eaa..57239e6 100644 --- a/oca_dependencies.txt +++ b/oca_dependencies.txt @@ -1,3 +1,4 @@ +oca-server-tools https://github.com/oca/server-tools.git odoo-infrastructure https://github.com/ingadhoc/odoo-infrastructure.git odoo-web https://github.com/ingadhoc/odoo-web.git adhoc-miscellaneous https://github.com/ingadhoc/miscellaneous.git diff --git a/web_support_client/README.RST b/web_support_client/README.RST new file mode 100644 index 0000000..c1a17c9 --- /dev/null +++ b/web_support_client/README.RST @@ -0,0 +1,7 @@ +Functionalities to interact with support provider +================================================= + +It adds a new modle "support.contract" with some useful methods: +* First you should get active contract with "contract = self.get_active_contract()" +* then you can cal different methods as: + * \ No newline at end of file diff --git a/web_support_client/models/support.py b/web_support_client/models/support.py index 7f0e34e..5a56fbf 100755 --- a/web_support_client/models/support.py +++ b/web_support_client/models/support.py @@ -127,4 +127,16 @@ def get_active_contract(self, do_not_raise=False): raise Warning(_('No active contract configured')) return active_contract + @api.multi + def check_modules_installed(self, modules=[]): + """ + where modules should be a list of modules names + for eg. modules = ['database_tools'] + """ + self.ensure_one() + client = self.get_connection() + for module in modules: + if client.modules(name=module, installed=True) is None: + return False + return True # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/web_support_client/security/groups.xml b/web_support_client/security/groups.xml new file mode 100644 index 0000000..a8c3351 --- /dev/null +++ b/web_support_client/security/groups.xml @@ -0,0 +1,12 @@ + + + + + + Support Provider Manger + + Special group for superuser or any support provider user. Should not be used for client users + + + +