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

Files changed (81) hide show
  1. {arthexis-0.1.8.dist-info → arthexis-0.1.9.dist-info}/METADATA +42 -4
  2. arthexis-0.1.9.dist-info/RECORD +92 -0
  3. arthexis-0.1.9.dist-info/licenses/LICENSE +674 -0
  4. config/__init__.py +0 -1
  5. config/auth_app.py +0 -1
  6. config/celery.py +1 -2
  7. config/context_processors.py +1 -1
  8. config/offline.py +2 -0
  9. config/settings.py +133 -16
  10. config/urls.py +65 -6
  11. core/admin.py +1226 -191
  12. core/admin_history.py +50 -0
  13. core/admindocs.py +108 -1
  14. core/apps.py +158 -3
  15. core/backends.py +46 -4
  16. core/entity.py +62 -48
  17. core/fields.py +6 -1
  18. core/github_helper.py +25 -0
  19. core/github_issues.py +172 -0
  20. core/lcd_screen.py +1 -0
  21. core/liveupdate.py +25 -0
  22. core/log_paths.py +100 -0
  23. core/mailer.py +83 -0
  24. core/middleware.py +57 -0
  25. core/models.py +1071 -264
  26. core/notifications.py +11 -1
  27. core/public_wifi.py +227 -0
  28. core/release.py +27 -20
  29. core/sigil_builder.py +131 -0
  30. core/sigil_context.py +20 -0
  31. core/sigil_resolver.py +284 -0
  32. core/system.py +129 -10
  33. core/tasks.py +118 -19
  34. core/test_system_info.py +22 -0
  35. core/tests.py +358 -63
  36. core/tests_liveupdate.py +17 -0
  37. core/urls.py +2 -2
  38. core/user_data.py +329 -167
  39. core/views.py +383 -57
  40. core/widgets.py +51 -0
  41. core/workgroup_urls.py +7 -3
  42. core/workgroup_views.py +43 -6
  43. nodes/actions.py +0 -2
  44. nodes/admin.py +159 -284
  45. nodes/apps.py +9 -15
  46. nodes/backends.py +53 -0
  47. nodes/lcd.py +24 -10
  48. nodes/models.py +375 -178
  49. nodes/tasks.py +1 -5
  50. nodes/tests.py +524 -129
  51. nodes/utils.py +13 -2
  52. nodes/views.py +66 -23
  53. ocpp/admin.py +150 -61
  54. ocpp/apps.py +1 -1
  55. ocpp/consumers.py +432 -69
  56. ocpp/evcs.py +25 -8
  57. ocpp/models.py +408 -68
  58. ocpp/simulator.py +13 -6
  59. ocpp/store.py +258 -30
  60. ocpp/tasks.py +11 -7
  61. ocpp/test_export_import.py +8 -7
  62. ocpp/test_rfid.py +211 -16
  63. ocpp/tests.py +1198 -135
  64. ocpp/transactions_io.py +68 -22
  65. ocpp/urls.py +35 -2
  66. ocpp/views.py +654 -101
  67. pages/admin.py +173 -13
  68. pages/checks.py +0 -1
  69. pages/context_processors.py +19 -6
  70. pages/middleware.py +153 -0
  71. pages/models.py +37 -9
  72. pages/tests.py +759 -40
  73. pages/urls.py +3 -0
  74. pages/utils.py +0 -1
  75. pages/views.py +576 -25
  76. arthexis-0.1.8.dist-info/RECORD +0 -80
  77. arthexis-0.1.8.dist-info/licenses/LICENSE +0 -21
  78. config/workgroup_app.py +0 -7
  79. core/checks.py +0 -29
  80. {arthexis-0.1.8.dist-info → arthexis-0.1.9.dist-info}/WHEEL +0 -0
  81. {arthexis-0.1.8.dist-info → arthexis-0.1.9.dist-info}/top_level.txt +0 -0
core/tests.py CHANGED
@@ -2,42 +2,56 @@ import os
2
2
 
3
3
  os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
4
4
  import django
5
+
5
6
  django.setup()
6
7
 
