arthexis 0.1.11__py3-none-any.whl → 0.1.12__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.

Potentially problematic release.


This version of arthexis might be problematic. Click here for more details.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: arthexis
3
- Version: 0.1.11
3
+ Version: 0.1.12
4
4
  Summary: Django-based MESH system
5
5
  Author-email: "Rafael J. Guillén-Osorio" <tecnologia@gelectriic.com>
6
6
  License-Expression: GPL-3.0-only
@@ -115,7 +115,7 @@ Dynamic: license-file
115
115
 
116
116
  # Arthexis Constellation
117
117
 
118
- [![Coverage](https://raw.githubusercontent.com/arthexis/arthexis/main/coverage.svg)](https://github.com/arthexis/arthexis/actions/workflows/coverage.yml) [![OCPP 1.6 Coverage](https://raw.githubusercontent.com/arthexis/arthexis/main/ocpp_coverage.svg)](https://raw.githubusercontent.com/arthexis/arthexis/main/ocpp_coverage.svg)
118
+ [![Coverage](https://raw.githubusercontent.com/arthexis/arthexis/main/coverage.svg)](https://github.com/arthexis/arthexis/actions/workflows/coverage.yml) [![OCPP 1.6 Coverage](https://raw.githubusercontent.com/arthexis/arthexis/main/ocpp_coverage.svg)](docs/development/ocpp-user-manual.md)
119
119
 
120
120
  ## Purpose
121
121
 
@@ -1,4 +1,4 @@
1
- arthexis-0.1.11.dist-info/licenses/LICENSE,sha256=IwGE9guuL-ryRPEKi6wFPI_zOhg7zDZbTYuHbSt_SAk,35823
1
+ arthexis-0.1.12.dist-info/licenses/LICENSE,sha256=IwGE9guuL-ryRPEKi6wFPI_zOhg7zDZbTYuHbSt_SAk,35823
2
2
  config/__init__.py,sha256=8_b7rx_-Xcuzu3Z7mSR94q3PAhjyYqLFQi3IOEz6hcI,108
3
3
  config/active_app.py,sha256=MET_G7oHL7GkoSo3VkkMzymM-PwsSZazMLZxpgjFLTo,388
4
4
  config/asgi.py,sha256=n09URedOmQ_59II3UCl3iodGSDWOuN_A8DFyfLjuylA,803
@@ -10,14 +10,14 @@ config/loadenv.py,sha256=bhFbHTbRJSkSwrFk3UInKEKQ8ZY-poatOGi7rC57YAI,298
10
10
  config/logging.py,sha256=334jADN4dM5GNHaCWlYPOKYa5BhfxbsuejH_QDALG6g,1793
11
11
  config/middleware.py,sha256=EvraDumepnKwCDswHGXb1mK7vud_dEEoZ4eh0IQ7fhQ,744
12
12
  config/offline.py,sha256=mhQjCUzdOwSzZ6oLgPDJR48xaPIDzOi34ARUEz43seE,1431
13
- config/settings.py,sha256=pJzArbxvM6Imyn4gs1YRn1WP3GaZThXFJM8ZxAAmNzg,22548
13
+ config/settings.py,sha256=SVsBKHoGYix3oHKlFAB4zjYRSQ1r-E9NREFYRvmcc1o,22710
14
14
  config/urls.py,sha256=MmbES50lTHyMZ6risgXAGfevncN7j4HC74jR4PX_5xY,5228
15
15
  config/wsgi.py,sha256=Fu-ONO2SgYeU6rhmy909P-uLX-n8ALJQObdm9MHPS-k,450
16
16
  core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
- core/admin.py,sha256=MKubxItiTTtEOo2Lh8CqUvQ3KLBa80e5C5Y0gsMqhe4,93074
17
+ core/admin.py,sha256=P7Mbh_POpxrqzLCMf6YH3axhDbJPMQWQyTOKV9Z88UA,101803
18
18
  core/admin_history.py,sha256=NIDWkosJoHMaucBvUjq8VmmL-0e8ngJ4l4XA89d4jwQ,1833
19
19
  core/admindocs.py,sha256=GufdugiNEG87xGSDYVq4CBMhGRubsQCzgz-FqDIqzpM,5367
20
- core/apps.py,sha256=qOKq5hB4FSRb85I3Sv3OjZzhVkn7xONix65r1qMg3HM,10312
20
+ core/apps.py,sha256=Kyv7vnYPBWDg21uYyUo1aOki0Rb5zC3_3LRJTgEHE3s,11236
21
21
  core/auto_upgrade.py,sha256=BkoE7rJuYAmwoMux22NqujWZYjYXtN40GBloC0sNMY4,1799
22
22
  core/backends.py,sha256=VsZZwskII6QLnxP6Ff593V7o9lqXXfN2_bIfZXrvjyI,8222
23
23
  core/entity.py,sha256=dkPywTVk981fV8bOEoZw-1SMrdh8T0jVAUZnRxg3dDg,4505
@@ -30,70 +30,73 @@ core/liveupdate.py,sha256=kTgbE2gnU3PPIV-88Bw2swSl1aGp6stSdBYqBFbLvx0,716
30
30
  core/log_paths.py,sha256=6UXYk6QIUmRO3ecFaFH3dgJ_pf4C_wUN6b0JqUhLVBY,3045
31
31
  core/mailer.py,sha256=OF3UgrTVs2St60tQG3ORV7_N6AWK6EtuB04KQXDop_Q,2829
32
32
  core/middleware.py,sha256=a4XL0pld4YiG-vanHrzYbJNHv64s75lvmG9inoG1ln0,3479
33
- core/models.py,sha256=ZpovGr3Tnyayu0f9Id2F0uYtnVhHo90flMFH9ixjubQ,90459
33
+ core/models.py,sha256=t38b5raQFARB8MsjFkWm-vng65ITX3Ogtb_HuYZ3vqA,91507
34
34
  core/notifications.py,sha256=YtNDGxNveZ6t3tlMXJ7wIaZZTWIZfOKy6vN9mXkeYnA,4021
35
35
  core/public_wifi.py,sha256=A08IPJqcdgUKSWbktyqsV4ol8C0uxDxZy1s1ECuPdBE,6526
36
- core/reference_utils.py,sha256=sRkwL68c16neg8EBMeZTVGjXQvVhB1zQqrd3EIsb4nA,3735
37
- core/release.py,sha256=thtgbfAnxHlOODM8xUVETgY5aAIbddWan0wW9lKmtQI,11064
36
+ core/reference_utils.py,sha256=2AQxtUkEbArieu1FMz3rTTHKPCDTJhHp_7bhzQCkN3U,3735
37
+ core/release.py,sha256=zVDcN2k4NsIydQqSq33jmMdSSYgR-1xocFWBnzaIZhU,11179
38
38
  core/sigil_builder.py,sha256=63KSTh7AohG0szr_amBov0zoZuTE8bWqmgsEfPsZHCg,4992
39
39
  core/sigil_context.py,sha256=8xrGiB2L1dFfSTrVLsFPLKfkhRwCXXZ0-EXvZdPeTMU,459
40
40
  core/sigil_resolver.py,sha256=PAGwF3ugsqN85tKyIvT0YCDCM2MU_Mct2mCJz8XY1s4,11413
41
- core/system.py,sha256=i-35lmzW6JxfwDZSUuDRT0cwPjqbIkWEYR0WVvqfADM,15080
42
- core/tasks.py,sha256=hwowcVsbwME-75KN90D2r0kX_YPMZEVevOrrxIxpM34,10595
41
+ core/system.py,sha256=iH3-Ymonn_wVAvTmg2fJBI81F0nDCcrSJsAwIlD4sR0,15212
42
+ core/tasks.py,sha256=9njUdJUZNtu23XfWhSxk5lX1kKuvckX0qamxlxTHFxA,12328
43
43
  core/temp_passwords.py,sha256=Kp4C-y1Hh4GeV1vSOCw3ZyIjcRMts186lUgtEkd3rxY,5452
44
44
  core/test_system_info.py,sha256=sHCuo-qyw0BKacbGXFhTWxkuPgywdMGiQ_6YK022n-E,4803
45
- core/tests.py,sha256=A_85lR9Qf5TE9xuCHGuM3l0_0SrH8F1qsi6sf-ip8q4,58690
45
+ core/tests.py,sha256=wCzvp3HO2bewvYPN9OstYjqtCLLWhK1msYuZxBg-lQ4,61577
46
46
  core/tests_liveupdate.py,sha256=D1o2gPopnK7wDCeDQlJ-tfitWh4umZQFRxSTCFY-puc,527
47
47
  core/urls.py,sha256=x-LNCxCgrLINdBsJTUUcAuMS5EK5Sh1ybvsVuUnJfLw,436
48
48
  core/user_data.py,sha256=kOzcWJcR-d05E7QYoNzKqQSm9bnKJvtG6zq0zRf-tDI,21371
49
- core/views.py,sha256=xnwgWVv3q6SnpYZ4ItIrILmjtD5bKWAsjtUYQ-5vnd0,44773
49
+ core/views.py,sha256=o8gjDuBzo_7YXEdFt4_OJpuz1Zg4Y7srcesNr-DIYYs,49169
50
50
  core/widgets.py,sha256=ihah_-NtFJ3oRCS3TdcT6iHCUTlg1tUULJsVCu05C0o,1379
51
51
  core/workgroup_urls.py,sha256=2bC8mOMkxIj04WsNis0Td6AmmJFr6z27Ol5epIvhO28,424
52
52
  core/workgroup_views.py,sha256=pFJ4PIRN3WWpRyombpWDKKteYQYoI7lu7ddSEKojD7I,2983
53
53
  nodes/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
54
54
  nodes/actions.py,sha256=HHnwByTBc3guOORvrKOuvUFID-_BpBq6OENE_PDgk9s,2335
55
- nodes/admin.py,sha256=40YXBolvHjhzZRJM1o4svnIyghMI9KWUDlS-PtKheTc,21483
55
+ nodes/admin.py,sha256=VhTQawEOFnosSl5cQo_EnXYmeyHBjHCwSS44bMTbaeA,32766
56
56
  nodes/apps.py,sha256=KijGydsQBS-8Q8uBc0ahDZXSSCMzP3tQcYIEigJxdCE,2077
57
57
  nodes/backends.py,sha256=-g7PIbGtIn_3kGoi2w_mJ6zVjvUKTRRWhHABPnnf29g,6081
58
58
  nodes/dns.py,sha256=X8PML9rPR7i6NwCSqqxnlO1qpHHRo8uA-XePDTQZYrY,7251
59
+ nodes/feature_checks.py,sha256=ZK9PufvF2pFWHUm4khg8lfHVNncA6zrMBxu-WZEw7-E,4129
59
60
  nodes/lcd.py,sha256=7MqS3jV_De2ysSmTtXQfYTWaxdfbmm6YfNdb_7qIVZc,5923
60
- nodes/models.py,sha256=D-MPUe672JaF5NUU-ASAes860hJmiHeU5VHUD03Pe-c,46986
61
+ nodes/models.py,sha256=aF_Bhbi54H2A3HuQiwsvTEwk6JAsImJjdAdL8OcHeTg,49040
62
+ nodes/reports.py,sha256=pQwwTa5E-JhUwQIbKKQvgD_Cw9556CH0ieRgRjBMnMQ,12470
61
63
  nodes/tasks.py,sha256=jorbN4h0PqWMBRMFdkGgJtvG8GPFwGr5Hxlm5In3EeY,1568
62
- nodes/tests.py,sha256=4lKlM_uHYKirtLgOne4ygwEDowi2PPl2ZvEtwJzUfhI,86073
64
+ nodes/tests.py,sha256=28oh6oAINpq8a-EknsB-GKU-4RYfnNSdedqV411W5Jg,100131
63
65
  nodes/urls.py,sha256=20yZDZf4DNgIZ9hQWsUzjp8k5Fryg9ukk761_KGQt9k,548
64
- nodes/utils.py,sha256=B9BD3bKBkwDjIqyp0pyjnKQQlRbGRVQndfoaMEJoNDc,2815
66
+ nodes/utils.py,sha256=h3qVmCBBLyrh06FmDldprxfDQG7dgLw82Tf5PrU-5Qc,4120
65
67
  nodes/views.py,sha256=lrT90AsOXeqpvkZnlCPv78mXILGJ8FwJzgsjU-wAZ4Y,15611
66
68
  ocpp/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
67
- ocpp/admin.py,sha256=Rw0PinweVSaBjIQIcgj_cgfalNRBHcrAh3JInx8Hn7A,17129
69
+ ocpp/admin.py,sha256=LUJw7fk5q336oAoftfGHs5_pbvEozjMPF69-WCTlAiM,26675
68
70
  ocpp/apps.py,sha256=mCZ5Z0ei7z7c62luIcIhbEuwL1N4czQFSToOkGvRmms,867
69
- ocpp/consumers.py,sha256=0kLqiDAt0rsC62s8lmS6mW2Oj-LZuzjs0Pz4ci6L4oQ,36577
71
+ ocpp/consumers.py,sha256=drfud7XSCP3zhrAc6ljzyrqM8K6VDPNFRs4Z_NeHfLk,57896
70
72
  ocpp/evcs.py,sha256=O53rCHdxKcgPsj7o57rDiNHTVvEii3DTtQ3djWFTohw,34065
71
- ocpp/models.py,sha256=46BYGGX4AhbBigXLV3PWLKP043JF96Wolho1q-ZPoa8,23929
73
+ ocpp/evcs_discovery.py,sha256=47pWgqX1_JOT5BGSrIdEyuoPTGWN4b6rNjJr8kWeQCg,4034
74
+ ocpp/models.py,sha256=fZlqwPCq3LNHSE-RnGUqgFWI5FT_QyRandxujbxNZZs,32177
72
75
  ocpp/reference_utils.py,sha256=sTgbXfmz00f23LBBkpO-sBGoJf1qaEshHeSofLReYCc,1114
73
76
  ocpp/routing.py,sha256=g9vPnLw-D1N8L_mW0_oCe-nTDibjC0Et-SFxe8NFAOI,308
74
- ocpp/simulator.py,sha256=es6SJNzKUwLwrLy-zDdRu__n34V2RZSQRb8SeZHh4Fw,15636
75
- ocpp/store.py,sha256=FyyZW2YKTWleuNdHTo_RsUO2InZZJhvMYyuZmLgQZe4,14051
77
+ ocpp/simulator.py,sha256=hFwXuX8SaSJKEXWVWakyBxGa6ZbPvwa0PVTnKrXR1nw,28049
78
+ ocpp/store.py,sha256=OjGWT3WkpoahGpUwn_ZpIOsxgOnryoXXSwPCvFsLkzM,17410
76
79
  ocpp/tasks.py,sha256=cOcJBshckFKs8GnACvmYZUBG116amtLRAzEP-JNqlZ0,905
77
80
  ocpp/test_export_import.py,sha256=TK1__E4K5WrLYPIx-1iJWoIhuRCnOtXc2cYEpGigd3U,4660
78
81
  ocpp/test_rfid.py,sha256=hX8VZ2HRyBGjtQLClZ1gy28dOj25RT2r9eK3l9XkmJg,25877
79
- ocpp/tests.py,sha256=CcTn-bRDsb78NbK2hbH66TnlUwOno8y7ug0u8bgIz9k,107230
80
- ocpp/transactions_io.py,sha256=OvRynP3DeC9ZpHD3Ez-0YPNPJXFcdSOSKg0nRrwzBJ0,6507
82
+ ocpp/tests.py,sha256=aQZBGmGzhzMh2GrEsunN50gRKUgtIqdD4Y4NbjsPt7E,138325
83
+ ocpp/transactions_io.py,sha256=sFCJvAU5IqWZ4rLvagy4gdcGVvH2rBZvNCfHTXDYJAs,7044
81
84
  ocpp/urls.py,sha256=jl6AQLtmLMvrNXP3dQKgPe9Kvaul4oaYd80DskpzVBc,1750
82
- ocpp/views.py,sha256=3E6hJ0Tqr-_KRjvB7cunEsjtNI8Y9XzktPDhaAlIM7o,37166
85
+ ocpp/views.py,sha256=FyLt7_67IeteBDADoRkqO3WJGit9noiMs_5RVapwUfo,45669
83
86
  pages/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
84
- pages/admin.py,sha256=JcwBwtQD8E2XKhPpt6bfznW3cU18XZyWvp5B8Zlr0eQ,12772
87
+ pages/admin.py,sha256=0D2ZGqOhMjk8LfXnPr0g0uMUPJl2qCgvDJz_2IS3WFM,16904
85
88
  pages/apps.py,sha256=mfCxegmnRqKcszyEwpQhZpW9JWOuEYdVereU_w49BXg,298
86
89
  pages/checks.py,sha256=an-MlMCIG-FSKmdgOhYV0VOoB_wDQD7dxKD03Hjrzts,1567
87
- pages/context_processors.py,sha256=iNSTFYFJsrhj0DjsB3ixVNDZw7nrowlnZAcCDyR4NS4,3697
88
- pages/defaults.py,sha256=rF_V5oD5JQX1tMxdMs_rCsS21Vn_NZpTESahzLNTPws,595
89
- pages/forms.py,sha256=NuG7ONP1HYY6O5gRoQaRSwSRchcSonRm3rulsqGSqvY,5081
90
+ pages/context_processors.py,sha256=4lkk2mTzmsOKhDhQanWXuU2bbwow0h-1Zn5GWOrL2z4,4132
91
+ pages/defaults.py,sha256=cOhb5p_SneIsK6Qfn9SV1fyq7L3gDheWYo1xp7SplF8,535
92
+ pages/forms.py,sha256=xwQ1uKqZBrIjn9DJQIqZtn0u_xO46w9vSjKVX9Mt6pc,6349
90
93
  pages/middleware.py,sha256=fUdAscLa6h2EqJTFUjhVijfSf82gBfm6XUZTVygWAnk,5227
91
- pages/models.py,sha256=mSlw5JTgh2s_SsFzrNgq48BOhX8uc6mjZFy-kYXUjMo,9955
92
- pages/tests.py,sha256=6WXtb2p301tqXPT_327Y8Zw5uAsqp5hREh6nF8wWa-o,74552
93
- pages/urls.py,sha256=hvbdrMDc735CFwCbCU96t80IY9jDpMTpI0zQhoaxL0M,981
94
+ pages/models.py,sha256=O4wtjD8jkZlguxEQFcvijSPM55KBjRBdhzvg0FHnxt4,14577
95
+ pages/tests.py,sha256=Pk0waQkWxi-5EZq44XOUGdhvZC1oXh8dy4j0FqRb6ns,84353
96
+ pages/urls.py,sha256=iwe4hvcr_ko-n4h24t6t2882eZp2qyLXeEYkrIahoGU,1067
94
97
  pages/utils.py,sha256=7kik1W0Gk6SFxYGhg6shfI2W9Xdcv1sCpkRCQ883a88,311
95
- pages/views.py,sha256=X_ZoJH2DEQp8xUZMtZotTxnslABwsmFKQzYfWopktZM,41135
96
- arthexis-0.1.11.dist-info/METADATA,sha256=uA-HTbP7vjQzwm9FtJjxgl28g8MszSr7ntnxH9MyVlM,10175
97
- arthexis-0.1.11.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
98
- arthexis-0.1.11.dist-info/top_level.txt,sha256=J2a2q8_BWrCZ8H2WFUNMBfO2jz8j2gax6zZh-_1QDac,29
99
- arthexis-0.1.11.dist-info/RECORD,,
98
+ pages/views.py,sha256=_7NqHt0omhAge63Fz6ukbbBE06oQzKgVoO5e3WtSMvk,43044
99
+ arthexis-0.1.12.dist-info/METADATA,sha256=nErwfvr1FpIptkmRFT7yLGLigaHdMcXaLL4iFUTEhXc,10137
100
+ arthexis-0.1.12.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
101
+ arthexis-0.1.12.dist-info/top_level.txt,sha256=J2a2q8_BWrCZ8H2WFUNMBfO2jz8j2gax6zZh-_1QDac,29
102
+ arthexis-0.1.12.dist-info/RECORD,,
config/settings.py CHANGED
@@ -685,10 +685,15 @@ LOGGING = {
685
685
  "backupCount": 7,
686
686
  "encoding": "utf-8",
687
687
  "formatter": "standard",
688
- }
688
+ },
689
+ "console": {
690
+ "class": "logging.StreamHandler",
691
+ "level": "ERROR",
692
+ "formatter": "standard",
693
+ },
689
694
  },
690
695
  "root": {
691
- "handlers": ["file"],
696
+ "handlers": ["file", "console"],
692
697
  "level": "DEBUG",
693
698
  },
694
699
  }
core/admin.py CHANGED
@@ -76,6 +76,7 @@ from .user_data import (
76
76
  )
77
77
  from .widgets import OdooProductWidget
78
78
  from .mcp import process as mcp_process
79
+ from .mcp.server import resolve_base_urls
79
80
 
80
81
 
81
82
  admin.site.unregister(Group)
@@ -371,6 +372,29 @@ class ReleaseManagerAdminForm(forms.ModelForm):
371
372
  "github_token": forms.Textarea(attrs={"rows": 3, "style": "width: 40em;"}),
372
373
  }
