arthexis 0.1.5__tar.gz → 0.1.7__tar.gz

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.

Files changed (137) hide show
  1. {arthexis-0.1.5 → arthexis-0.1.7}/PKG-INFO +8 -8
  2. arthexis-0.1.7/README.md +18 -0
  3. {arthexis-0.1.5 → arthexis-0.1.7}/arthexis.egg-info/PKG-INFO +8 -8
  4. {arthexis-0.1.5 → arthexis-0.1.7}/arthexis.egg-info/SOURCES.txt +10 -6
  5. {arthexis-0.1.5 → arthexis-0.1.7}/config/celery.py +7 -0
  6. arthexis-0.1.7/config/horologia_app.py +7 -0
  7. {arthexis-0.1.5 → arthexis-0.1.7}/config/logging.py +8 -3
  8. {arthexis-0.1.5 → arthexis-0.1.7}/config/settings.py +2 -2
  9. arthexis-0.1.7/config/workgroup_app.py +7 -0
  10. {arthexis-0.1.5 → arthexis-0.1.7}/core/admin.py +69 -13
  11. {arthexis-0.1.5 → arthexis-0.1.7}/core/apps.py +2 -1
  12. arthexis-0.1.7/core/checks.py +29 -0
  13. {arthexis-0.1.5 → arthexis-0.1.7}/core/entity.py +29 -7
  14. {arthexis-0.1.5 → arthexis-0.1.7}/core/models.py +49 -9
  15. {arthexis-0.1.5 → arthexis-0.1.7}/core/release.py +29 -141
  16. {arthexis-0.1.5 → arthexis-0.1.7}/core/system.py +2 -2
  17. arthexis-0.1.7/core/test_system_info.py +21 -0
  18. {arthexis-0.1.5 → arthexis-0.1.7}/core/tests.py +200 -1
  19. {arthexis-0.1.5 → arthexis-0.1.7}/core/views.py +153 -134
  20. {arthexis-0.1.5 → arthexis-0.1.7}/nodes/admin.py +224 -1
  21. {arthexis-0.1.5 → arthexis-0.1.7}/nodes/apps.py +1 -1
  22. {arthexis-0.1.5 → arthexis-0.1.7}/nodes/models.py +103 -7
  23. {arthexis-0.1.5 → arthexis-0.1.7}/nodes/tests.py +27 -5
  24. {arthexis-0.1.5 → arthexis-0.1.7}/nodes/views.py +0 -1
  25. {arthexis-0.1.5 → arthexis-0.1.7}/ocpp/apps.py +1 -1
  26. {arthexis-0.1.5 → arthexis-0.1.7}/ocpp/models.py +1 -1
  27. {arthexis-0.1.5 → arthexis-0.1.7}/ocpp/simulator.py +4 -0
  28. {arthexis-0.1.5 → arthexis-0.1.7}/ocpp/tests.py +5 -1
  29. {arthexis-0.1.5 → arthexis-0.1.7}/pages/admin.py +8 -3
  30. {arthexis-0.1.5 → arthexis-0.1.7}/pages/apps.py +1 -1
  31. {arthexis-0.1.5 → arthexis-0.1.7}/pages/tests.py +23 -4
  32. {arthexis-0.1.5 → arthexis-0.1.7}/pages/views.py +22 -3
  33. {arthexis-0.1.5 → arthexis-0.1.7}/pyproject.toml +2 -2
  34. arthexis-0.1.7/tests/test_celery_no_debug.py +19 -0
  35. arthexis-0.1.7/tests/test_dist_cleanup.py +32 -0
  36. {arthexis-0.1.5 → arthexis-0.1.7}/tests/test_email_inbox_search_action.py +5 -3
  37. arthexis-0.1.7/tests/test_env_refresh_clean.py +19 -0
  38. arthexis-0.1.7/tests/test_fixture_check.py +62 -0
  39. {arthexis-0.1.5 → arthexis-0.1.7}/tests/test_install_script.py +21 -1
  40. arthexis-0.1.7/tests/test_release_progress.py +80 -0
  41. {arthexis-0.1.5 → arthexis-0.1.7}/tests/test_sigil_resolution.py +28 -0
  42. {arthexis-0.1.5 → arthexis-0.1.7}/tests/test_user_datum_admin.py +0 -1
  43. arthexis-0.1.7/tests/test_version_file.py +23 -0
  44. arthexis-0.1.7/tests/test_workgroup_admin_group.py +33 -0
  45. arthexis-0.1.5/README.md +0 -18
  46. arthexis-0.1.5/tests/test_env_refresh_clean.py +0 -30
  47. arthexis-0.1.5/tests/test_footer_admin_link.py +0 -91
  48. arthexis-0.1.5/tests/test_github_token.py +0 -29
  49. arthexis-0.1.5/tests/test_package_release_admin_actions.py +0 -199
  50. arthexis-0.1.5/tests/test_release_fixture_cleanup.py +0 -37
  51. arthexis-0.1.5/tests/test_release_mapping.py +0 -24
  52. arthexis-0.1.5/tests/test_release_progress.py +0 -165
  53. {arthexis-0.1.5 → arthexis-0.1.7}/LICENSE +0 -0
  54. {arthexis-0.1.5 → arthexis-0.1.7}/arthexis.egg-info/dependency_links.txt +0 -0
  55. {arthexis-0.1.5 → arthexis-0.1.7}/arthexis.egg-info/requires.txt +0 -0
  56. {arthexis-0.1.5 → arthexis-0.1.7}/arthexis.egg-info/top_level.txt +0 -0
  57. {arthexis-0.1.5 → arthexis-0.1.7}/config/__init__.py +0 -0
  58. {arthexis-0.1.5 → arthexis-0.1.7}/config/active_app.py +0 -0
  59. {arthexis-0.1.5 → arthexis-0.1.7}/config/asgi.py +0 -0
  60. {arthexis-0.1.5 → arthexis-0.1.7}/config/auth_app.py +0 -0
  61. {arthexis-0.1.5 → arthexis-0.1.7}/config/context_processors.py +0 -0
  62. {arthexis-0.1.5 → arthexis-0.1.7}/config/loadenv.py +0 -0
  63. {arthexis-0.1.5 → arthexis-0.1.7}/config/middleware.py +0 -0
  64. {arthexis-0.1.5 → arthexis-0.1.7}/config/offline.py +0 -0
  65. {arthexis-0.1.5 → arthexis-0.1.7}/config/urls.py +0 -0
  66. {arthexis-0.1.5 → arthexis-0.1.7}/config/wsgi.py +0 -0
  67. {arthexis-0.1.5 → arthexis-0.1.7}/core/__init__.py +0 -0
  68. {arthexis-0.1.5 → arthexis-0.1.7}/core/backends.py +0 -0
  69. {arthexis-0.1.5 → arthexis-0.1.7}/core/environment.py +0 -0
  70. {arthexis-0.1.5 → arthexis-0.1.7}/core/fields.py +0 -0
  71. {arthexis-0.1.5 → arthexis-0.1.7}/core/lcd_screen.py +0 -0
  72. {arthexis-0.1.5 → arthexis-0.1.7}/core/middleware.py +0 -0
  73. {arthexis-0.1.5 → arthexis-0.1.7}/core/notifications.py +0 -0
  74. {arthexis-0.1.5 → arthexis-0.1.7}/core/tasks.py +0 -0
  75. {arthexis-0.1.5 → arthexis-0.1.7}/core/urls.py +0 -0
  76. {arthexis-0.1.5 → arthexis-0.1.7}/core/user_data.py +0 -0
  77. {arthexis-0.1.5 → arthexis-0.1.7}/nodes/__init__.py +0 -0
  78. {arthexis-0.1.5 → arthexis-0.1.7}/nodes/actions.py +0 -0
  79. {arthexis-0.1.5 → arthexis-0.1.7}/nodes/lcd.py +0 -0
  80. {arthexis-0.1.5 → arthexis-0.1.7}/nodes/tasks.py +0 -0
  81. {arthexis-0.1.5 → arthexis-0.1.7}/nodes/urls.py +0 -0
  82. {arthexis-0.1.5 → arthexis-0.1.7}/nodes/utils.py +0 -0
  83. {arthexis-0.1.5 → arthexis-0.1.7}/ocpp/__init__.py +0 -0
  84. {arthexis-0.1.5 → arthexis-0.1.7}/ocpp/admin.py +0 -0
  85. {arthexis-0.1.5 → arthexis-0.1.7}/ocpp/consumers.py +0 -0
  86. {arthexis-0.1.5 → arthexis-0.1.7}/ocpp/evcs.py +0 -0
  87. {arthexis-0.1.5 → arthexis-0.1.7}/ocpp/routing.py +0 -0
  88. {arthexis-0.1.5 → arthexis-0.1.7}/ocpp/store.py +0 -0
  89. {arthexis-0.1.5 → arthexis-0.1.7}/ocpp/tasks.py +0 -0
  90. {arthexis-0.1.5 → arthexis-0.1.7}/ocpp/test_export_import.py +0 -0
  91. {arthexis-0.1.5 → arthexis-0.1.7}/ocpp/test_rfid.py +0 -0
  92. {arthexis-0.1.5 → arthexis-0.1.7}/ocpp/transactions_io.py +0 -0
  93. {arthexis-0.1.5 → arthexis-0.1.7}/ocpp/urls.py +0 -0
  94. {arthexis-0.1.5 → arthexis-0.1.7}/ocpp/views.py +0 -0
  95. {arthexis-0.1.5 → arthexis-0.1.7}/pages/__init__.py +0 -0
  96. {arthexis-0.1.5 → arthexis-0.1.7}/pages/checks.py +0 -0
  97. {arthexis-0.1.5 → arthexis-0.1.7}/pages/context_processors.py +0 -0
  98. {arthexis-0.1.5 → arthexis-0.1.7}/pages/models.py +0 -0
  99. {arthexis-0.1.5 → arthexis-0.1.7}/pages/urls.py +0 -0
  100. {arthexis-0.1.5 → arthexis-0.1.7}/pages/utils.py +0 -0
  101. {arthexis-0.1.5 → arthexis-0.1.7}/setup.cfg +0 -0
  102. {arthexis-0.1.5 → arthexis-0.1.7}/tests/test_acronym_capitalization.py +0 -0
  103. {arthexis-0.1.5 → arthexis-0.1.7}/tests/test_admin_history.py +0 -0
  104. {arthexis-0.1.5 → arthexis-0.1.7}/tests/test_admin_index_actions.py +0 -0
  105. {arthexis-0.1.5 → arthexis-0.1.7}/tests/test_awg_admin.py +0 -0
  106. {arthexis-0.1.5 → arthexis-0.1.7}/tests/test_csrf_failure.py +0 -0
  107. {arthexis-0.1.5 → arthexis-0.1.7}/tests/test_csrf_origin_subnet.py +0 -0
  108. {arthexis-0.1.5 → arthexis-0.1.7}/tests/test_email_collector.py +0 -0
  109. {arthexis-0.1.5 → arthexis-0.1.7}/tests/test_email_inbox.py +0 -0
  110. {arthexis-0.1.5 → arthexis-0.1.7}/tests/test_email_inbox_admin.py +0 -0
  111. {arthexis-0.1.5 → arthexis-0.1.7}/tests/test_env_refresh_unlink.py +0 -0
  112. {arthexis-0.1.5 → arthexis-0.1.7}/tests/test_footer_no_references.py +0 -0
  113. {arthexis-0.1.5 → arthexis-0.1.7}/tests/test_footer_presence.py +0 -0
  114. {arthexis-0.1.5 → arthexis-0.1.7}/tests/test_footer_render.py +0 -0
  115. {arthexis-0.1.5 → arthexis-0.1.7}/tests/test_language_switch.py +0 -0
  116. {arthexis-0.1.5 → arthexis-0.1.7}/tests/test_lcd_smbus2.py +0 -0
  117. {arthexis-0.1.5 → arthexis-0.1.7}/tests/test_localhost_admin_backend.py +0 -0
  118. {arthexis-0.1.5 → arthexis-0.1.7}/tests/test_model_verbose_name_capitalization.py +0 -0
  119. {arthexis-0.1.5 → arthexis-0.1.7}/tests/test_notifications_fallback.py +0 -0
  120. {arthexis-0.1.5 → arthexis-0.1.7}/tests/test_notify_command.py +0 -0
  121. {arthexis-0.1.5 → arthexis-0.1.7}/tests/test_odoo_profile.py +0 -0
  122. {arthexis-0.1.5 → arthexis-0.1.7}/tests/test_odoo_profile_admin.py +0 -0
  123. {arthexis-0.1.5 → arthexis-0.1.7}/tests/test_offline.py +0 -0
  124. {arthexis-0.1.5 → arthexis-0.1.7}/tests/test_pypi_token.py +0 -0
  125. {arthexis-0.1.5 → arthexis-0.1.7}/tests/test_readme_language.py +0 -0
  126. {arthexis-0.1.5 → arthexis-0.1.7}/tests/test_reference_qr_code.py +0 -0
  127. {arthexis-0.1.5 → arthexis-0.1.7}/tests/test_reference_transaction_uuid.py +0 -0
  128. {arthexis-0.1.5 → arthexis-0.1.7}/tests/test_register_site_apps_command.py +0 -0
  129. {arthexis-0.1.5 → arthexis-0.1.7}/tests/test_release_logs.py +0 -0
  130. {arthexis-0.1.5 → arthexis-0.1.7}/tests/test_release_tasks.py +0 -0
  131. {arthexis-0.1.5 → arthexis-0.1.7}/tests/test_rfid_admin_reference_clear.py +0 -0
  132. {arthexis-0.1.5 → arthexis-0.1.7}/tests/test_rfid_admin_scan_csrf.py +0 -0
  133. {arthexis-0.1.5 → arthexis-0.1.7}/tests/test_rfid_background_reader.py +0 -0
  134. {arthexis-0.1.5 → arthexis-0.1.7}/tests/test_save_as_copy.py +0 -0
  135. {arthexis-0.1.5 → arthexis-0.1.7}/tests/test_seed_data.py +0 -0
  136. {arthexis-0.1.5 → arthexis-0.1.7}/tests/test_urls_autodiscover.py +0 -0
  137. {arthexis-0.1.5 → arthexis-0.1.7}/tests/test_vscode_manage.py +0 -0
