arthexis 0.1.7__tar.gz → 0.1.8__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 (140) hide show
  1. {arthexis-0.1.7 → arthexis-0.1.8}/PKG-INFO +13 -9
  2. arthexis-0.1.8/README.md +22 -0
  3. {arthexis-0.1.7 → arthexis-0.1.8}/arthexis.egg-info/PKG-INFO +13 -9
  4. {arthexis-0.1.7 → arthexis-0.1.8}/arthexis.egg-info/SOURCES.txt +10 -0
  5. {arthexis-0.1.7 → arthexis-0.1.8}/config/settings.py +1 -0
  6. {arthexis-0.1.7 → arthexis-0.1.8}/config/urls.py +9 -0
  7. {arthexis-0.1.7 → arthexis-0.1.8}/core/admin.py +137 -6
  8. arthexis-0.1.8/core/admindocs.py +44 -0
  9. {arthexis-0.1.7 → arthexis-0.1.8}/core/models.py +75 -5
  10. {arthexis-0.1.7 → arthexis-0.1.8}/core/tests.py +94 -2
  11. arthexis-0.1.8/core/workgroup_urls.py +13 -0
  12. arthexis-0.1.8/core/workgroup_views.py +57 -0
  13. {arthexis-0.1.7 → arthexis-0.1.8}/ocpp/apps.py +3 -2
  14. {arthexis-0.1.7 → arthexis-0.1.8}/pyproject.toml +2 -2
  15. arthexis-0.1.8/tests/test_admin_doc_commands.py +32 -0
  16. arthexis-0.1.8/tests/test_business_admin_group.py +27 -0
  17. arthexis-0.1.8/tests/test_chat_profile_admin.py +38 -0
  18. arthexis-0.1.8/tests/test_chat_profile_api.py +31 -0
  19. arthexis-0.1.8/tests/test_experience_admin_group.py +27 -0
  20. {arthexis-0.1.7 → arthexis-0.1.8}/tests/test_reference_qr_code.py +1 -1
  21. {arthexis-0.1.7 → arthexis-0.1.8}/tests/test_rfid_background_reader.py +15 -5
  22. arthexis-0.1.8/tests/test_show_leads_command.py +63 -0
  23. arthexis-0.1.8/tests/test_update_fixtures_command.py +28 -0
  24. {arthexis-0.1.7 → arthexis-0.1.8}/tests/test_workgroup_admin_group.py +16 -1
  25. arthexis-0.1.7/README.md +0 -18
  26. {arthexis-0.1.7 → arthexis-0.1.8}/LICENSE +0 -0
  27. {arthexis-0.1.7 → arthexis-0.1.8}/arthexis.egg-info/dependency_links.txt +0 -0
  28. {arthexis-0.1.7 → arthexis-0.1.8}/arthexis.egg-info/requires.txt +0 -0
  29. {arthexis-0.1.7 → arthexis-0.1.8}/arthexis.egg-info/top_level.txt +0 -0
  30. {arthexis-0.1.7 → arthexis-0.1.8}/config/__init__.py +0 -0
  31. {arthexis-0.1.7 → arthexis-0.1.8}/config/active_app.py +0 -0
  32. {arthexis-0.1.7 → arthexis-0.1.8}/config/asgi.py +0 -0
  33. {arthexis-0.1.7 → arthexis-0.1.8}/config/auth_app.py +0 -0
  34. {arthexis-0.1.7 → arthexis-0.1.8}/config/celery.py +0 -0
  35. {arthexis-0.1.7 → arthexis-0.1.8}/config/context_processors.py +0 -0
  36. {arthexis-0.1.7 → arthexis-0.1.8}/config/horologia_app.py +0 -0
  37. {arthexis-0.1.7 → arthexis-0.1.8}/config/loadenv.py +0 -0
  38. {arthexis-0.1.7 → arthexis-0.1.8}/config/logging.py +0 -0
  39. {arthexis-0.1.7 → arthexis-0.1.8}/config/middleware.py +0 -0
  40. {arthexis-0.1.7 → arthexis-0.1.8}/config/offline.py +0 -0
  41. {arthexis-0.1.7 → arthexis-0.1.8}/config/workgroup_app.py +0 -0
  42. {arthexis-0.1.7 → arthexis-0.1.8}/config/wsgi.py +0 -0
  43. {arthexis-0.1.7 → arthexis-0.1.8}/core/__init__.py +0 -0
  44. {arthexis-0.1.7 → arthexis-0.1.8}/core/apps.py +0 -0
  45. {arthexis-0.1.7 → arthexis-0.1.8}/core/backends.py +0 -0
  46. {arthexis-0.1.7 → arthexis-0.1.8}/core/checks.py +0 -0
  47. {arthexis-0.1.7 → arthexis-0.1.8}/core/entity.py +0 -0
  48. {arthexis-0.1.7 → arthexis-0.1.8}/core/environment.py +0 -0
  49. {arthexis-0.1.7 → arthexis-0.1.8}/core/fields.py +0 -0
  50. {arthexis-0.1.7 → arthexis-0.1.8}/core/lcd_screen.py +0 -0
  51. {arthexis-0.1.7 → arthexis-0.1.8}/core/middleware.py +0 -0
  52. {arthexis-0.1.7 → arthexis-0.1.8}/core/notifications.py +0 -0
  53. {arthexis-0.1.7 → arthexis-0.1.8}/core/release.py +0 -0
  54. {arthexis-0.1.7 → arthexis-0.1.8}/core/system.py +0 -0
  55. {arthexis-0.1.7 → arthexis-0.1.8}/core/tasks.py +0 -0
  56. {arthexis-0.1.7 → arthexis-0.1.8}/core/test_system_info.py +0 -0
  57. {arthexis-0.1.7 → arthexis-0.1.8}/core/urls.py +0 -0
  58. {arthexis-0.1.7 → arthexis-0.1.8}/core/user_data.py +0 -0
  59. {arthexis-0.1.7 → arthexis-0.1.8}/core/views.py +0 -0
  60. {arthexis-0.1.7 → arthexis-0.1.8}/nodes/__init__.py +0 -0
  61. {arthexis-0.1.7 → arthexis-0.1.8}/nodes/actions.py +0 -0
  62. {arthexis-0.1.7 → arthexis-0.1.8}/nodes/admin.py +0 -0
  63. {arthexis-0.1.7 → arthexis-0.1.8}/nodes/apps.py +0 -0
  64. {arthexis-0.1.7 → arthexis-0.1.8}/nodes/lcd.py +0 -0
  65. {arthexis-0.1.7 → arthexis-0.1.8}/nodes/models.py +0 -0
  66. {arthexis-0.1.7 → arthexis-0.1.8}/nodes/tasks.py +0 -0
  67. {arthexis-0.1.7 → arthexis-0.1.8}/nodes/tests.py +0 -0
  68. {arthexis-0.1.7 → arthexis-0.1.8}/nodes/urls.py +0 -0
  69. {arthexis-0.1.7 → arthexis-0.1.8}/nodes/utils.py +0 -0
  70. {arthexis-0.1.7 → arthexis-0.1.8}/nodes/views.py +0 -0
  71. {arthexis-0.1.7 → arthexis-0.1.8}/ocpp/__init__.py +0 -0
  72. {arthexis-0.1.7 → arthexis-0.1.8}/ocpp/admin.py +0 -0
  73. {arthexis-0.1.7 → arthexis-0.1.8}/ocpp/consumers.py +0 -0
  74. {arthexis-0.1.7 → arthexis-0.1.8}/ocpp/evcs.py +0 -0
  75. {arthexis-0.1.7 → arthexis-0.1.8}/ocpp/models.py +0 -0
  76. {arthexis-0.1.7 → arthexis-0.1.8}/ocpp/routing.py +0 -0
  77. {arthexis-0.1.7 → arthexis-0.1.8}/ocpp/simulator.py +0 -0
  78. {arthexis-0.1.7 → arthexis-0.1.8}/ocpp/store.py +0 -0
  79. {arthexis-0.1.7 → arthexis-0.1.8}/ocpp/tasks.py +0 -0
  80. {arthexis-0.1.7 → arthexis-0.1.8}/ocpp/test_export_import.py +0 -0
  81. {arthexis-0.1.7 → arthexis-0.1.8}/ocpp/test_rfid.py +0 -0
  82. {arthexis-0.1.7 → arthexis-0.1.8}/ocpp/tests.py +0 -0
  83. {arthexis-0.1.7 → arthexis-0.1.8}/ocpp/transactions_io.py +0 -0
  84. {arthexis-0.1.7 → arthexis-0.1.8}/ocpp/urls.py +0 -0
  85. {arthexis-0.1.7 → arthexis-0.1.8}/ocpp/views.py +0 -0
  86. {arthexis-0.1.7 → arthexis-0.1.8}/pages/__init__.py +0 -0
  87. {arthexis-0.1.7 → arthexis-0.1.8}/pages/admin.py +0 -0
  88. {arthexis-0.1.7 → arthexis-0.1.8}/pages/apps.py +0 -0
  89. {arthexis-0.1.7 → arthexis-0.1.8}/pages/checks.py +0 -0
  90. {arthexis-0.1.7 → arthexis-0.1.8}/pages/context_processors.py +0 -0
  91. {arthexis-0.1.7 → arthexis-0.1.8}/pages/models.py +0 -0
  92. {arthexis-0.1.7 → arthexis-0.1.8}/pages/tests.py +0 -0
  93. {arthexis-0.1.7 → arthexis-0.1.8}/pages/urls.py +0 -0
  94. {arthexis-0.1.7 → arthexis-0.1.8}/pages/utils.py +0 -0
  95. {arthexis-0.1.7 → arthexis-0.1.8}/pages/views.py +0 -0
  96. {arthexis-0.1.7 → arthexis-0.1.8}/setup.cfg +0 -0
  97. {arthexis-0.1.7 → arthexis-0.1.8}/tests/test_acronym_capitalization.py +0 -0
  98. {arthexis-0.1.7 → arthexis-0.1.8}/tests/test_admin_history.py +0 -0
  99. {arthexis-0.1.7 → arthexis-0.1.8}/tests/test_admin_index_actions.py +0 -0
  100. {arthexis-0.1.7 → arthexis-0.1.8}/tests/test_awg_admin.py +0 -0
  101. {arthexis-0.1.7 → arthexis-0.1.8}/tests/test_celery_no_debug.py +0 -0
  102. {arthexis-0.1.7 → arthexis-0.1.8}/tests/test_csrf_failure.py +0 -0
  103. {arthexis-0.1.7 → arthexis-0.1.8}/tests/test_csrf_origin_subnet.py +0 -0
  104. {arthexis-0.1.7 → arthexis-0.1.8}/tests/test_dist_cleanup.py +0 -0
  105. {arthexis-0.1.7 → arthexis-0.1.8}/tests/test_email_collector.py +0 -0
  106. {arthexis-0.1.7 → arthexis-0.1.8}/tests/test_email_inbox.py +0 -0
  107. {arthexis-0.1.7 → arthexis-0.1.8}/tests/test_email_inbox_admin.py +0 -0
  108. {arthexis-0.1.7 → arthexis-0.1.8}/tests/test_email_inbox_search_action.py +0 -0
  109. {arthexis-0.1.7 → arthexis-0.1.8}/tests/test_env_refresh_clean.py +0 -0
  110. {arthexis-0.1.7 → arthexis-0.1.8}/tests/test_env_refresh_unlink.py +0 -0
  111. {arthexis-0.1.7 → arthexis-0.1.8}/tests/test_fixture_check.py +0 -0
  112. {arthexis-0.1.7 → arthexis-0.1.8}/tests/test_footer_no_references.py +0 -0
  113. {arthexis-0.1.7 → arthexis-0.1.8}/tests/test_footer_presence.py +0 -0
  114. {arthexis-0.1.7 → arthexis-0.1.8}/tests/test_footer_render.py +0 -0
  115. {arthexis-0.1.7 → arthexis-0.1.8}/tests/test_install_script.py +0 -0
  116. {arthexis-0.1.7 → arthexis-0.1.8}/tests/test_language_switch.py +0 -0
  117. {arthexis-0.1.7 → arthexis-0.1.8}/tests/test_lcd_smbus2.py +0 -0
  118. {arthexis-0.1.7 → arthexis-0.1.8}/tests/test_localhost_admin_backend.py +0 -0
  119. {arthexis-0.1.7 → arthexis-0.1.8}/tests/test_model_verbose_name_capitalization.py +0 -0
  120. {arthexis-0.1.7 → arthexis-0.1.8}/tests/test_notifications_fallback.py +0 -0
  121. {arthexis-0.1.7 → arthexis-0.1.8}/tests/test_notify_command.py +0 -0
  122. {arthexis-0.1.7 → arthexis-0.1.8}/tests/test_odoo_profile.py +0 -0
  123. {arthexis-0.1.7 → arthexis-0.1.8}/tests/test_odoo_profile_admin.py +0 -0
  124. {arthexis-0.1.7 → arthexis-0.1.8}/tests/test_offline.py +0 -0
  125. {arthexis-0.1.7 → arthexis-0.1.8}/tests/test_pypi_token.py +0 -0
  126. {arthexis-0.1.7 → arthexis-0.1.8}/tests/test_readme_language.py +0 -0
  127. {arthexis-0.1.7 → arthexis-0.1.8}/tests/test_reference_transaction_uuid.py +0 -0
  128. {arthexis-0.1.7 → arthexis-0.1.8}/tests/test_register_site_apps_command.py +0 -0
  129. {arthexis-0.1.7 → arthexis-0.1.8}/tests/test_release_logs.py +0 -0
  130. {arthexis-0.1.7 → arthexis-0.1.8}/tests/test_release_progress.py +0 -0
  131. {arthexis-0.1.7 → arthexis-0.1.8}/tests/test_release_tasks.py +0 -0
  132. {arthexis-0.1.7 → arthexis-0.1.8}/tests/test_rfid_admin_reference_clear.py +0 -0
  133. {arthexis-0.1.7 → arthexis-0.1.8}/tests/test_rfid_admin_scan_csrf.py +0 -0
  134. {arthexis-0.1.7 → arthexis-0.1.8}/tests/test_save_as_copy.py +0 -0
  135. {arthexis-0.1.7 → arthexis-0.1.8}/tests/test_seed_data.py +0 -0
  136. {arthexis-0.1.7 → arthexis-0.1.8}/tests/test_sigil_resolution.py +0 -0
  137. {arthexis-0.1.7 → arthexis-0.1.8}/tests/test_urls_autodiscover.py +0 -0
  138. {arthexis-0.1.7 → arthexis-0.1.8}/tests/test_user_datum_admin.py +0 -0
  139. {arthexis-0.1.7 → arthexis-0.1.8}/tests/test_version_file.py +0 -0
  140. {arthexis-0.1.7 → arthexis-0.1.8}/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.7