373
374
 
375
+ def __init__(self, *args, **kwargs):
376
+ super().__init__(*args, **kwargs)
377
+ self.fields["pypi_token"].help_text = format_html(
378
+ "{} <a href=\"{}\" target=\"_blank\" rel=\"noopener noreferrer\">{}</a>{}",
379
+ "Generate an API token from your PyPI account settings.",
380
+ "https://pypi.org/manage/account/token/",
381
+ "pypi.org/manage/account/token/",
382
+ (
383
+ " by clicking “Add API token”, optionally scoping it to the package, "
384
+ "and paste the full `pypi-***` value here."
385
+ ),
386
+ )
387
+ self.fields["github_token"].help_text = format_html(
388
+ "{} <a href=\"{}\" target=\"_blank\" rel=\"noopener noreferrer\">{}</a>{}",
389
+ "Create a personal access token at GitHub → Settings → Developer settings →",
390
+ "https://github.com/settings/tokens",
391
+ "github.com/settings/tokens",
392
+ (
393
+ " with the repository access needed for releases (repo scope for classic tokens "
394
+ "or an equivalent fine-grained token) and paste it here."
395
+ ),
396
+ )
397
+
374
398
 
375
399
  @admin.register(ReleaseManager)
376
400
  class ReleaseManagerAdmin(ProfileAdminMixin, SaveBeforeChangeAction, EntityModelAdmin):
