django-restit 4.2.175__py3-none-any.whl → 4.2.178__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.
account/models/group.py CHANGED
@@ -52,6 +52,7 @@ class Group(models.Model, RestModel, MetaDataModel):
52
52
  LIST_PARENT_KINDS = ["org", "iso"]
53
53
  POST_SAVE_FIELDS = ["child_of"]
54
54
  GROUP_FIELD = "self"
55
+ CAN_BATCH = True
55
56
  VIEW_PERMS = [
56
57
  "view_all_groups",
57
58
  "manage_groups",
account/models/member.py CHANGED
@@ -72,6 +72,7 @@ class Member(User, RestModel, MetaDataModel):
72
72
  SEARCH_FIELDS = ["username", "email", "first_name", "last_name", "display_name", "phone_number"]
73
73
  VIEW_PERMS = ["view_members", "manage_members", "manage_users", "owner"]
74
74
  SAVE_PERMS = ["invite_members", "manage_members", "manage_users", "owner"]
75
+ CAN_BATCH = True
75
76
  LIST_DEFAULT_FILTERS = {
76
77
  "is_active": True
77
78
  }
@@ -149,7 +150,7 @@ class Member(User, RestModel, MetaDataModel):
149
150
  display_name = models.CharField(max_length=64, blank=True, null=True, default=None)
150
151
  picture = models.ForeignKey("medialib.MediaItem", blank=True, null=True, help_text="Profile picture", related_name='+', on_delete=models.CASCADE)
151
152
 
152
- # we use this token to allow us to invalidate JWT tokens
153
+ # we use this token to allow us to invalidate JWT tokens
153
154
  security_token = models.CharField(max_length=64, blank=True, null=True, default=None, db_index=True)
154
155
  auth_code = models.CharField(max_length=64, blank=True, null=True, default=None, db_index=True)
155
156
  auth_code_expires = models.DateTimeField(blank=True, null=True, default=None)
@@ -263,6 +264,24 @@ class Member(User, RestModel, MetaDataModel):
263
264
  return self.has_usable_password()
264
265
  return False
265
266
 
267
+ def getFailedLoginCount(self):
268
+ c = RemoteEvents.hget("users:failed:username", self.username)
269
+ if c is not None:
270
+ return int(c)
271
+ return c
272
+
273
+ @classmethod
274
+ def GetFailedLoginsForIP(cls, ip):
275
+ c = RemoteEvents.hget("users:failed:ip", ip)
276
+ if c is not None:
277
+ return int(c)
278
+ return c
279
+
280
+ @classmethod
281
+ def GetIPsWithFailedLoginAttempts(cls, threshold):
282
+ failed_ips = RemoteEvents.hgetall("users:failed:ip")
283
+ return {ip: int(count) for ip, count in failed_ips.items() if int(count) > threshold}
284
+
266
285
  def recordFailedLogin(self, request):
267
286
  c = RemoteEvents.hincrby("users:failed:username", self.username, 1)
268
287
  if c >= settings.LOCK_PASSWORD_ATTEMPTS:
@@ -273,13 +292,15 @@ class Member(User, RestModel, MetaDataModel):
273
292
  "account", f"incorrect password for {self.username}", level=8,
274
293
  error_code=498,
275
294
  request=request)
276
- c = RemoteEvents.hincrby("users:failed:ip", request.ip, 1)
295
+ ic = RemoteEvents.hincrby("users:failed:ip", request.ip, 1)
296
+ return c
277
297
 
278
298
  def recordSuccessLogin(self, request):
279
299
  self.last_login = datetime.now()
280
300
  self.save()
281
301
  RemoteEvents.hdel("users:failed:username", self.username)
282
- RemoteEvents.hdel("users:failed:ip", request.ip)
302
+ if request:
303
+ RemoteEvents.hdel("users:failed:ip", request.ip)
283
304
 
284
305
  def hasPasswordExpired(self):
285
306
  now = datetime.now()
@@ -288,38 +309,33 @@ class Member(User, RestModel, MetaDataModel):
288
309
  self.save()
289
310
  return now - self.password_changed > timedelta(days=settings.PASSWORD_EXPIRES_DAYS)
290
311
 
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
312
+ def login(self, password=None, request=None):
295
313
  if not request:
296
314
  request = rh.getActiveRequest()
315
+ if not self.is_active or self.is_blocked:
316
+ return False
297
317
  if not self.checkPassword(password):
298
- # invalid password
299
318
  self.recordFailedLogin(request)
300
319
  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)
320
+ self.authLogin(request)
310
321
  return True
311
322
 
312
323
  def loginNoPassword(self, request=None):
313
324
  if not self.is_active or self.is_blocked:
314
325
  return False
315
- # can force login
316
326
  if not request:
317
327
  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)
328
+ self.authLogin(request)
321
329
  return True
322
330
 
