arthexis 0.1.5__py3-none-any.whl → 0.1.7__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.
- {arthexis-0.1.5.dist-info → arthexis-0.1.7.dist-info}/METADATA +8 -8
- {arthexis-0.1.5.dist-info → arthexis-0.1.7.dist-info}/RECORD +33 -29
- config/celery.py +7 -0
- config/horologia_app.py +7 -0
- config/logging.py +8 -3
- config/settings.py +2 -2
- config/workgroup_app.py +7 -0
- core/admin.py +69 -13
- core/apps.py +2 -1
- core/checks.py +29 -0
- core/entity.py +29 -7
- core/models.py +49 -9
- core/release.py +29 -141
- core/system.py +2 -2
- core/test_system_info.py +21 -0
- core/tests.py +200 -1
- core/views.py +153 -134
- nodes/admin.py +224 -1
- nodes/apps.py +1 -1
- nodes/models.py +103 -7
- nodes/tests.py +27 -5
- nodes/views.py +0 -1
- ocpp/apps.py +1 -1
- ocpp/models.py +1 -1
- ocpp/simulator.py +4 -0
- ocpp/tests.py +5 -1
- pages/admin.py +8 -3
- pages/apps.py +1 -1
- pages/tests.py +23 -4
- pages/views.py +22 -3
- {arthexis-0.1.5.dist-info → arthexis-0.1.7.dist-info}/WHEEL +0 -0
- {arthexis-0.1.5.dist-info → arthexis-0.1.7.dist-info}/licenses/LICENSE +0 -0
- {arthexis-0.1.5.dist-info → arthexis-0.1.7.dist-info}/top_level.txt +0 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: arthexis
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.7
|
|
4
4
|
Summary: Django-based MESH system
|
|
5
|
-
Author-email: "Rafael J.
|
|
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
|
-
-
|
|
116
|
-
-
|
|
117
|
-
- Runs
|
|
118
|
-
-
|
|
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
|
-
|
|
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
|
|
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
|
|
|
@@ -1,73 +1,77 @@
|
|
|
1
|
-
arthexis-0.1.
|
|
1
|
+
arthexis-0.1.7.dist-info/licenses/LICENSE,sha256=rBjcKtvD5yIpzAnkw3XRofK8KCAeL43DXmNaiRuYJrg,1092
|
|
2
2
|
config/__init__.py,sha256=5EyLFDrM6aTOHSnyBcLXRvMInZMPmAAy9Icu0cHHK5o,110
|
|
3
3
|
config/active_app.py,sha256=MET_G7oHL7GkoSo3VkkMzymM-PwsSZazMLZxpgjFLTo,388
|
|
4
4
|
config/asgi.py,sha256=n09URedOmQ_59II3UCl3iodGSDWOuN_A8DFyfLjuylA,803
|
|
5
5
|
config/auth_app.py,sha256=65cHsGtnwa0Q7-IjWIwJ73Kq4FpEcxSjHH_fu9Dr868,207
|
|
6
|
-
config/celery.py,sha256=
|
|
6
|
+
config/celery.py,sha256=u2AwuzgcMWPFCVPeqrVpvzyU3ygkVSdd2KQ5b4KAE4s,756
|
|
7
7
|
config/context_processors.py,sha256=_oQ5F8MZpAuv2QZl5QHoobEwTharl1lC7SRCUEmf3cQ,2341
|
|
8
|
+
config/horologia_app.py,sha256=u1hTYcEmIqh82Gt5YNPvR5ta2MnVatELvD9ByFrCH1A,194
|
|
8
9
|
config/loadenv.py,sha256=bhFbHTbRJSkSwrFk3UInKEKQ8ZY-poatOGi7rC57YAI,298
|
|
9
|
-
config/logging.py,sha256=
|
|
10
|
+
config/logging.py,sha256=334jADN4dM5GNHaCWlYPOKYa5BhfxbsuejH_QDALG6g,1793
|
|
10
11
|
config/middleware.py,sha256=EvraDumepnKwCDswHGXb1mK7vud_dEEoZ4eh0IQ7fhQ,744
|
|
11
12
|
config/offline.py,sha256=7XHGFlV7IAWUJ93dEjknrHkAHDEyzWxK_hj_ht4-6-Q,1427
|
|
12
|
-
config/settings.py,sha256=
|
|
13
|
+
config/settings.py,sha256=hyqTxQhVh1UYMRxZVuPil32IChJEvthJ7hkh2UGVehI,10897
|
|
13
14
|
config/urls.py,sha256=mgC0PqY1JBH6GdOxM6A_p5rDzx9vDoEVAOrH36DWSB4,2991
|
|
15
|
+
config/workgroup_app.py,sha256=rm6KrjxtRNdkrxl2_in9FAaHpIltRUhpDrt4tP9yanE,202
|
|
14
16
|
config/wsgi.py,sha256=Fu-ONO2SgYeU6rhmy909P-uLX-n8ALJQObdm9MHPS-k,450
|
|
15
17
|
core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
16
|
-
core/admin.py,sha256=
|
|
17
|
-
core/apps.py,sha256=
|
|
18
|
+
core/admin.py,sha256=bKncxl6b_dEPXfbIbJYMMF6Nf3a6Su6IZ7ymzy_RD8Q,29687
|
|
19
|
+
core/apps.py,sha256=5wmxTrED-4WBWA0khEpqVAUH7V-g7DLtPDNEtSINnSM,2617
|
|
18
20
|
core/backends.py,sha256=f8gb5D3_f8sM6UJop0aS8TAkrkh14NaP0JrqTcT_L3Y,2686
|
|
19
|
-
core/
|
|
21
|
+
core/checks.py,sha256=sRIA9tYAKXO6kJf96ADFrkYyvp47gYqv18Y12vukUqg,931
|
|
22
|
+
core/entity.py,sha256=SeEXuv2zGN08biNxOgSwVWtt3jiNxMxJINQHGgavovE,4120
|
|
20
23
|
core/environment.py,sha256=QcOshpWNG0l_agW-b9efNvVFKqdatj6sUK8FT6p92gU,1238
|
|
21
24
|
core/fields.py,sha256=uS5nDozL6IsTAbjg08VcOB8K85e0XROUYCBAOIfAcPE,2127
|
|
22
25
|
core/lcd_screen.py,sha256=7iSg8OZMK8qHGM1FVOwbgsWCogEe5eR6mie-A5YQKY4,2649
|
|
23
26
|
core/middleware.py,sha256=utMGESBrR-rHRV5_q1X2g2wnmpziaUcc9jGzoZwvqqQ,1258
|
|
24
|
-
core/models.py,sha256=
|
|
27
|
+
core/models.py,sha256=_Ng5pxlF2gTKzHd4si1t7r312sj--Zk3AOArCQ8BZbA,44518
|
|
25
28
|
core/notifications.py,sha256=w9rPCsZfZNSoBUH-1KVe9Yr8LvG9vtkgmzw3DKLcDoM,3787
|
|
26
|
-
core/release.py,sha256=
|
|
27
|
-
core/system.py,sha256=
|
|
29
|
+
core/release.py,sha256=DzK44yr7vkAK_xzDeejVFwQnkE23yqWvmIWZHQZ1nwI,11003
|
|
30
|
+
core/system.py,sha256=EVvxYxnmJ1Wn-XIkt8pk2E0Rr8poRozkZ218E3TNHCY,3504
|
|
28
31
|
core/tasks.py,sha256=Q4QweRlhahbXYEeL9ytlyQWwWQFP63q-ZY_fN0EXwtg,3145
|
|
29
|
-
core/
|
|
32
|
+
core/test_system_info.py,sha256=AXo-HqTLMyOpsQIShDDzyl_nMHOyKbAIuNr9fYZzoKg,605
|
|
33
|
+
core/tests.py,sha256=6dZ3CvAZ5fg7t5a11OsPktNfjRH-Fk8HDTPIRVhXWkg,26581
|
|
30
34
|
core/urls.py,sha256=pBkcWdiA0Aag7z4UOu7HT4Im4ghPPfBZKeogmoO0H5U,406
|
|
31
35
|
core/user_data.py,sha256=pfi4fgHl0CosG9jkfe__bSNN-Knhnzp7UirnCT_tae8,12029
|
|
32
|
-
core/views.py,sha256
|
|
36
|
+
core/views.py,sha256=-4k3YSv9RfsAY14aLXkngzjXT0yyXD90D6KOJ3cwSLQ,16137
|
|
33
37
|
nodes/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
34
38
|
nodes/actions.py,sha256=2QOtRdhm__4f0Pgy3n2tg9HCbS47ZK9qBdTXJ9YZJfA,2339
|
|
35
|
-
nodes/admin.py,sha256=
|
|
36
|
-
nodes/apps.py,sha256=
|
|
39
|
+
nodes/admin.py,sha256=a-LAbgNG-0QHA8Og24fslyPyze_kPKhuUs3syAVbNr4,21824
|
|
40
|
+
nodes/apps.py,sha256=pKadzdXffQdwOQ39tkYLgnPH3A_JkVek8Hp3OE6Ro8g,2295
|
|
37
41
|
nodes/lcd.py,sha256=axttp6AhKO2QMMXAf6nPe3Ox7nrBP-8yZD_oDUfMAvI,5602
|
|
38
|
-
nodes/models.py,sha256=
|
|
42
|
+
nodes/models.py,sha256=XUkcB3CZVYKvsOl4PkP4Hjlyhm-RjWxkCvK__UIR_Xw,23885
|
|
39
43
|
nodes/tasks.py,sha256=1YeUqwSvqqTisusH-25nMQJCMQY2faEyDVrcBYqQghU,1588
|
|
40
|
-
nodes/tests.py,sha256=
|
|
44
|
+
nodes/tests.py,sha256=h06XvxuBT8gFc9n9q2MDVSzCMs6YUe0NzqSvg5DBaZo,43959
|
|
41
45
|
nodes/urls.py,sha256=20yZDZf4DNgIZ9hQWsUzjp8k5Fryg9ukk761_KGQt9k,548
|
|
42
46
|
nodes/utils.py,sha256=aVEHtoisHyhWe_Fy2KPiP24Fs0wELtQMyfs4JGYnkuY,2320
|
|
43
|
-
nodes/views.py,sha256=
|
|
47
|
+
nodes/views.py,sha256=LISm9jqIdyUwQm45PegGg8RvmGRZH1jvCTCC14owRdQ,8444
|
|
44
48
|
ocpp/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
45
49
|
ocpp/admin.py,sha256=9A_uLUg7l9DiqJq1foy47oFLb7zPuBOJeS73p7l0M0k,11851
|
|
46
|
-
ocpp/apps.py,sha256=
|
|
50
|
+
ocpp/apps.py,sha256=Hja6uavSO4Srbp7kZmp0yGuhEq1abKBhH004QcUG90I,759
|
|
47
51
|
ocpp/consumers.py,sha256=ykAkAicHRmn_qfUTp6psNUFkDrOnzuJfnHca9vgJLQI,11768
|
|
48
52
|
ocpp/evcs.py,sha256=ZQKLqN8yJ4IOdedVt1z_gz_2eXBPdHHPvgxU2NkWgpw,33333
|
|
49
|
-
ocpp/models.py,sha256=
|
|
53
|
+
ocpp/models.py,sha256=5g6JDdaLtX07jbuuJAbwPi-Z58LuCR25XjJHh7oe8kk,10367
|
|
50
54
|
ocpp/routing.py,sha256=g9vPnLw-D1N8L_mW0_oCe-nTDibjC0Et-SFxe8NFAOI,308
|
|
51
|
-
ocpp/simulator.py,sha256=
|
|
55
|
+
ocpp/simulator.py,sha256=TX0GHe2w3p8Yq8VNBfHlYRa88ba1n3tmhIrP8R82BVA,13404
|
|
52
56
|
ocpp/store.py,sha256=qzVWNXZY3sUAC3DtOnAv4BOj185DAGUxguVN9G4qkbw,5850
|
|
53
57
|
ocpp/tasks.py,sha256=F15s9Hkx2Y54ytqXq3tyn0YmxgVepXsIctjuck2LWVI,841
|
|
54
58
|
ocpp/test_export_import.py,sha256=w3GRh0zTWZ5byev7H1g9mZ3xbgnlHxQx2InHcp69x4w,4667
|
|
55
59
|
ocpp/test_rfid.py,sha256=UrlB-amytnpvD1I4c0fRUKaPozRoEynYrZkKPCLfZDU,12318
|
|
56
|
-
ocpp/tests.py,sha256=
|
|
60
|
+
ocpp/tests.py,sha256=vG6Ky10c1q440JtFgbm1qJHyoetSQrXM9ieMSKyJLNs,47862
|
|
57
61
|
ocpp/transactions_io.py,sha256=ulRkBZTjubN01Geur9t4zjANQu-yCeS8N3prR03rQE8,4062
|
|
58
62
|
ocpp/urls.py,sha256=MMrYiCW9y8cf2CZqpcEiF-jsjWdqLAl4a1cFnG36AEg,877
|
|
59
63
|
ocpp/views.py,sha256=lX5vyCKxAOe-pBA5FpjbDiOt0zkx18GcH8NH0ncrDac,13201
|
|
60
64
|
pages/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
61
|
-
pages/admin.py,sha256=
|
|
62
|
-
pages/apps.py,sha256=
|
|
65
|
+
pages/admin.py,sha256=aD_f-RnuUCfb-6r8DJ33vuuzyuEpPtHqq870nFew0f0,7679
|
|
66
|
+
pages/apps.py,sha256=mfCxegmnRqKcszyEwpQhZpW9JWOuEYdVereU_w49BXg,298
|
|
63
67
|
pages/checks.py,sha256=E2_ZYPD-2vw27ImA-Q29OvxbdQUvKx26ACZwiPflTaI,1569
|
|
64
68
|
pages/context_processors.py,sha256=mQj43pb4Y1u8cfBrl3m0bP-_iWHDKXxfuRE4Rbn6l-g,2432
|
|
65
69
|
pages/models.py,sha256=vG2knV91jRJ5PpIB-UKbsyuzB-e0KXYi9lwCFbIVUSA,7536
|
|
66
|
-
pages/tests.py,sha256=
|
|
70
|
+
pages/tests.py,sha256=B3MX2du5aZ8NztR-e8pTbypXTqhA-_NHZMy6O_OuqWE,28022
|
|
67
71
|
pages/urls.py,sha256=glhQExK2vVLzzvaRypWfmMdnggBrpjlxCaN1BUbC_MY,457
|
|
68
72
|
pages/utils.py,sha256=bCcjku0mQhzgvTR46QwQgyc8YnSS7VumC6Qv968aCic,313
|
|
69
|
-
pages/views.py,sha256=
|
|
70
|
-
arthexis-0.1.
|
|
71
|
-
arthexis-0.1.
|
|
72
|
-
arthexis-0.1.
|
|
73
|
-
arthexis-0.1.
|
|
73
|
+
pages/views.py,sha256=lkgx2X2AwOGa_URCRVCxju_z_uG1_xW43PUNmjOEibU,7993
|
|
74
|
+
arthexis-0.1.7.dist-info/METADATA,sha256=UpY_iJF5Qcumd_YWTnqBZXZDVLl1lG8k4Chpf95Zeuo,4458
|
|
75
|
+
arthexis-0.1.7.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
76
|
+
arthexis-0.1.7.dist-info/top_level.txt,sha256=J2a2q8_BWrCZ8H2WFUNMBfO2jz8j2gax6zZh-_1QDac,29
|
|
77
|
+
arthexis-0.1.7.dist-info/RECORD,,
|
config/celery.py
CHANGED
|
@@ -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()
|
config/horologia_app.py
ADDED
config/logging.py
CHANGED
|
@@ -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
|
|
18
|
-
return
|
|
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
|
-
|
|
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."""
|
config/settings.py
CHANGED
|
@@ -148,8 +148,8 @@ INSTALLED_APPS = [
|
|
|
148
148
|
"django_object_actions",
|
|
149
149
|
"django.contrib.sites",
|
|
150
150
|
"channels",
|
|
151
|
-
"
|
|
152
|
-
"
|
|
151
|
+
"config.workgroup_app.WorkgroupConfig",
|
|
152
|
+
"config.horologia_app.HorologiaConfig",
|
|
153
153
|
] + LOCAL_APPS
|
|
154
154
|
|
|
155
155
|
if DEBUG:
|
config/workgroup_app.py
ADDED
core/admin.py
CHANGED
|
@@ -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 =
|
|
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
|
-
"
|
|
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", "
|
|
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
|
|
core/apps.py
CHANGED
|
@@ -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
|
|
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()
|
core/checks.py
ADDED
|
@@ -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 []
|
core/entity.py
CHANGED
|
@@ -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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
|
core/models.py
CHANGED
|
@@ -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
|
|
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
|
|
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):
|