arthexis 0.1.20__py3-none-any.whl → 0.1.21__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.
- {arthexis-0.1.20.dist-info → arthexis-0.1.21.dist-info}/METADATA +3 -4
- {arthexis-0.1.20.dist-info → arthexis-0.1.21.dist-info}/RECORD +25 -27
- config/asgi.py +1 -15
- config/settings.py +0 -26
- config/urls.py +0 -1
- core/admin.py +1 -233
- core/apps.py +0 -6
- core/environment.py +29 -10
- core/models.py +8 -77
- core/tests.py +1 -7
- core/views.py +0 -96
- nodes/admin.py +29 -6
- nodes/tests.py +49 -0
- nodes/views.py +60 -1
- ocpp/admin.py +48 -6
- ocpp/models.py +48 -0
- ocpp/tests.py +83 -0
- ocpp/views.py +85 -3
- pages/context_processors.py +0 -12
- pages/tests.py +0 -41
- pages/urls.py +0 -1
- pages/views.py +0 -5
- core/workgroup_urls.py +0 -17
- core/workgroup_views.py +0 -94
- {arthexis-0.1.20.dist-info → arthexis-0.1.21.dist-info}/WHEEL +0 -0
- {arthexis-0.1.20.dist-info → arthexis-0.1.21.dist-info}/licenses/LICENSE +0 -0
- {arthexis-0.1.20.dist-info → arthexis-0.1.21.dist-info}/top_level.txt +0 -0
ocpp/tests.py
CHANGED
|
@@ -2335,6 +2335,36 @@ class ChargerAdminTests(TestCase):
|
|
|
2335
2335
|
resp = self.client.get(url)
|
|
2336
2336
|
self.assertContains(resp, "AdminLoc")
|
|
2337
2337
|
|
|
2338
|
+
def test_admin_changelist_displays_quick_stats(self):
|
|
2339
|
+
charger = Charger.objects.create(charger_id="STATMAIN", display_name="Main EVCS")
|
|
2340
|
+
connector = Charger.objects.create(
|
|
2341
|
+
charger_id="STATMAIN", connector_id=1, display_name="Connector 1"
|
|
2342
|
+
)
|
|
2343
|
+
start = timezone.now() - timedelta(minutes=30)
|
|
2344
|
+
Transaction.objects.create(
|
|
2345
|
+
charger=connector,
|
|
2346
|
+
start_time=start,
|
|
2347
|
+
stop_time=start + timedelta(minutes=10),
|
|
2348
|
+
meter_start=1000,
|
|
2349
|
+
meter_stop=6000,
|
|
2350
|
+
)
|
|
2351
|
+
|
|
2352
|
+
url = reverse("admin:ocpp_charger_changelist")
|
|
2353
|
+
resp = self.client.get(url)
|
|
2354
|
+
|
|
2355
|
+
self.assertContains(resp, "Total kW")
|
|
2356
|
+
self.assertContains(resp, "Today kW")
|
|
2357
|
+
self.assertContains(resp, "5.00")
|
|
2358
|
+
|
|
2359
|
+
def test_admin_changelist_does_not_indent_connectors(self):
|
|
2360
|
+
Charger.objects.create(charger_id="INDENTMAIN")
|
|
2361
|
+
Charger.objects.create(charger_id="INDENTMAIN", connector_id=1)
|
|
2362
|
+
|
|
2363
|
+
url = reverse("admin:ocpp_charger_changelist")
|
|
2364
|
+
resp = self.client.get(url)
|
|
2365
|
+
|
|
2366
|
+
self.assertNotContains(resp, 'class="charger-connector-entry"')
|
|
2367
|
+
|
|
2338
2368
|
def test_last_fields_are_read_only(self):
|
|
2339
2369
|
now = timezone.now()
|
|
2340
2370
|
charger = Charger.objects.create(
|
|
@@ -4795,6 +4825,59 @@ class LiveUpdateViewTests(TestCase):
|
|
|
4795
4825
|
)
|
|
4796
4826
|
self.assertEqual(aggregate_entry["state"], available_label)
|
|
4797
4827
|
|
|
4828
|
+
def test_dashboard_groups_connectors_under_parent(self):
|
|
4829
|
+
aggregate = Charger.objects.create(charger_id="GROUPED")
|
|
4830
|
+
first = Charger.objects.create(
|
|
4831
|
+
charger_id=aggregate.charger_id, connector_id=1
|
|
4832
|
+
)
|
|
4833
|
+
second = Charger.objects.create(
|
|
4834
|
+
charger_id=aggregate.charger_id, connector_id=2
|
|
4835
|
+
)
|
|
4836
|
+
|
|
4837
|
+
resp = self.client.get(reverse("ocpp-dashboard"))
|
|
4838
|
+
self.assertEqual(resp.status_code, 200)
|
|
4839
|
+
groups = resp.context["charger_groups"]
|
|
4840
|
+
target = next(
|
|
4841
|
+
group
|
|
4842
|
+
for group in groups
|
|
4843
|
+
if group.get("parent")
|
|
4844
|
+
and group["parent"]["charger"].pk == aggregate.pk
|
|
4845
|
+
)
|
|
4846
|
+
child_ids = [item["charger"].pk for item in target["children"]]
|
|
4847
|
+
self.assertEqual(child_ids, [first.pk, second.pk])
|
|
4848
|
+
|
|
4849
|
+
def test_dashboard_includes_energy_totals(self):
|
|
4850
|
+
aggregate = Charger.objects.create(charger_id="KWSTATS")
|
|
4851
|
+
now = timezone.now()
|
|
4852
|
+
Transaction.objects.create(
|
|
4853
|
+
charger=aggregate,
|
|
4854
|
+
start_time=now - timedelta(hours=1),
|
|
4855
|
+
stop_time=now,
|
|
4856
|
+
meter_start=0,
|
|
4857
|
+
meter_stop=3000,
|
|
4858
|
+
)
|
|
4859
|
+
past_start = now - timedelta(days=2)
|
|
4860
|
+
Transaction.objects.create(
|
|
4861
|
+
charger=aggregate,
|
|
4862
|
+
start_time=past_start,
|
|
4863
|
+
stop_time=past_start + timedelta(hours=1),
|
|
4864
|
+
meter_start=0,
|
|
4865
|
+
meter_stop=1000,
|
|
4866
|
+
)
|
|
4867
|
+
|
|
4868
|
+
resp = self.client.get(reverse("ocpp-dashboard"))
|
|
4869
|
+
self.assertEqual(resp.status_code, 200)
|
|
4870
|
+
groups = resp.context["charger_groups"]
|
|
4871
|
+
target = next(
|
|
4872
|
+
group
|
|
4873
|
+
for group in groups
|
|
4874
|
+
if group.get("parent")
|
|
4875
|
+
and group["parent"]["charger"].pk == aggregate.pk
|
|
4876
|
+
)
|
|
4877
|
+
stats = target["parent"]["stats"]
|
|
4878
|
+
self.assertAlmostEqual(stats["total_kw"], 4.0, places=2)
|
|
4879
|
+
self.assertAlmostEqual(stats["today_kw"], 3.0, places=2)
|
|
4880
|
+
|
|
4798
4881
|
def test_cp_simulator_includes_interval(self):
|
|
4799
4882
|
resp = self.client.get(reverse("cp-simulator"))
|
|
4800
4883
|
self.assertEqual(resp.context["request"].live_update_interval, 5)
|
ocpp/views.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import uuid
|
|
3
3
|
from datetime import datetime, timedelta, timezone as dt_timezone
|
|
4
|
+
from datetime import datetime, time, timedelta
|
|
4
5
|
from types import SimpleNamespace
|
|
5
6
|
|
|
6
7
|
from django.http import Http404, HttpResponse, JsonResponse
|
|
@@ -752,8 +753,48 @@ def dashboard(request):
|
|
|
752
753
|
request.get_full_path(), login_url=reverse("pages:login")
|
|
753
754
|
)
|
|
754
755
|
is_watchtower = role_name in {"Watchtower", "Constellation"}
|
|
755
|
-
|
|
756
|
-
|
|
756
|
+
visible_chargers = (
|
|
757
|
+
_visible_chargers(request.user)
|
|
758
|
+
.select_related("location")
|
|
759
|
+
.order_by("charger_id", "connector_id")
|
|
760
|
+
)
|
|
761
|
+
stats_cache: dict[int, dict[str, float]] = {}
|
|
762
|
+
|
|
763
|
+
def _charger_display_name(charger: Charger) -> str:
|
|
764
|
+
if charger.display_name:
|
|
765
|
+
return charger.display_name
|
|
766
|
+
if charger.location:
|
|
767
|
+
return charger.location.name
|
|
768
|
+
return charger.charger_id
|
|
769
|
+
|
|
770
|
+
today = timezone.localdate()
|
|
771
|
+
tz = timezone.get_current_timezone()
|
|
772
|
+
day_start = datetime.combine(today, time.min)
|
|
773
|
+
if timezone.is_naive(day_start):
|
|
774
|
+
day_start = timezone.make_aware(day_start, tz)
|
|
775
|
+
day_end = day_start + timedelta(days=1)
|
|
776
|
+
|
|
777
|
+
def _charger_stats(charger: Charger) -> dict[str, float]:
|
|
778
|
+
cache_key = charger.pk or id(charger)
|
|
779
|
+
if cache_key not in stats_cache:
|
|
780
|
+
stats_cache[cache_key] = {
|
|
781
|
+
"total_kw": charger.total_kw,
|
|
782
|
+
"today_kw": charger.total_kw_for_range(day_start, day_end),
|
|
783
|
+
}
|
|
784
|
+
return stats_cache[cache_key]
|
|
785
|
+
|
|
786
|
+
def _status_url(charger: Charger) -> str:
|
|
787
|
+
return _reverse_connector_url(
|
|
788
|
+
"charger-status",
|
|
789
|
+
charger.charger_id,
|
|
790
|
+
charger.connector_slug,
|
|
791
|
+
)
|
|
792
|
+
|
|
793
|
+
chargers: list[dict[str, object]] = []
|
|
794
|
+
charger_groups: list[dict[str, object]] = []
|
|
795
|
+
group_lookup: dict[str, dict[str, object]] = {}
|
|
796
|
+
|
|
797
|
+
for charger in visible_chargers:
|
|
757
798
|
tx_obj = store.get_transaction(charger.charger_id, charger.connector_id)
|
|
758
799
|
if not tx_obj:
|
|
759
800
|
tx_obj = (
|
|
@@ -761,13 +802,54 @@ def dashboard(request):
|
|
|
761
802
|
.order_by("-start_time")
|
|
762
803
|
.first()
|
|
763
804
|
)
|
|
805
|
+
has_session = _has_active_session(tx_obj)
|
|
764
806
|
state, color = _charger_state(charger, tx_obj)
|
|
765
|
-
|
|
807
|
+
if (
|
|
808
|
+
charger.connector_id is not None
|
|
809
|
+
and not has_session
|
|
810
|
+
and (charger.last_status or "").strip().casefold() == "charging"
|
|
811
|
+
):
|
|
812
|
+
state, color = STATUS_BADGE_MAP["charging"]
|
|
813
|
+
entry = {
|
|
814
|
+
"charger": charger,
|
|
815
|
+
"state": state,
|
|
816
|
+
"color": color,
|
|
817
|
+
"display_name": _charger_display_name(charger),
|
|
818
|
+
"stats": _charger_stats(charger),
|
|
819
|
+
"status_url": _status_url(charger),
|
|
820
|
+
}
|
|
821
|
+
chargers.append(entry)
|
|
822
|
+
if charger.connector_id is None:
|
|
823
|
+
group = {"parent": entry, "children": []}
|
|
824
|
+
charger_groups.append(group)
|
|
825
|
+
group_lookup[charger.charger_id] = group
|
|
826
|
+
else:
|
|
827
|
+
group = group_lookup.get(charger.charger_id)
|
|
828
|
+
if group is None:
|
|
829
|
+
group = {"parent": None, "children": []}
|
|
830
|
+
charger_groups.append(group)
|
|
831
|
+
group_lookup[charger.charger_id] = group
|
|
832
|
+
group["children"].append(entry)
|
|
833
|
+
|
|
834
|
+
for group in charger_groups:
|
|
835
|
+
parent_entry = group.get("parent")
|
|
836
|
+
if not parent_entry or not group["children"]:
|
|
837
|
+
continue
|
|
838
|
+
connector_statuses = [
|
|
839
|
+
(child["charger"].last_status or "").strip().casefold()
|
|
840
|
+
for child in group["children"]
|
|
841
|
+
if child["charger"].connector_id is not None
|
|
842
|
+
]
|
|
843
|
+
if connector_statuses and all(status == "charging" for status in connector_statuses):
|
|
844
|
+
label, badge_color = STATUS_BADGE_MAP["charging"]
|
|
845
|
+
parent_entry["state"] = label
|
|
846
|
+
parent_entry["color"] = badge_color
|
|
766
847
|
scheme = "wss" if request.is_secure() else "ws"
|
|
767
848
|
host = request.get_host()
|
|
768
849
|
ws_url = f"{scheme}://{host}/ocpp/<CHARGE_POINT_ID>/"
|
|
769
850
|
context = {
|
|
770
851
|
"chargers": chargers,
|
|
852
|
+
"charger_groups": charger_groups,
|
|
771
853
|
"show_demo_notice": is_watchtower,
|
|
772
854
|
"demo_ws_url": ws_url,
|
|
773
855
|
"ws_rate_limit": store.MAX_CONNECTIONS_PER_IP,
|
pages/context_processors.py
CHANGED
|
@@ -2,7 +2,6 @@ from utils.sites import get_site
|
|
|
2
2
|
from django.urls import Resolver404, resolve
|
|
3
3
|
from django.conf import settings
|
|
4
4
|
from pathlib import Path
|
|
5
|
-
from types import SimpleNamespace
|
|
6
5
|
from nodes.models import Node
|
|
7
6
|
from core.models import Reference
|
|
8
7
|
from core.reference_utils import filter_visible_references
|
|
@@ -49,7 +48,6 @@ def nav_links(request):
|
|
|
49
48
|
modules = []
|
|
50
49
|
|
|
51
50
|
valid_modules = []
|
|
52
|
-
datasette_enabled = False
|
|
53
51
|
current_module = None
|
|
54
52
|
user = getattr(request, "user", None)
|
|
55
53
|
user_is_authenticated = getattr(user, "is_authenticated", False)
|
|
@@ -117,15 +115,6 @@ def nav_links(request):
|
|
|
117
115
|
):
|
|
118
116
|
current_module = module
|
|
119
117
|
|
|
120
|
-
datasette_lock = Path(settings.BASE_DIR) / "locks" / "datasette.lck"
|
|
121
|
-
if datasette_lock.exists():
|
|
122
|
-
datasette_enabled = True
|
|
123
|
-
datasette_module = SimpleNamespace(
|
|
124
|
-
menu_label="Data",
|
|
125
|
-
path="/data/",
|
|
126
|
-
enabled_landings=[SimpleNamespace(path="/data/", label="Datasette")],
|
|
127
|
-
)
|
|
128
|
-
valid_modules.append(datasette_module)
|
|
129
118
|
|
|
130
119
|
valid_modules.sort(key=lambda m: m.menu_label.lower())
|
|
131
120
|
|
|
@@ -159,5 +148,4 @@ def nav_links(request):
|
|
|
159
148
|
"nav_modules": valid_modules,
|
|
160
149
|
"favicon_url": favicon_url,
|
|
161
150
|
"header_references": header_references,
|
|
162
|
-
"datasette_enabled": datasette_enabled,
|
|
163
151
|
}
|
pages/tests.py
CHANGED
|
@@ -3221,47 +3221,6 @@ class AdminModelGraphViewTests(TestCase):
|
|
|
3221
3221
|
self.assertEqual(kwargs.get("format"), "pdf")
|
|
3222
3222
|
|
|
3223
3223
|
|
|
3224
|
-
class DatasetteTests(TestCase):
|
|
3225
|
-
def setUp(self):
|
|
3226
|
-
self.client = Client()
|
|
3227
|
-
User = get_user_model()
|
|
3228
|
-
self.user = User.objects.create_user(username="ds", password="pwd")
|
|
3229
|
-
Site.objects.update_or_create(id=1, defaults={"name": "Terminal"})
|
|
3230
|
-
|
|
3231
|
-
def test_datasette_auth_endpoint(self):
|
|
3232
|
-
resp = self.client.get(reverse("pages:datasette-auth"))
|
|
3233
|
-
self.assertEqual(resp.status_code, 401)
|
|
3234
|
-
self.client.force_login(self.user)
|
|
3235
|
-
resp = self.client.get(reverse("pages:datasette-auth"))
|
|
3236
|
-
self.assertEqual(resp.status_code, 200)
|
|
3237
|
-
|
|
3238
|
-
def test_navbar_includes_datasette_when_enabled(self):
|
|
3239
|
-
lock_dir = Path(settings.BASE_DIR) / "locks"
|
|
3240
|
-
lock_dir.mkdir(exist_ok=True)
|
|
3241
|
-
lock_file = lock_dir / "datasette.lck"
|
|
3242
|
-
try:
|
|
3243
|
-
lock_file.touch()
|
|
3244
|
-
resp = self.client.get(reverse("pages:index"))
|
|
3245
|
-
self.assertContains(resp, 'href="/data/"')
|
|
3246
|
-
finally:
|
|
3247
|
-
lock_file.unlink(missing_ok=True)
|
|
3248
|
-
|
|
3249
|
-
def test_admin_home_includes_datasette_button_when_enabled(self):
|
|
3250
|
-
lock_dir = Path(settings.BASE_DIR) / "locks"
|
|
3251
|
-
lock_dir.mkdir(exist_ok=True)
|
|
3252
|
-
lock_file = lock_dir / "datasette.lck"
|
|
3253
|
-
try:
|
|
3254
|
-
lock_file.touch()
|
|
3255
|
-
self.user.is_staff = True
|
|
3256
|
-
self.user.is_superuser = True
|
|
3257
|
-
self.user.save()
|
|
3258
|
-
self.client.force_login(self.user)
|
|
3259
|
-
resp = self.client.get(reverse("admin:index"))
|
|
3260
|
-
self.assertContains(resp, 'href="/data/"')
|
|
3261
|
-
self.assertContains(resp, ">Datasette<")
|
|
3262
|
-
finally:
|
|
3263
|
-
lock_file.unlink(missing_ok=True)
|
|
3264
|
-
|
|
3265
3224
|
|
|
3266
3225
|
class UserStorySubmissionTests(TestCase):
|
|
3267
3226
|
def setUp(self):
|
pages/urls.py
CHANGED
|
@@ -21,7 +21,6 @@ urlpatterns = [
|
|
|
21
21
|
views.invitation_login,
|
|
22
22
|
name="invitation-login",
|
|
23
23
|
),
|
|
24
|
-
path("datasette-auth/", views.datasette_auth, name="datasette-auth"),
|
|
25
24
|
path("man/", views.manual_list, name="manual-list"),
|
|
26
25
|
path("man/<slug:slug>/", views.manual_detail, name="manual-detail"),
|
|
27
26
|
path("man/<slug:slug>/pdf/", views.manual_pdf, name="manual-pdf"),
|
pages/views.py
CHANGED
|
@@ -710,11 +710,6 @@ def release_checklist(request):
|
|
|
710
710
|
return response
|
|
711
711
|
|
|
712
712
|
|
|
713
|
-
@csrf_exempt
|
|
714
|
-
def datasette_auth(request):
|
|
715
|
-
if request.user.is_authenticated:
|
|
716
|
-
return HttpResponse("OK")
|
|
717
|
-
return HttpResponse(status=401)
|
|
718
713
|
|
|
719
714
|
|
|
720
715
|
class CustomLoginView(LoginView):
|
core/workgroup_urls.py
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
"""URL routes for assistant profile endpoints."""
|
|
2
|
-
|
|
3
|
-
from django.urls import path
|
|
4
|
-
|
|
5
|
-
from . import workgroup_views as views
|
|
6
|
-
|
|
7
|
-
app_name = "workgroup"
|
|
8
|
-
|
|
9
|
-
urlpatterns = [
|
|
10
|
-
path(
|
|
11
|
-
"assistant-profiles/<int:user_id>/",
|
|
12
|
-
views.issue_key,
|
|
13
|
-
name="assistantprofile-issue",
|
|
14
|
-
),
|
|
15
|
-
path("assistant/test/", views.assistant_test, name="assistant-test"),
|
|
16
|
-
path("chat/", views.chat, name="chat"),
|
|
17
|
-
]
|
core/workgroup_views.py
DELETED
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
"""REST endpoints for AssistantProfile issuance and authentication."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
from functools import wraps
|
|
6
|
-
|
|
7
|
-
from django.apps import apps
|
|
8
|
-
from django.contrib.auth import get_user_model
|
|
9
|
-
from django.forms.models import model_to_dict
|
|
10
|
-
from django.http import HttpResponse, JsonResponse
|
|
11
|
-
from django.views.decorators.csrf import csrf_exempt
|
|
12
|
-
from django.views.decorators.http import require_GET, require_POST
|
|
13
|
-
|
|
14
|
-
from .models import AssistantProfile, hash_key
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
@csrf_exempt
|
|
18
|
-
@require_POST
|
|
19
|
-
def issue_key(request, user_id: int) -> JsonResponse:
|
|
20
|
-
"""Issue a new ``user_key`` for ``user_id``.
|
|
21
|
-
|
|
22
|
-
The response reveals the plain key once. Store only the hash server-side.
|
|
23
|
-
"""
|
|
24
|
-
|
|
25
|
-
user = get_user_model().objects.get(pk=user_id)
|
|
26
|
-
profile, key = AssistantProfile.issue_key(user)
|
|
27
|
-
return JsonResponse({"user_id": user_id, "user_key": key})
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
def authenticate(view_func):
|
|
31
|
-
"""View decorator that validates the ``Authorization`` header."""
|
|
32
|
-
|
|
33
|
-
@wraps(view_func)
|
|
34
|
-
def wrapper(request, *args, **kwargs):
|
|
35
|
-
header = request.META.get("HTTP_AUTHORIZATION", "")
|
|
36
|
-
if not header.startswith("Bearer "):
|
|
37
|
-
return HttpResponse(status=401)
|
|
38
|
-
|
|
39
|
-
key_hash = hash_key(header.split(" ", 1)[1])
|
|
40
|
-
try:
|
|
41
|
-
profile = AssistantProfile.objects.get(
|
|
42
|
-
user_key_hash=key_hash, is_active=True
|
|
43
|
-
)
|
|
44
|
-
except AssistantProfile.DoesNotExist:
|
|
45
|
-
return HttpResponse(status=401)
|
|
46
|
-
|
|
47
|
-
profile.touch()
|
|
48
|
-
request.assistant_profile = profile
|
|
49
|
-
request.chat_profile = profile
|
|
50
|
-
return view_func(request, *args, **kwargs)
|
|
51
|
-
|
|
52
|
-
return wrapper
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
@require_GET
|
|
56
|
-
@authenticate
|
|
57
|
-
def assistant_test(request):
|
|
58
|
-
"""Return a simple greeting to confirm authentication."""
|
|
59
|
-
|
|
60
|
-
profile = getattr(request, "assistant_profile", None)
|
|
61
|
-
user_id = profile.user_id if profile else None
|
|
62
|
-
return JsonResponse({"message": f"Hello from user {user_id}"})
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
@require_GET
|
|
66
|
-
@authenticate
|
|
67
|
-
def chat(request):
|
|
68
|
-
"""Return serialized data from any model.
|
|
69
|
-
|
|
70
|
-
Clients must provide ``model`` as ``app_label.ModelName`` and may include a
|
|
71
|
-
``pk`` to fetch a specific record. When ``pk`` is omitted, the view returns
|
|
72
|
-
up to 100 records.
|
|
73
|
-
"""
|
|
74
|
-
|
|
75
|
-
model_label = request.GET.get("model")
|
|
76
|
-
if not model_label:
|
|
77
|
-
return JsonResponse({"error": "model parameter required"}, status=400)
|
|
78
|
-
try:
|
|
79
|
-
model = apps.get_model(model_label)
|
|
80
|
-
except LookupError:
|
|
81
|
-
return JsonResponse({"error": "unknown model"}, status=400)
|
|
82
|
-
|
|
83
|
-
qs = model.objects.all()
|
|
84
|
-
pk = request.GET.get("pk")
|
|
85
|
-
if pk is not None:
|
|
86
|
-
try:
|
|
87
|
-
obj = qs.get(pk=pk)
|
|
88
|
-
except model.DoesNotExist:
|
|
89
|
-
return JsonResponse({"error": "object not found"}, status=404)
|
|
90
|
-
data = model_to_dict(obj)
|
|
91
|
-
else:
|
|
92
|
-
data = [model_to_dict(o) for o in qs[:100]]
|
|
93
|
-
|
|
94
|
-
return JsonResponse({"data": data})
|
|
File without changes
|
|
File without changes
|
|
File without changes
|