django-restit 4.2.174__py3-none-any.whl → 4.2.176__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.
@@ -0,0 +1,18 @@
1
+ # Generated by Django 4.2.18 on 2025-03-23 19:54
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ('account', '0022_alter_memberdevice_modified'),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.AddField(
14
+ model_name='authsession',
15
+ name='method',
16
+ field=models.CharField(blank=True, db_index=True, default=None, max_length=127, null=True),
17
+ ),
18
+ ]
account/models/member.py CHANGED
@@ -149,7 +149,7 @@ class Member(User, RestModel, MetaDataModel):
149
149
  display_name = models.CharField(max_length=64, blank=True, null=True, default=None)
150
150
  picture = models.ForeignKey("medialib.MediaItem", blank=True, null=True, help_text="Profile picture", related_name='+', on_delete=models.CASCADE)
151
151
 
152
- # we use this token to allow us to invalidate JWT tokens
152
+ # we use this token to allow us to invalidate JWT tokens
153
153
  security_token = models.CharField(max_length=64, blank=True, null=True, default=None, db_index=True)
154
154
  auth_code = models.CharField(max_length=64, blank=True, null=True, default=None, db_index=True)
155
155
  auth_code_expires = models.DateTimeField(blank=True, null=True, default=None)
@@ -263,6 +263,24 @@ class Member(User, RestModel, MetaDataModel):
263
263
  return self.has_usable_password()
264
264
  return False
265
265
 
266
+ def getFailedLoginCount(self):
267
+ c = RemoteEvents.hget("users:failed:username", self.username)
268
+ if c is not None:
269
+ return int(c)
270
+ return c
271
+
272
+ @classmethod
273
+ def GetFailedLoginsForIP(cls, ip):
274
+ c = RemoteEvents.hget("users:failed:ip", ip)
275
+ if c is not None:
276
+ return int(c)
277
+ return c
278
+
279
+ @classmethod
280
+ def GetIPsWithFailedLoginAttempts(cls, threshold):
281
+ failed_ips = RemoteEvents.hgetall("users:failed:ip")
282
+ return {ip: int(count) for ip, count in failed_ips.items() if int(count) > threshold}
283
+
266
284
  def recordFailedLogin(self, request):
267
285
  c = RemoteEvents.hincrby("users:failed:username", self.username, 1)
268
286
  if c >= settings.LOCK_PASSWORD_ATTEMPTS:
@@ -273,13 +291,15 @@ class Member(User, RestModel, MetaDataModel):
273
291
  "account", f"incorrect password for {self.username}", level=8,
274
292
  error_code=498,
275
293
  request=request)
276
- c = RemoteEvents.hincrby("users:failed:ip", request.ip, 1)
294
+ ic = RemoteEvents.hincrby("users:failed:ip", request.ip, 1)
295
+ return c
277
296
 
278
297
  def recordSuccessLogin(self, request):
279
298
  self.last_login = datetime.now()
280
299
  self.save()
281
300
  RemoteEvents.hdel("users:failed:username", self.username)
282
- RemoteEvents.hdel("users:failed:ip", request.ip)
301
+ if request:
302
+ RemoteEvents.hdel("users:failed:ip", request.ip)
283
303
 
284
304
  def hasPasswordExpired(self):
285
305
  now = datetime.now()
@@ -288,38 +308,33 @@ class Member(User, RestModel, MetaDataModel):
288
308
  self.save()
289
309
  return now - self.password_changed > timedelta(days=settings.PASSWORD_EXPIRES_DAYS)
290
310
 
291
- def login(self, password=None, request=None, use_jwt=False):
292
- if not self.is_active or self.is_blocked:
293
- return False
294
- # can force login
311
+ def login(self, password=None, request=None):
295
312
  if not request:
296
313
  request = rh.getActiveRequest()
314
+ if not self.is_active or self.is_blocked:
315
+ return False
297
316
  if not self.checkPassword(password):
298
- # invalid password
299
317
  self.recordFailedLogin(request)
300
318
  return False
