[ADD] module_auto_update: Create module - #882
Conversation
|
Hey @hughesbm, thank you for your Pull Request. It looks like some users haven't signed our Contributor License Agreement, yet.
Appreciation of efforts, |
3e111b3 to
0c83b8e
Compare
| "version": "10.0.1.0.0", | ||
| "category": "Uncategorized", | ||
| "website": "https://odoo-community.org/", | ||
| 'author': 'Brent Hughes, ' |
There was a problem hiding this comment.
LasLabs if on the clock. You go in Contributors section of ReadMe using LasLabs email.
| 'author': 'Brent Hughes, ' | ||
| 'Juan José Scarafía, ' | ||
| 'Odoo Community Association (OCA)', | ||
| 'license': 'AGPL-3', |
There was a problem hiding this comment.
relicense to LGPL-3, this was OK'd in the RFC #790 (comment)
| @@ -0,0 +1,31 @@ | |||
| # -*- coding: utf-8 -*- | |||
| # Copyright 2017 LasLabs Inc. | |||
| # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). | |||
| @@ -0,0 +1,83 @@ | |||
| .. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg | |||
| :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html | |||
| :alt: License: AGPL-3 | |||
| @@ -0,0 +1,83 @@ | |||
| .. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg | |||
| @api.depends('checksum_dir') | ||
| def post_upgrade_actions(self): | ||
| for r in self: | ||
| r.checksum_installed = r.checksum_dir |
There was a problem hiding this comment.
r.write({
'checksum_installed': r.checksum_dir,
'upgrade_available': False,
})
There was a problem hiding this comment.
Can't this be into button_immediate_upgrade directly?
There was a problem hiding this comment.
@yajo It's also called from upgrade_module in base.module.upgrade; button_immediate_upgrade isn't used for applying scheduled upgrades.
| @@ -0,0 +1,25 @@ | |||
| <?xml version="1.0" encoding="utf-8"?> | |||
| <odoo> | |||
| <data> | |||
| <field name="key2">client_action_multi</field> | ||
| </record> | ||
| </data> | ||
| </odoo> No newline at end of file |
| @@ -0,0 +1,79 @@ | |||
| <?xml version="1.0" encoding="utf-8"?> | |||
| <odoo> | |||
| <data> | |||
| mods = self.env['ir.module.module'].search( | ||
| [('state', '=', 'to upgrade')] | ||
| ) | ||
|
|
|
|
||
| class Module(models.Model): | ||
| _inherit = 'ir.module.module' | ||
| _rec_name = 'name' |
| @@ -0,0 +1,2 @@ | |||
| id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink | |||
| access_ir_module_module_user,ir_module_module user,model_ir_module_module,,1,0,0,0 | |||
|
The requested changes have been addressed. I still need to add tests, however. |
44bdb3d to
3ac2baf
Compare
|
|
||
| from . import models | ||
| from . import wizard | ||
| from . import tests |
There was a problem hiding this comment.
This line is not needed as tests is automatically inspected by Odoo.
|
Ping @jjscarafia |
yajo
left a comment
There was a problem hiding this comment.
I feel this needs a little more simplification.
| "name": "Module Auto Update", | ||
| "summary": "Automatically update Odoo modules", | ||
| "version": "10.0.1.0.0", | ||
| "category": "Uncategorized", |
| string="Current Module Checksum", | ||
| compute='_compute_checksum_dir', | ||
| ) | ||
| upgrade_available = fields.Boolean( |
There was a problem hiding this comment.
Could we just set state=to upgrade?
| @api.depends('checksum_dir') | ||
| def determine_upgradeability(self): | ||
| upgradeable_modules = self.search( | ||
| [('state', 'in', ['installed', 'to upgrade', 'to remove', ])] |
There was a problem hiding this comment.
If we use state to track upgradeable addons, then you should remove to upgrade from here.
| self.update_list() | ||
| self.determine_upgradeability() | ||
|
|
||
| @api.depends('checksum_dir') |
There was a problem hiding this comment.
I'm not sure... does api.depends work if the method is not marked as a compute method on some field?
There was a problem hiding this comment.
It does not; you can only edit computed fields with depends methods. They technically run, but the changes aren't stored to DB because you are in the cache layer.
There was a problem hiding this comment.
Agreed - I was mainly just explaining for my newb 😉
| 'upgrade_available': True, | ||
| }) | ||
|
|
||
| @api.depends('checksum_dir') |
| 'view_mode': 'tree,form', | ||
| 'target': 'main', | ||
| 'context': '{"search_default_upgrade_available": 1}', | ||
| } |
There was a problem hiding this comment.
If we use the state as said above, could we move all the checksum computing into update_list(), so we don't have to create additional actions, menu items and methods? This way, if the admin uses the standard system to upgrade the addons list, he will always update their checksums too. It seems like what he'd expect to happen.
| </record> | ||
|
|
||
| <record model="ir.cron" id="base_module_upgrade_cron"> | ||
| <field name="name">Perform Scheduled Module Upgrades</field> |
There was a problem hiding this comment.
I see no sense on having 2 crons. Checking the upgrades without upgrading makes no sense, as upgrading without checking what's to upgrade neither. Besides, you could have a race condition where there are more than 1 cron workers. Please have a Python method that does all and run it in a single cron.
There was a problem hiding this comment.
I'm good with automating the whole thing. Let's hide the Apply Scheduled Upgrades button in the UI then as well.
There was a problem hiding this comment.
wouldn't it be better to extend that method and make the cron call it instead?
There was a problem hiding this comment.
Ohhhh yes, I like that better. That way we can trigger a manual update without the cron
| @api.multi | ||
| def upgrade_module(self): | ||
| mods = self.env['ir.module.module'].search( | ||
| [('state', '=', 'to upgrade')] |
| @api.multi | ||
| def _compute_checksum_dir(self): | ||
| for r in self: | ||
| r.checksum_dir = dirhash(get_module_path(r.name), 'sha1') |
There was a problem hiding this comment.
One question... If we would have addons directory full of .pyc or .pyo files, and under a containerized frozen environment where those files are autoregenerated on each build, would that produce a different checksum? I guess it should be the same because if the pycompiled file is based on the same input python file, it should produce the same output, but did you check this? Or is it possible to ignore such files?
There was a problem hiding this comment.
I think you're correct here Jairo. The pyc files contain a moddate header, which is the date of compilation as I understand it. Due to this, the hash can and will change when the file is recomputed, regardless of whether there were actually any changes.
Luckily checksumdir has an excluded_extension key, which interestingly enough has pyc in it as the example:
sha256hash = dirhash(directory, 'sha256', excluded_extensions=['pyc'])
IMO we should use an array declared at class level to determine the extensions that should be excluded. We could also use a method, but I think that's a bit overengineered
There was a problem hiding this comment.
The simplest balance between simplicity and configurability is an unexisting ir.config_parameter with sane defaults:
exclude = self.env["ir.config_parameter"].get_param(
"module_auto_update.checksum_excluded_extensions",
"pyc,pyo").split(",")This code with a little note in the configuration section of README should do the trick.
There was a problem hiding this comment.
@hughesbm - here's an example of ir.config_param
There was a problem hiding this comment.
Ummm but don't copy whatever the hell I did with the tab/space mixture there. Spaces4Lyfe!
There was a problem hiding this comment.
Yeah, although you don't really need to provide a ir.config_param record if you add a fallback value as in the code I put above.
| default=False, | ||
| ) | ||
|
|
||
| @api.multi |
|
In addition to the requested changes, I made a fairly large change to how To deal with this, this module now uses the |
yajo
left a comment
There was a problem hiding this comment.
The way you are using the computation of checksum_installed seems to go against the purpose of such kind of fields.
If you want to check when an addon's state changes to installed, but skip doing things under some situations, you should override write and create methods, and put something like this inside them:
def write(self, vals):
if vals.get("state") == "installed" and not self.env.context.get("retain_checksum_installed"):
for one in self:
one.checksum_installed = one.checksum_dir
return super(Class, self).write(vals)Also, on the methods where you need to retain the installed checksum:
def medthod_X(self):
return super(Class, self.with_context(retain_checksum_installed=True)).method_X()This would follow the normal Odoo framework logic and hooks to make all work as expected and be written as expected 😉
| upgrade_available = fields.Boolean( | ||
| default=False, | ||
| checksum_installed = fields.Char( | ||
| compute='_compute_checksum_installed', |
There was a problem hiding this comment.
This field must not be computed. It only has to be updated when addon gets updated too, but it is a normal char field.
|
|
||
| @api.multi | ||
| def button_uninstall_cancel(self): | ||
| return self.retain_checksum_installed( |
There was a problem hiding this comment.
You don't need these kind of weird hacks if checksum_installed is not a computed field.
There was a problem hiding this comment.
Yeah, this is a lot better. 😆 Thanks!
| @api.model | ||
| def create(self, vals): | ||
| self._store_checksum_installed(vals) | ||
| return super(Module, self).create(vals) |
There was a problem hiding this comment.
First, create the record, and then call to result._store_checksum_installed(vals), or it will have no effect.
| @api.multi | ||
| def write(self, vals): | ||
| self._store_checksum_installed(vals) | ||
| return super(Module, self).write(vals) |
There was a problem hiding this comment.
First write the record and then call the other method, or it could have undesired results.
| for r in self: | ||
| if r.state == 'installed': | ||
| def _store_checksum_installed(self, vals): | ||
| if (vals.get('state') == 'installed' and |
There was a problem hiding this comment.
I'd also check "checksum_installed" not in vals, to avoid double writes if the field is being manually written.
| mods = self.env['ir.module.module'].search( | ||
| [('state', '=', 'to upgrade')] | ||
| ) | ||
|
|
| @api.multi | ||
| def button_immediate_upgrade(self): | ||
| res = super(Module, self).button_immediate_upgrade() | ||
|
|
|
BTW, you have conflicts. |
b9e621e to
9b3d6ee
Compare
| r.checksum_installed = r.checksum_dir | ||
| elif vals.get('state') == 'uninstalled': | ||
| for r in self: | ||
| r.checksum_installed = '' |
There was a problem hiding this comment.
Set it to False, which is the standard in Odoo when we have an empty field.
There was a problem hiding this comment.
self.write({'checksum_installed': False}) and kill the loop
| checksum_dir = fields.Char( | ||
| compute='_compute_checksum_dir', | ||
| ) | ||
| checksum_installed = fields.Char() |
There was a problem hiding this comment.
I feel this one should be read only, mostly now that you are exposing it in the UI.
You should also add string and help attributes to both fields for the same reason, to let the user know what's this.
There was a problem hiding this comment.
Makes sense. Does exposing these fields in the UI have value, or should I take it out again? I keep wavering on this.
There was a problem hiding this comment.
The value would be for the human. If a human cares, expose it - otherwise don't. In this case, I'd say don't.
| #. Navigate to Settings / Technical / Automation / Scheduled Actions | ||
| #. Click "Check for Module Upgrades" or "Perform Scheduled Module Upgrades." | ||
| #. Click "Edit", enter desired changes, then click "Save." | ||
| This module will ignore .pyc and .pyo file extensions by default. To modify this, create a module_auto_update.checksum_excluded_extensions system parameter with the desired extensions. |
There was a problem hiding this comment.
with the desired extensions listed as comma separated values.
| checksum_dir = fields.Char( | ||
| compute='_compute_checksum_dir', | ||
| ) | ||
| checksum_installed = fields.Char() |
There was a problem hiding this comment.
The value would be for the human. If a human cares, expose it - otherwise don't. In this case, I'd say don't.
|
|
||
|
|
||
| def post_init_hook(cr, registry): | ||
| env = api.Environment(cr, SUPERUSER_ID, {}) |
There was a problem hiding this comment.
This isn't tested. Also I don't think it's implemented correctly, because coverage should have a hit regardless of test status for hooks.
| def _compute_checksum_dir(self): | ||
| exclude = self.env["ir.config_parameter"].get_param( | ||
| "module_auto_update.checksum_excluded_extensions", | ||
| "pyc,pyo" |
| r.checksum_dir = dirhash( | ||
| get_module_path(r.name), | ||
| 'sha1', | ||
| excluded_extensions=exclude |
| def button_uninstall_cancel(self): | ||
| return super( | ||
| Module, | ||
| self.with_context(retain_checksum_installed=True) |
| def button_upgrade_cancel(self): | ||
| return super( | ||
| Module, | ||
| self.with_context(retain_checksum_installed=True) |
| upgradeable_modules = installed_modules.filtered( | ||
| lambda r: r.checksum_dir != r.checksum_installed | ||
| ) | ||
| upgradeable_modules.write({'state': "to upgrade", }) |
There was a problem hiding this comment.
no trailing comma. The point of the trailing comma is to allow for an option to be added without editing multiple lines in the diff. In the instance of a single line, this is a null point
| # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). | ||
|
|
||
| from . import test_module | ||
| from . import test_module_upgrade No newline at end of file |
| def upgrade_module_cancel(self): | ||
| return super( | ||
| ModuleUpgrade, | ||
| self.with_context(retain_checksum_installed=True) |
a8d59bd to
e4009a5
Compare
|
Runbot is currently failing when creating the odoo instance, not due to changes here as far as I can tell. |
|
Runbot fail due to OCA/maintainer-quality-tools#464 |
Agreed - this is pretty much a rewrite at this point. Only a bit of boilerplate was carried over from what I can tell. |
e4009a5 to
e81ea1f
Compare
|
Everything is now in a single commit. Thanks for the fantastic code reviews! 😄 |
| 'views/module_views.xml', | ||
| 'data/cron_data.xml', | ||
| ], | ||
| 'auto_install': True, |
There was a problem hiding this comment.
I was just about to merge, but saw this. Please do not set it to autoinstall. Also, add dependency on base module.
There was a problem hiding this comment.
Thanks! I have made the requested changes and squashed.
There was a problem hiding this comment.
Apparently those changes weren't as consequence-free as I expected and a test is now failing. Working on it now.
* Add checksum_dir and checksum_installed fields to ir.module.module
* Add checksum_dir to compute current checksum of module directory
in addons path
* Add checksum_installed to store checksum of module directory
when module was last installed or upgraded
* Use checksumdir Python library to compute module directory sha1
hashes, ignoring pyc and pyo extensions
* Extend update_list method to compare modules' checksum_dir and
checksum_installed, then change state of modules with differing
checksums to 'to upgrade'
* Replace Apps/Updates menu item with menu item of same name, which
updates apps list and displays tree view of ir.module.module
records with state 'to upgrade'
* Extend create and write methods to store computed checksum_dir as
checksum_installed during module installation and upgrade, and
set checksum_installed to False on uninstall
* Use context to stop checksum_installed from being updated during
upgrade/uninstall cancellation
* Add cron job to periodically check for module upgrades by
comparing checksums, then perform any available upgrades
* Extend upgrade_module method (called by cron and 'Apply Scheduled
Upgrades' menu item) to call update_list
* Add post_init_hook to store checksum_installed of existing
modules
* Add test coverage
e81ea1f to
fc1f809
Compare
* Use dummy module to test update_list method instead of module_auto_update
|
@yajo The changes you requested are done, and I fixed the test that broke as a result. I'll clean up the commits after approval. Sorry about the mess, I got too hasty trying to wrap this up quickly. |
yajo
left a comment
There was a problem hiding this comment.
Great! That's what I love about good tests like yours, they fail whey they must! 😉
|
Congrats on your first module Brent! Not very often your first one gets to be this fun or popular! |
|
Hooray! I'll test this next week too. Kudos! |
Automate module updates by comparing module directory checksums.
Completes RFC #790