@@ -1,8 +1,8 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: arthexis
3
- Version: 0.1.5
3
+ Version: 0.1.7
4
4
  Summary: Django-based MESH system
5
- Author-email: "Rafael J. Guillén-Osorio" <tecnologia@gelectriic.com>
5
+ Author-email: "Rafael J. Guillén-Osorio" <tecnologia@gelectriic.com>
6
6
  License-Expression: MIT
7
7
  Project-URL: Repository, https://github.com/arthexis/arthexis
8
8
  Project-URL: Homepage, https://arthexis.com
@@ -112,15 +112,15 @@ Dynamic: license-file
112
112
  Arthexis Constellation is a narrative-driven Django-based suite that centralizes tools for managing charging infrastructure and orchestrating energy related products and services.
113
113
 
114
114
  ## Features
115
- - Compatibility with all OCPP 1.6 certified chargers
116
- - Native API integration with Odoo 1.6-based CRMs
117
- - Runs everywhere Python 3.10+ is installed
118
- - Single codebase with special features per role
115
+ - Compatible with OCPP 1.6+ chargers
116
+ - API integration with Odoo 1.6+
117
+ - Runs on Windows and Linux (Ubuntu 16+)
118
+ - One codebase. Six* specialized Roles.
119
119
 
120
120
  ## Support