@@ -690,28 +714,30 @@ class OdooProfileAdminForm(forms.ModelForm):
690
714
  )
691
715
 
692
716
 
693
- class EmailInboxAdminForm(forms.ModelForm):
694
- """Admin form for :class:`core.models.EmailInbox` with hidden password."""
695
-
696
- password = forms.CharField(
697
- widget=forms.PasswordInput(render_value=True),
698
- required=False,
699
- help_text="Leave blank to keep the current password.",
700
- )
717
+ class MaskedPasswordFormMixin:
718
+ """Mixin that hides stored passwords while allowing updates."""
701
719
 
702
- class Meta:
703
- model = EmailInbox
704
- fields = "__all__"
720
+ password_sigil_fields: tuple[str, ...] = ()
705
721
 
706
722
  def __init__(self, *args, **kwargs):
707
723
  super().__init__(*args, **kwargs)
724
+ field = self.fields.get("password")
725
+ if field is None:
726
+ return
727
+ if not isinstance(field.widget, forms.PasswordInput):
728
+ field.widget = forms.PasswordInput()
729
+ field.widget.attrs.setdefault("autocomplete", "new-password")
730
+ field.help_text = field.help_text or "Leave blank to keep the current password."
708
731
  if self.instance.pk:
709
- self.fields["password"].initial = ""
732
+ field.initial = ""
710
733
  self.initial["password"] = ""