7
- from django.test import Client, TestCase, RequestFactory
8
+ from django.test import Client, TestCase, RequestFactory, override_settings
8
9
  from django.urls import reverse
9
10
  from django.http import HttpRequest
10
11
  import json
12
+ from decimal import Decimal
11
13
  from unittest import mock
14
+ from unittest.mock import patch
12
15
  from pathlib import Path
13
16
  import subprocess
17
+ from glob import glob
18
+ from datetime import timedelta
19
+ import tempfile
14
20
 
15
21
  from django.utils import timezone
22
+ from django.contrib.auth.models import Permission
16
23
  from .models import (
17
24
  User,
25
+ UserPhoneNumber,
18
26
  EnergyAccount,
19
27
  ElectricVehicle,
20
28
  EnergyCredit,
21
- Address,
22
29
  Product,
23
- Subscription,
24
30
  Brand,
25
31
  EVModel,
26
32
  RFID,
27
- FediverseProfile,
28
33
  SecurityGroup,
29
34
  Package,
30
35
  PackageRelease,
36
+ ReleaseManager,
37
+ Todo,
38
+ PublicWifiAccess,
31
39
  )
32
40
  from django.contrib.admin.sites import AdminSite
33
- from core.admin import PackageReleaseAdmin, PackageAdmin
41
+ from core.admin import (
42
+ PackageReleaseAdmin,
43
+ PackageAdmin,
44
+ UserAdmin,
45
+ USER_PROFILE_INLINES,
46
+ )
34
47
  from ocpp.models import Transaction, Charger
35
48
 
36
49
  from django.core.exceptions import ValidationError
37
50
  from django.core.management import call_command
38
51
  from django.db import IntegrityError
39
52
  from .backends import LocalhostAdminBackend
40
- from core.views import _step_check_pypi, _step_promote_build, _step_publish
53
+ from core.views import _step_check_version, _step_promote_build, _step_publish
54
+ from core import public_wifi
41
55
 
42
56
 
43
57
  class DefaultAdminTests(TestCase):
@@ -78,6 +92,205 @@ class DefaultAdminTests(TestCase):
78
92
  )
79
93
 
80
94
 