121
- You may contact us at tecnologia at gelectriic dot com or visit our [web page](https://www.gelectriic.com/) for professional services and commercial support.
121
+ Contact us at tecnologia at gelectriic dot com or visit our [web page](https://www.gelectriic.com/) for professional services and commercial support.
122
122
 
123
123
  ## About Me
124
- > "What? You want to know about me too? Well, I enjoy developing software, roleplaying games, long walks on the beach and a fourth secret thing."
124
+ > "What, you wanna know about me too? Well, I enjoy developing software, role-playing games, long walks on the beach and a fourth secret thing."
125
125
  > --Arthexis
126
126
 
@@ -0,0 +1,18 @@
1
+ # Arthexis Constellation
2
+
3
+ ## Purpose
4
+ Arthexis Constellation is a narrative-driven Django-based suite that centralizes tools for managing charging infrastructure and orchestrating energy related products and services.
5
+
6
+ ## Features
7
+ - Compatible with OCPP 1.6+ chargers
8
+ - API integration with Odoo 1.6+
9
+ - Runs on Windows and Linux (Ubuntu 16+)
10
+ - One codebase. Six* specialized Roles.
11
+
12
+ ## Support
13
+ Contact us at tecnologia at gelectriic dot com or visit our [web page](https://www.gelectriic.com/) for professional services and commercial support.
14
+
15
+ ## About Me
16
+ > "What, you wanna know about me too? Well, I enjoy developing software, role-playing games, long walks on the beach and a fourth secret thing."
17
+ > --Arthexis
18
+
@@ -1,8 +1,8 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: arthexis
3
- Version: 0.1.5
3
+ Version: 0.1.7
4
4
  Summary: Django-based MESH system
5
- Author-email: "Rafael J. Guillén-Osorio" <tecnologia@gelectriic.com>
5
+ Author-email: "Rafael J. Guillén-Osorio" <tecnologia@gelectriic.com>
6
6
  License-Expression: MIT
7
7
  Project-URL: Repository, https://github.com/arthexis/arthexis
8
8
  Project-URL: Homepage, https://arthexis.com
@@ -112,15 +112,15 @@ Dynamic: license-file
112
112
  Arthexis Constellation is a narrative-driven Django-based suite that centralizes tools for managing charging infrastructure and orchestrating energy related products and services.
113
113
 
114
114
  ## Features
115
- - Compatibility with all OCPP 1.6 certified chargers
116
- - Native API integration with Odoo 1.6-based CRMs
117
- - Runs everywhere Python 3.10+ is installed
118
- - Single codebase with special features per role
115
+ - Compatible with OCPP 1.6+ chargers
116
+ - API integration with Odoo 1.6+
117
+ - Runs on Windows and Linux (Ubuntu 16+)
118
+ - One codebase. Six* specialized Roles.
119
119
 
120
120
  ## Support
121
- You may contact us at tecnologia at gelectriic dot com or visit our [web page](https://www.gelectriic.com/) for professional services and commercial support.
121
+ Contact us at tecnologia at gelectriic dot com or visit our [web page](https://www.gelectriic.com/) for professional services and commercial support.
122
122
 
123
123
  ## About Me
124
- > "What? You want to know about me too? Well, I enjoy developing software, roleplaying games, long walks on the beach and a fourth secret thing."
124
+ > "What, you wanna know about me too? Well, I enjoy developing software, role-playing games, long walks on the beach and a fourth secret thing."
125
125
  > --Arthexis
126
126
 
@@ -12,17 +12,20 @@ config/asgi.py
12
12
  config/auth_app.py
13
13
  config/celery.py
14
14
  config/context_processors.py
15
+ config/horologia_app.py
15
16
  config/loadenv.py
16
17
  config/logging.py
17
18
  config/middleware.py
18
19
  config/offline.py
19
20
  config/settings.py
20
21
  config/urls.py
22
+ config/workgroup_app.py
21
23
  config/wsgi.py
22
24
  core/__init__.py
23
25
  core/admin.py
24
26
  core/apps.py
25
27
  core/backends.py
28
+ core/checks.py
26
29
  core/entity.py
27
30
  core/environment.py
28
31
  core/fields.py
@@ -33,6 +36,7 @@ core/notifications.py
33
36
  core/release.py
34
37
  core/system.py
35
38
  core/tasks.py
39
+ core/test_system_info.py
36
40
  core/tests.py
37
41
  core/urls.py
38
42
  core/user_data.py
@@ -78,19 +82,20 @@ tests/test_acronym_capitalization.py
78
82
  tests/test_admin_history.py
79
83
  tests/test_admin_index_actions.py
80
84
  tests/test_awg_admin.py
85
+ tests/test_celery_no_debug.py
81
86
  tests/test_csrf_failure.py
82
87
  tests/test_csrf_origin_subnet.py
88
+ tests/test_dist_cleanup.py
83
89
  tests/test_email_collector.py
84
90
  tests/test_email_inbox.py
85
91
  tests/test_email_inbox_admin.py
86
92
  tests/test_email_inbox_search_action.py
87
93
  tests/test_env_refresh_clean.py
88
94
  tests/test_env_refresh_unlink.py
89
- tests/test_footer_admin_link.py
95
+ tests/test_fixture_check.py
90
96
  tests/test_footer_no_references.py
91
97
  tests/test_footer_presence.py
92
98
  tests/test_footer_render.py
93
- tests/test_github_token.py
94
99
  tests/test_install_script.py
95
100
  tests/test_language_switch.py
96
101
  tests/test_lcd_smbus2.py
@@ -101,15 +106,12 @@ tests/test_notify_command.py
101
106
  tests/test_odoo_profile.py
102
107
  tests/test_odoo_profile_admin.py
103
108
  tests/test_offline.py
104
- tests/test_package_release_admin_actions.py
105
109
  tests/test_pypi_token.py
106
110
  tests/test_readme_language.py
107
111
  tests/test_reference_qr_code.py
108
112
  tests/test_reference_transaction_uuid.py
109
113
  tests/test_register_site_apps_command.py
110
- tests/test_release_fixture_cleanup.py
111
114
  tests/test_release_logs.py
112
- tests/test_release_mapping.py
113
115
  tests/test_release_progress.py
114
116
  tests/test_release_tasks.py
115
117
  tests/test_rfid_admin_reference_clear.py
@@ -120,4 +122,6 @@ tests/test_seed_data.py
120
122
  tests/test_sigil_resolution.py
121
123
  tests/test_urls_autodiscover.py
122
124
  tests/test_user_datum_admin.py
123
- tests/test_vscode_manage.py
125
+ tests/test_version_file.py
126
+ tests/test_vscode_manage.py
127
+ tests/test_workgroup_admin_group.py
@@ -7,6 +7,13 @@ from celery import Celery
7
7
 
8
8
  os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
9
9
 
10
+ # When running on production-oriented nodes, avoid Celery debug mode.
11
+ NODE_ROLE = os.environ.get("NODE_ROLE", "")
12
+ if NODE_ROLE in {"Constellation", "Satellite", "Virtual"}:
13
+ for var in ["CELERY_TRACE_APP", "CELERY_DEBUG"]:
14
+ os.environ.pop(var, None)
15
+ os.environ.setdefault("CELERY_LOG_LEVEL", "INFO")
16
+
10
17
  app = Celery("config")
11
18
  app.config_from_object("django.conf:settings", namespace="CELERY")
12
19
  app.autodiscover_tasks()
@@ -0,0 +1,7 @@
1
+ from django_celery_beat.apps import BeatConfig as BaseBeatConfig
2
+
3
+
4
+ class HorologiaConfig(BaseBeatConfig):
5
+ """Customize Periodic Tasks app label."""
6
+
7
+ verbose_name = "5. Horologia"
@@ -13,14 +13,17 @@ class ActiveAppFileHandler(TimedRotatingFileHandler):
13
13
  """File handler that writes to a file named after the active app."""
14
14
 
15
15
  def _current_file(self) -> Path:
16
+ log_dir = Path(settings.LOG_DIR)
17
+ log_dir.mkdir(parents=True, exist_ok=True)
16
18
  if "test" in sys.argv:
17
- return Path(settings.LOG_DIR) / "tests.log"
18
- return Path(settings.LOG_DIR) / f"{get_active_app()}.log"
19
+ return log_dir / "tests.log"
20
+ return log_dir / f"{get_active_app()}.log"
19
21
 
20
22
  def emit(self, record: logging.LogRecord) -> None:
21
23
  current = str(self._current_file())
22
24
  if self.baseFilename != current:
23
25
  self.baseFilename = current
26
+ Path(self.baseFilename).parent.mkdir(parents=True, exist_ok=True)
24
27
  if self.stream:
25
28
  self.stream.close()
26
29
  self.stream = self._open()
@@ -29,7 +32,9 @@ class ActiveAppFileHandler(TimedRotatingFileHandler):
29
32
  def rotation_filename(self, default_name: str) -> str:
30
33
  """Place rotated logs inside the old log directory."""
31
34
  default_path = Path(default_name)
32
- return str(Path(settings.OLD_LOG_DIR) / default_path.name)
35
+ old_log_dir = Path(settings.OLD_LOG_DIR)
36
+ old_log_dir.mkdir(parents=True, exist_ok=True)
37
+ return str(old_log_dir / default_path.name)
33
38
 
34
39
  def getFilesToDelete(self):
35
40
  """Return files to delete in the old log directory respecting backupCount."""
@@ -148,8 +148,8 @@ INSTALLED_APPS = [
148
148
  "django_object_actions",
149
149
  "django.contrib.sites",
150
150
  "channels",
151
- "post_office",
152
- "django_celery_beat",
151
+ "config.workgroup_app.WorkgroupConfig",
152
+ "config.horologia_app.HorologiaConfig",
153
153
  ] + LOCAL_APPS
154
154
 
155
155
  if DEBUG:
@@ -0,0 +1,7 @@
1
+ from post_office.apps import PostOfficeConfig as BasePostOfficeConfig
2
+
3
+
4
+ class WorkgroupConfig(BasePostOfficeConfig):
5
+ """Customize Post Office app label."""
6
+
7
+ verbose_name = "6. Workgroup"
@@ -21,6 +21,7 @@ from django.templatetags.static import static
21
21
  from django.utils.html import format_html
22
22
  import json
23
23
  import uuid
24
+ import requests
24
25
  from django_object_actions import DjangoObjectActions
25
26
  from .user_data import UserDatumAdminMixin
26
27
  from .models import (
@@ -38,11 +39,12 @@ from .models import (
38
39
  Reference,
39
40
  OdooProfile,
40
41
  FediverseProfile,
41
- EmailInbox,
42
+ EmailInbox as CoreEmailInbox,
42
43
  Package,
43
44
  PackageRelease,
44
45
  ReleaseManager,
45
46
  SecurityGroup,
47
+ InviteLead,
46
48
  )
47
49
  from .user_data import UserDatumAdminMixin
48
50
 
@@ -224,6 +226,20 @@ class SecurityGroupAdmin(DjangoGroupAdmin):
224
226
  filter_horizontal = ("permissions",)
225
227
 
226
228
 
229
+ @admin.register(InviteLead)
230
+ class InviteLeadAdmin(admin.ModelAdmin):
231
+ list_display = ("email", "created_on")
232
+ search_fields = ("email", "comment")
233
+ readonly_fields = (
234
+ "created_on",
235
+ "user",
236
+ "path",
237
+ "referer",
238
+ "user_agent",
239
+ "ip_address",
240
+ )
241
+
242
+
227
243
  class EnergyAccountRFIDForm(forms.ModelForm):
228
244
  """Form for assigning existing RFIDs to an energy account."""
229
245
 
@@ -380,6 +396,14 @@ class FediverseProfileAdmin(admin.ModelAdmin):
380
396
  self.message_user(request, f"{profile}: {exc}", level=messages.ERROR)
381
397
 
382
398
 
399
+ class EmailInbox(CoreEmailInbox):
400
+ class Meta:
401
+ proxy = True
402
+ app_label = "post_office"
403
+ verbose_name = CoreEmailInbox._meta.verbose_name
404
+ verbose_name_plural = CoreEmailInbox._meta.verbose_name_plural
405
+
406
+
383
407
  class EmailInboxAdminForm(forms.ModelForm):
384
408
  """Admin form for :class:`core.models.EmailInbox` with hidden password."""
385
409
 
@@ -390,7 +414,7 @@ class EmailInboxAdminForm(forms.ModelForm):
390
414
  )
391
415
 
392
416
  class Meta:
393
- model = EmailInbox
417
+ model = CoreEmailInbox
394
418
  fields = "__all__"
395
419
 
396
420
  def __init__(self, *args, **kwargs):
@@ -445,6 +469,10 @@ class EmailInboxAdmin(admin.ModelAdmin):
445
469
  ),
446
470
  )
447
471
 
472
+ def save_model(self, request, obj, form, change):
473
+ super().save_model(request, obj, form, change)
474
+ obj.__class__ = EmailInbox
475
+
448
476
  @admin.action(description="Test selected inboxes")
449
477
  def test_connection(self, request, queryset):
450
478
  for inbox in queryset:
@@ -772,17 +800,16 @@ class RFIDAdmin(ImportExportModelAdmin):
772
800
  class PackageReleaseAdmin(SaveBeforeChangeAction, admin.ModelAdmin):
773
801
  list_display = (
774
802
  "version",
775
- "package",
803
+ "package_link",
776
804
  "is_current",
777
805
  "pypi_url",
778
- "pr_link",
779
806
  "revision_short",
780
807
  "published_status",
781
808
  )
782
809
  list_display_links = ("version",)
783
- actions = ["publish_release"]
810
+ actions = ["publish_release", "validate_releases"]
784
811
  change_actions = ["publish_release_action"]
785
- readonly_fields = ("pypi_url", "pr_url", "is_current", "revision")
812
+ readonly_fields = ("pypi_url", "is_current", "revision")
786
813
  fields = (
787
814
  "package",
788
815
  "release_manager",
@@ -790,9 +817,13 @@ class PackageReleaseAdmin(SaveBeforeChangeAction, admin.ModelAdmin):
790
817
  "revision",
791
818
  "is_current",
792
819
  "pypi_url",
793
- "pr_url",
794
820
  )
795
821
 
822
+ @admin.display(description="package", ordering="package")
823
+ def package_link(self, obj):
824
+ url = reverse("admin:core_package_change", args=[obj.package_id])
825
+ return format_html('<a href="{}">{}</a>', url, obj.package)
826
+
796
827
  def revision_short(self, obj):
797
828
  return obj.revision_short
798
829
 
@@ -821,6 +852,37 @@ class PackageReleaseAdmin(SaveBeforeChangeAction, admin.ModelAdmin):
821
852
  publish_release_action.label = "Publish selected Release"
822
853
  publish_release_action.short_description = "Publish this release"
823
854
 
855
+ @admin.action(description="Validate selected Releases")
856
+ def validate_releases(self, request, queryset):
857
+ deleted = False
858
+ for release in queryset:
859
+ if not release.pypi_url:
860
+ self.message_user(
861
+ request,
862
+ f"{release} has not been published yet",
863
+ messages.WARNING,
864
+ )
865
+ continue
866
+ url = (
867
+ f"https://pypi.org/pypi/{release.package.name}/{release.version}/json"
868
+ )
869
+ try:
870
+ resp = requests.get(url, timeout=10)
871
+ except Exception as exc: # pragma: no cover - network failure
872
+ self.message_user(request, f"{release}: {exc}", messages.ERROR)
873
+ continue
874
+ if resp.status_code == 200:
875
+ continue
876
+ release.delete()
877
+ deleted = True
878
+ self.message_user(
879
+ request,
880
+ f"Deleted {release} as it was not found on PyPI",
881
+ messages.WARNING,
882
+ )
883
+ if deleted:
884
+ PackageRelease.dump_fixture()
885
+
824
886
  @staticmethod
825
887
  def _boolean_icon(value: bool) -> str:
826
888
  icon = static("admin/img/icon-yes.svg" if value else "admin/img/icon-no.svg")
@@ -835,10 +897,4 @@ class PackageReleaseAdmin(SaveBeforeChangeAction, admin.ModelAdmin):
835
897
  def is_current(self, obj):
836
898
  return self._boolean_icon(obj.is_current)
837
899
 
838
- def pr_link(self, obj):
839
- if obj.pr_url:
840
- return format_html('<a href="{0}" target="_blank">{0}</a>', obj.pr_url)
841
- return ""
842
-
843
- pr_link.short_description = "PR URL"
844
900
 
@@ -5,7 +5,7 @@ from django.utils.translation import gettext_lazy as _
5
5
  class CoreConfig(AppConfig):
6
6
  default_auto_field = "django.db.models.BigAutoField"
7
7
  name = "core"
8
- verbose_name = _("Business Models")
8
+ verbose_name = _("2. Business")
9
9
 
10
10
  def ready(self): # pragma: no cover - called by Django
11
11
  from django.contrib.auth import get_user_model
@@ -16,6 +16,7 @@ class CoreConfig(AppConfig):
16
16
  )
17
17
  from .system import patch_admin_system_view
18
18
  from .environment import patch_admin_environment_view
19
+ from . import checks # noqa: F401
19
20
 
20
21
  def create_default_arthexis(**kwargs):
21
22
  User = get_user_model()
@@ -0,0 +1,29 @@
1
+ import hashlib
2
+ from pathlib import Path
3
+ from django.conf import settings
4
+ from django.core import checks
5
+
6
+
7
+ def _fixture_hash() -> str:
8
+ base_dir = Path(settings.BASE_DIR)
9
+ md5 = hashlib.md5()
10
+ for path in sorted(base_dir.glob("**/fixtures/*.json")):
11
+ md5.update(path.read_bytes())
12
+ return md5.hexdigest()
13
+
14
+
15
+ @checks.register(checks.Tags.database)
16
+ def check_unapplied_fixtures(app_configs=None, **kwargs):
17
+ """Warn if fixture files have changed since last refresh."""
18
+ hash_file = Path(settings.BASE_DIR) / "fixtures.md5"
19
+ stored = hash_file.read_text().strip() if hash_file.exists() else ""
20
+ current = _fixture_hash()
21
+ if stored != current:
22
+ return [
23
+ checks.Warning(
24
+ "Unapplied fixture changes detected.",
25
+ hint="Run env-refresh to apply fixtures.",
26
+ id="core.W001",
27
+ )
28
+ ]
29
+ return []
@@ -1,4 +1,5 @@
1
1
  import copy
2
+ import logging
2
3
  import os
3
4
  import re
4
5
 
@@ -7,6 +8,8 @@ from django.conf import settings
7
8
  from django.db import models
8
9
  from django.contrib.auth.models import UserManager as DjangoUserManager
9
10
 
11
+ logger = logging.getLogger(__name__)
12
+
10
13
 
11
14
  class EntityQuerySet(models.QuerySet):
12
15
  def delete(self): # pragma: no cover - delegates to instance delete
@@ -78,14 +81,33 @@ class Entity(models.Model):
78
81
  root_name, key = match.group(1), match.group(2)
79
82
  try:
80
83
  root = SigilRoot.objects.get(prefix__iexact=root_name)
84
+ if root.context_type == SigilRoot.Context.CONFIG:
85
+ if root.prefix.upper() == "ENV":
86
+ if key in os.environ:
87
+ return os.environ[key]
88
+ logger.warning(
89
+ "Missing environment variable for sigil [%s.%s]",
90
+ root_name,
91
+ key,
92
+ )
93
+ return match.group(0)
94
+ if root.prefix.upper() == "SYS":
95
+ if hasattr(settings, key):
96
+ return str(getattr(settings, key))
97
+ logger.warning(
98
+ "Missing settings attribute for sigil [%s.%s]",
99
+ root_name,
100
+ key,
101
+ )
102
+ return match.group(0)
103
+ logger.warning(
104
+ "Unresolvable sigil [%s.%s]: unsupported context", root_name, key
105
+ )
81
106
  except SigilRoot.DoesNotExist:
82
- return ""
83
- if root.context_type == SigilRoot.Context.CONFIG:
84
- if root.prefix.upper() == "ENV":
85
- return os.environ.get(key, "")
86
- if root.prefix.upper() == "SYS":
87
- return str(getattr(settings, key, ""))
88
- return ""
107
+ logger.warning("Unknown sigil root [%s]", root_name)
108
+ except Exception:
109
+ logger.exception("Error resolving sigil [%s.%s]", root_name, key)
110
+ return match.group(0)
89
111
 
90
112
  return pattern.sub(repl, text)
91
113
 
@@ -10,12 +10,13 @@ from django.utils.translation import gettext_lazy as _
10
10
  from django.core.validators import RegexValidator
11
11
  from django.core.exceptions import ValidationError
12
12
  from django.apps import apps
13
- from django.db.models.signals import m2m_changed, post_delete, post_save
13
+ from django.db.models.signals import m2m_changed
14
14
  from django.dispatch import receiver
15
15
  from datetime import timedelta
16
16
  from django.contrib.contenttypes.models import ContentType
17
17
  import hashlib
18
18
  import os
19
+ import subprocess
19
20
  from io import BytesIO
20
21
  from django.core.files.base import ContentFile
21
22
  import qrcode
@@ -62,6 +63,34 @@ class SigilRoot(Entity):
62
63
  verbose_name_plural = "Sigil Roots"
63
64
 
64
65
 
66
+ class Lead(models.Model):
67
+ """Common request lead information."""
68
+
69
+ user = models.ForeignKey(
70
+ settings.AUTH_USER_MODEL, null=True, blank=True, on_delete=models.SET_NULL
71
+ )
72
+ path = models.TextField(blank=True)
73
+ referer = models.TextField(blank=True)
74
+ user_agent = models.TextField(blank=True)
75
+ ip_address = models.GenericIPAddressField(null=True, blank=True)
76
+ created_on = models.DateTimeField(auto_now_add=True)
77
+
78
+ class Meta:
79
+ abstract = True
80
+
81
+
82
+ class InviteLead(Lead):
83
+ email = models.EmailField()
84
+ comment = models.TextField(blank=True)
85
+
86
+ class Meta:
87
+ verbose_name = "Invite Lead"
88
+ verbose_name_plural = "Invite Leads"
89
+
90
+ def __str__(self) -> str: # pragma: no cover - simple representation
91
+ return self.email
92
+
93
+
65
94
  class Address(Entity):
66
95
  """Physical location information for a user."""
67
96
 
@@ -1075,7 +1104,7 @@ class ReleaseManager(Entity):
1075
1104
  max_length=200,
1076
1105
  blank=True,
1077
1106
  help_text=(
1078
- "Personal access token used to create GitHub pull requests. "
1107
+ "Personal access token for GitHub operations. "
1079
1108
  "Used before the GITHUB_TOKEN environment variable."
1080
1109
  ),
1081
1110
  )
@@ -1159,7 +1188,6 @@ class PackageRelease(Entity):
1159
1188
  max_length=40, blank=True, default=revision_utils.get_revision, editable=False
1160
1189
  )