711
734
  else:
712
- self.fields["password"].required = True
735
+ field.required = True
713
736
 
714
737
  def clean_password(self):
738
+ field = self.fields.get("password")
739
+ if field is None:
740
+ return self.cleaned_data.get("password")
715
741
  pwd = self.cleaned_data.get("password")
716
742
  if not pwd and self.instance.pk:
717
743
  return keep_existing("password")
@@ -719,10 +745,23 @@ class EmailInboxAdminForm(forms.ModelForm):
719
745
 
720
746
  def _post_clean(self):
721
747
  super()._post_clean()
722
- _restore_sigil_values(
723
- self,
724
- ["username", "host", "password", "protocol"],
725
- )
748
+ if self.password_sigil_fields:
749
+ _restore_sigil_values(self, self.password_sigil_fields)
750
+
751
+
752
+ class EmailInboxAdminForm(MaskedPasswordFormMixin, forms.ModelForm):
753
+ """Admin form for :class:`core.models.EmailInbox` with hidden password."""
754
+
755
+ password = forms.CharField(
756
+ widget=forms.PasswordInput(attrs={"autocomplete": "new-password"}),
757
+ required=False,
758
+ help_text="Leave blank to keep the current password.",
759
+ )
760
+ password_sigil_fields = ("username", "host", "password", "protocol")
761
+
762
+ class Meta:
763
+ model = EmailInbox
764
+ fields = "__all__"
726
765
 