301
- else:
302
- self.recordSuccessLogin(request)
303
- if use_jwt:
304
- self.recordSuccessLogin(request)
305
- self.locateByIP(request.ip)
306
- return True
307
- self.user_ptr.backend = 'django.contrib.auth.backends.ModelBackend'
308
- auth_login(request, self.user_ptr)
309
- self.locateByIP(request.ip)
319
+ self.authLogin(request)
310
320
  return True
311
321
 
312
322
  def loginNoPassword(self, request=None):
313
323
  if not self.is_active or self.is_blocked:
314
324
  return False
315
- # can force login
316
325
  if not request:
317
326
  request = rh.getActiveRequest()
318
- self.user_ptr.backend = 'django.contrib.auth.backends.ModelBackend'
319
- auth_login(request, self.user_ptr)
320
- self.locateByIP(request.ip)
327
+ self.authLogin(request)
321
328
  return True
322
329
 
330
+ def authLogin(self, request, use_session=True):
331
+ if use_session:
332
+ self.user_ptr.backend = 'django.contrib.auth.backends.ModelBackend'
333
+ auth_login(request, self.user_ptr)
334
+ if request:
335
+ self.locateByIP(request.ip)
336
+ self.recordSuccessLogin(request)
337
+
323
338
  def canLogin(self, request=None, throw_exception=True, using_password=True):
324
339
  if not self.is_active:
325
340
  self.log("login_blocked", F"account is not active {self.username}", request, method="login", level=31)
@@ -577,7 +592,7 @@ class Member(User, RestModel, MetaDataModel):
577
592
  SMS.send(phone, msg)
578
593
  return True
579
594
 