331
+ def authLogin(self, request, use_session=True):
332
+ if use_session:
333
+ self.user_ptr.backend = 'django.contrib.auth.backends.ModelBackend'
334
+ auth_login(request, self.user_ptr)
335
+ if request:
336
+ self.locateByIP(request.ip)
337
+ self.recordSuccessLogin(request)
338
+
323
339
  def canLogin(self, request=None, throw_exception=True, using_password=True):
324
340
  if not self.is_active:
325
341
  self.log("login_blocked", F"account is not active {self.username}", request, method="login", level=31)
@@ -577,7 +593,7 @@ class Member(User, RestModel, MetaDataModel):
577
593
  SMS.send(phone, msg)
578
594
  return True
579
595
 
580
- def sendEmail(self, subject, body, attachments=[], do_async=True, template=None,
596
+ def sendEmail(self, subject, body, attachments=[], do_async=True, template=None,
581
597
  context=None):
582
598
  default_domain = self.getProperty("default_domain", settings.EMAIL_DEFAULT_DOMAIN)
583
599
  from_email = rh.getFromEmailForHost(default_domain)
@@ -675,7 +691,7 @@ class Member(User, RestModel, MetaDataModel):
675
691
  allow_email = not self.email_disabled and valid_email and (force or via in ["all", "email"])
676
692
  else:
677
693
  allow_email = valid_email
678
-
694
+
679
695
  if not allow_email and not allow_sms:
680
696
  return False
681
697
 
@@ -796,7 +812,7 @@ class Member(User, RestModel, MetaDataModel):
796
812
  def block(self, reason, request=None):
797
813
  if not request:
798
814
  request = self.getActiveRequest()
799
- PersistentLog.log("account blocked, {}".format(reason), 5, request, "account.Member", self.pk, "blocked")
815
+ self.auditLog(f"account blocked, {reason}", "blocked", level=5)
800
816
  RemoteEvents.hset("users:blocked:username", self.username, time.time())
801
817
  self.reportIncident(
802
818
  "account", f"account '{self.username}' blocked: {reason}", level=2,
@@ -832,7 +848,7 @@ class Member(User, RestModel, MetaDataModel):
832
848
 
833
849
  # BEGIN REST CALLBACKS
834
850
  def on_permission_change(self, key, value, old_value, category):
835
- # called when a metadata.permissions field changes.. see django settings USER_METADATA_PROPERTIES
851
+ # called when a metadata.permissions field changes.. see django settings USER_METADATA_PROPERTIES
836
852
  # we want to log both the person changing permissions
837
853
  # and those being changed
838
854
  request = RestModel.getActiveRequest()
@@ -865,7 +881,7 @@ class Member(User, RestModel, MetaDataModel):
865
881
  request = self.getActiveRequest()
866
882
  if not request.member.is_superuser:
867
883
  raise PermissionDeniedException("Permission Denied: attempting to super user")
868
- self.is_superuser = int(value)
884
+ self.is_superuser = int(value)
869
885
 
870
886
  def set_disable(self, value):
871
887
  if value is not None and value in [1, '1', True, 'true']:
@@ -973,7 +989,12 @@ class Member(User, RestModel, MetaDataModel):
973
989
  request.member.log("permission_denied", "attempting to set password for user: {}".format(self.username), method="password_change", level=3)
974
990
  raise PermissionDeniedException("Permission Denied: attempting to change password")
975
991
  if request.member.id != self.id:
976
- self.log("modified_by", "password changed by: {}".format(request.member.username), method="password_change", level=31)
992
+ error = f"{self.username} password changed by: {request.member.username}"
993
+ self.reportIncident("account", error, details=error, error_code=497)
994
+ self.log("modified_by", error, method="password_change", level=31)
995
+
996
+ error = f"{self.username} password changed by: {request.member.username}"
997
+ request.member.reportIncident("account", error, details=error, error_code=497)
977
998
  request.member.log("member_edit", "{} password changed".format(self.username), method="password_change", level=31)
978
999
  self.setPassword(value, skip_history=True)
979
1000
  else:
@@ -1219,7 +1240,7 @@ class Member(User, RestModel, MetaDataModel):
1219
1240
  def authWS4RedisConnection(cls, auth_data):
1220
1241
  """
1221
1242
  This method is used by the async/websocket service to authenticate.
1222
- If the model can authenticate the connection it should return dict
1243
+ If the model can authenticate the connection it should return dict
1223
1244
  with kind and pk of the model that is authenticaed
1224
1245
  """
1225
1246
  from rest import UberDict
@@ -1338,5 +1359,3 @@ class AuthToken(models.Model, RestModel):
1338
1359
 
1339
1360
  def __str__(self):
1340
1361
  return "{o.member}: {o.secure_token}".format(o=self)
1341
-
1342
-
account/rpc/auth.py CHANGED
@@ -55,7 +55,7 @@ def jwt_login(request):
55
55
  resp = checkForTOTP(request, member)
56
56
  if resp is not None:
57
57
  return resp
58
- if not member.login(request=request, password=password, use_jwt=True):
58
+ if not member.login(request=request, password=password):
59
59
  # we do not want permission denied catcher invoked as it is already handled in login method
60
60
  return rv.restStatus(request, False, error=f"Invalid Credentials {username}", error_code=401)
61
61
  return on_complete_jwt(request, member, auth_method)
@@ -221,7 +221,8 @@ def member_login_uname_code(request, username, auth_code):
221
221
  member.setPassword(password)
222
222
  member.auth_code = None
223
223
  member.auth_code_expires = None
224
- member.login(request=request)
224
+ member.canLogin(request, using_password=False) # throws exception if cannot login
225
+ member.loginNoPassword(request)
225
226
  member.save()
226
227
  member.log("code_login", "code login", request, method="login", level=8)
227
228
  if request.DATA.get("auth_method") == "basic" and ALLOW_BASIC_LOGIN:
@@ -411,3 +412,12 @@ def totp_verify(request):
411
412
  return rv.restPermissionDenied(request, "invalid code")
412
413
  request.member.setProperty("totp_verified", 1)
413
414
  return rv.restStatus(request, True)
415
+
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)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: django-restit
3
- Version: 4.2.175
3
+ Version: 4.2.178
4
4
  Summary: A Rest Framework for DJANGO
5
5
  License: MIT
6
6
  Author: Ian Starnes
@@ -29,9 +29,9 @@ account/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuF
29
29
  account/models/__init__.py,sha256=cV_lMnT2vL_mjiYtT4hlcIHo52ocFbGSNVkOIHHLXZY,385
30
30
  account/models/device.py,sha256=8D-Sbv9PZWAnX6UVpp1lNJ03P24fknNnN1VOhqY7RVg,6306
31
31
  account/models/feeds.py,sha256=vI7fG4ASY1M0Zjke24RdnfDcuWeATl_yR_25jPmT64g,2011
32
- account/models/group.py,sha256=JVyMIakLskUuGXBJFyessw4LlD9Fl6AsHtpo1yZEYjk,22884
32
+ account/models/group.py,sha256=Pmm6G5Qb9C-OQ70xUxp5M1_PAg1DtvUxb5WFFW-WmOY,22909
33
33
  account/models/legacy.py,sha256=zYdtv4LC0ooxPVqWM-uToPwV-lYWQLorSE6p6yn1xDw,2720
34
- account/models/member.py,sha256=qmLCOVbNTRr4L-E7BbOMtv4V64QN7K-0pXDgnuB-AbY,54722
34
+ account/models/member.py,sha256=xlX0yqE3yFwPaZkcdU4e3g3yTommHimsLG8wBp09x90,55376
35
35
  account/models/membership.py,sha256=90EpAhOsGaqphDAkONP6j_qQ0OWSRaQsI8H7E7fgMkE,9249
36
36
  account/models/notify.py,sha256=YKYEXT56i98b7-ydLt5UuEVOqW7lipQMi-KuiPhcSwY,15627
37
37
  account/models/passkeys.py,sha256=lObapudvL--ABSTZTIELmYvHE3dPF0tO_KmuYk0ZJXc,1699
@@ -43,7 +43,7 @@ account/passkeys/__init__.py,sha256=FwXYJXwSJXfkLojGBcVpF1dFpgFhzDdd9N_3naYQ0cc,
43
43
  account/passkeys/core.py,sha256=4aUBNCuF_kjOvE1zFapK1Pj28ap5slO71dRyfnWi0YU,4148
44
44
  account/periodic.py,sha256=-u0n-7QTJgDOkasGhBAPwHAwjpqWGA-MZLEFkVTqCGU,874
45
45
  account/rpc/__init__.py,sha256=SGF0M_-H0dKh3b1apSX29BotNWAvITYccGQVC0MIjL8,336
46
- account/rpc/auth.py,sha256=0LUTb-dO6wk6rWitPVAXeR0IqpbX5Yw8tHM5U9QJibA,17234
46
+ account/rpc/auth.py,sha256=MDXXhTlzTRcKB8TQ0OPawvsb0P4EeH_J3ukeSljgytk,17615
47
47
  account/rpc/device.py,sha256=lU2BHNPreHV0dDTjAPc7Sc-5m2JP8SiWVqiKuBfV7Fo,2281
48
48
  account/rpc/group.py,sha256=hw7iczZ6W_IrRbx5ZDw6cZ5I_ztqxhtUFJD9WR91_4s,4948
49
49
  account/rpc/member.py,sha256=8XnJX-iri0Om4nc-V2_tDJzfCSzziKLw6dUx9egtEZE,2236
@@ -93,7 +93,7 @@ inbox/models/template.py,sha256=i5vf0vsM1U0251UmVsF61MDCV_c7xt-zdCdx1SiKOG0,1013
93
93
  inbox/rpc.py,sha256=7JXvpXlEGKG7by_NkANPGYLCzagyCnTIGM4rme_htpk,1534
94
94
  inbox/utils/__init__.py,sha256=P_UR2rGK3L0tZNlTN-mf99tpeYM-tLkA18iDKXSSLDM,89
95
95
  inbox/utils/parsing.py,sha256=y_71dwz8bm3JvF35ol8698XJ36sBF8fQWUrn0sYd2Fs,5597
96
- inbox/utils/render.py,sha256=CU_F2qUBQE7mjb9Q6Dn9ro5CS_O_zEY-wDMHEClKkIA,4331
96
+ inbox/utils/render.py,sha256=Jk_YYY6uztURh0qQfDDZYpOj9awvqkqkAaBmwJIWivU,4543
97
97
  inbox/utils/sending.py,sha256=BKelTZnbkdSLGpjOY6IRTrzj-Hnw2pPZ7RYQGwe-tqk,2179
98
98
  incident/README.md,sha256=4vbZTJj7uUmq8rogYngxqNYjFTlBOujfWUGheLoFKMc,1114
99
99
  incident/__init__.py,sha256=FXNMmcGP6YAKjwik84ppze33uL0kDTa7YFr3aOEXhhk,3658
@@ -177,7 +177,7 @@ medialib/forms.py,sha256=nrE6QTPNPiIeX7Nx4l9DEmAQeQXqFyCg1C3JEDBYJfE,5442
177
177
  medialib/migrations/0001_initial.py,sha256=H3JliH5aw7tiHef8MhrJr_9rGetqgA7UjTF-eKziRSM,20518
178
178
  medialib/migrations/0002_alter_mediaitemrendition_bytes.py,sha256=igC1R02smbNoWlk2T4uCi9cNilOsxGKD-D24fQv92dM,414
179
179
  medialib/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
180
- medialib/models.py,sha256=fsgNxBtJNR7lufbWUsI_LtxfYmLBTpIQ0uOW-T_CsD8,55032
180
+ medialib/models.py,sha256=V-ouyUVgPKDIVvzGyqSoMWdhSBV8ZXHQKLXEyOTLAx0,54976
181
181
  medialib/ocr.py,sha256=zlP7-NBiXhW7jR9pljmEPl5xzLVZpLN5QLAELQgU0Fk,1189
182
182
  medialib/pdf.py,sha256=l28WwM0JKbT9boV-b_9TFh9jhvGcrquR8GqC8wfEaLk,1275
183
183
  medialib/qrcode.py,sha256=vHyA5egXOX70EFiUDgr1njI9zcF6bXQJ_hKAQrppRow,545
@@ -339,7 +339,7 @@ medialib/youtube/upload.py,sha256=MTuPxm1ZC-y5pXAGtLNtp1hBSNZgCKYt1ewP5hwMQHI,28
339
339
  medialib/youtube/uritemplate/__init__.py,sha256=ONWR_KRz9au0O-XUUTrO_UN7GHTmZCTKyvflUQb8wxM,4996
340
340
  metrics/README.md,sha256=YwbCA2y6xJBlaO6yEtl1zWpqrQ4ZzkQSuQT-h6btET8,2307
341
341
  metrics/__init__.py,sha256=70sdDZGOwGIEFWgDkHWPMVODFelo206jp1g-BFV2u_4,90
342
- metrics/client.py,sha256=vc49J0KLLJlryhb77I-ejShDnWGF8NqSU6mhdPv8V2s,25570
342
+ metrics/client.py,sha256=-YPZfNJqbg86V3rmHTDx_raRLaZDjzKmFIl6BjeSHN4,25565
343
343
  metrics/eod.py,sha256=gnq-tNE7xfm2ah52e2TUyERgUQNwkFuT2rtDv8XOUVQ,9182
344
344
  metrics/examples/eod_example.py,sha256=gYtansjsKILVxe8XJD12XPaxmBJ-B6dOXGZG2JTGWA8,1664
345
345
  metrics/management/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -380,7 +380,7 @@ pushit/utils.py,sha256=IeTCGa-164nmB1jIsK1lu1O1QzUhS3BKfuXHGjCW-ck,2121
380
380
  rest/.gitignore,sha256=TbEvWRMnAiajCTOdhiNrd9eeCAaIjRp9PRjE_VkMM5g,118
381
381
  rest/README.md,sha256=V3ETc-cJu8PZIbKr9xSe_pA4JEUpC8Dhw4bQeVCDJPw,5460
382
382
  rest/RemoteEvents.py,sha256=nL46U7AuxIrlw2JunphR1tsXyqi-ep_gD9CYGpYbNgE,72
383
- rest/__init__.py,sha256=5nuojPskvcFtHA31BnuuT6Z7MtZov2YpOn2IHnzm9cw,122
383
+ rest/__init__.py,sha256=BF5iQVc_58Sj3--bfe7Nv0kMyoAmtLcNU6EnH6vA1bU,122
384
384
  rest/arc4.py,sha256=y644IbF1ec--e4cUJ3KEYsewTCITK0gmlwa5mJruFC0,1967
385
385
  rest/cache.py,sha256=1Qg0rkaCJCaVP0-l5hZg2CIblTdeBSlj_0fP6vlKUpU,83
386
386
  rest/crypto/__init__.py,sha256=Tl0U11rgj1eBYqd6OXJ2_XSdNLumW_JkBZnaJqI6Ldw,72
@@ -395,12 +395,13 @@ rest/extra/__init__.py,sha256=YzmNsch5H5FFLkUK9mIAKyoRK_rJCA9HGb0kubp4h30,54
395
395
  rest/extra/hostinfo.py,sha256=5R23EafcNbFARyNEqdjBkqcpC8rPfmPd1zqNqle6-nM,4298
396
396
  rest/extra/json_metadata.py,sha256=p_ffzmANmOFix_oC3voR6_NNTjcn7-T7aXcH-I4_Npg,1078
397
397
  rest/fields.py,sha256=_v1TJVc6vyWlqmwFRJ6mtuR5Fo-lS0KcUhPWIrzKZUo,9719
398
+ rest/filetypes.py,sha256=wZXljB1g6JbA5H41xC81Jk3Wy_gu64ecNKU4k7aMcig,4171
398
399
  rest/forms.py,sha256=66Wm5cdy8tKib_mGicjq_yd-gNVMFWRECnrDksnNnwU,6316
399
400
  rest/helpers.py,sha256=t7smlOUzchVno-zeq7xMJIwogAR2DeSrffWxgysOHX8,29531
400
401
  rest/joke.py,sha256=0PpKaX2iN7jlS62kgjfmmqkFBYLPURz15aQ8R7OJkJ8,260
401
402
  rest/jwtoken.py,sha256=F7Vvpm31rAplTXr8XFP-Lb4BnDB3j1B2nQq0P1iTCLQ,2576
402
403
  rest/log.py,sha256=hd1_4HBOS395sfXJIL6BTw9yekm1SLgBwYx_PdfIhKA,20930
403
- rest/mail.py,sha256=Rm40hWDYop0tMqxdN-J2NT-dCnP-f4SfCZxSO02ajzs,7965
404
+ rest/mail.py,sha256=rp88V-SVM9F6ZIupoyrQvDwArhG0mfbzjHrDBRiHbvI,8204
404
405
  rest/mailman.py,sha256=v5O1G5s3HiAKmz-J1z0uT6_q3xsONPpxVl9saEyQQ2I,9174
405
406
  rest/management/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
406
407
  rest/management/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -413,7 +414,7 @@ rest/middleware/request.py,sha256=JchRNy5L-bGd-7h-KFYekGRvREe2eCkZXKOYqIkP2hI,41
413
414
  rest/middleware/session.py,sha256=zHSoQpIzRLmpqr_JvW406wzpvU3W3gDbm5JhtzLAMlE,10240
414
415
  rest/middleware/session_store.py,sha256=1nSdeXK8PyuYgGgIufqrS6j6QpIrQ7zbMNT0ol75e6U,1901
415
416
  rest/models/__init__.py,sha256=M8pvFDq-WCF-QcM58X7pMufYYe0aaQ3U0PwGe9TKbbY,130
416
- rest/models/base.py,sha256=oPj4m4-fnmjYUqd9dru17ulyh8jdFbXtWJt1CiuiBls,72633
417
+ rest/models/base.py,sha256=lbl20SK27Dsz3N6wcbBVEscrZ2n3VDZZH-gnqFgZz9s,72637
417
418
  rest/models/cacher.py,sha256=eKz8TINVhWEqKhJGMsRkKZTtBUIv5rN3NHbZwOC56Uk,578
418
419
  rest/models/metadata.py,sha256=ni8-BRF07lv4CdPUWnUdfPTOClQAVEeRZvO-ic623HU,12904
419
420
  rest/net.py,sha256=LcB2QV6VNRtsSdmiQvYZgwQUDwOPMn_VBdRiZ6OpI-I,2974
@@ -517,7 +518,7 @@ ws4redis/servers/uwsgi.py,sha256=VyhoCI1DnVFqBiJYHoxqn5Idlf6uJPHvfBKgkjs34mo,172
517
518
  ws4redis/settings.py,sha256=KKq00EwoGnz1yLwCZr5Dfoq2izivmAdsNEEM4EhZwN4,1610
518
519
  ws4redis/utf8validator.py,sha256=S0OlfjeGRP75aO6CzZsF4oTjRQAgR17OWE9rgZdMBZA,5122
519
520
  ws4redis/websocket.py,sha256=R0TUyPsoVRD7Y_oU7w2I6NL4fPwiz5Vl94-fUkZgLHA,14848
520
- django_restit-4.2.175.dist-info/LICENSE.md,sha256=VHN4hhEeVOoFjtG-5fVv4jesA4SWi0Z-KgOzzN6a1ps,1068
521
- django_restit-4.2.175.dist-info/METADATA,sha256=DuVZg0LP0bkuNNdfm9IlJXwcpPE-O4-V6qSZYRHICTk,7714
522
- django_restit-4.2.175.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
523
- django_restit-4.2.175.dist-info/RECORD,,
521
+ django_restit-4.2.178.dist-info/LICENSE.md,sha256=VHN4hhEeVOoFjtG-5fVv4jesA4SWi0Z-KgOzzN6a1ps,1068
522
+ django_restit-4.2.178.dist-info/METADATA,sha256=7mIBsuAJDUF-6Mbw7FgAVHoc7zqWDFiIKuLpbbieMZ8,7714
523
+ django_restit-4.2.178.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
524
+ django_restit-4.2.178.dist-info/RECORD,,
inbox/utils/render.py CHANGED
@@ -12,10 +12,17 @@ import re
12
12
 
13
13
 
14
14
  def createMessage(sender, recipients, subject, text, html, attachments=None, replyto=None):
15
- message = objict(sender=sender, recipients=recipients)
16
15
 
17
- if not isinstance(message.recipients, (tuple, list)):
18
- message.recipients = [message.recipients]
16
+ if isinstance(recipients, str):
17
+ if "," in recipients:
18
+ recipients = [t.strip() for t in recipients.split(',')]
19
+ elif ";" in recipients:
20
+ recipients = [t.strip() for t in recipients.split(';')]
21
+
22
+ if not isinstance(recipients, (tuple, list)):
23
+ recipients = [recipients]
24
+
25
+ message = objict(sender=sender, recipients=recipients)
19
26
 
20
27
  if attachments is None:
21
28
  attachments = []
medialib/models.py CHANGED
@@ -16,16 +16,14 @@ import urllib.request, urllib.error, urllib.parse
16
16
  from urllib.parse import urlparse
17
17
  from datetime import datetime, date
18
18
  import os
19
- import hashlib
20
19
  import time
21
- import mimetypes
22
20
  import tempfile
23
21
  import re
24
22
 
25
23
  from medialib import utils
26
24
  from rest import settings
27
25
  from rest.models import RestModel, MetaDataBase, MetaDataModel
28
- from rest.decorators import rest_async
26
+ from rest import filetypes
29
27
 
30
28
  from taskqueue.models import Task
31
29
 
@@ -547,7 +545,7 @@ class MediaItem(models.Model, RestModel, MetaDataModel):
547
545
  if kind in ["png", "jpg", "jpeg", "jfif", "bmp", "gif", "tif"]:
548
546
  self.kind = "I"
549
547
  return
550
- (mt, _) = mimetypes.guess_type(filename)
548
+ mt = filetypes.guess_type(filename)
551
549
  if mt == None:
552
550
  self.kind = '*'
553
551
  elif mt[:6] == 'image/':
@@ -833,7 +831,7 @@ class MediaItem(models.Model, RestModel, MetaDataModel):
833
831
  return image
834
832
  return {
835
833
  "kind": image.kind,
836
- "content_type": mimetypes.guess_type(image.url)[0],
834
+ "content_type": filetypes.guess_type(image.url),
837
835
  "bytes":image.bytes,
838
836
  "url":image.view_url(request=request, expires=None, is_secure=True),
839
837
  "width":image.width,
@@ -860,7 +858,7 @@ class MediaItem(models.Model, RestModel, MetaDataModel):
860
858
  return None
861
859
  return {
862
860
  "kind": orig.kind,
863
- "content_type": mimetypes.guess_type(self.name)[0],
861
+ "content_type": filetypes.guess_type(self.name),
864
862
  "bytes":orig.bytes,
865
863
  "url":orig.view_url(request=request, expires=None, is_secure=True),
866
864
  "width":orig.width,
@@ -1466,4 +1464,3 @@ class MediaItemParameterSetting(models.Model):
1466
1464
 
1467
1465
  def __str__(self):
1468
1466
  return "%s - %s" % (str(self.item), str(self.parameter))
1469
-
metrics/client.py CHANGED
@@ -329,7 +329,7 @@ class R(object):
329
329
  for s in slug:
330
330
  self.metric(s, num, category, expire, date)
331
331
  return
332
-
332
+
333
333
  # Add the slug to the set of metric slugs
334
334
  try:
335
335
  self.r.sadd(self._metric_slugs_key, slug)
@@ -382,12 +382,12 @@ class R(object):
382
382
  keys = utils.build_keys(slug, min_granularity=min_granularity, max_granularity=max_granularity)
383
383
 
384
384
  for granularity, key in zip(granularities, keys):
385
- rh.debug("-- get_metric --", granularity, key)
385
+ # rh.debug("-- get_metric --", granularity, key)
386
386
  try:
387
387
  results[granularity] = int(self.r.get(key))
388
388
  except Exception:
389
389
  results[granularity] = 0
390
- rh.log_exception("get_metric", granularity, key)
390
+ # rh.log_exception("get_metric", granularity, key)
391
391
  if min_granularity and min_granularity == max_granularity:
392
392
  return results[min_granularity]
393
393
  return results
@@ -661,4 +661,3 @@ class R(object):
661
661
  key = self._gauge_key(slug)
662
662
  self.r.delete(key) # Remove the Gauge
663
663
  self.r.srem(self._gauge_slugs_key, slug) # Remove from the set of keys
664
-
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.175"
4
+ __version__ = "4.2.178"
rest/filetypes.py ADDED
@@ -0,0 +1,128 @@
1
+ EXTENSION_TO_MIME = {
2
+ # Documents
3
+ '.pdf': 'application/pdf',
4
+ '.doc': 'application/msword',
5
+ '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
6
+ '.xls': 'application/vnd.ms-excel',
7
+ '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
8
+ '.ppt': 'application/vnd.ms-powerpoint',
9
+ '.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
10
+ '.csv': 'text/csv',
11
+ '.tsv': 'text/tab-separated-values',
12
+ '.txt': 'text/plain',
13
+ '.rtf': 'application/rtf',
14
+ '.odt': 'application/vnd.oasis.opendocument.text',
15
+ '.ods': 'application/vnd.oasis.opendocument.spreadsheet',
16
+ '.md': 'text/markdown',
17
+ '.epub': 'application/epub+zip',
18
+ '.ics': 'text/calendar',
19
+
20
+ # Images
21
+ '.jpg': 'image/jpeg',
22
+ '.jpeg': 'image/jpeg',
23
+ '.png': 'image/png',
24
+ '.gif': 'image/gif',
25
+ '.bmp': 'image/bmp',
26
+ '.webp': 'image/webp',
27
+ '.tiff': 'image/tiff',
28
+ '.svg': 'image/svg+xml',
29
+ '.heic': 'image/heic',
30
+ '.ico': 'image/vnd.microsoft.icon',
31
+
32
+ # Audio
33
+ '.mp3': 'audio/mpeg',
34
+ '.wav': 'audio/wav',
35
+ '.ogg': 'audio/ogg',
36
+ '.m4a': 'audio/mp4',
37
+ '.flac': 'audio/flac',
38
+
39
+ # Video
40
+ '.mp4': 'video/mp4',
41
+ '.mov': 'video/quicktime',
42
+ '.avi': 'video/x-msvideo',
43
+ '.wmv': 'video/x-ms-wmv',
44
+ '.webm': 'video/webm',
45
+ '.mkv': 'video/x-matroska',
46
+
47
+ # Archives
48
+ '.zip': 'application/zip',
49
+ '.tar': 'application/x-tar',
50
+ '.gz': 'application/gzip',
51
+ '.rar': 'application/vnd.rar',
52
+ '.7z': 'application/x-7z-compressed',
53
+ '.tar.gz': 'application/gzip', # Special case
54
+ '.tgz': 'application/gzip',
55
+ '.bz2': 'application/x-bzip2',
56
+ '.tar.bz2': 'application/x-bzip2',
57
+
58
+ # Code / Data
59
+ '.json': 'application/json',
60
+ '.xml': 'application/xml',
61
+ '.html': 'text/html',
62
+ '.htm': 'text/html',
63
+ '.css': 'text/css',
64
+ '.js': 'application/javascript',
65
+ '.py': 'text/x-python',
66
+ '.java': 'text/x-java-source',
67
+ '.c': 'text/x-c',
68
+ '.cpp': 'text/x-c++',
69
+ '.ts': 'application/typescript',
70
+ '.yml': 'application/x-yaml',
71
+ '.yaml': 'application/x-yaml',
72
+ '.sql': 'application/sql',
73
+
74
+ # Fonts
75
+ '.woff': 'font/woff',
76
+ '.woff2': 'font/woff2',
77
+ '.ttf': 'font/ttf',
78
+ '.otf': 'font/otf',
79
+
80
+ # Misc
81
+ '.apk': 'application/vnd.android.package-archive',
82
+ '.exe': 'application/vnd.microsoft.portable-executable',
83
+ '.dmg': 'application/x-apple-diskimage',
84
+ '.bat': 'application/x-bat',
85
+ '.sh': 'application/x-sh',
86
+ '.pdfa': 'application/pdf',
87
+ '.webmanifest': 'application/manifest+json',
88
+ }
89
+
90
+ def parse_extension(filename):
91
+ """
92
+ Extract the extension from a filename, handling both single and double extensions,
93
+ and return the extension along with the filename without the extension.
94
+
95
+ Parameters:
96
+ filename (str): The name of the file for which to extract the extension.
97
+
98
+ Returns:
99
+ tuple: A tuple containing the extracted extension (or an empty string if no valid
100
+ extension is found) and the filename without the extension.
101
+ """
102
+ filename = filename.lower()
103
+ parts = filename.rsplit('.', 2) # Max 2 splits: base.name.ext1.ext2
104
+ if len(parts) == 3:
105
+ double_ext = f".{parts[-2]}.{parts[-1]}"
106
+ if double_ext in EXTENSION_TO_MIME:
107
+ return double_ext, '.'.join(parts[:-1])
108
+ ext = f".{parts[-1]}" if '.' in filename else ''
109
+ base_filename = parts[0] if len(parts) == 1 else '.'.join(parts[:-1])
110
+ return ext.lower(), base_filename
111
+
112
+ def guess_type(filename):
113
+ """
114
+ Guess the MIME type of a file based on its extension.
115
+
116
+ This function attempts to determine the MIME type of a file by examining
117
+ its extension. It can handle both single and double extensions, such as
118
+ ".tar.gz" or ".tar.bz2".
119
+
120
+ Parameters:
121
+ filename (str): The name of the file whose MIME type needs to be guessed.
122
+
123
+ Returns:
124
+ str: The guessed MIME type if found in EXTENSION_TO_MIME mapping, or
125
+ 'application/octet-stream' as a default fallback.
126
+ """
127
+ ext, _ = parse_extension(filename)
128
+ return EXTENSION_TO_MIME.get(ext, 'application/octet-stream')
rest/mail.py CHANGED
@@ -3,7 +3,6 @@ import os
3
3
  import threading
4
4
  from io import StringIO
5
5
  import csv
6
- import mimetypes
7
6
  import boto3
8
7
 
9
8
  from email.mime.multipart import MIMEMultipart
@@ -25,6 +24,7 @@ from django.template import TemplateDoesNotExist
25
24
 
26
25
  import metrics
27
26
  from rest import settings
27
+ from rest import filetypes
28
28
  from rest.uberdict import UberDict
29
29
  from rest.middleware import get_request
30
30
  from rest.log import getLogger
@@ -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
@@ -158,7 +163,8 @@ def sendMail(msg, sender, recipients, fail_silently=True):
158
163
 
159
164
  def makeAttachment(filename, data):
160
165
  atment = UberDict(name=filename, data=data)
161
- atment.mimetype, junk = mimetypes.MimeTypes().guess_type(filename)
166
+ # atment.mimetype, junk = mimetypes.MimeTypes().guess_type(filename)
167
+ atment.mimetype = filetypes.guess_type(filename)
162
168
  return atment
163
169
 
164
170
 
@@ -254,5 +260,3 @@ def render_to_mail(name, context):
254
260
  body = html_content
255
261
 
256
262
  send(toaddrs, subject, body=body, from_email=fromaddr, do_async=True, replyto=replyto)
257
-
258
-
rest/models/base.py CHANGED
@@ -1226,8 +1226,8 @@ class RestModel(object):
1226
1226
  @classmethod
1227
1227
  def on_rest_batch(cls, request, action):
1228
1228
  # this method is called when rest_batch='somme action'
1229
- if not ALLOW_BATCHING or not getattr(cls.RestMeta, "CAN_BATCH", True):
1230
- return GRAPH_HELPERS.restStatus(request, False, error="model does not allow batch actions")
1229
+ if not ALLOW_BATCHING or not getattr(cls.RestMeta, "CAN_BATCH", False):
1230
+ raise re.PermissionDeniedException(f"{cls.__name__} model does not allow batch actions", 439)
1231
1231
  cls._boundRest()
1232
1232
  # if not request.member.hasPerm("can_batch_update"):
1233
1233
  # raise re.PermissionDeniedException(f"batch updated not allowed by user")
@@ -1246,7 +1246,7 @@ class RestModel(object):
1246
1246
  if not can_delete:
1247
1247
  raise re.PermissionDeniedException(f"deletion not allowed for {cls.get_class_name()}", 438)
1248
1248
  count = qset.delete()[0]
1249
- return GRAPH_HELPERS.restStatus(request, True, error="delete {} items".format(count))
1249
+ return GRAPH_HELPERS.restStatus(request, True, error="deleted {} items".format(count))
1250
1250
  elif action == "update":
1251
1251
  update_fields = request.DATA.get(["batch_data", "batch_update"])
1252
1252
  if not isinstance(update_fields, dict):