727
766
 
728
767
  class ProfileInlineFormSet(BaseInlineFormSet):
@@ -880,16 +919,25 @@ class SocialProfileInlineForm(ProfileFormMixin, forms.ModelForm):
880
919
  fields = ("network", "handle", "domain", "did")
881
920
 
882
921
 
883
- class EmailOutboxInlineForm(ProfileFormMixin, forms.ModelForm):
884
- profile_fields = EmailOutbox.profile_fields
922
+ class EmailOutboxAdminForm(MaskedPasswordFormMixin, forms.ModelForm):
923
+ """Admin form for :class:`nodes.models.EmailOutbox` with hidden password."""
924
+
885
925
  password = forms.CharField(
886
- widget=forms.PasswordInput(render_value=True),
926
+ widget=forms.PasswordInput(attrs={"autocomplete": "new-password"}),
887
927
  required=False,
888
928
  help_text="Leave blank to keep the current password.",
889
929
  )
930
+ password_sigil_fields = ("password", "host", "username", "from_email")
890
931
 
891
932
  class Meta:
892
933
  model = EmailOutbox
934
+ fields = "__all__"
935
+
936
+
937
+ class EmailOutboxInlineForm(ProfileFormMixin, EmailOutboxAdminForm):
938
+ profile_fields = EmailOutbox.profile_fields
939
+
940
+ class Meta(EmailOutboxAdminForm.Meta):
893
941
  fields = (
894
942
  "password",
895
943
  "host",
@@ -901,27 +949,6 @@ class EmailOutboxInlineForm(ProfileFormMixin, forms.ModelForm):
901
949
  "is_enabled",
902
950
  )