1161
1190
  pypi_url = models.URLField("PyPI URL", blank=True, editable=False)
1162
- pr_url = models.URLField("PR URL", blank=True, editable=False)
1163
1191
 
1164
1192
  class Meta:
1165
1193
  verbose_name = "Package Release"
@@ -1251,17 +1279,29 @@ class PackageRelease(Entity):
1251
1279
  )
1252
1280
  self.revision = revision_utils.get_revision()
1253
1281
  self.save(update_fields=["revision"])
1282
+ PackageRelease.dump_fixture()
1283
+ if kwargs.get("git"):
1284
+ diff = subprocess.run(
1285
+ ["git", "status", "--porcelain", "core/fixtures/releases.json"],
1286
+ capture_output=True,
1287
+ text=True,
1288
+ )
1289
+ if diff.stdout.strip():
1290
+ release_utils._run(["git", "add", "core/fixtures/releases.json"])
1291
+ release_utils._run(
1292
+ [
1293
+ "git",
1294
+ "commit",
1295
+ "-m",
1296
+ f"chore: update release fixture for v{self.version}",
1297
+ ]
1298
+ )
1299
+ release_utils._run(["git", "push"])
1254
1300
 
1255
1301
  @property
1256
1302
  def revision_short(self) -> str:
1257
1303
  return self.revision[-6:] if self.revision else ""
1258
1304
 
1259
-
1260
- @receiver([post_save, post_delete], sender=PackageRelease)
1261
- def _update_release_fixture(sender, instance, **kwargs) -> None:
1262
- """Keep the release fixture in sync with the database."""
1263
- PackageRelease.dump_fixture()
1264
-
1265
1305
  # Ensure each RFID can only be linked to one energy account
1266
1306
  @receiver(m2m_changed, sender=EnergyAccount.rfids.through)
1267
1307
  def _rfid_unique_energy_account(sender, instance, action, reverse, model, pk_set, **kwargs):