odoo-addon-odoo-repository 16.0.1.3.0.13__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. odoo/addons/odoo_repository/README.rst +81 -0
  2. odoo/addons/odoo_repository/__init__.py +2 -0
  3. odoo/addons/odoo_repository/__manifest__.py +58 -0
  4. odoo/addons/odoo_repository/controllers/__init__.py +1 -0
  5. odoo/addons/odoo_repository/controllers/main.py +32 -0
  6. odoo/addons/odoo_repository/data/ir_cron.xml +38 -0
  7. odoo/addons/odoo_repository/data/odoo.repository.csv +216 -0
  8. odoo/addons/odoo_repository/data/odoo_branch.xml +82 -0
  9. odoo/addons/odoo_repository/data/odoo_module.xml +16 -0
  10. odoo/addons/odoo_repository/data/odoo_repository.xml +71 -0
  11. odoo/addons/odoo_repository/data/odoo_repository_addons_path.xml +59 -0
  12. odoo/addons/odoo_repository/data/odoo_repository_org.xml +14 -0
  13. odoo/addons/odoo_repository/data/queue_job.xml +56 -0
  14. odoo/addons/odoo_repository/lib/__init__.py +0 -0
  15. odoo/addons/odoo_repository/lib/scanner.py +1302 -0
  16. odoo/addons/odoo_repository/migrations/16.0.1.1.0/post-migration.py +26 -0
  17. odoo/addons/odoo_repository/migrations/16.0.1.2.0/pre-migration.py +43 -0
  18. odoo/addons/odoo_repository/migrations/16.0.1.3.0/post-migration.py +19 -0
  19. odoo/addons/odoo_repository/models/__init__.py +18 -0
  20. odoo/addons/odoo_repository/models/authentication_token.py +12 -0
  21. odoo/addons/odoo_repository/models/odoo_author.py +16 -0
  22. odoo/addons/odoo_repository/models/odoo_branch.py +111 -0
  23. odoo/addons/odoo_repository/models/odoo_license.py +16 -0
  24. odoo/addons/odoo_repository/models/odoo_maintainer.py +31 -0
  25. odoo/addons/odoo_repository/models/odoo_module.py +24 -0
  26. odoo/addons/odoo_repository/models/odoo_module_branch.py +873 -0
  27. odoo/addons/odoo_repository/models/odoo_module_branch_version.py +123 -0
  28. odoo/addons/odoo_repository/models/odoo_module_category.py +15 -0
  29. odoo/addons/odoo_repository/models/odoo_module_dev_status.py +15 -0
  30. odoo/addons/odoo_repository/models/odoo_python_dependency.py +16 -0
  31. odoo/addons/odoo_repository/models/odoo_repository.py +664 -0
  32. odoo/addons/odoo_repository/models/odoo_repository_addons_path.py +40 -0
  33. odoo/addons/odoo_repository/models/odoo_repository_branch.py +98 -0
  34. odoo/addons/odoo_repository/models/odoo_repository_org.py +23 -0
  35. odoo/addons/odoo_repository/models/res_company.py +23 -0
  36. odoo/addons/odoo_repository/models/res_config_settings.py +23 -0
  37. odoo/addons/odoo_repository/models/ssh_key.py +12 -0
  38. odoo/addons/odoo_repository/readme/CONTRIBUTORS.rst +2 -0
  39. odoo/addons/odoo_repository/readme/DESCRIPTION.rst +1 -0
  40. odoo/addons/odoo_repository/security/ir.model.access.csv +27 -0
  41. odoo/addons/odoo_repository/security/res_groups.xml +25 -0
  42. odoo/addons/odoo_repository/static/description/README +4 -0
  43. odoo/addons/odoo_repository/static/description/icon.png +0 -0
  44. odoo/addons/odoo_repository/static/description/index.html +430 -0
  45. odoo/addons/odoo_repository/tests/__init__.py +6 -0
  46. odoo/addons/odoo_repository/tests/common.py +162 -0
  47. odoo/addons/odoo_repository/tests/test_base_scanner.py +214 -0
  48. odoo/addons/odoo_repository/tests/test_odoo_module_branch.py +97 -0
  49. odoo/addons/odoo_repository/tests/test_odoo_repository_scan.py +242 -0
  50. odoo/addons/odoo_repository/tests/test_repository_scanner.py +215 -0
  51. odoo/addons/odoo_repository/tests/test_sync_node.py +55 -0
  52. odoo/addons/odoo_repository/tests/test_utils.py +25 -0
  53. odoo/addons/odoo_repository/utils/__init__.py +0 -0
  54. odoo/addons/odoo_repository/utils/github.py +30 -0
  55. odoo/addons/odoo_repository/utils/module.py +25 -0
  56. odoo/addons/odoo_repository/utils/scanner.py +90 -0
  57. odoo/addons/odoo_repository/views/authentication_token.xml +63 -0
  58. odoo/addons/odoo_repository/views/menu.xml +38 -0
  59. odoo/addons/odoo_repository/views/odoo_author.xml +54 -0
  60. odoo/addons/odoo_repository/views/odoo_branch.xml +84 -0
  61. odoo/addons/odoo_repository/views/odoo_license.xml +40 -0
  62. odoo/addons/odoo_repository/views/odoo_maintainer.xml +69 -0
  63. odoo/addons/odoo_repository/views/odoo_module.xml +90 -0
  64. odoo/addons/odoo_repository/views/odoo_module_branch.xml +353 -0
  65. odoo/addons/odoo_repository/views/odoo_module_category.xml +40 -0
  66. odoo/addons/odoo_repository/views/odoo_module_dev_status.xml +40 -0
  67. odoo/addons/odoo_repository/views/odoo_python_dependency.xml +40 -0
  68. odoo/addons/odoo_repository/views/odoo_repository.xml +165 -0
  69. odoo/addons/odoo_repository/views/odoo_repository_addons_path.xml +49 -0
  70. odoo/addons/odoo_repository/views/odoo_repository_branch.xml +60 -0
  71. odoo/addons/odoo_repository/views/odoo_repository_org.xml +54 -0
  72. odoo/addons/odoo_repository/views/res_config_settings.xml +123 -0
  73. odoo/addons/odoo_repository/views/ssh_key.xml +63 -0
  74. odoo_addon_odoo_repository-16.0.1.3.0.13.dist-info/METADATA +100 -0
  75. odoo_addon_odoo_repository-16.0.1.3.0.13.dist-info/RECORD +77 -0
  76. odoo_addon_odoo_repository-16.0.1.3.0.13.dist-info/WHEEL +5 -0
  77. odoo_addon_odoo_repository-16.0.1.3.0.13.dist-info/top_level.txt +1 -0