903
951
 
904
- def __init__(self, *args, **kwargs):
905
- super().__init__(*args, **kwargs)
906
- if self.instance.pk:
907
- self.fields["password"].initial = ""
908
- self.initial["password"] = ""
909
- else:
910
- self.fields["password"].required = True
911
-
912
- def clean_password(self):
913
- pwd = self.cleaned_data.get("password")
914
- if not pwd and self.instance.pk:
915
- return keep_existing("password")
916
- return pwd
917
-
918
- def _post_clean(self):
919
- super()._post_clean()
920
- _restore_sigil_values(
921
- self,
922
- ["password", "host", "username", "from_email"],
923
- )
924
-
925
952
 
926
953
  class ReleaseManagerInlineForm(ProfileFormMixin, forms.ModelForm):
927
954
  profile_fields = ReleaseManager.profile_fields
@@ -1324,10 +1351,8 @@ class OdooProfileAdmin(ProfileAdminMixin, SaveBeforeChangeAction, EntityModelAdm
1324
1351
  changelist_actions = ["my_profile"]
1325
1352
  fieldsets = (
1326
1353
  ("Owner", {"fields": ("user", "group")}),
1327
- (
1328
- "Configuration",
1329
- {"fields": ("host", "database", "username", "password")},
1330
- ),
1354
+ ("Configuration", {"fields": ("host", "database")}),
1355
+ ("Credentials", {"fields": ("username", "password")}),
1331
1356
  (
1332
1357
  "Odoo Employee",
1333
1358
  {"fields": ("verified_on", "odoo_uid", "name", "email")},
@@ -1417,18 +1442,10 @@ class EmailInboxAdmin(ProfileAdminMixin, SaveBeforeChangeAction, EntityModelAdmi
1417
1442
 
1418
1443
  fieldsets = (
1419
1444
  ("Owner", {"fields": ("user", "group")}),
1445
+ ("Credentials", {"fields": ("username", "password")}),
1420
1446
  (
1421
- None,
1422
- {
1423
- "fields": (
1424
- "username",
1425
- "host",
1426
- "port",
1427
- "password",
1428
- "protocol",
1429
- "use_ssl",
1430
- )
1431
- },
1447
+ "Configuration",
1448
+ {"fields": ("host", "port", "protocol", "use_ssl")},
1432
1449
  ),
1433
1450
  )
1434
1451
 
@@ -1526,17 +1543,10 @@ class AssistantProfileAdmin(
1526
1543
  changelist_actions = ["my_profile"]
1527
1544
  fieldsets = (
1528
1545
  ("Owner", {"fields": ("user", "group")}),
1546
+ ("Credentials", {"fields": ("user_key_hash",)}),
1529
1547
  (
1530
- None,
1531
- {
1532
- "fields": (
1533
- "scopes",
1534
- "is_active",
1535
- "user_key_hash",
1536
- "created_at",
1537
- "last_used_at",
1538
- )
1539
- },
1548
+ "Configuration",
1549
+ {"fields": ("scopes", "is_active", "created_at", "last_used_at")},
1540
1550
  ),
1541
1551
  )
1542
1552
 
@@ -1627,14 +1637,19 @@ class AssistantProfileAdmin(
1627
1637
  config = dict(getattr(settings, "MCP_SIGIL_SERVER", {}))
1628
1638
  host = config.get("host") or "127.0.0.1"
1629
1639
  port = config.get("port", 8800)
1640
+ base_url, issuer_url = resolve_base_urls(config)
1630
1641
  if isinstance(response, dict):
1631
1642
  response.setdefault("mcp_server_host", host)
1632
1643
  response.setdefault("mcp_server_port", port)
1644
+ response.setdefault("mcp_server_base_url", base_url)
1645
+ response.setdefault("mcp_server_issuer_url", issuer_url)
1633
1646
  else:
1634
1647
  context_data = getattr(response, "context_data", None)
1635
1648
  if context_data is not None:
1636
1649
  context_data.setdefault("mcp_server_host", host)
1637
1650
  context_data.setdefault("mcp_server_port", port)
1651
+ context_data.setdefault("mcp_server_base_url", base_url)
1652
+ context_data.setdefault("mcp_server_issuer_url", issuer_url)
1638
1653
  return response
1639
1654
 
1640
1655
  def start_server(self, request):
@@ -1922,7 +1937,7 @@ class ProductFetchWizardForm(forms.Form):
1922
1937
  @admin.register(Product)
1923
1938
  class ProductAdmin(EntityModelAdmin):
1924
1939
  form = ProductAdminForm
1925
- actions = ["fetch_odoo_product"]
1940
+ actions = ["fetch_odoo_product", "register_from_odoo"]
1926
1941
 
1927
1942
  def _odoo_profile_admin(self):
1928
1943
  return self.admin_site._registry.get(OdooProfile)
@@ -1932,7 +1947,7 @@ class ProductAdmin(EntityModelAdmin):
1932
1947
  return profile.execute(
1933
1948
  "product.product",
1934
1949
  "search_read",
1935
- domain,
1950
+ [domain],
1936
1951
  {
1937
1952
  "fields": [
1938
1953
  "name",
@@ -2072,6 +2087,169 @@ class ProductAdmin(EntityModelAdmin):
2072
2087
  context["media"] = self.media + form.media
2073
2088
  return TemplateResponse(request, "admin/core/product/fetch_odoo.html", context)
2074
2089
 
2090
+ def get_urls(self):
2091
+ urls = super().get_urls()
2092
+ custom = [
2093
+ path(
2094
+ "register-from-odoo/",
2095
+ self.admin_site.admin_view(self.register_from_odoo_view),
2096
+ name=f"{self.opts.app_label}_{self.opts.model_name}_register_from_odoo",
2097
+ )
2098
+ ]
2099
+ return custom + urls
2100
+
2101
+ @admin.action(description="Register from Odoo")
2102
+ def register_from_odoo(self, request, queryset=None): # pragma: no cover - simple redirect
2103
+ return HttpResponseRedirect(
2104
+ reverse(
2105
+ f"admin:{self.opts.app_label}_{self.opts.model_name}_register_from_odoo"
2106
+ )
2107
+ )
2108
+
2109
+ def _build_register_context(self, request):
2110
+ opts = self.model._meta
2111
+ context = self.admin_site.each_context(request)
2112
+ context.update(
2113
+ {
2114
+ "opts": opts,
2115
+ "title": _("Register from Odoo"),
2116
+ "has_credentials": False,
2117
+ "profile_url": None,
2118
+ "products": [],
2119
+ "selected_product_id": request.POST.get("product_id", ""),
2120
+ }
2121
+ )
2122
+
2123
+ profile_admin = self._odoo_profile_admin()
2124
+ if profile_admin is not None:
2125
+ context["profile_url"] = profile_admin.get_my_profile_url(request)
2126
+
2127
+ profile = getattr(request.user, "odoo_profile", None)
2128
+ if not profile or not profile.is_verified:
2129
+ context["credential_error"] = _(
2130
+ "Configure your Odoo employee credentials before registering products."
2131
+ )
2132
+ return context, None
2133
+
2134
+ try:
2135
+ products = profile.execute(
2136
+ "product.product",
2137
+ "search_read",
2138
+ [[]],
2139
+ {
2140
+ "fields": [
2141
+ "name",
2142
+ "description_sale",
2143
+ "list_price",
2144
+ "standard_price",
2145
+ ],
2146
+ "limit": 0,
2147
+ },
2148
+ )
2149
+ except Exception:
2150
+ context["error"] = _("Unable to fetch products from Odoo.")
2151
+ return context, []
2152
+
2153
+ context["has_credentials"] = True
2154
+ simplified = []
2155
+ for product in products:
2156
+ simplified.append(
2157
+ {
2158
+ "id": product.get("id"),
2159
+ "name": product.get("name", ""),
2160
+ "description_sale": product.get("description_sale", ""),
2161
+ "list_price": product.get("list_price"),
2162
+ "standard_price": product.get("standard_price"),
2163
+ }
2164
+ )
2165
+ context["products"] = simplified
2166
+ return context, simplified
2167
+
2168
+ def register_from_odoo_view(self, request):
2169
+ context, products = self._build_register_context(request)
2170
+ if products is None:
2171
+ return TemplateResponse(
2172
+ request, "admin/core/product/register_from_odoo.html", context
2173
+ )
2174
+
2175
+ if request.method == "POST" and context.get("has_credentials"):
2176
+ if not self.has_add_permission(request):
2177
+ context["form_error"] = _(
2178
+ "You do not have permission to add products."
2179
+ )
2180
+ else:
2181
+ product_id = request.POST.get("product_id")
2182
+ if not product_id:
2183
+ context["form_error"] = _("Select a product to register.")
2184
+ else:
2185
+ try:
2186
+ odoo_id = int(product_id)
2187
+ except (TypeError, ValueError):
2188
+ context["form_error"] = _("Invalid product selection.")
2189
+ else:
2190
+ match = next(
2191
+ (item for item in products if item.get("id") == odoo_id),
2192
+ None,
2193
+ )
2194
+ if not match:
2195
+ context["form_error"] = _(
2196
+ "The selected product was not found. Reload the page and try again."
2197
+ )
2198
+ else:
2199
+ existing = self.model.objects.filter(
2200
+ odoo_product__id=odoo_id
2201
+ ).first()
2202
+ if existing:
2203
+ self.message_user(
2204
+ request,
2205
+ _(
2206
+ "Product %(name)s already imported; opening existing record."
2207
+ )
2208
+ % {"name": existing.name},
2209
+ level=messages.WARNING,
2210
+ )
2211
+ return HttpResponseRedirect(
2212
+ reverse(
2213
+ "admin:%s_%s_change"
2214
+ % (
2215
+ existing._meta.app_label,
2216
+ existing._meta.model_name,
2217
+ ),
2218
+ args=[existing.pk],
2219
+ )
2220
+ )
2221
+ product = self.model.objects.create(
2222
+ name=match.get("name") or f"Odoo Product {odoo_id}",
2223
+ description=match.get("description_sale", "") or "",
2224
+ renewal_period=30,
2225
+ odoo_product={
2226
+ "id": odoo_id,
2227
+ "name": match.get("name", ""),
2228
+ },
2229
+ )
2230
+ self.log_addition(
2231
+ request, product, "Registered product from Odoo"
2232
+ )
2233
+ self.message_user(
2234
+ request,
2235
+ _("Imported %(name)s from Odoo.")
2236
+ % {"name": product.name},
2237
+ )
2238
+ return HttpResponseRedirect(
2239
+ reverse(
2240
+ "admin:%s_%s_change"
2241
+ % (
2242
+ product._meta.app_label,
2243
+ product._meta.model_name,
2244
+ ),
2245
+ args=[product.pk],
2246
+ )
2247
+ )
2248
+
2249
+ return TemplateResponse(
2250
+ request, "admin/core/product/register_from_odoo.html", context
2251
+ )
2252
+
2075
2253
 
2076
2254
  class RFIDResource(resources.ModelResource):
2077
2255
  reference = fields.Field(
core/apps.py CHANGED
@@ -21,6 +21,7 @@ class CoreConfig(AppConfig):
21
21
  from pathlib import Path
22
22
 
23
23
  from django.conf import settings
24
+ from django.core.exceptions import ObjectDoesNotExist
24
25
  from django.contrib.auth import get_user_model
25
26
  from django.db.models.signals import post_migrate
26
27
  from django.core.signals import got_request_exception
@@ -39,6 +40,26 @@ class CoreConfig(AppConfig):
39
40
  )
40
41
  from .admin_history import patch_admin_history
41
42
 
43
+ from django_otp.plugins.otp_totp.models import TOTPDevice as OTP_TOTPDevice
44
+
45
+ if not hasattr(
46
+ OTP_TOTPDevice._read_str_from_settings, "_core_totp_issuer_patch"
47
+ ):
48
+ original_read_str = OTP_TOTPDevice._read_str_from_settings
49
+
50
+ def _core_totp_read_str(self, key):
51
+ if key == "OTP_TOTP_ISSUER":
52
+ try:
53
+ settings_obj = self.custom_settings
54
+ except ObjectDoesNotExist:
55
+ settings_obj = None
56
+ if settings_obj and settings_obj.issuer:
57
+ return settings_obj.issuer
58
+ return original_read_str(self, key)
59
+
60
+ _core_totp_read_str._core_totp_issuer_patch = True
61
+ OTP_TOTPDevice._read_str_from_settings = _core_totp_read_str
62
+
42
63
  def create_default_arthexis(**kwargs):
43
64
  User = get_user_model()
44
65
  if not User.all_objects.exists():