580
- def sendEmail(self, subject, body, attachments=[], do_async=True, template=None,
595
+ def sendEmail(self, subject, body, attachments=[], do_async=True, template=None,
581
596
  context=None):
582
597
  default_domain = self.getProperty("default_domain", settings.EMAIL_DEFAULT_DOMAIN)
583
598
  from_email = rh.getFromEmailForHost(default_domain)
@@ -675,7 +690,7 @@ class Member(User, RestModel, MetaDataModel):
675
690
  allow_email = not self.email_disabled and valid_email and (force or via in ["all", "email"])
676
691
  else:
677
692
  allow_email = valid_email
678
-
693
+
679
694
  if not allow_email and not allow_sms:
680
695
  return False
681
696
 
@@ -796,7 +811,7 @@ class Member(User, RestModel, MetaDataModel):
796
811
  def block(self, reason, request=None):
797
812
  if not request:
798
813
  request = self.getActiveRequest()
799
- PersistentLog.log("account blocked, {}".format(reason), 5, request, "account.Member", self.pk, "blocked")
814
+ self.auditLog(f"account blocked, {reason}", "blocked", level=5)
800
815
  RemoteEvents.hset("users:blocked:username", self.username, time.time())
801
816
  self.reportIncident(
802
817
  "account", f"account '{self.username}' blocked: {reason}", level=2,
@@ -832,7 +847,7 @@ class Member(User, RestModel, MetaDataModel):
832
847
 
833
848
  # BEGIN REST CALLBACKS
834
849
  def on_permission_change(self, key, value, old_value, category):
835
- # called when a metadata.permissions field changes.. see django settings USER_METADATA_PROPERTIES
850
+ # called when a metadata.permissions field changes.. see django settings USER_METADATA_PROPERTIES
836
851
  # we want to log both the person changing permissions
837
852
  # and those being changed
838
853
  request = RestModel.getActiveRequest()
@@ -865,7 +880,7 @@ class Member(User, RestModel, MetaDataModel):
865
880
  request = self.getActiveRequest()
866
881
  if not request.member.is_superuser:
867
882
  raise PermissionDeniedException("Permission Denied: attempting to super user")
868
- self.is_superuser = int(value)
883
+ self.is_superuser = int(value)
869
884
 
870
885
  def set_disable(self, value):
871
886
  if value is not None and value in [1, '1', True, 'true']:
@@ -1219,7 +1234,7 @@ class Member(User, RestModel, MetaDataModel):
1219
1234
  def authWS4RedisConnection(cls, auth_data):
1220
1235
  """
1221
1236
  This method is used by the async/websocket service to authenticate.
1222
- If the model can authenticate the connection it should return dict
1237
+ If the model can authenticate the connection it should return dict
1223
1238
  with kind and pk of the model that is authenticaed
1224
1239
  """
1225
1240
  from rest import UberDict
@@ -1338,5 +1353,3 @@ class AuthToken(models.Model, RestModel):
1338
1353
 
1339
1354
  def __str__(self):
1340
1355
  return "{o.member}: {o.secure_token}".format(o=self)
1341
-
1342
-
account/models/session.py CHANGED
@@ -42,6 +42,7 @@ class AuthSession(models.Model, RestModel):
42
42
  signature = models.CharField(max_length=127, db_index=True)
43
43
  ip = models.CharField(max_length=127, null=True, blank=True, db_index=True)
44
44
  buid = models.CharField(max_length=127, null=True, blank=True, default=None)
45
+ method = models.CharField(max_length=127, null=True, blank=True, db_index=True, default=None)
45
46
 
46
47
  member = models.ForeignKey("account.Member", null=True, blank=True, related_name="auth_sessions", on_delete=models.CASCADE)
47
48
  location = models.ForeignKey("location.GeoIP", related_name="auth_sessions", blank=True, null=True, default=None, on_delete=models.CASCADE)
@@ -72,8 +73,9 @@ class AuthSession(models.Model, RestModel):
72
73
  return f"{self.member.username} - {self.ip} - {self.os} - {self.browser}"
73
74
 
74
75
  @classmethod
75
- def NewSession(cls, request):
76
+ def NewSession(cls, request, method="jwt"):
76
77
  obj = cls(ip=request.ip, member=request.member, signature=request.signature)
78
+ obj.method = method
77
79
  obj.user_agent = request.META.get('HTTP_USER_AGENT', None)
78
80
  obj.last_activity = datetime.now()
79
81
  obj.buid = request.POST.get("__buid__", request.GET.get("__buid__", None))
@@ -92,4 +94,3 @@ class AuthSession(models.Model, RestModel):
92
94
  session.touch()
93
95
  return session
94
96
  return None
95
-
account/rpc/auth.py CHANGED
@@ -37,6 +37,7 @@ def member_login(request):
37
37
  @rd.never_cache
38
38
  def jwt_login(request):
39
39
  # poor mans JWT, carried over
40
+ auth_method = "basic"
40
41
  username = request.DATA.get('username', None)
41
42
  if not username:
42
43
  return rv.restPermissionDenied(request, "Password and/or Username is incorrect", error_code=422)
@@ -45,29 +46,31 @@ def jwt_login(request):
45
46
  return rv.restPermissionDenied(request, error=f"Password and/or Username is incorrect for {username}", error_code=422)
46
47
  auth_code = request.DATA.get(["auth_code", "code", "invite_token"], None)
47
48
  if username and auth_code:
49
+ # this is typically used for OAUTH (final)
48
50
  return member_login_uname_code(request, username, auth_code)
49
51
  password = request.DATA.get('password', None)
50
52
  member.canLogin(request) # throws exception if cannot login
51
53
  if member.requires_totp or member.has_totp:
54
+ auth_method = "basic+totp"
52
55
  resp = checkForTOTP(request, member)
53
56
  if resp is not None:
54
57
  return resp
55
- if not member.login(request=request, password=password, use_jwt=True):
58
+ if not member.login(request=request, password=password):
56
59
  # we do not want permission denied catcher invoked as it is already handled in login method
57
60
  return rv.restStatus(request, False, error=f"Invalid Credentials {username}", error_code=401)
58
- return on_complete_jwt(request, member)
61
+ return on_complete_jwt(request, member, auth_method)
59
62
 
60
63
 
61
- def on_complete_jwt(request, member):
64
+ def on_complete_jwt(request, member, method="basic"):
62
65
  if member.security_token is None or member.security_token == JWT_KEY or member.force_single_session:
63
66
  member.refreshSecurityToken()
64
67
 
65
68
  member.log(
66
- "jwt_login", "jwt login succesful",
69
+ "jwt_login", "jwt login succesful",
67
70
  request, method="login", level=7)
68
71
 
69
72
  device_id = request.DATA.get(["device_id", "deviceID"])
70
-
73
+
71
74
  token = JWToken(
72
75
  user_id=member.pk,
73
76
  key=member.security_token,
@@ -80,13 +83,13 @@ def on_complete_jwt(request, member):
80
83
  request.signature = token.session_id
81
84
  request.device_id = device_id
82
85
  request.buid = request.DATA.get("__buid__", None)
83
- request.auth_session = am.AuthSession.NewSession(request)
86
+ request.auth_session = am.AuthSession.NewSession(request, method)
84
87
  if bool(device_id):
85
88
  am.MemberDevice.register(request, member, device_id)
86
-
89
+
87
90
  request.jwt_token = token.access_token # this tells the middleware to store in cookie
88
91
  return rv.restGet(
89
- request,
92
+ request,
90
93
  dict(
91
94
  access=token.access_token,
92
95
  refresh=token.refresh_token,
@@ -218,12 +221,13 @@ def member_login_uname_code(request, username, auth_code):
218
221
  member.setPassword(password)
219
222
  member.auth_code = None
220
223
  member.auth_code_expires = None
221
- member.login(request=request)
224
+ member.canLogin(request, using_password=False) # throws exception if cannot login
225
+ member.loginNoPassword(request)
222
226
  member.save()
223
227
  member.log("code_login", "code login", request, method="login", level=8)
224
- if request.DATA.get("auth_method") == "basic":
228
+ if request.DATA.get("auth_method") == "basic" and ALLOW_BASIC_LOGIN:
225
229
  return rv.restGet(request, dict(id=member.pk, session_key=request.session.session_key))
226
- return on_complete_jwt(request, member)
230
+ return on_complete_jwt(request, member, "auth_code")
227
231
 
228
232
 
229
233
  @rd.url(r'^logout$')
@@ -296,7 +300,7 @@ def get_member_from_request(request):
296
300
  request.member = member
297
301
  member.log("login_blocked", "account is locked out", request, method="login", level=31)
298
302
  return member, rv.restPermissionDenied(request, error=f"{member.username} Account locked out", error_code=411)
299
- return member, None
303
+ return member, None
300
304
 
301
305
 
302
306
  @rd.urlPOST('forgot')
@@ -410,3 +414,10 @@ def totp_verify(request):
410
414
  return rv.restStatus(request, True)
411
415
 
412
416
 
417
+ # time based one time passwords
418
+ @rd.urlPOST('security/ip/failed_logins')
419
+ @rd.login_required
420
+ def failed_logins_by_ip(request):
421
+ ips = am.Member.GetIPsWithFailedLoginAttempts(request.DATA.get("threshold", 10))
422
+ lst = [dict(ip=k, count=v) for k, v in ips.items()]
423
+ return rv.restList(request, lst)
account/rpc/oauth.py CHANGED
@@ -20,37 +20,43 @@ def oauth_google_login(request):
20
20
  state = request.DATA.get("state")
21
21
  app_url = settings.DEFAULT_LOGIN_URL
22
22
 
23
- rh.log_print("google/login", request.DATA.toDict(), request.session.get("state"))
23
+ # rh.log_print("google/login", request.DATA.toDict(), request.session.get("state"))
24
24
 
25
25
  if state:
26
+ # this is where we should pull out the passed in state and get the proper URL
26
27
  state = objict.fromJSON(rh.hexToString(state))
27
28
  rh.log_print("state", state)
28
29
  app_url = state.url
29
30
 
30
31
  if not code:
31
32
  params = urlencode({'error': error})
32
- return redirect(f"{app_url}?{params}")
33
+ separator = '&' if '?' in app_url and app_url[-1] != '?' else '?'
34
+ return redirect(f"{app_url}{separator}{params}")
33
35
 
34
- redirect_uri = f"{settings.BASE_URL_SECURE}{REST_PREFIX}account/oauth/google/login"
36
+ redirect_uri = f"{request.scheme}://{request.get_host()}/{REST_PREFIX}account/oauth/google/login"
35
37
  auth_data = google.getAccessToken(code, redirect_uri)
36
38
  if auth_data is None or auth_data.access_token is None:
37
39
  params = urlencode({'error': "failed to get access token from google"})
38
- return redirect(f"{app_url}?{params}")
40
+ separator = '&' if '?' in app_url and app_url[-1] != '?' else '?'
41
+ return redirect(f"{app_url}{separator}{params}")
39
42
 
40
43
  user_data = google.getUserInfo(auth_data.access_token)
41
44
  if user_data is None:
42
45
  params = urlencode({'error': "failed to get user data from google"})
43
- return redirect(f"{app_url}?{params}")
46
+ separator = '&' if '?' in app_url and app_url[-1] != '?' else '?'
47
+ return redirect(f"{app_url}{separator}{params}")
44
48
 
45
49
  if not user_data.email:
46
50
  params = urlencode({'error': "no email with account"})
47
- return redirect(f"{app_url}?{params}")
51
+ separator = '&' if '?' in app_url and app_url[-1] != '?' else '?'
52
+ return redirect(f"{app_url}{separator}{params}")
48
53
 
49
54
  # TODO allow new accounts?
50
55
  member = Member.objects.filter(email=user_data.email).last()
51
56
  if member is None:
52
57
  params = urlencode({'error': "user not found"})
53
- return redirect(f"{app_url}?{params}")
58
+ separator = '&' if '?' in app_url and app_url[-1] != '?' else '?'
59
+ return redirect(f"{app_url}{separator}{params}")
54
60
 
55
61
  member.setProperties(auth_data, category="google_auth")
56
62
  member.setProperties(user_data, category="google")
@@ -69,13 +75,8 @@ def oauth_google_login(request):
69
75
  member.save()
70
76
  member.auditLog("user succesfully authenticated with google", "google_oauth", level=17)
71
77
 
72
- params = urlencode({'oauth_code': member.auth_code, "username":member.username})
78
+ params = urlencode({'oauth_code': member.auth_code, "username":member.username, "auth_method":"google_oauth"})
73
79
  rurl = None
74
- if "?" in app_url:
75
- if app_url[-1] == "?":
76
- rurl = f"{app_url}{params}"
77
- else:
78
- rurl = f"{app_url}&{params}"
79
- else:
80
- rurl = f"{app_url}?{params}"
80
+ separator = '&' if '?' in app_url and app_url[-1] != '?' else '?'
81
+ rurl = f"{app_url}{separator}{params}"
81
82
  return redirect(rurl)
account/rpc/passkeys.py CHANGED
@@ -51,4 +51,4 @@ def rest_on_passkeys_auth_complete(request):
51
51
  request.session.pop("fido2_state"),
52
52
  request.session.pop("fido2_rp_id"))
53
53
  # we now want to handle the JWT or basic login flow
54
- return on_complete_jwt(request, uk.member)
54
+ return on_complete_jwt(request, uk.member, "passkey")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: django-restit
3
- Version: 4.2.174
3
+ Version: 4.2.176
4
4
  Summary: A Rest Framework for DJANGO
5
5
  License: MIT
6
6
  Author: Ian Starnes
@@ -24,17 +24,18 @@ account/migrations/0019_group_location.py,sha256=EfMB_w4qWUGDqQeNc453PFZwpjpTeoA
24
24
  account/migrations/0020_cloudcredentials_cloudcredentialsmetadata.py,sha256=mHwxkyDfA4ueQOt34w5ndJB4XwNTDLv79CkKgzhlz-c,2250
25
25
  account/migrations/0021_alter_cloudcredentials_group.py,sha256=zoFYmE-hd3uRGX6DRO9k-osPwH0jFeTU7S-pjCOtakk,561
26
26
  account/migrations/0022_alter_memberdevice_modified.py,sha256=9eeKcdr9p6qFJ8ZxSnKSj1KxZjW8NZfM0YCMck6i0QQ,424
27
+ account/migrations/0023_authsession_method.py,sha256=5oIOXyj24rlrLFq7frij5VBaWk_ckh7hRPqq_jpO_b0,452
27
28
  account/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
28
29
  account/models/__init__.py,sha256=cV_lMnT2vL_mjiYtT4hlcIHo52ocFbGSNVkOIHHLXZY,385
29
30
  account/models/device.py,sha256=8D-Sbv9PZWAnX6UVpp1lNJ03P24fknNnN1VOhqY7RVg,6306
30
31
  account/models/feeds.py,sha256=vI7fG4ASY1M0Zjke24RdnfDcuWeATl_yR_25jPmT64g,2011
31
32
  account/models/group.py,sha256=JVyMIakLskUuGXBJFyessw4LlD9Fl6AsHtpo1yZEYjk,22884
32
33
  account/models/legacy.py,sha256=zYdtv4LC0ooxPVqWM-uToPwV-lYWQLorSE6p6yn1xDw,2720
33
- account/models/member.py,sha256=qmLCOVbNTRr4L-E7BbOMtv4V64QN7K-0pXDgnuB-AbY,54722
34
+ account/models/member.py,sha256=6-KbOYFyvloyiITgz5gKMyORmPtDESdzbBbMrjmz4Rs,55058
34
35
  account/models/membership.py,sha256=90EpAhOsGaqphDAkONP6j_qQ0OWSRaQsI8H7E7fgMkE,9249
35
36
  account/models/notify.py,sha256=YKYEXT56i98b7-ydLt5UuEVOqW7lipQMi-KuiPhcSwY,15627
36
37
  account/models/passkeys.py,sha256=lObapudvL--ABSTZTIELmYvHE3dPF0tO_KmuYk0ZJXc,1699
37
- account/models/session.py,sha256=ELkWjB_2KXQvPtRPrvuGJpJsqrxCQX_4J53SbqGz_2U,3737
38
+ account/models/session.py,sha256=5tpyRF1BHTPBL5gBIx8Eu55sWc6pTziqDpm7Zm5bdJI,3876
38
39
  account/models/settings.py,sha256=gOyRWBVd3BQpjfj_hJPtqX3H46ztyRAFxBrPbv11lQg,2137
39
40
  account/oauth/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
40
41
  account/oauth/google.py,sha256=q5M6Qhpfp9QslKRVYFZBvtG6kgXV6vYMrR5fp6Xdb9I,2078
@@ -42,13 +43,13 @@ account/passkeys/__init__.py,sha256=FwXYJXwSJXfkLojGBcVpF1dFpgFhzDdd9N_3naYQ0cc,
42
43
  account/passkeys/core.py,sha256=4aUBNCuF_kjOvE1zFapK1Pj28ap5slO71dRyfnWi0YU,4148
43
44
  account/periodic.py,sha256=-u0n-7QTJgDOkasGhBAPwHAwjpqWGA-MZLEFkVTqCGU,874
44
45
  account/rpc/__init__.py,sha256=SGF0M_-H0dKh3b1apSX29BotNWAvITYccGQVC0MIjL8,336
45
- account/rpc/auth.py,sha256=O-co_vZM9T4lsu3hidjKAeL2tTPWgbmoLsrjhmFZ7lA,17064
46
+ account/rpc/auth.py,sha256=MDXXhTlzTRcKB8TQ0OPawvsb0P4EeH_J3ukeSljgytk,17615
46
47
  account/rpc/device.py,sha256=lU2BHNPreHV0dDTjAPc7Sc-5m2JP8SiWVqiKuBfV7Fo,2281
47
48
  account/rpc/group.py,sha256=hw7iczZ6W_IrRbx5ZDw6cZ5I_ztqxhtUFJD9WR91_4s,4948
48
49
  account/rpc/member.py,sha256=8XnJX-iri0Om4nc-V2_tDJzfCSzziKLw6dUx9egtEZE,2236
49
50
  account/rpc/notify.py,sha256=Q2YWejP36egeF060Hih5uX4Psv_B8NWlLLPi7iDYlIw,3344
50
- account/rpc/oauth.py,sha256=ISLVsR5HvKALANokaOFRvF4FTRxWtXPvVnZAYANKxpo,2864
51
- account/rpc/passkeys.py,sha256=5x28nYILJUMMSwfVuWYL66hfoGUXahMqOwiHhM4I3Do,1729
51
+ account/rpc/oauth.py,sha256=1dMvEpoMcvGXEdgDq2OBZcRMnXcE1jD18-itVrZwH9A,3333
52
+ account/rpc/passkeys.py,sha256=AKgF2xgaGJi8UCFgfSMBh7rUqrJgVL8-RfDDRoPWiDM,1740
52
53
  account/rpc/settings.py,sha256=EvPuwW63Gp_Va0ANIPAZ894tnS_JCctQ0FzqYRdKUNM,271
53
54
  account/settings.py,sha256=XEvZdcA6p_iUpDq9NmICK8rxzIQ8NViKfrpyuYgSV4o,53
54
55
  account/templates/email/base.html,sha256=GUuatccaZtO_hLLNZmMQQKew1Bjfz3e6Z7p3dM6BrWk,9669
@@ -379,7 +380,7 @@ pushit/utils.py,sha256=IeTCGa-164nmB1jIsK1lu1O1QzUhS3BKfuXHGjCW-ck,2121
379
380
  rest/.gitignore,sha256=TbEvWRMnAiajCTOdhiNrd9eeCAaIjRp9PRjE_VkMM5g,118
380
381
  rest/README.md,sha256=V3ETc-cJu8PZIbKr9xSe_pA4JEUpC8Dhw4bQeVCDJPw,5460
381
382
  rest/RemoteEvents.py,sha256=nL46U7AuxIrlw2JunphR1tsXyqi-ep_gD9CYGpYbNgE,72
382
- rest/__init__.py,sha256=j76Rqm_TIvJvezmNJkUripR1cpoqTtqZAXhwIQ0t_Uk,122
383
+ rest/__init__.py,sha256=57yJpEKX4zpzd53NtME1OKpJzJqzJxz38KVjs86WJ0g,122
383
384
  rest/arc4.py,sha256=y644IbF1ec--e4cUJ3KEYsewTCITK0gmlwa5mJruFC0,1967
384
385
  rest/cache.py,sha256=1Qg0rkaCJCaVP0-l5hZg2CIblTdeBSlj_0fP6vlKUpU,83
385
386
  rest/crypto/__init__.py,sha256=Tl0U11rgj1eBYqd6OXJ2_XSdNLumW_JkBZnaJqI6Ldw,72
@@ -399,7 +400,7 @@ rest/helpers.py,sha256=t7smlOUzchVno-zeq7xMJIwogAR2DeSrffWxgysOHX8,29531
399
400
  rest/joke.py,sha256=0PpKaX2iN7jlS62kgjfmmqkFBYLPURz15aQ8R7OJkJ8,260
400
401
  rest/jwtoken.py,sha256=F7Vvpm31rAplTXr8XFP-Lb4BnDB3j1B2nQq0P1iTCLQ,2576
401
402
  rest/log.py,sha256=hd1_4HBOS395sfXJIL6BTw9yekm1SLgBwYx_PdfIhKA,20930
402
- rest/mail.py,sha256=Rm40hWDYop0tMqxdN-J2NT-dCnP-f4SfCZxSO02ajzs,7965
403
+ rest/mail.py,sha256=FFvHgNcq84sJP5DyaJQKbuoao5NAsS6r-PT4MRfjWFU,8139
403
404
  rest/mailman.py,sha256=v5O1G5s3HiAKmz-J1z0uT6_q3xsONPpxVl9saEyQQ2I,9174
404
405
  rest/management/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
405
406
  rest/management/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -429,7 +430,7 @@ rest/serializers/legacy.py,sha256=a5O-x2PqMKX8wYWrhCmdcivVbkPnru7UdyLbrhCaAdY,61
429
430
  rest/serializers/localizers.py,sha256=BegaCvTQVaruhWzvGHq3zWeVFmtBChatquRqAtkke10,410
430
431
  rest/serializers/model.py,sha256=08HJeqpmytjxvyiJFfsSRRG0uH-iK2mXCw6w0oMfWrI,8598
431
432
  rest/serializers/profiler.py,sha256=OxOimhEyvCAuzUBC9Q1dz2xaakjAqmSnekMATsjduXM,997
432
- rest/serializers/response.py,sha256=aSZ9bvhsYGHR26NA6sin7fGqFptjTsBhmIcDKrqeeoY,8656
433
+ rest/serializers/response.py,sha256=fsGgS9mtZSceXzNjMcjEJplisn4bG8qQSSdRGrq5Spo,8657
433
434
  rest/serializers/util.py,sha256=-In89fpuVTd6_Ul8nwEUt3DjVKdpeoEyAxudlyB8K6Y,2734
434
435
  rest/service.py,sha256=jl8obnMDEUzB8y3LROGPvmfKKoFU_SzOvywUQjoQZpg,4046
435
436
  rest/settings_helper.py,sha256=_Vn9nmL5_GPss9zIsXzacbTQkn99NbO42CqvOZC3ge4,1532
@@ -516,7 +517,7 @@ ws4redis/servers/uwsgi.py,sha256=VyhoCI1DnVFqBiJYHoxqn5Idlf6uJPHvfBKgkjs34mo,172
516
517
  ws4redis/settings.py,sha256=KKq00EwoGnz1yLwCZr5Dfoq2izivmAdsNEEM4EhZwN4,1610
517
518
  ws4redis/utf8validator.py,sha256=S0OlfjeGRP75aO6CzZsF4oTjRQAgR17OWE9rgZdMBZA,5122
518
519
  ws4redis/websocket.py,sha256=R0TUyPsoVRD7Y_oU7w2I6NL4fPwiz5Vl94-fUkZgLHA,14848
519
- django_restit-4.2.174.dist-info/LICENSE.md,sha256=VHN4hhEeVOoFjtG-5fVv4jesA4SWi0Z-KgOzzN6a1ps,1068
520
- django_restit-4.2.174.dist-info/METADATA,sha256=r1t8GFE4qKAq75b6CnYkpJCE6BbfW7-t2qt2dy-XkaA,7714
521
- django_restit-4.2.174.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
522
- django_restit-4.2.174.dist-info/RECORD,,
520
+ django_restit-4.2.176.dist-info/LICENSE.md,sha256=VHN4hhEeVOoFjtG-5fVv4jesA4SWi0Z-KgOzzN6a1ps,1068
521
+ django_restit-4.2.176.dist-info/METADATA,sha256=WVtqfF02L5jyZtQQoObLI57ZpcubGld7ZDyqc1vSXlg,7714
522
+ django_restit-4.2.176.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
523
+ django_restit-4.2.176.dist-info/RECORD,,
rest/__init__.py CHANGED
@@ -1,4 +1,4 @@
1
1
  from .uberdict import UberDict # noqa: F401
2
2
  from .settings_helper import settings # noqa: F401
3
3
 
4
- __version__ = "4.2.174"
4
+ __version__ = "4.2.176"
rest/mail.py CHANGED
@@ -37,11 +37,16 @@ SES_REGION = settings.SES_REGION
37
37
  EMAIL_METRICS = settings.get("EMAIL_METRICS", False)
38
38
 
39
39
 
40
- def send(to, subject, body=None, attachments=[],
40
+ def send(to, subject, body=None, attachments=[],
41
41
  from_email=settings.DEFAULT_FROM_EMAIL,
42
- fail_silently=True, template=None,
42
+ fail_silently=True, template=None,
43
43
  context=None, do_async=False, replyto=None):
44
44
  # make sure to is list
45
+ if isinstance(to, str):
46
+ if "," in to:
47
+ to = [t.strip() for t in to.split(',')]
48
+ elif ";" in to:
49
+ to = [t.strip() for t in to.split(';')]
45
50
  if not isinstance(to, (tuple, list)):
46
51
  to = [to]
47
52
  # if template lets render
@@ -254,5 +259,3 @@ def render_to_mail(name, context):
254
259
  body = html_content
255
260
 
256
261
  send(toaddrs, subject, body=body, from_email=fromaddr, do_async=True, replyto=replyto)
257
-
258
-
@@ -15,7 +15,7 @@ from . import csv
15
15
  from . import excel
16
16
  # from . import profiler
17
17
  STATUS_ON_PERM_DENIED = settings.get("STATUS_ON_PERM_DENIED", 403)
18
- REST_LIST_CACHE_COUNT = settings.get("REST_LIST_CACHE_COUNT", True)
18
+ REST_LIST_CACHE_COUNT = settings.get("REST_LIST_CACHE_COUNT", False)
19
19
 
20
20
 
21
21
  def get_query_hash(queryset):