@@ -0,0 +1,664 @@
1
+ # Copyright 2023 Camptocamp SA
2
+ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
3
+
4
+ import json
5
+ import logging
6
+ import os
7
+ import pathlib
8
+ from urllib.parse import urljoin
9
+
10
+ import requests
11
+
12
+ from odoo import _, api, fields, models, tools
13
+ from odoo.exceptions import UserError
14
+ from odoo.osv.expression import AND, OR
15
+
16
+ from odoo.addons.queue_job.delay import chain
17
+ from odoo.addons.queue_job.exception import RetryableJobError
18
+ from odoo.addons.queue_job.job import identity_exact
19
+
20
+ from ..utils.scanner import RepositoryScannerOdooEnv
21
+
22
+ _logger = logging.getLogger(__name__)
23
+
24
+
25
+ class OdooRepository(models.Model):
26
+ _name = "odoo.repository"
27
+ _description = "Odoo Modules Repository"
28
+ _order = "sequence, display_name"
29
+
30
+ _repositories_path_key = "odoo_repository_storage_path"
31
+
32
+ display_name = fields.Char(compute="_compute_display_name", store=True)
33
+ active = fields.Boolean(default=True)
34
+ sequence = fields.Integer(default=100)
35
+ org_id = fields.Many2one(
36
+ comodel_name="odoo.repository.org",
37
+ ondelete="cascade",
38
+ string="Organization",
39
+ required=True,
40
+ index=True,
41
+ )
42
+ name = fields.Char(required=True, index=True)
43
+ repo_url = fields.Char(
44
+ string="Web URL",
45
+ help="Web access to this repository.",
46
+ required=True,
47
+ )
48
+ to_scan = fields.Boolean(
49
+ default=True,
50
+ help="Scan this repository to collect data.",
51
+ )
52
+ clone_url = fields.Char(
53
+ string="Clone URL",
54
+ help="Used to clone the repository.",
55
+ )
56
+ clone_name = fields.Char(
57
+ help=(
58
+ "Force the name of the cloned repository (folder on disk). "
59
+ "If not set, the name of the repository will be used."
60
+ ),
61
+ )
62
+ repo_type = fields.Selection(
63
+ selection=[
64
+ ("github", "GitHub"),
65
+ ("gitlab", "GitLab"),
66
+ ],
67
+ )
68
+ ssh_key_id = fields.Many2one(
69
+ comodel_name="ssh.key",
70
+ ondelete="restrict",
71
+ string="SSH Key",
72
+ help="SSH key used to clone/fetch this repository.",
73
+ )
74
+ token_id = fields.Many2one(
75
+ comodel_name="authentication.token",
76
+ ondelete="restrict",
77
+ string="Token",
78
+ help="Token used to clone/fetch this repository.",
79
+ )
80
+ active = fields.Boolean(default=True)
81
+ addons_path_ids = fields.Many2many(
82
+ comodel_name="odoo.repository.addons_path",
83
+ string="Addons Path",
84
+ help="Relative path of folders in this repository hosting Odoo modules",
85
+ )
86
+ branch_ids = fields.One2many(
87
+ comodel_name="odoo.repository.branch",
88
+ inverse_name="repository_id",
89
+ string="Branches",
90
+ )
91
+ scan_weekday_ids = fields.Many2many(
92
+ comodel_name="time.weekday",
93
+ string="Scanning days",
94
+ help=(
95
+ "Limit scanning of this repository by the scheduled action to "
96
+ "certain days only. If not defined, the scan will happen every day."
97
+ ),
98
+ )
99
+ manual_branches = fields.Boolean(
100
+ string="Configure branches manually",
101
+ help=(
102
+ "By default repository branches follows the configured Odoo versions "
103
+ "(e.g: 17.0, 18.0...). Enable this option to configure your own branches."
104
+ ),
105
+ )
106
+ specific = fields.Boolean(
107
+ help=(
108
+ "Host specific modules (that are not generic). "
109
+ "Used for project repositories."
110
+ ),
111
+ )
112
+
113
+ @api.model
114
+ def default_get(self, fields_list):
115
+ """'default_get' method overloaded."""
116
+ res = super().default_get(fields_list)
117
+ if "addons_path_ids" not in res:
118
+ res["addons_path_ids"] = [
119
+ (
120
+ 4,
121
+ self.env.ref(
122
+ "odoo_repository.odoo_repository_addons_path_community"
123
+ ).id,
124
+ )
125
+ ]
126
+ return res
127
+
128
+ @api.depends("org_id", "name")
129
+ def _compute_github_url(self):
130
+ for rec in self:
131
+ rec.github_url = f"{rec.org_id.github_url}/{rec.name}"
132
+
133
+ _sql_constraints = [
134
+ (
135
+ "org_id_name_uniq",
136
+ "UNIQUE (org_id, name)",
137
+ "This repository already exists.",
138
+ ),
139
+ ]
140
+
141
+ @api.depends("org_id.name", "name")
142
+ def _compute_display_name(self):
143
+ for rec in self:
144
+ rec.display_name = f"{rec.org_id.name}/{rec.name}"
145
+
146
+ @api.onchange("repo_url", "to_scan", "clone_url")
147
+ def _onchange_repo_url(self):
148
+ if not self.repo_url:
149
+ return
150
+ for type_, __ in self._fields["repo_type"].selection:
151
+ if type_ not in self.repo_url:
152
+ continue
153
+ self.repo_type = type_
154
+ if not self.clone_url and self.to_scan:
155
+ self.clone_url = self.repo_url
156
+ break
157
+
158
+ def _get_odoo_branches_to_scan(self):
159
+ self.ensure_one()
160
+ if self.manual_branches:
161
+ return self.branch_ids.branch_id
162
+ return self.env["odoo.branch"]._get_all_odoo_versions(active_test=True)
163
+
164
+ def _cron_scanner_domain(self):
165
+ today = fields.Date.today()
166
+ weekday = today.weekday()
167
+ return AND(
168
+ [
169
+ [("to_scan", "=", True)],
170
+ OR(
171
+ [
172
+ [("scan_weekday_ids.name", "=", weekday)],
173
+ [("scan_weekday_ids", "=", False)],
174
+ ]
175
+ ),
176
+ ]
177
+ )
178
+
179
+ @api.model
180
+ def cron_scanner(self, branches=None, force=False):
181
+ """Scan and collect Odoo repositories data.
182
+
183
+ As the scanner is run on the same server than Odoo, a special class
184
+ `RepositoryScannerOdooEnv` is used so the scanner can request Odoo
185
+ through an environment (api.Environment).
186
+
187
+ `branches` parameter allows to filter the `odoo.branch` to take into
188
+ account for the scan, e.g. `branches=["16.0", "18.0"]`.
189
+ """
190
+ repositories = self.search(self._cron_scanner_domain())
191
+ branches_ = self.env["odoo.branch"]._get_all_odoo_versions(active_test=True)
192
+ if branches:
193
+ branches_ = branches_.filtered(lambda br: br.name in branches)
194
+ for repo in repositories:
195
+ repo.action_scan(branch_ids=branches_.ids, force=force, raise_exc=False)
196
+
197
+ def _check_config(self):
198
+ # Check the configuration of repositories folder
199
+ key = self._repositories_path_key
200
+ repositories_path = self.env["ir.config_parameter"].sudo().get_param(key, "")
201
+ if not repositories_path:
202
+ raise UserError(
203
+ _(
204
+ "Please define the '{key}' system parameter to "
205
+ "clone repositories in the folder of your choice.".format(key=key)
206
+ )
207
+ )
208
+ # Ensure the folder exists
209
+ pathlib.Path(repositories_path).mkdir(parents=True, exist_ok=True)
210
+
211
+ def _check_existing_jobs(self, raise_exc=True):
212
+ """Check if a scan is already triggered for this repository."""
213
+ self.ensure_one()
214
+ existing_job = (
215
+ self.env["queue.job"]
216
+ .sudo()
217
+ .search(
218
+ [
219
+ ("model_name", "=", self._name),
220
+ ("records", "ilike", f'%"ids": [{self.id}]%'),
221
+ (
222
+ "state",
223
+ "in",
224
+ [
225
+ "wait_dependencies",
226
+ "pending",
227
+ "enqueued",
228
+ "started",
229
+ ],
230
+ ),
231
+ ],
232
+ limit=1,
233
+ )
234
+ )
235
+ if existing_job:
236
+ msg = _("A scan is already ongoing for repository %s") % self.display_name
237
+ if raise_exc:
238
+ raise UserError(msg)
239
+ _logger.warning(msg)
240
+ return True
241
+ return False
242
+
243
+ def action_scan(self, branch_ids=None, force=False, raise_exc=True):
244
+ """Scan the whole repository."""
245
+ self._check_config()
246
+ for rec in self:
247
+ if not rec.to_scan:
248
+ continue
249
+ if rec._check_existing_jobs(raise_exc=raise_exc):
250
+ continue
251
+ # Get branch records to scan
252
+ branches = rec._get_odoo_branches_to_scan()
253
+ if branch_ids:
254
+ branches = branches & self.env["odoo.branch"].search(
255
+ [("id", "in", branch_ids)]
256
+ )
257
+ if not branches:
258
+ continue
259
+ # Create a list of tuples ({odoo_version}, {branch_name})
260
+ versions_branches = [(branch.name, branch.name) for branch in branches]
261
+ if rec.manual_branches:
262
+ versions_branches = [
263
+ (rb.branch_id.name, rb.cloned_branch or rb.branch_id.name)
264
+ for rb in rec.branch_ids
265
+ if rb.branch_id in branches
266
+ ]
267
+ if force:
268
+ rec._reset_scanned_commits(branch_ids=branch_ids)
269
+ # Scan repository branches sequentially as they need to be checked out
270
+ # to perform the analysis
271
+ # Here the launched job is responsible to:
272
+ # 1) detect modules updated on the first branch
273
+ # 2) spawn jobs to scan each module on that branch
274
+ # 3) spawn a job to update the last scanned commit of the repo/branch
275
+ # 4) spawn the next job responsible to detect modules updated
276
+ # on the next branch
277
+ version_branch = versions_branches[0]
278
+ next_versions_branches = versions_branches[1:]
279
+ job = rec._create_job_detect_modules_to_scan_on_branch(
280
+ version_branch, next_versions_branches, versions_branches
281
+ )
282
+ job.delay()
283
+ return True
284
+
285
+ def _create_job_detect_modules_to_scan_on_branch(
286
+ self, version_branch, next_versions_branches, all_versions_branches
287
+ ):
288
+ self.ensure_one()
289
+ version, branch = version_branch
290
+ branch_str = branch
291
+ if version != branch:
292
+ branch_str = f"{branch} ({version})"
293
+ delayable = self.delayable(
294
+ description=f"Detect modules to scan in {self.display_name}#{branch_str}",
295
+ identity_key=identity_exact,
296
+ )
297
+ return delayable._detect_modules_to_scan_on_branch(
298
+ version_branch, next_versions_branches, all_versions_branches
299
+ )
300
+
301
+ def _detect_modules_to_scan_on_branch(
302
+ self, version_branch, next_versions_branches, all_versions_branches
303
+ ):
304
+ """Detect the modules to scan on `branch`.
305
+
306
+ It will spawn a job for each module to scan, and two other jobs to:
307
+ - update the last scanned commit on the repo/branch
308
+ - scan the next branch (so each branch is scanned in cascade)
309
+
310
+ This ensure to scan different branches sequentially for a given repository.
311
+ """
312
+ version, branch = version_branch
313
+ try:
314
+ # Get the list of modules updated since last scan
315
+ params = self._prepare_scanner_parameters(version, branch)
316
+ scanner = RepositoryScannerOdooEnv(**params)
317
+ data = scanner.detect_modules_to_scan()
318
+ # Prepare all subsequent jobs based on modules to scan
319
+ jobs = self._create_subsequent_jobs(
320
+ version_branch, next_versions_branches, all_versions_branches, data
321
+ )
322
+ # Chain them altogether
323
+ if jobs:
324
+ chain(*jobs).delay()
325
+ except Exception as exc:
326
+ raise RetryableJobError("Scanner error") from exc
327
+
328
+ def _create_subsequent_jobs(
329
+ self, version_branch, next_versions_branches, all_versions_branches, data
330
+ ):
331
+ jobs = []
332
+ version, branch = version_branch
333
+ # Spawn one job per module to scan
334
+ for data_ in data.get("addons_paths", {}).values():
335
+ for module_path in data_["modules_to_scan"]:
336
+ job = self._create_job_scan_module_on_branch(
337
+ version, branch, module_path, data_["specs"]
338
+ )
339
+ jobs.append(job)
340
+ # + another one to update the last scanned commit of the repository
341
+ if data.get("repo_branch_id"):
342
+ job = self._create_job_update_last_scanned_commit(
343
+ data["repo_branch_id"],
344
+ data["last_fetched_commit"],
345
+ )
346
+ jobs.append(job)
347
+ # + another one to detect modules to scan on the next branch
348
+ version_branch = next_versions_branches and next_versions_branches[0]
349
+ next_versions_branches = next_versions_branches[1:]
350
+ if version_branch:
351
+ jobs.append(
352
+ self._create_job_detect_modules_to_scan_on_branch(
353
+ version_branch, next_versions_branches, all_versions_branches
354
+ )
355
+ )
356
+ return jobs
357
+
358
+ def _create_job_scan_module_on_branch(self, version, branch, module_path, specs):
359
+ self.ensure_one()
360
+ branch_str = branch
361
+ if version != branch:
362
+ branch_str = f"{branch} ({version})"
363
+ delayable = self.delayable(
364
+ description=f"Scan {self.display_name}#{branch_str} - {module_path}",
365
+ identity_key=identity_exact,
366
+ )
367
+ return delayable._scan_module_on_branch(version, branch, module_path, specs)
368
+
369
+ def _scan_module_on_branch(self, version, branch, module_path, specs):
370
+ """Scan `module_path` from `branch`."""
371
+ try:
372
+ params = self._prepare_scanner_parameters(version, branch)
373
+ scanner = RepositoryScannerOdooEnv(**params)
374
+ return scanner.scan_module(module_path, specs)
375
+ except Exception as exc:
376
+ raise RetryableJobError("Scanner error") from exc
377
+
378
+ def _create_job_update_last_scanned_commit(
379
+ self, repo_branch_id, last_scanned_commit, last_scan=False
380
+ ):
381
+ self.ensure_one()
382
+ repo_branch_model = self.env["odoo.repository.branch"]
383
+ repo_branch = repo_branch_model.browse(repo_branch_id).exists()
384
+ delayable = repo_branch.delayable(
385
+ description=f"Update last scanned commit of {repo_branch.display_name}",
386
+ identity_key=identity_exact,
387
+ )
388
+ return delayable._update_last_scanned_commit(last_scanned_commit)
389
+
390
+ def _reset_scanned_commits(self, branch_ids=None):
391
+ """Reset the scanned commits.
392
+
393
+ This will make the next repository scan restarting from the beginning,
394
+ and thus making it slower.
395
+ """
396
+ self.ensure_one()
397
+ if branch_ids is None:
398
+ branch_ids = self.branch_ids.branch_id.ids
399
+ repo_branches = self.branch_ids.filtered(
400
+ lambda rb: rb.branch_id.id in branch_ids
401
+ )
402
+ repo_branches.write({"last_scanned_commit": False})
403
+ repo_branches.module_ids.sudo().write({"last_scanned_commit": False})
404
+
405
+ def _get_token(self):
406
+ """Return the first available token found for this repository.
407
+
408
+ It will check the available tokens in this order:
409
+ - specific token linked to this repository
410
+ - default token defined in the global settings
411
+ - token defined through an environment variable
412
+ """
413
+ self.ensure_one()
414
+ return (
415
+ self.token_id.token
416
+ or self.env.company.config_odoo_repository_default_token_id.token
417
+ or os.environ.get("GITHUB_TOKEN")
418
+ )
419
+
420
+ def _prepare_scanner_parameters(self, version, branch):
421
+ ir_config = self.env["ir.config_parameter"]
422
+ repositories_path = ir_config.sudo().get_param(self._repositories_path_key)
423
+ return {
424
+ "org": self.org_id.name,
425
+ "name": self.name,
426
+ "clone_url": self.clone_url,
427
+ "version": version,
428
+ "branch": branch,
429
+ "addons_paths_data": self.addons_path_ids.read(
430
+ [
431
+ "relative_path",
432
+ "is_standard",
433
+ "is_enterprise",
434
+ "is_community",
435
+ ]
436
+ ),
437
+ "repositories_path": repositories_path,
438
+ "repo_type": self.repo_type,
439
+ "ssh_key": self.ssh_key_id.private_key,
440
+ "token": self._get_token(),
441
+ "workaround_fs_errors": (
442
+ self.env.company.config_odoo_repository_workaround_fs_errors
443
+ ),
444
+ "clone_name": self.clone_name,
445
+ "env": self.env,
446
+ }
447
+
448
+ def action_force_scan(self, branch_ids=None, raise_exc=True):
449
+ """Force the scan of the repositories.
450
+
451
+ It will restart the scan without considering the last scanned commit,
452
+ overriding already collected module data if any.
453
+ """
454
+ self.ensure_one()
455
+ return self.action_scan(branch_ids=branch_ids, force=True, raise_exc=raise_exc)
456
+
457
+ @api.model
458
+ def cron_fetch_data(self, branches=None, force=False):
459
+ """Fetch Odoo repositories data from the main node (if any)."""
460
+ main_node_url = (
461
+ self.env["ir.config_parameter"]
462
+ .sudo()
463
+ .get_param("odoo_repository_main_node_url")
464
+ )
465
+ if not main_node_url:
466
+ return False
467
+ branch_domain = []
468
+ if branches:
469
+ branch_domain.append(("name", "in", branches))
470
+ branches = self.env["odoo.branch"].search(branch_domain)
471
+ branch_names = ",".join(branches.mapped("name"))
472
+ url = f"{main_node_url}?branches=%s" % branch_names
473
+ try:
474
+ response = requests.get(url, timeout=60)
475
+ except Exception as exc:
476
+ raise UserError(_("Unable to fetch data from %s") % main_node_url) from exc
477
+ else:
478
+ if response.status_code == 200:
479
+ try:
480
+ data = json.loads(response.text)
481
+ except json.decoder.JSONDecodeError as exc:
482
+ raise UserError(
483
+ _("Unable to decode data received from %s") % main_node_url
484
+ ) from exc
485
+ else:
486
+ self._import_data(data)
487
+
488
+ def _import_data(self, data):
489
+ for module_data in data:
490
+ # TODO Move these methods to 'odoo.module.branch'?
491
+ values = self._prepare_module_branch_values(module_data)
492
+ self._create_or_update_module_branch(values, module_data)
493
+
494
+ def _prepare_module_branch_values(self, data):
495
+ # Get branch, repository and technical module
496
+ branch = self.env["odoo.branch"].search([("name", "=", data["branch"])])
497
+ org = self._get_repository_org(data["repository"]["org"])
498
+ repository = self._get_repository(
499
+ org.id, data["repository"]["name"], data["repository"]
500
+ )
501
+ repository_branch = self._get_repository_branch(
502
+ org.id, repository.id, branch.id, data["repository"]
503
+ )
504
+
505
+ mb_model = self.env["odoo.module.branch"]
506
+ module = mb_model._get_module(data["module"])
507
+ # Prepare values
508
+ category_id = mb_model._get_module_category_id(data["category"])
509
+ author_ids = mb_model._get_author_ids(tuple(data["authors"]))
510
+ maintainer_ids = mb_model._get_maintainer_ids(tuple(data["maintainers"]))
511
+ dev_status_id = mb_model._get_dev_status_id(data["development_status"])
512
+ dependency_ids = mb_model._get_dependency_ids(
513
+ repository_branch, data["depends"]
514
+ )
515
+ external_dependencies = data["external_dependencies"]
516
+ python_dependency_ids = mb_model._get_python_dependency_ids(
517
+ tuple(external_dependencies.get("python", []))
518
+ )
519
+ license_id = mb_model._get_license_id(data["license"])
520
+ versions_values = self._prepare_version_ids_values(
521
+ repository_branch, module, data["versions"]
522
+ )
523
+ values = {
524
+ "repository_branch_id": repository_branch.id,
525
+ "branch_id": repository_branch.branch_id.id,
526
+ "module_id": module.id,
527
+ "title": data["title"],
528
+ "summary": data["summary"],
529
+ "category_id": category_id,
530
+ "author_ids": [(6, 0, author_ids)],
531
+ "maintainer_ids": [(6, 0, maintainer_ids)],
532
+ "dependency_ids": [(6, 0, dependency_ids)],
533
+ "external_dependencies": external_dependencies,
534
+ "python_dependency_ids": [(6, 0, python_dependency_ids)],
535
+ "license_id": license_id,
536
+ "version": data["version"],
537
+ "version_ids": versions_values,
538
+ "development_status_id": dev_status_id,
539
+ "installable": data["installable"],
540
+ "auto_install": data["auto_install"],
541
+ "application": data["application"],
542
+ "is_standard": data["is_standard"],
543
+ "is_enterprise": data["is_enterprise"],
544
+ "is_community": data["is_community"],
545
+ "sloc_python": data["sloc_python"],
546
+ "sloc_xml": data["sloc_xml"],
547
+ "sloc_js": data["sloc_js"],
548
+ "sloc_css": data["sloc_css"],
549
+ "last_scanned_commit": data["last_scanned_commit"],
550
+ "pr_url": data["pr_url"],
551
+ }
552
+ return values
553
+
554
+ def _prepare_version_ids_values(self, repo_branch, module, versions: list[dict]):
555
+ version_ids = []
556
+ for version in versions:
557
+ version_model = self.env["odoo.module.branch.version"]
558
+ rec = version_model.search(
559
+ [
560
+ ("module_branch_id.branch_id", "=", repo_branch.branch_id.id),
561
+ ("module_branch_id.module_id", "=", module.id),
562
+ ("name", "=", version["name"]),
563
+ ],
564
+ limit=1,
565
+ )
566
+ if rec:
567
+ version_ids.append(fields.Command.update(rec.id, version))
568
+ else:
569
+ version_ids.append(fields.Command.create(version))
570
+ return version_ids
571
+
572
+ def _create_or_update_module_branch(self, values, raw_data):
573
+ mb_model = self.env["odoo.module.branch"]
574
+ rec = mb_model.search(
575
+ [
576
+ ("module_id", "=", values["module_id"]),
577
+ # Module could have been already created to satisfy dependencies
578
+ # (without 'repository_branch_id' set)
579
+ "|",
580
+ ("repository_branch_id", "=", values["repository_branch_id"]),
581
+ ("branch_id", "=", values["branch_id"]),
582
+ ],
583
+ limit=1,
584
+ )
585
+ values = self._pre_create_or_update_module_branch(rec, values, raw_data)
586
+ if rec:
587
+ rec.sudo().write(values)
588
+ else:
589
+ rec = mb_model.sudo().create(values)
590
+ self._post_create_or_update_module_branch(rec, values, raw_data)
591
+ return rec
592
+
593
+ def _pre_create_or_update_module_branch(self, rec, values, raw_data):
594
+ """Hook executed before the creation or update of `rec`. Return values."""
595
+ return values
596
+
597
+ def _post_create_or_update_module_branch(self, rec, values, raw_data):
598
+ """Hook executed after the creation or update of `rec`."""
599
+
600
+ @tools.ormcache("name")
601
+ def _get_repository_org(self, name):
602
+ rec = self.env["odoo.repository.org"].search([("name", "=", name)], limit=1)
603
+ if not rec:
604
+ rec = self.env["odoo.repository.org"].sudo().create({"name": name})
605
+ return rec
606
+
607
+ @tools.ormcache("org_id", "name")
608
+ def _get_repository(self, org_id, name, data):
609
+ rec = self.env["odoo.repository"].search(
610
+ [
611
+ ("org_id", "=", org_id),
612
+ ("name", "=", name),
613
+ ],
614
+ limit=1,
615
+ )
616
+ values = {
617
+ "org_id": org_id,
618
+ "name": name,
619
+ "repo_url": data["repo_url"],
620
+ "repo_type": data["repo_type"],
621
+ "active": data["active"],
622
+ }
623
+ if rec:
624
+ rec.sudo().write(values)
625
+ else:
626
+ rec = self.env["odoo.repository"].sudo().create(values)
627
+ return rec
628
+
629
+ @tools.ormcache("org_id", "repository_id", "branch_id")
630
+ def _get_repository_branch(self, org_id, repository_id, branch_id, data):
631
+ rec = self.env["odoo.repository.branch"].search(
632
+ [
633
+ ("repository_id", "=", repository_id),
634
+ ("branch_id", "=", branch_id),
635
+ ],
636
+ limit=1,
637
+ )
638
+ values = {
639
+ "repository_id": repository_id,
640
+ "branch_id": branch_id,
641
+ "last_scanned_commit": data["last_scanned_commit"],
642
+ }
643
+ if rec:
644
+ rec.sudo().write(values)
645
+ else:
646
+ rec = self.env["odoo.repository.branch"].sudo().create(values)
647
+ return rec
648
+
649
+ def _get_resource_url(self, branch, path):
650
+ self.ensure_one()
651
+ # NOTE: GitHub and GitLab supports the same URL pattern
652
+ url = "/".join(["tree", branch, path])
653
+ return urljoin(self.repo_url + "/", url)
654
+
655
+ def unlink(self):
656
+ # There is no deletion on cascade policy by default, but for specific
657
+ # repositories we want to remove specific modules anyway.
658
+ # This will also avoid to raise UNIQUE constraint
659
+ # 'odoo_module_branch_uniq_null(module_id, branch_id)' if module names
660
+ # are shared between repositories.
661
+ for rec in self:
662
+ if rec.specific:
663
+ rec.branch_ids.module_ids.sudo().unlink()
664
+ return super().unlink()