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 +1 -0
- account/models/member.py +49 -30
- account/rpc/auth.py +12 -2
- {django_restit-4.2.175.dist-info → django_restit-4.2.178.dist-info}/METADATA +1 -1
- {django_restit-4.2.175.dist-info → django_restit-4.2.178.dist-info}/RECORD +14 -13
- inbox/utils/render.py +10 -3
- medialib/models.py +4 -7
- metrics/client.py +3 -4
- rest/__init__.py +1 -1
- rest/filetypes.py +128 -0
- rest/mail.py +10 -6
- rest/models/base.py +3 -3
- {django_restit-4.2.175.dist-info → django_restit-4.2.178.dist-info}/LICENSE.md +0 -0
- {django_restit-4.2.175.dist-info → django_restit-4.2.178.dist-info}/WHEEL +0 -0
account/models/group.py
CHANGED
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
|
-
|
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
|
-
|
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
|
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
|
-
|
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.
|
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
|
-
|
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.
|
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
|
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.
|
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)
|
@@ -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=
|
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=
|
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=
|
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=
|
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=
|
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
|
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=
|
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=
|
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=
|
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.
|
521
|
-
django_restit-4.2.
|
522
|
-
django_restit-4.2.
|
523
|
-
django_restit-4.2.
|
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
|
18
|
-
|
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
|
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
|
-
|
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":
|
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":
|
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
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",
|
1230
|
-
|
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="
|
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):
|
File without changes
|
File without changes
|