95
+ class UserOperateAsTests(TestCase):
96
+ @classmethod
97
+ def setUpTestData(cls):
98
+ cls.permission = Permission.objects.get(codename="view_todo")
99
+
100
+ def test_staff_user_delegates_permissions(self):
101
+ delegate = User.objects.create_user(username="delegate", password="secret")
102
+ delegate.user_permissions.add(self.permission)
103
+ operator = User.objects.create_user(
104
+ username="operator", password="secret", is_staff=True
105
+ )
106
+ self.assertFalse(operator.has_perm("core.view_todo"))
107
+ operator.operate_as = delegate
108
+ operator.full_clean()
109
+ operator.save()
110
+ operator.refresh_from_db()
111
+ self.assertTrue(operator.has_perm("core.view_todo"))
112
+
113
+ def test_only_staff_may_operate_as(self):
114
+ delegate = User.objects.create_user(username="delegate", password="secret")
115
+ operator = User.objects.create_user(username="operator", password="secret")
116
+ operator.operate_as = delegate
117
+ with self.assertRaises(ValidationError):
118
+ operator.full_clean()
119
+
120
+ def test_non_superuser_cannot_operate_as_staff(self):
121
+ staff_delegate = User.objects.create_user(
122
+ username="delegate", password="secret", is_staff=True
123
+ )
124
+ operator = User.objects.create_user(
125
+ username="operator", password="secret", is_staff=True
126
+ )
127
+ operator.operate_as = staff_delegate
128
+ with self.assertRaises(ValidationError):
129
+ operator.full_clean()
130
+
131
+ def test_recursive_chain_and_cycle_detection(self):
132
+ base = User.objects.create_user(username="base", password="secret")
133
+ base.user_permissions.add(self.permission)
134
+ middle = User.objects.create_user(
135
+ username="middle", password="secret", is_staff=True
136
+ )
137
+ middle.operate_as = base
138
+ middle.full_clean()
139
+ middle.save()
140
+ top = User.objects.create_superuser(
141
+ username="top", email="top@example.com", password="secret"
142
+ )
143
+ top.operate_as = middle
144
+ top.full_clean()
145
+ top.save()
146
+ top.refresh_from_db()
147
+ self.assertTrue(top.has_perm("core.view_todo"))
148
+
149
+ first = User.objects.create_superuser(
150
+ username="first", email="first@example.com", password="secret"
151
+ )
152
+ second = User.objects.create_superuser(
153
+ username="second", email="second@example.com", password="secret"
154
+ )
155
+ first.operate_as = second
156
+ first.full_clean()
157
+ first.save()
158
+ second.operate_as = first
159
+ second.full_clean()
160
+ second.save()
161
+ self.assertFalse(first._check_operate_as_chain(lambda user: False))
162
+
163
+ def test_module_permissions_fall_back(self):
164
+ delegate = User.objects.create_user(username="helper", password="secret")
165
+ delegate.user_permissions.add(self.permission)
166
+ operator = User.objects.create_user(
167
+ username="mod", password="secret", is_staff=True
168
+ )
169
+ operator.operate_as = delegate
170
+ operator.full_clean()
171
+ operator.save()
172
+ self.assertTrue(operator.has_module_perms("core"))
173
+
174
+ def test_has_profile_via_delegate(self):
175
+ delegate = User.objects.create_user(
176
+ username="delegate", password="secret", is_staff=True
177
+ )
178
+ ReleaseManager.objects.create(user=delegate)
179
+ operator = User.objects.create_superuser(
180
+ username="operator",
181
+ email="operator@example.com",
182
+ password="secret",
183
+ )
184
+ operator.operate_as = delegate
185
+ operator.full_clean()
186
+ operator.save()
187
+ profile = operator.get_profile(ReleaseManager)
188
+ self.assertIsNotNone(profile)
189
+ self.assertEqual(profile.user, delegate)
190
+ self.assertTrue(operator.has_profile(ReleaseManager))
191
+
192
+ def test_has_profile_via_group_membership(self):
193
+ member = User.objects.create_user(username="member", password="secret")
194
+ group = SecurityGroup.objects.create(name="Managers")
195
+ group.user_set.add(member)
196
+ profile = ReleaseManager.objects.create(group=group)
197
+ self.assertEqual(member.get_profile(ReleaseManager), profile)
198
+ self.assertTrue(member.has_profile(ReleaseManager))
199
+
200
+ def test_release_manager_property_uses_delegate_profile(self):
201
+ delegate = User.objects.create_user(
202
+ username="delegate-property", password="secret", is_staff=True
203
+ )
204
+ profile = ReleaseManager.objects.create(user=delegate)
205
+ operator = User.objects.create_superuser(
206
+ username="operator-property",
207
+ email="operator-property@example.com",
208
+ password="secret",
209
+ )
210
+ operator.operate_as = delegate
211
+ operator.full_clean()
212
+ operator.save()
213
+ self.assertEqual(operator.release_manager, profile)
214
+
215
+
216
+ class UserPhoneNumberTests(TestCase):
217
+ def test_get_phone_numbers_by_priority(self):
218
+ user = User.objects.create_user(username="phone-user", password="secret")
219
+ later = UserPhoneNumber.objects.create(
220
+ user=user, number="+15555550101", priority=10
221
+ )
222
+ earlier = UserPhoneNumber.objects.create(
223
+ user=user, number="+15555550100", priority=1
224
+ )
225
+ immediate = UserPhoneNumber.objects.create(
226
+ user=user, number="+15555550099", priority=0
227
+ )
228
+
229
+ phones = user.get_phones_by_priority()
230
+ self.assertEqual(phones, [immediate, earlier, later])
231
+
232
+ def test_get_phone_numbers_by_priority_orders_by_id_when_equal(self):
233
+ user = User.objects.create_user(username="phone-order", password="secret")
234
+ first = UserPhoneNumber.objects.create(
235
+ user=user, number="+19995550000", priority=0
236
+ )
237
+ second = UserPhoneNumber.objects.create(
238
+ user=user, number="+19995550001", priority=0
239
+ )
240
+
241
+ phones = user.get_phones_by_priority()
242
+ self.assertEqual(phones, [first, second])
243
+
244
+ def test_get_phone_numbers_by_priority_alias(self):
245
+ user = User.objects.create_user(username="phone-alias", password="secret")
246
+ phone = UserPhoneNumber.objects.create(
247
+ user=user, number="+14445550000", priority=3
248
+ )
249
+
250
+ self.assertEqual(user.get_phone_numbers_by_priority(), [phone])
251
+
252
+
253
+ class ProfileValidationTests(TestCase):
254
+ def test_system_user_cannot_receive_profiles(self):
255
+ system_user = User.objects.get(username=User.SYSTEM_USERNAME)
256
+ profile = ReleaseManager(user=system_user)
257
+ with self.assertRaises(ValidationError) as exc:
258
+ profile.full_clean()
259
+ self.assertIn("user", exc.exception.error_dict)
260
+
261
+
262
+ class UserAdminInlineTests(TestCase):
263
+ def setUp(self):
264
+ self.site = AdminSite()
265
+ self.factory = RequestFactory()
266
+ self.admin = UserAdmin(User, self.site)
267
+ self.system_user = User.objects.get(username=User.SYSTEM_USERNAME)
268
+ self.superuser = User.objects.create_superuser(
269
+ username="inline-super",
270
+ email="inline-super@example.com",
271
+ password="secret",
272
+ )
273
+
274
+ def test_profile_inlines_hidden_for_system_user(self):
275
+ request = self.factory.get("/")
276
+ request.user = self.superuser
277
+ system_inlines = self.admin.get_inline_instances(request, self.system_user)
278
+ system_profiles = [
279
+ inline
280
+ for inline in system_inlines
281
+ if inline.__class__ in USER_PROFILE_INLINES
282
+ ]
283
+ self.assertFalse(system_profiles)
284
+
285
+ other_inlines = self.admin.get_inline_instances(request, self.superuser)
286
+ other_profiles = [
287
+ inline
288
+ for inline in other_inlines
289
+ if inline.__class__ in USER_PROFILE_INLINES
290
+ ]
291
+ self.assertEqual(len(other_profiles), len(USER_PROFILE_INLINES))
292
+
293
+
81
294
  class RFIDLoginTests(TestCase):