3
+ Version: 0.1.8
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
@@ -109,18 +109,22 @@ Dynamic: license-file
109
109
  # Arthexis Constellation
110
110
 
111
111
  ## Purpose
112
- Arthexis Constellation is a narrative-driven Django-based suite that centralizes tools for managing charging infrastructure and orchestrating energy related products and services.
112
+
113
+ Arthexis Constellation is a [narrative-driven](https://en.wikipedia.org/wiki/Narrative) [Django](https://www.djangoproject.com/)-based [software suite](https://en.wikipedia.org/wiki/Software_suite) that centralizes tools for managing [electric vehicle charging infrastructure](https://en.wikipedia.org/wiki/Charging_station) and orchestrating [energy](https://en.wikipedia.org/wiki/Energy)-related [products](https://en.wikipedia.org/wiki/Product_(business)) and [services](https://en.wikipedia.org/wiki/Service_(economics)).
113
114
 
114
115
  ## Features
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.
116
+
117
+ - Compatible with the [Open Charge Point Protocol (OCPP) 1.6](https://www.openchargealliance.org/protocols/ocpp-16/)
118
+ - [API](https://en.wikipedia.org/wiki/API) integration with [Odoo](https://www.odoo.com/) 1.6
119
+ - Runs on [Windows 11](https://www.microsoft.com/windows/windows-11) and [Ubuntu 22.04 LTS](https://releases.ubuntu.com/22.04/)
120
+ - Tested for the [Raspberry Pi 4 Model B](https://www.raspberrypi.com/products/raspberry-pi-4-model-b/)
119
121
 
120
122
  ## 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.
123
+
124
+ Contact us at [tecnologia@gelectriic.com](mailto:tecnologia@gelectriic.com) or visit our [web page](https://www.gelectriic.com/) for [professional services](https://en.wikipedia.org/wiki/Professional_services) and [commercial support](https://en.wikipedia.org/wiki/Technical_support).
122
125
 
123
126
  ## About Me
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."
127
+
128
+ > "What, you want to know about me too? Well, I enjoy [developing software](https://en.wikipedia.org/wiki/Software_development), [role-playing games](https://en.wikipedia.org/wiki/Role-playing_game), long walks on the [beach](https://en.wikipedia.org/wiki/Beach) and a fourth secret thing."
125
129
  > --Arthexis
126
130
 
@@ -0,0 +1,22 @@
1
+ # Arthexis Constellation
2
+
3
+ ## Purpose
4
+
5
+ Arthexis Constellation is a [narrative-driven](https://en.wikipedia.org/wiki/Narrative) [Django](https://www.djangoproject.com/)-based [software suite](https://en.wikipedia.org/wiki/Software_suite) that centralizes tools for managing [electric vehicle charging infrastructure](https://en.wikipedia.org/wiki/Charging_station) and orchestrating [energy](https://en.wikipedia.org/wiki/Energy)-related [products](https://en.wikipedia.org/wiki/Product_(business)) and [services](https://en.wikipedia.org/wiki/Service_(economics)).
6
+
7
+ ## Features
8
+
9
+ - Compatible with the [Open Charge Point Protocol (OCPP) 1.6](https://www.openchargealliance.org/protocols/ocpp-16/)
10
+ - [API](https://en.wikipedia.org/wiki/API) integration with [Odoo](https://www.odoo.com/) 1.6
11
+ - Runs on [Windows 11](https://www.microsoft.com/windows/windows-11) and [Ubuntu 22.04 LTS](https://releases.ubuntu.com/22.04/)
12
+ - Tested for the [Raspberry Pi 4 Model B](https://www.raspberrypi.com/products/raspberry-pi-4-model-b/)
13
+
14
+ ## Support
15
+
16
+ Contact us at [tecnologia@gelectriic.com](mailto:tecnologia@gelectriic.com) or visit our [web page](https://www.gelectriic.com/) for [professional services](https://en.wikipedia.org/wiki/Professional_services) and [commercial support](https://en.wikipedia.org/wiki/Technical_support).
17
+
18
+ ## About Me
19
+
20
+ > "What, you want to know about me too? Well, I enjoy [developing software](https://en.wikipedia.org/wiki/Software_development), [role-playing games](https://en.wikipedia.org/wiki/Role-playing_game), long walks on the [beach](https://en.wikipedia.org/wiki/Beach) and a fourth secret thing."
21
+ > --Arthexis
22
+
@@ -1,8 +1,8 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: arthexis
3
- Version: 0.1.7
3
+ Version: 0.1.8
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
@@ -109,18 +109,22 @@ Dynamic: license-file
109
109
  # Arthexis Constellation
110
110
 
111
111
  ## Purpose
112
- Arthexis Constellation is a narrative-driven Django-based suite that centralizes tools for managing charging infrastructure and orchestrating energy related products and services.
112
+
113
+ Arthexis Constellation is a [narrative-driven](https://en.wikipedia.org/wiki/Narrative) [Django](https://www.djangoproject.com/)-based [software suite](https://en.wikipedia.org/wiki/Software_suite) that centralizes tools for managing [electric vehicle charging infrastructure](https://en.wikipedia.org/wiki/Charging_station) and orchestrating [energy](https://en.wikipedia.org/wiki/Energy)-related [products](https://en.wikipedia.org/wiki/Product_(business)) and [services](https://en.wikipedia.org/wiki/Service_(economics)).
113
114
 
114
115
  ## Features
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.
116
+
117
+ - Compatible with the [Open Charge Point Protocol (OCPP) 1.6](https://www.openchargealliance.org/protocols/ocpp-16/)
118
+ - [API](https://en.wikipedia.org/wiki/API) integration with [Odoo](https://www.odoo.com/) 1.6
119
+ - Runs on [Windows 11](https://www.microsoft.com/windows/windows-11) and [Ubuntu 22.04 LTS](https://releases.ubuntu.com/22.04/)
120
+ - Tested for the [Raspberry Pi 4 Model B](https://www.raspberrypi.com/products/raspberry-pi-4-model-b/)
119
121
 
120
122
  ## 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.
123
+
124
+ Contact us at [tecnologia@gelectriic.com](mailto:tecnologia@gelectriic.com) or visit our [web page](https://www.gelectriic.com/) for [professional services](https://en.wikipedia.org/wiki/Professional_services) and [commercial support](https://en.wikipedia.org/wiki/Technical_support).
122
125
 
123
126
  ## About Me
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."
127
+
128
+ > "What, you want to know about me too? Well, I enjoy [developing software](https://en.wikipedia.org/wiki/Software_development), [role-playing games](https://en.wikipedia.org/wiki/Role-playing_game), long walks on the [beach](https://en.wikipedia.org/wiki/Beach) and a fourth secret thing."
125
129
  > --Arthexis
126
130
 
@@ -23,6 +23,7 @@ config/workgroup_app.py
23
23
  config/wsgi.py
24
24
  core/__init__.py
25
25
  core/admin.py
26
+ core/admindocs.py
26
27
  core/apps.py
27
28
  core/backends.py
28
29
  core/checks.py
@@ -41,6 +42,8 @@ core/tests.py
41
42
  core/urls.py
42
43
  core/user_data.py
43
44
  core/views.py
45
+ core/workgroup_urls.py
46
+ core/workgroup_views.py
44
47
  nodes/__init__.py
45
48
  nodes/actions.py
46
49
  nodes/admin.py
@@ -79,10 +82,14 @@ pages/urls.py
79
82
  pages/utils.py
80
83
  pages/views.py
81
84
  tests/test_acronym_capitalization.py
85
+ tests/test_admin_doc_commands.py
82
86
  tests/test_admin_history.py
83
87
  tests/test_admin_index_actions.py
84
88
  tests/test_awg_admin.py
89
+ tests/test_business_admin_group.py
85
90
  tests/test_celery_no_debug.py
91
+ tests/test_chat_profile_admin.py
92
+ tests/test_chat_profile_api.py
86
93
  tests/test_csrf_failure.py
87
94
  tests/test_csrf_origin_subnet.py
88
95
  tests/test_dist_cleanup.py
@@ -92,6 +99,7 @@ tests/test_email_inbox_admin.py
92
99
  tests/test_email_inbox_search_action.py
93
100
  tests/test_env_refresh_clean.py
94
101
  tests/test_env_refresh_unlink.py
102
+ tests/test_experience_admin_group.py
95
103
  tests/test_fixture_check.py
96
104
  tests/test_footer_no_references.py
97
105
  tests/test_footer_presence.py
@@ -119,7 +127,9 @@ tests/test_rfid_admin_scan_csrf.py
119
127
  tests/test_rfid_background_reader.py
120
128
  tests/test_save_as_copy.py
121
129
  tests/test_seed_data.py
130
+ tests/test_show_leads_command.py
122
131
  tests/test_sigil_resolution.py
132
+ tests/test_update_fixtures_command.py
123
133
  tests/test_urls_autodiscover.py
124
134
  tests/test_user_datum_admin.py
125
135
  tests/test_version_file.py
@@ -85,6 +85,7 @@ ALLOWED_HOSTS = [
85
85
  "10.42.0.0/16",
86
86
  "192.168.0.0/16",
87
87
  "arthexis.com",
88
+ "www.arthexis.com",
88
89
  ]
89
90
 
90
91
 
@@ -13,12 +13,15 @@ from django.apps import apps
13
13
  from django.conf import settings
14
14
  from django.conf.urls.static import static
15
15
  from django.contrib import admin
16
+ from django.contrib.admin import autodiscover
16
17
  from django.urls import include, path
17
18
  from django.views.decorators.csrf import csrf_exempt
18
19
  from django.views.i18n import set_language
19
20
  from django.utils.translation import gettext_lazy as _
20
21
  from core import views as core_views
22
+ from core.admindocs import CommandsView
21
23
 
24
+ autodiscover()
22
25
  admin.site.site_header = _("Constellation")
23
26
  admin.site.site_title = _("Constellation")
24
27
 
@@ -61,6 +64,11 @@ def autodiscovered_urlpatterns():
61
64
 
62
65
 
63
66
  urlpatterns = [
67
+ path(
68
+ "admin/doc/commands/",
69
+ CommandsView.as_view(),
70
+ name="django-admindocs-commands",
71
+ ),
64
72
  path("admin/doc/", include("django.contrib.admindocs.urls")),
65
73
  path(
66
74
  "admin/core/releases/<int:pk>/<str:action>/",
@@ -69,6 +77,7 @@ urlpatterns = [
69
77
  ),
70
78
  path("admin/", admin.site.urls),
71
79
  path("i18n/setlang/", csrf_exempt(set_language), name="set_language"),
80
+ path("api/", include("core.workgroup_urls")),
72
81
  path("", include("pages.urls")),
73
82
  ]
74
83
 
@@ -3,7 +3,7 @@ from django.contrib import admin
3
3
  from django.contrib.admin.widgets import RelatedFieldWidgetWrapper
4
4
  from django.urls import path, reverse
5
5
  from django.shortcuts import redirect, render
6
- from django.http import JsonResponse, HttpResponseBase
6
+ from django.http import JsonResponse, HttpResponseBase, HttpResponseRedirect
7
7
  from django.template.response import TemplateResponse
8
8
  from django.views.decorators.csrf import csrf_exempt
9
9
  from django.core.exceptions import ValidationError
@@ -45,6 +45,7 @@ from .models import (
45
45
  ReleaseManager,
46
46
  SecurityGroup,
47
47
  InviteLead,
48
+ ChatProfile,
48
49
  )
49
50
  from .user_data import UserDatumAdminMixin
50
51
 
@@ -52,6 +53,30 @@ from .user_data import UserDatumAdminMixin
52
53
  admin.site.unregister(Group)
53
54
 
54
55
 
56
+ class WorkgroupReleaseManager(ReleaseManager):
57
+ class Meta:
58
+ proxy = True
59
+ app_label = "post_office"
60
+ verbose_name = ReleaseManager._meta.verbose_name
61
+ verbose_name_plural = ReleaseManager._meta.verbose_name_plural
62
+
63
+
64
+ class WorkgroupSecurityGroup(SecurityGroup):
65
+ class Meta:
66
+ proxy = True
67
+ app_label = "post_office"
68
+ verbose_name = SecurityGroup._meta.verbose_name
69
+ verbose_name_plural = SecurityGroup._meta.verbose_name_plural
70
+
71
+
72
+ class ExperienceReference(Reference):
73
+ class Meta:
74
+ proxy = True
75
+ app_label = "pages"
76
+ verbose_name = Reference._meta.verbose_name
77
+ verbose_name_plural = Reference._meta.verbose_name_plural
78
+
79
+
55
80
  class SaveBeforeChangeAction(DjangoObjectActions):
56
81
  def response_change(self, request, obj):
57
82
  action = request.POST.get("_action")
@@ -65,7 +90,7 @@ class SaveBeforeChangeAction(DjangoObjectActions):
65
90
  return super().response_change(request, obj)
66
91
 
67
92
 
68
- @admin.register(Reference)
93
+ @admin.register(ExperienceReference)
69
94
  class ReferenceAdmin(admin.ModelAdmin):
70
95
  list_display = (
71
96
  "alt_text",
@@ -141,14 +166,20 @@ class ReferenceAdmin(admin.ModelAdmin):
141
166
  qr_code.short_description = "QR Code"
142
167
 
143
168
 
144
- @admin.register(ReleaseManager)
169
+ @admin.register(WorkgroupReleaseManager)
145
170
  class ReleaseManagerAdmin(admin.ModelAdmin):
146
171
  list_display = ("user", "pypi_username", "pypi_url")
147
172
 
148
173
 
149
174
  @admin.register(Package)
150
175
  class PackageAdmin(SaveBeforeChangeAction, admin.ModelAdmin):
151
- list_display = ("name", "description", "homepage_url", "release_manager")
176
+ list_display = (
177
+ "name",
178
+ "description",
179
+ "homepage_url",
180
+ "release_manager",
181
+ "is_active",
182
+ )
152
183
  actions = ["prepare_next_release"]
153
184
  change_actions = ["prepare_next_release_action"]
154
185
 
@@ -177,6 +208,24 @@ class PackageAdmin(SaveBeforeChangeAction, admin.ModelAdmin):
177
208
  reverse("admin:core_packagerelease_change", args=[release.pk])
178
209
  )
179
210
 
211
+ def get_urls(self):
212
+ urls = super().get_urls()
213
+ custom = [
214
+ path(
215
+ "prepare-next-release/",
216
+ self.admin_site.admin_view(self.prepare_next_release_active),
217
+ name="core_package_prepare_next_release",
218
+ )
219
+ ]
220
+ return custom + urls
221
+
222
+ def prepare_next_release_active(self, request):
223
+ package = Package.objects.filter(is_active=True).first()
224
+ if not package:
225
+ self.message_user(request, "No active package", messages.ERROR)
226
+ return redirect("admin:core_package_changelist")
227
+ return self._prepare(request, package)
228
+
180
229
  @admin.action(description="Prepare next Release")
181
230
  def prepare_next_release(self, request, queryset):
182
231
  if queryset.count() != 1:
@@ -201,7 +250,7 @@ class SecurityGroupAdminForm(forms.ModelForm):
201
250
  )
202
251
 
203
252
  class Meta:
204
- model = SecurityGroup
253
+ model = WorkgroupSecurityGroup
205
254
  fields = "__all__"
206
255
 
207
256
  def __init__(self, *args, **kwargs):
@@ -219,7 +268,7 @@ class SecurityGroupAdminForm(forms.ModelForm):
219
268
  return instance
220
269
 
221
270
 
222
- @admin.register(SecurityGroup)
271
+ @admin.register(WorkgroupSecurityGroup)
223
272
  class SecurityGroupAdmin(DjangoGroupAdmin):
224
273
  form = SecurityGroupAdminForm
225
274
  fieldsets = ((None, {"fields": ("name", "parent", "users", "permissions")}),)
@@ -517,6 +566,46 @@ class EmailInboxAdmin(admin.ModelAdmin):
517
566
  return TemplateResponse(request, "admin/core/emailinbox/search.html", context)
518
567
 
519
568
 
569
+ class WorkgroupChatProfile(ChatProfile):
570
+ class Meta:
571
+ proxy = True
572
+ app_label = "post_office"
573
+ verbose_name = ChatProfile._meta.verbose_name
574
+ verbose_name_plural = ChatProfile._meta.verbose_name_plural
575
+
576
+
577
+ @admin.register(WorkgroupChatProfile)
578
+ class ChatProfileAdmin(admin.ModelAdmin):
579
+ list_display = ("user", "created_at", "last_used_at", "is_active")
580
+ readonly_fields = ("user_key_hash",)
581
+
582
+ change_form_template = "admin/workgroupchatprofile_change_form.html"
583
+
584
+ def get_urls(self):
585
+ urls = super().get_urls()
586
+ custom = [
587
+ path(
588
+ "<path:object_id>/generate-key/",
589
+ self.admin_site.admin_view(self.generate_key),
590
+ name="post_office_workgroupchatprofile_generate_key",
591
+ ),
592
+ ]
593
+ return custom + urls
594
+
595
+ def generate_key(self, request, object_id, *args, **kwargs):
596
+ profile = self.get_object(request, object_id)
597
+ if profile is None:
598
+ return HttpResponseRedirect("../")
599
+ profile, key = ChatProfile.issue_key(profile.user)
600
+ context = {
601
+ **self.admin_site.each_context(request),
602
+ "opts": self.model._meta,
603
+ "original": profile,
604
+ "user_key": key,
605
+ }
606
+ return TemplateResponse(request, "admin/chatprofile_key.html", context)
607
+
608
+
520
609
  class EnergyCreditInline(admin.TabularInline):
521
610
  model = EnergyCredit
522
611
  fields = ("amount_kw", "created_by", "created_on")
@@ -720,6 +809,7 @@ class RFIDForm(forms.ModelForm):
720
809
  super().__init__(*args, **kwargs)
721
810
  self.fields["reference"].required = False
722
811
  rel = RFID._meta.get_field("reference").remote_field
812
+ rel.model = ExperienceReference
723
813
  widget = self.fields["reference"].widget
724
814
  self.fields["reference"].widget = RelatedFieldWidgetWrapper(
725
815
  widget,
@@ -809,6 +899,7 @@ class PackageReleaseAdmin(SaveBeforeChangeAction, admin.ModelAdmin):
809
899
  list_display_links = ("version",)
810
900
  actions = ["publish_release", "validate_releases"]
811
901
  change_actions = ["publish_release_action"]
902
+ changelist_actions = ["refresh_from_pypi"]
812
903
  readonly_fields = ("pypi_url", "is_current", "revision")
813
904
  fields = (
814
905
  "package",
@@ -829,6 +920,46 @@ class PackageReleaseAdmin(SaveBeforeChangeAction, admin.ModelAdmin):
829
920
 
830
921
  revision_short.short_description = "revision"
831
922
 
923
+ def refresh_from_pypi(self, request, queryset):
924
+ package = Package.objects.filter(is_active=True).first()
925
+ if not package:
926
+ self.message_user(request, "No active package", messages.ERROR)
927
+ return
928
+ try:
929
+ resp = requests.get(
930
+ f"https://pypi.org/pypi/{package.name}/json", timeout=10
931
+ )
932
+ resp.raise_for_status()
933
+ except Exception as exc: # pragma: no cover - network failure
934
+ self.message_user(request, str(exc), messages.ERROR)
935
+ return
936
+ releases = resp.json().get("releases", {})
937
+ created = 0
938
+ for version in releases:
939
+ exists = PackageRelease.all_objects.filter(
940
+ package=package, version=version
941
+ ).exists()
942
+ if not exists:
943
+ PackageRelease.objects.create(
944
+ package=package,
945
+ release_manager=package.release_manager,
946
+ version=version,
947
+ pypi_url=f"https://pypi.org/project/{package.name}/{version}/",
948
+ )
949
+ created += 1
950
+ if created:
951
+ PackageRelease.dump_fixture()
952
+ self.message_user(
953
+ request,
954
+ f"Created {created} release{'s' if created != 1 else ''} from PyPI",
955
+ messages.SUCCESS,
956
+ )
957
+ else:
958
+ self.message_user(request, "No new releases found", messages.INFO)
959
+
960
+ refresh_from_pypi.label = "Refresh from PyPI"
961
+ refresh_from_pypi.short_description = "Refresh from PyPI"
962
+
832
963
  def _publish_release(self, request, release):
833
964
  try:
834
965
  release.full_clean()
@@ -0,0 +1,44 @@
1
+ import argparse
2
+ from django.core.management import get_commands, load_command_class
3
+ from django.contrib.admindocs.views import BaseAdminDocsView
4
+
5
+
6
+ class CommandsView(BaseAdminDocsView):
7
+ template_name = "admin_doc/commands.html"
8
+
9
+ def get_context_data(self, **kwargs):
10
+ commands = []
11
+ for name, app_name in sorted(get_commands().items()):
12
+ try:
13
+ cmd = load_command_class(app_name, name)
14
+ parser = cmd.create_parser("manage.py", name)
15
+ except Exception: # pragma: no cover - command import issues
16
+ continue
17
+ args = []
18
+ options = []
19
+ for action in parser._actions:
20
+ if isinstance(action, argparse._HelpAction):
21
+ continue
22
+ if action.option_strings:
23
+ options.append(
24
+ {
25
+ "opts": ", ".join(action.option_strings),
26
+ "help": action.help or "",
27
+ }
28
+ )
29
+ else:
30
+ args.append(
31
+ {
32
+ "name": action.metavar or action.dest,
33
+ "help": action.help or "",
34
+ }
35
+ )
36
+ commands.append(
37
+ {
38
+ "name": name,
39
+ "help": getattr(cmd, "help", ""),
40
+ "args": args,
41
+ "options": options,
42
+ }
43
+ )
44
+ return super().get_context_data(**{**kwargs, "commands": commands})
@@ -17,6 +17,7 @@ from django.contrib.contenttypes.models import ContentType
17
17
  import hashlib
18
18
  import os
19
19
  import subprocess
20
+ import secrets
20
21
  from io import BytesIO
21
22
  from django.core.files.base import ContentFile
22
23
  import qrcode
@@ -1153,14 +1154,30 @@ class Package(Entity):
1153
1154
  release_manager = models.ForeignKey(
1154
1155
  ReleaseManager, on_delete=models.SET_NULL, null=True, blank=True
1155
1156
  )
1157
+ is_active = models.BooleanField(
1158
+ default=False,
1159
+ help_text="Designates the active package for version comparisons",
1160
+ )
1156
1161
 
1157
1162
  class Meta:
1158
1163
  verbose_name = "Package"
1159
1164
  verbose_name_plural = "Packages"
1165
+ constraints = [
1166
+ models.UniqueConstraint(
1167
+ fields=("is_active",),
1168
+ condition=models.Q(is_active=True),
1169
+ name="unique_active_package",
1170
+ )
1171
+ ]
1160
1172
 
1161
1173
  def __str__(self) -> str: # pragma: no cover - trivial
1162
1174
  return self.name
1163
1175
 
1176
+ def save(self, *args, **kwargs):
1177
+ if self.is_active:
1178
+ type(self).objects.exclude(pk=self.pk).update(is_active=False)
1179
+ super().save(*args, **kwargs)
1180
+
1164
1181
  def to_package(self) -> ReleasePackage:
1165
1182
  """Return a :class:`ReleasePackage` instance from package data."""
1166
1183
  return ReleasePackage(
@@ -1250,11 +1267,13 @@ class PackageRelease(Entity):
1250
1267
 
1251
1268
  @property
1252
1269
  def is_current(self) -> bool:
1253
- """Return ``True`` if this release matches the current revision."""
1254
- from utils import revision as revision_utils
1255
-
1256
- current = revision_utils.get_revision()
1257
- return bool(current) and current == self.revision
1270
+ """Return ``True`` when this release's version matches the VERSION file
1271
+ and its package is active."""
1272
+ version_path = Path("VERSION")
1273
+ if not version_path.exists():
1274
+ return False
1275
+ current_version = version_path.read_text().strip()
1276
+ return current_version == self.version and self.package.is_active
1258
1277
 
1259
1278
  @classmethod
1260
1279
  def latest(cls):
@@ -1316,3 +1335,54 @@ def _rfid_unique_energy_account(sender, instance, action, reverse, model, pk_set
1316
1335
  ).exclude(energy_accounts=instance)
1317
1336
  if conflict.exists():
1318
1337
  raise ValidationError("RFID tags may only be assigned to one energy account.")
1338
+
1339
+
1340
+ def hash_key(key: str) -> str:
1341
+ """Return a SHA-256 hash for ``key``."""
1342
+
1343
+ return hashlib.sha256(key.encode()).hexdigest()
1344
+
1345
+
1346
+ class ChatProfile(models.Model):
1347
+ """Stores a hashed user key used by the assistant for authentication.
1348
+
1349
+ The plain-text ``user_key`` is generated server-side and shown only once.
1350
+ Users must supply this key in the ``Authorization: Bearer <user_key>``
1351
+ header when requesting protected endpoints. Only the hash is stored.
1352
+ """
1353
+
1354
+ id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
1355
+ user = models.OneToOneField(
1356
+ settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="chat_profile"
1357
+ )
1358
+ user_key_hash = models.CharField(max_length=64, unique=True)
1359
+ scopes = models.JSONField(default=list, blank=True)
1360
+ created_at = models.DateTimeField(auto_now_add=True)
1361
+ last_used_at = models.DateTimeField(null=True, blank=True)
1362
+ is_active = models.BooleanField(default=True)
1363
+
1364
+ class Meta:
1365
+ db_table = "workgroup_chatprofile"
1366
+ verbose_name = "Chat Profile"
1367
+ verbose_name_plural = "Chat Profiles"
1368
+
1369
+ @classmethod
1370
+ def issue_key(cls, user) -> tuple["ChatProfile", str]:
1371
+ """Create or update a profile and return it with a new plain key."""
1372
+
1373
+ key = secrets.token_hex(32)
1374
+ key_hash = hash_key(key)
1375
+ profile, _ = cls.objects.update_or_create(
1376
+ user=user,
1377
+ defaults={"user_key_hash": key_hash, "last_used_at": None, "is_active": True},
1378
+ )
1379
+ return profile, key
1380
+
1381
+ def touch(self) -> None:
1382
+ """Record that the key was used."""
1383
+
1384
+ self.last_used_at = timezone.now()
1385
+ self.save(update_fields=["last_used_at"])
1386
+
1387
+ def __str__(self) -> str: # pragma: no cover - simple representation
1388
+ return f"ChatProfile for {self.user}"