82
295
  def setUp(self):
83
296
  self.client = Client()
@@ -112,7 +325,7 @@ class RFIDBatchApiTests(TestCase):
112
325
  self.client.force_login(self.user)
113
326
 
114
327
  def test_export_rfids(self):
115
- tag_black = RFID.objects.create(rfid="CARD999")
328
+ tag_black = RFID.objects.create(rfid="CARD999", custom_label="Main Tag")
116
329
  tag_white = RFID.objects.create(rfid="CARD998", color=RFID.WHITE)
117
330
  self.account.rfids.add(tag_black, tag_white)
118
331
  response = self.client.get(reverse("rfid-batch"))
@@ -123,6 +336,7 @@ class RFIDBatchApiTests(TestCase):
123
336
  "rfids": [
124
337
  {
125
338
  "rfid": "CARD999",
339
+ "custom_label": "Main Tag",
126
340
  "energy_accounts": [self.account.id],
127
341
  "allowed": True,
128
342
  "color": "B",
@@ -141,6 +355,7 @@ class RFIDBatchApiTests(TestCase):
141
355
  "rfids": [
142
356
  {
143
357
  "rfid": "CARD111",
358
+ "custom_label": "",
144
359
  "energy_accounts": [],
145
360
  "allowed": True,
146
361
  "color": "W",
@@ -160,6 +375,7 @@ class RFIDBatchApiTests(TestCase):
160
375
  "rfids": [
161
376
  {
162
377
  "rfid": "CARD112",
378
+ "custom_label": "",
163
379
  "energy_accounts": [],
164
380
  "allowed": True,
165
381
  "color": "B",
@@ -174,6 +390,7 @@ class RFIDBatchApiTests(TestCase):
174
390
  "rfids": [
175
391
  {
176
392
  "rfid": "A1B2C3D4",
393
+ "custom_label": "Imported Tag",
177
394
  "energy_accounts": [self.account.id],
178
395
  "allowed": True,
179
396
  "color": "W",
@@ -191,6 +408,7 @@ class RFIDBatchApiTests(TestCase):
191
408
  self.assertTrue(
192
409
  RFID.objects.filter(
193
410
  rfid="A1B2C3D4",
411
+ custom_label="Imported Tag",
194
412
  energy_accounts=self.account,
195
413
  color=RFID.WHITE,
196
414
  released=True,
@@ -237,6 +455,11 @@ class RFIDValidationTests(TestCase):
237
455
  found = RFID.get_account_by_rfid("abcd1234")
238
456
  self.assertEqual(found, acc)
239
457
 
458
+ def test_custom_label_length(self):
459
+ tag = RFID(rfid="FACE1234", custom_label="x" * 33)
460
+ with self.assertRaises(ValidationError):
461
+ tag.full_clean()
462
+
240
463
 
241
464
  class RFIDAssignmentTests(TestCase):
242
465
  def setUp(self):
@@ -279,7 +502,9 @@ class EnergyAccountTests(TestCase):
279
502
 
280
503
  def test_service_account_ignores_balance(self):
281
504
  user = User.objects.create_user(username="service", password="x")
282
- acc = EnergyAccount.objects.create(user=user, service_account=True, name="SERVICE")
505
+ acc = EnergyAccount.objects.create(
506
+ user=user, service_account=True, name="SERVICE"
507
+ )
283
508
  self.assertTrue(acc.can_authorize())
284
509
 
285
510
  def test_account_without_user(self):
@@ -331,7 +556,43 @@ class AddressTests(TestCase):
331
556
  self.assertEqual(user.address, addr)
332
557
 
333
558
 
334
- class SubscriptionTests(TestCase):
559
+ class PublicWifiUtilitiesTests(TestCase):
560
+ def setUp(self):
561
+ self.user = User.objects.create_user(username="wifi", password="pwd")
562
+
563
+ def test_grant_public_access_records_allowlist(self):
564
+ with tempfile.TemporaryDirectory() as tmp:
565
+ base = Path(tmp)
566
+ allow_file = base / "locks" / "public_wifi_allow.list"
567
+ with override_settings(BASE_DIR=base):
568
+ with patch("core.public_wifi._iptables_available", return_value=False):
569
+ public_wifi.grant_public_access(self.user, "AA:BB:CC:DD:EE:FF")
570
+ self.assertTrue(allow_file.exists())
571
+ content = allow_file.read_text()
572
+ self.assertIn("aa:bb:cc:dd:ee:ff", content)
573
+ self.assertTrue(
574
+ PublicWifiAccess.objects.filter(
575
+ user=self.user, mac_address="aa:bb:cc:dd:ee:ff"
576
+ ).exists()
577
+ )
578
+
579
+ def test_revoke_public_access_for_user_updates_allowlist(self):
580
+ with tempfile.TemporaryDirectory() as tmp:
581
+ base = Path(tmp)
582
+ allow_file = base / "locks" / "public_wifi_allow.list"
583
+ with override_settings(BASE_DIR=base):
584
+ with patch("core.public_wifi._iptables_available", return_value=False):
585
+ access = public_wifi.grant_public_access(
586
+ self.user, "AA:BB:CC:DD:EE:FF"
587
+ )
588
+ public_wifi.revoke_public_access_for_user(self.user)
589
+ access.refresh_from_db()
590
+ self.assertIsNotNone(access.revoked_on)
591
+ if allow_file.exists():
592
+ self.assertNotIn("aa:bb:cc:dd:ee:ff", allow_file.read_text())
593
+
594
+
595
+ class LiveSubscriptionTests(TestCase):
335
596
  def setUp(self):
336
597
  self.client = Client()
337
598
  self.user = User.objects.create_user(username="bob", password="pwd")
@@ -339,22 +600,41 @@ class SubscriptionTests(TestCase):
339
600
  self.product = Product.objects.create(name="Gold", renewal_period=30)
340
601
  self.client.force_login(self.user)
341
602
 
342
- def test_create_and_list_subscription(self):
603
+ def test_create_and_list_live_subscription(self):
343
604
  response = self.client.post(
344
- reverse("add-subscription"),
605
+ reverse("add-live-subscription"),
345
606
  data={"account_id": self.account.id, "product_id": self.product.id},
346
607
  content_type="application/json",
347
608
  )
348
609
  self.assertEqual(response.status_code, 200)
349
- self.assertEqual(Subscription.objects.count(), 1)
610
+ self.account.refresh_from_db()
611
+ self.assertEqual(
612
+ self.account.live_subscription_product,
613
+ self.product,
614
+ )
615
+ self.assertIsNotNone(self.account.live_subscription_start_date)
616
+ self.assertEqual(
617
+ self.account.live_subscription_start_date,
618
+ timezone.localdate(),
619
+ )
620
+ self.assertEqual(
621
+ self.account.live_subscription_next_renewal,
622
+ self.account.live_subscription_start_date
623
+ + timedelta(days=self.product.renewal_period),
624
+ )
350
625
 
351
626
  list_resp = self.client.get(
352
- reverse("subscription-list"), {"account_id": self.account.id}
627
+ reverse("live-subscription-list"), {"account_id": self.account.id}
353
628
  )
354
629
  self.assertEqual(list_resp.status_code, 200)
355
630
  data = list_resp.json()
356
- self.assertEqual(len(data["subscriptions"]), 1)
357
- self.assertEqual(data["subscriptions"][0]["product__name"], "Gold")
631
+ self.assertEqual(len(data["live_subscriptions"]), 1)
632
+ self.assertEqual(data["live_subscriptions"][0]["product__name"], "Gold")
633
+ self.assertEqual(data["live_subscriptions"][0]["id"], self.account.id)
634
+ self.assertEqual(
635
+ data["live_subscriptions"][0]["next_renewal"],
636
+ str(self.account.live_subscription_next_renewal),
637
+ )
358
638
 
359
639
  def test_product_list(self):
360
640
  response = self.client.get(reverse("product-list"))
@@ -394,8 +674,8 @@ class EVBrandFixtureTests(TestCase):
394
674
  def test_ev_brand_fixture_loads(self):
395
675
  call_command(
396
676
  "loaddata",
397
- "core/fixtures/ev_brands.json",
398
- "core/fixtures/ev_models.json",
677
+ *sorted(glob("core/fixtures/ev_brands__*.json")),
678
+ *sorted(glob("core/fixtures/ev_models__*.json")),
399
679
  verbosity=0,
400
680
  )
401
681
  porsche = Brand.objects.get(name="Porsche")
@@ -408,11 +688,14 @@ class EVBrandFixtureTests(TestCase):
408
688
  )
409
689
  self.assertTrue(EVModel.objects.filter(brand=porsche, name="Taycan").exists())
410
690
  self.assertTrue(EVModel.objects.filter(brand=audi, name="e-tron GT").exists())
691
+ self.assertTrue(EVModel.objects.filter(brand=porsche, name="Macan").exists())
692
+ model3 = EVModel.objects.get(brand__name="Tesla", name="Model 3 RWD")
693
+ self.assertEqual(model3.est_battery_kwh, Decimal("57.50"))
411
694
 
412
695
  def test_brand_from_vin(self):
413
696
  call_command(
414
697
  "loaddata",
415
- "core/fixtures/ev_brands.json",
698
+ *sorted(glob("core/fixtures/ev_brands__*.json")),
416
699
  verbosity=0,
417
700
  )
418
701
  self.assertEqual(Brand.from_vin("WP0ZZZ12345678901").name, "Porsche")
@@ -424,9 +707,9 @@ class RFIDFixtureTests(TestCase):
424
707
  def test_fixture_assigns_gelectriic_rfid(self):
425
708
  call_command(
426
709
  "loaddata",
427
- "core/fixtures/users.json",
428
- "core/fixtures/energy_accounts.json",
429
- "core/fixtures/rfids.json",
710
+ "core/fixtures/users__arthexis.json",
711
+ "core/fixtures/energy_accounts__gelectriic.json",
712
+ "core/fixtures/rfids__ffffffff.json",
430
713
  verbosity=0,
431
714
  )
432
715
  account = EnergyAccount.objects.get(name="GELECTRIIC")
@@ -458,37 +741,6 @@ class SecurityGroupTests(TestCase):
458
741
  self.assertIn(user, child.user_set.all())
459
742
 
460
743
 
461
- class FediverseProfileTests(TestCase):
462
- def setUp(self):
463
- self.user = User.objects.create_user(username="fed", password="secret")
464
-
465
- @mock.patch("requests.get")
466
- def test_connection_success_sets_verified(self, mock_get):
467
- mock_get.return_value.ok = True
468
- mock_get.return_value.raise_for_status.return_value = None
469
- profile = FediverseProfile.objects.create(
470
- user=self.user,
471
- service=FediverseProfile.MASTODON,
472
- host="example.com",
473
- handle="fed",
474
- access_token="tok",
475
- )
476
- self.assertTrue(profile.test_connection())
477
- self.assertIsNotNone(profile.verified_on)
478
-
479
- @mock.patch("requests.get", side_effect=Exception("boom"))
480
- def test_connection_failure_raises(self, mock_get):
481
- profile = FediverseProfile.objects.create(
482
- user=self.user,
483
- service=FediverseProfile.MASTODON,
484
- host="example.com",
485
- handle="fed",
486
- )
487
- with self.assertRaises(ValidationError):
488
- profile.test_connection()
489
- self.assertIsNone(profile.verified_on)
490
-
491
-
492
744
  class ReleaseProcessTests(TestCase):
493
745
  def setUp(self):
494
746
  self.package = Package.objects.create(name="pkg")
@@ -499,14 +751,14 @@ class ReleaseProcessTests(TestCase):
499
751
  @mock.patch("core.views.release_utils._git_clean", return_value=False)
500
752
  def test_step_check_requires_clean_repo(self, git_clean):
501
753
  with self.assertRaises(Exception):
502
- _step_check_pypi(self.release, {}, Path("rel.log"))
754
+ _step_check_version(self.release, {}, Path("rel.log"))
503
755
 
504
756
  @mock.patch("core.views.release_utils._git_clean", return_value=True)
505
757
  @mock.patch("core.views.release_utils.network_available", return_value=False)
506
758
  def test_step_check_keeps_repo_clean(self, network_available, git_clean):
507
759
  version_path = Path("VERSION")
508
760
  original = version_path.read_text(encoding="utf-8")
509
- _step_check_pypi(self.release, {}, Path("rel.log"))
761
+ _step_check_version(self.release, {}, Path("rel.log"))
510
762
  proc = subprocess.run(
511
763
  ["git", "status", "--porcelain", str(version_path)],
512
764
  capture_output=True,
@@ -524,9 +776,7 @@ class ReleaseProcessTests(TestCase):
524
776
  @mock.patch("core.views.subprocess.run")
525
777
  @mock.patch("core.views.PackageRelease.dump_fixture")
526
778
  @mock.patch("core.views.release_utils.promote", side_effect=Exception("boom"))
527
- def test_promote_cleans_repo_on_failure(
528
- self, promote, dump_fixture, run
529
- ):
779
+ def test_promote_cleans_repo_on_failure(self, promote, dump_fixture, run):
530
780
  with self.assertRaises(Exception):
531
781
  _step_promote_build(self.release, {}, Path("rel.log"))
532
782
  dump_fixture.assert_not_called()
@@ -688,12 +938,9 @@ class PackageReleaseAdminActionTests(TestCase):
688
938
  mock_get.return_value.json.return_value = {
689
939
  "releases": {"1.0.0": [], "1.1.0": []}
690
940
  }
691
- self.admin.refresh_from_pypi(
692
- self.request, PackageRelease.objects.none()
693
- )
694
- self.assertTrue(
695
- PackageRelease.objects.filter(version="1.1.0").exists()
696
- )
941
+ self.admin.refresh_from_pypi(self.request, PackageRelease.objects.none())
942
+ new_release = PackageRelease.objects.get(version="1.1.0")
943
+ self.assertEqual(new_release.revision, "")
697
944
  dump.assert_called_once()
698
945
 
699
946
 
@@ -772,3 +1019,51 @@ class PackageReleaseChangelistTests(TestCase):
772
1019
  )
773
1020
  self.assertContains(response, refresh_url, html=False)
774
1021
 
1022
+
1023
+ class TodoDoneTests(TestCase):
1024
+ def setUp(self):
1025
+ self.client = Client()
1026
+ User.objects.create_superuser("admin", "admin@example.com", "pw")
1027
+ self.client.force_login(User.objects.get(username="admin"))
1028
+
1029
+ def test_mark_done_sets_timestamp(self):
1030
+ todo = Todo.objects.create(request="Task", is_seed_data=True)
1031
+ resp = self.client.post(reverse("todo-done", args=[todo.pk]))
1032
+ self.assertRedirects(resp, reverse("admin:index"))
1033
+ todo.refresh_from_db()
1034
+ self.assertIsNotNone(todo.done_on)
1035
+ self.assertFalse(todo.is_deleted)
1036
+
1037
+
1038
+ class TodoUrlValidationTests(TestCase):
1039
+ def test_relative_url_valid(self):
1040
+ todo = Todo(request="Task", url="/path")
1041
+ todo.full_clean() # should not raise
1042
+
1043
+ def test_absolute_url_invalid(self):
1044
+ todo = Todo(request="Task", url="https://example.com/path")
1045
+ with self.assertRaises(ValidationError):
1046
+ todo.full_clean()
1047
+
1048
+
1049
+ class TodoUniqueTests(TestCase):
1050
+ def test_request_unique_case_insensitive(self):
1051
+ Todo.objects.create(request="Task")
1052
+ with self.assertRaises(IntegrityError):
1053
+ Todo.objects.create(request="task")
1054
+
1055
+
1056
+ class TodoAdminPermissionTests(TestCase):
1057
+ def setUp(self):
1058
+ self.client = Client()
1059
+ User.objects.create_superuser("admin", "admin@example.com", "pw")
1060
+ self.client.force_login(User.objects.get(username="admin"))
1061
+
1062
+ def test_add_view_disallowed(self):
1063
+ resp = self.client.get(reverse("admin:core_todo_add"))
1064
+ self.assertEqual(resp.status_code, 403)
1065
+
1066
+ def test_change_form_loads(self):
1067
+ todo = Todo.objects.create(request="Task")
1068
+ resp = self.client.get(reverse("admin:core_todo_change", args=[todo.pk]))
1069
+ self.assertEqual(resp.status_code, 200)
@@ -0,0 +1,17 @@
1
+ from django.test import RequestFactory, TestCase
2
+ from django.views.generic import TemplateView
3
+
4
+ from core.liveupdate import LiveUpdateMixin
5
+
6
+
7
+ class DummyView(LiveUpdateMixin, TemplateView):
8
+ template_name = "pages/base.html"
9
+ live_update_interval = 7
10
+
11
+
12
+ class LiveUpdateMixinTests(TestCase):
13
+ def test_mixin_sets_request_interval(self):
14
+ factory = RequestFactory()
15
+ request = factory.get("/")
16
+ DummyView.as_view()(request)
17
+ self.assertEqual(request.live_update_interval, 7)
core/urls.py CHANGED
@@ -6,6 +6,6 @@ urlpatterns = [
6
6
  path("rfid-login/", views.rfid_login, name="rfid-login"),
7
7
  path("rfids/", views.rfid_batch, name="rfid-batch"),
8
8
  path("products/", views.product_list, name="product-list"),
9
- path("subscribe/", views.add_subscription, name="add-subscription"),
10
- path("list/", views.subscription_list, name="subscription-list"),
9
+ path("live-subscribe/", views.add_live_subscription, name="add-live-subscription"),
10
+ path("live-list/", views.live_subscription_list, name="live-subscription-list"),
11
11
  ]