django-restit 4.2.71__py3-none-any.whl → 4.2.73__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/device.py CHANGED
@@ -113,9 +113,20 @@ class MemberDeviceMetaData(rm.MetaDataBase):
113
113
 
114
114
  class CloudCredentials(models.Model, rm.RestModel, rm.MetaDataModel):
115
115
  """
116
- MemberDevice Model tracks personal devices associated with a user.
117
- This can include mobile and desktop devices.
116
+ CloudCredentials is a global setting for a group to store this groups cloud credentials.
118
117
  """
118
+ class RestMeta:
119
+ VIEW_PERMS = ["view_cm", "manage_cm"]
120
+ EDIT_PERMS = ["manage_cm"]
121
+ GRAPHS = {
122
+ "default": {
123
+ "extra": ["metadata"],
124
+ "graphs": {
125
+ "group": "basic"
126
+ }
127
+ }
128
+ }
129
+
119
130
  created = models.DateTimeField(auto_now_add=True)
120
131
  modified = models.DateTimeField(auto_now=True)
121
132
  group = models.ForeignKey("account.Group", related_name="cloud_credentials", on_delete=models.CASCADE)
account/models/member.py CHANGED
@@ -88,7 +88,8 @@ class Member(User, RestModel, MetaDataModel):
88
88
  ("perms", "properties|permissions.")]
89
89
  QUERY_FIELDS_SPECIAL = {
90
90
  "ip": "auth_sessions__ip",
91
- "login_country": "auth_sessions__location__country"
91
+ "login_country": "auth_sessions__location__country",
92
+ "not_login_country": "auth_sessions__location__country__ne"
92
93
  }
93
94
  UNIQUE_LOOKUP = ["username", "email"]
94
95
  METADATA_FIELD_PROPERTIES = settings.USER_METADATA_PROPERTIES
@@ -221,6 +222,10 @@ class Member(User, RestModel, MetaDataModel):
221
222
  return token.secure_token
222
223
  return None
223
224
 
225
+ @property
226
+ def force_single_session(self):
227
+ return self.getProperty("force_single_session", category="permissions", default=None)
228
+
224
229
  @property
225
230
  def has_totp(self):
226
231
  token = self.getProperty("totp_token", category="secrets", default=None)
@@ -847,6 +852,8 @@ class Member(User, RestModel, MetaDataModel):
847
852
  if token is None:
848
853
  token = AuthToken(member=self, role="default")
849
854
  token.generateToken()
855
+ elif action == "refresh_keys":
856
+ self.refreshSecurityToken()
850
857
 
851
858
  def set_full_name(self, value):
852
859
  self.set_name(value)
@@ -82,13 +82,14 @@ class Membership(models.Model, RestModel, MetaDataModel):
82
82
  if value == "resend_invite":
83
83
  self.sendInvite(self.getActiveRequest())
84
84
 
85
- def sendInvite(self, request=None, url=None, subject=None, site_logo=None, company_name=None):
85
+ def sendInvite(self, request=None, url=None, subject=None, site_logo=None, company_name=None, msg=None):
86
86
  if request:
87
87
  powered_by = request.DATA.get("powered_by", True)
88
88
  subject = request.DATA.get("invite_subject")
89
89
  url = request.DATA.get("invite_url")
90
90
  site_logo = request.DATA.get("site_logo", settings.SITE_LOGO)
91
91
  company_name = request.DATA.get("company_name", settings.COMPANY_NAME)
92
+ msg = request.DATA.get("invite_msg", None)
92
93
  else:
93
94
  powered_by = True
94
95
  site_logo = settings.SITE_LOGO
@@ -109,7 +110,7 @@ class Membership(models.Model, RestModel, MetaDataModel):
109
110
  url = "{}&auth_code={}".format(url, auth_token.toBase64())
110
111
  self.member.sendInvite(
111
112
  subject, self.group, url=url,
112
- msg=request.DATA.get("invite_msg", None),
113
+ msg=msg,
113
114
  POWERED_BY=powered_by,
114
115
  SITE_LOGO=site_logo,
115
116
  COMPANY_NAME=company_name,
account/models/session.py CHANGED
@@ -8,7 +8,7 @@ from rest import ua
8
8
  # replacing legacy cookie session system with more robust session info
9
9
  class AuthSession(models.Model, RestModel):
10
10
  class RestMeta:
11
- SEARCH_FIELDS = ["ip", "member__username", "browser", "location__city"]
11
+ SEARCH_FIELDS = ["ip", "member__username", "browser", "location__city", "buid"]
12
12
  VIEW_PERMS = ["view_members", "manage_members", "manage_users", "owner"]
13
13
  CAN_SAVE = False
14
14
  CAN_DELETE = False
account/rpc/auth.py CHANGED
@@ -12,6 +12,7 @@ from django.http import HttpResponse
12
12
  from datetime import datetime, timedelta
13
13
 
14
14
  ALLOW_BASIC_LOGIN = settings.get("ALLOW_BASIC_LOGIN", False)
15
+ FORGET_ALWAYS_TRUE = settings.get("FORGET_ALWAYS_TRUE", True)
15
16
 
16
17
 
17
18
  @rd.urlPOST(r'^login$')
@@ -301,14 +302,20 @@ def member_forgot_password(request):
301
302
  """
302
303
  member, resp = get_member_from_request(request)
303
304
  if member is None:
305
+ if FORGET_ALWAYS_TRUE:
306
+ return rv.restStatus(request, True, msg="Password reset instructions have been sent to your email. (If valid account)")
304
307
  return resp
305
308
  if resp is not None and not member.is_active:
309
+ if FORGET_ALWAYS_TRUE:
310
+ return rv.restStatus(request, True, msg="Password reset instructions have been sent to your email. (If valid account)")
306
311
  return resp
307
312
 
308
313
  if request.DATA.get("use_code", False):
309
314
  return member_forgot_password_code(request, member)
310
315
 
311
316
  if member.auth_code is not None and member.auth_code_expires > datetime.now():
317
+ if FORGET_ALWAYS_TRUE:
318
+ return rv.restStatus(request, True, msg="Password reset instructions have been sent to your email. (If valid account)")
312
319
  return rv.restPermissionDenied(request, "already sent valid auth code")
313
320
 
314
321
  member.auth_code = crypto.randomString(16)
@@ -325,7 +332,7 @@ def member_forgot_password(request):
325
332
  'to': [member.email],
326
333
  })
327
334
 
328
- return rv.restStatus(request, True, msg="Password reset instructions have been sent to your email.")
335
+ return rv.restStatus(request, True, msg="Password reset instructions have been sent to your email. (If valid account)")
329
336
 
330
337
 
331
338
  def member_forgot_password_code(request, member):
account/rpc/device.py CHANGED
@@ -5,6 +5,13 @@ from account import models as am
5
5
  from objict import objict
6
6
 
7
7
 
8
+ @rd.url(r'^group/cloud/credentials$')
9
+ @rd.url(r'^group/cloud/credentials/(?P<pk>\d+)$')
10
+ @rd.login_required
11
+ def rest_on_group_cloud_creds(request, pk=None):
12
+ return am.CloudCredentials.on_rest_request(request, pk)
13
+
14
+
8
15
  @rd.url(r'^member/device$')
9
16
  @rd.url(r'^member/device/(?P<pk>\d+)$')
10
17
  @rd.login_required
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: django-restit
3
- Version: 4.2.71
3
+ Version: 4.2.73
4
4
  Summary: A Rest Framework for DJANGO
5
5
  License: MIT
6
6
  Author: Ian Starnes
@@ -24,15 +24,15 @@ 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/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
26
26
  account/models/__init__.py,sha256=cV_lMnT2vL_mjiYtT4hlcIHo52ocFbGSNVkOIHHLXZY,385
27
- account/models/device.py,sha256=MVWZEYrX_4zJaqmJS_feFplfVXYyqBJ-0_Fm3B1SVg8,5226
27
+ account/models/device.py,sha256=TloXvvrx3khF3BeGFuVYn6DhXjOW0AMZb4F9Fl5nBII,5491
28
28
  account/models/feeds.py,sha256=vI7fG4ASY1M0Zjke24RdnfDcuWeATl_yR_25jPmT64g,2011
29
29
  account/models/group.py,sha256=iDD_oSgswKV_t_gXZuVK80MvICrZZqdANm2jtGtOFy8,21985
30
30
  account/models/legacy.py,sha256=zYdtv4LC0ooxPVqWM-uToPwV-lYWQLorSE6p6yn1xDw,2720
31
- account/models/member.py,sha256=Pp4Np75L8TzVBC1N3g7Vnt5zy4cMBy7W0OG7mZ5Cfqg,50756
32
- account/models/membership.py,sha256=GJ6bSFLfU1CN9466k0XjSwn1sQIEwFeC8-oUYd2MrSs,9217
31
+ account/models/member.py,sha256=yM1tB0eXH5FZ49KPb2MKlt22v8qFOwcQRbO2_KRt5gw,51053
32
+ account/models/membership.py,sha256=90EpAhOsGaqphDAkONP6j_qQ0OWSRaQsI8H7E7fgMkE,9249
33
33
  account/models/notify.py,sha256=YnZujSHJHY7B09e6FIyZIEJRWLPYk1Sk1e92tFzB1IA,12078
34
34
  account/models/passkeys.py,sha256=TJxITUi4DT4_1tW2K7ZlOcRjJuMVl2NtKz7pKQU8-Tw,1516
35
- account/models/session.py,sha256=o3t98e8itXEtkknBHdBH_PSq9kuw1A858_wl6ZleXMM,3729
35
+ account/models/session.py,sha256=ELkWjB_2KXQvPtRPrvuGJpJsqrxCQX_4J53SbqGz_2U,3737
36
36
  account/models/settings.py,sha256=gOyRWBVd3BQpjfj_hJPtqX3H46ztyRAFxBrPbv11lQg,2137
37
37
  account/oauth/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
38
38
  account/oauth/google.py,sha256=q5M6Qhpfp9QslKRVYFZBvtG6kgXV6vYMrR5fp6Xdb9I,2078
@@ -40,8 +40,8 @@ account/passkeys/__init__.py,sha256=FwXYJXwSJXfkLojGBcVpF1dFpgFhzDdd9N_3naYQ0cc,
40
40
  account/passkeys/core.py,sha256=xj-vXjSrfWDvc5MYtEmXzwaMkNHl-cXrQKVrN9soRCg,4126
41
41
  account/periodic.py,sha256=-u0n-7QTJgDOkasGhBAPwHAwjpqWGA-MZLEFkVTqCGU,874
42
42
  account/rpc/__init__.py,sha256=SGF0M_-H0dKh3b1apSX29BotNWAvITYccGQVC0MIjL8,336
43
- account/rpc/auth.py,sha256=oTMXstslgQSXZmTA1Achqvpqyr5eEhHf9n7iumy_D-U,15882
44
- account/rpc/device.py,sha256=fbbZFp3cUdhVXvD7gVFOqFWj4hKS3bjZKD_aF5fQxd8,2852
43
+ account/rpc/auth.py,sha256=vcsAUCj-iyZEZA2D20KL4h7w7aeN2qgwoD_eUs0nJpQ,16452
44
+ account/rpc/device.py,sha256=mB14a6qvJIBnCa9ivLhPXwEt5Gk2foyqsKBtZxC506k,3070
45
45
  account/rpc/group.py,sha256=FD9GymgPY68y-gtDLsZxYVdwQJeLGpqcP4hjcDUh-GM,4022
46
46
  account/rpc/member.py,sha256=PU-Uz5KUI_BZFy-F-taDqAfnt_AwONYXSzUvfm7eyTw,1264
47
47
  account/rpc/notify.py,sha256=Q2YWejP36egeF060Hih5uX4Psv_B8NWlLLPi7iDYlIw,3344
@@ -110,7 +110,7 @@ incident/models/__init__.py,sha256=NMphuhb0RTMf7Ov4QkNv7iv6_I8Wtr3xQ54yjX_a31M,2
110
110
  incident/models/event.py,sha256=WRNzvjo0jypdnQNBksOOyi-_0kVT4qWUrDZf0Aw_MPM,7355
111
111
  incident/models/incident.py,sha256=Jx0RnOo70BzPX4BLMVF8jB5UOYwFPKPlxWznkTeMzOU,19332
112
112
  incident/models/ossec.py,sha256=p1ptr-8lnaj1EP_VmPR58b2LmaYBGaYYKAMqhWK5yZM,2227
113
- incident/models/rules.py,sha256=uT5GhW6Flso287lJGphAlWwL20NRnHDAZoGrWBBQfeE,6260
113
+ incident/models/rules.py,sha256=SMlDRw_r3fGv-vmRojRLmsklqRRxDcjrSLVBIz-gadA,6884
114
114
  incident/models/ticket.py,sha256=S3kqGQpYLE6Y4M9IKu_60sgW-f592xNr8uufqHnvDoU,2302
115
115
  incident/parsers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
116
116
  incident/parsers/ossec.py,sha256=Bc82n0AeXMBxMxzfAR-1puHyxldcikqeu5MeGRk1zMc,7142
@@ -351,7 +351,7 @@ metrics/providers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU
351
351
  metrics/providers/aws.py,sha256=RDM5RLeFADHexm4cHaJdAm3K6iz1NwMSNcV9GYuGtjY,7432
352
352
  metrics/rpc.py,sha256=L6gjRAK7MNzu9haG7Uw9ECWDCdYWknM_ft7kYr4H0ts,20412
353
353
  metrics/settings.py,sha256=wwHA9Z7BAHNeu3tFVn8Fh5j46KR-eGx0E8r5dzCFlAU,132
354
- metrics/tq.py,sha256=pl9RG4vXViX2hhSTOENZwXsAXD-5luXkBQjgzj4bwtk,799
354
+ metrics/tq.py,sha256=WHBRYSinmTuxF9l-_-lx0yfzEYkb0ffVMt_uvCj9bYo,825
355
355
  metrics/utils.py,sha256=w6H2v8zjlOZ5uqZsJOQvZoN-2Kyv1h8PN76gMGow7AE,11995
356
356
  pushit/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
357
357
  pushit/admin.py,sha256=69HdDZU_Iz8Fm72M8r8FUztsZvW37zdGwVmj8VTqr0c,451
@@ -400,7 +400,7 @@ rest/middleware/request.py,sha256=JchRNy5L-bGd-7h-KFYekGRvREe2eCkZXKOYqIkP2hI,41
400
400
  rest/middleware/session.py,sha256=zHSoQpIzRLmpqr_JvW406wzpvU3W3gDbm5JhtzLAMlE,10240
401
401
  rest/middleware/session_store.py,sha256=1nSdeXK8PyuYgGgIufqrS6j6QpIrQ7zbMNT0ol75e6U,1901
402
402
  rest/models/__init__.py,sha256=M8pvFDq-WCF-QcM58X7pMufYYe0aaQ3U0PwGe9TKbbY,130
403
- rest/models/base.py,sha256=YHGP978FmcXIPiyrdZKVskVdEtsy84zF10LEfdErdp0,67771
403
+ rest/models/base.py,sha256=MIZUQStR5Y2ndSjmOSu-NSIg3SZs9IFoMlRQ2re75OE,69565
404
404
  rest/models/cacher.py,sha256=eKz8TINVhWEqKhJGMsRkKZTtBUIv5rN3NHbZwOC56Uk,578
405
405
  rest/models/metadata.py,sha256=65GvfFbc26_7wJz8qEAzU7fEOZWVz0ttO5j5m_gs4hk,12860
406
406
  rest/net.py,sha256=LcB2QV6VNRtsSdmiQvYZgwQUDwOPMn_VBdRiZ6OpI-I,2974
@@ -501,7 +501,7 @@ ws4redis/servers/uwsgi.py,sha256=VyhoCI1DnVFqBiJYHoxqn5Idlf6uJPHvfBKgkjs34mo,172
501
501
  ws4redis/settings.py,sha256=K0yBiLUuY81iDM4Yr-k8hbvjn5VVHu5zQhmMK8Dtz0s,1536
502
502
  ws4redis/utf8validator.py,sha256=S0OlfjeGRP75aO6CzZsF4oTjRQAgR17OWE9rgZdMBZA,5122
503
503
  ws4redis/websocket.py,sha256=R0TUyPsoVRD7Y_oU7w2I6NL4fPwiz5Vl94-fUkZgLHA,14848
504
- django_restit-4.2.71.dist-info/LICENSE.md,sha256=VHN4hhEeVOoFjtG-5fVv4jesA4SWi0Z-KgOzzN6a1ps,1068
505
- django_restit-4.2.71.dist-info/METADATA,sha256=r7l9ZtyTLPW9I9XeXySkV-ElGFx6wgF92U2BIf4PbNw,7645
506
- django_restit-4.2.71.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
507
- django_restit-4.2.71.dist-info/RECORD,,
504
+ django_restit-4.2.73.dist-info/LICENSE.md,sha256=VHN4hhEeVOoFjtG-5fVv4jesA4SWi0Z-KgOzzN6a1ps,1068
505
+ django_restit-4.2.73.dist-info/METADATA,sha256=On1RFDnzXlyZJmbbSLrg9QAPXMYm2vNuDmrX9OVCP8U,7645
506
+ django_restit-4.2.73.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
507
+ django_restit-4.2.73.dist-info/RECORD,,
incident/models/rules.py CHANGED
@@ -26,6 +26,7 @@ added to to same incident.
26
26
  class Rule(models.Model, rm.RestModel):
27
27
  class RestMeta:
28
28
  SEARCH_FIELDS = ["name", "group__name"]
29
+ POST_SAVE_FIELDS = ["checks"]
29
30
  CAN_DELETE = True
30
31
  VIEW_PERMS = ["view_incidents"]
31
32
  # VIEW_PERMS = ["example_permission"]
@@ -50,6 +51,11 @@ class Rule(models.Model, rm.RestModel):
50
51
  "bundle_by",
51
52
  "match_by"
52
53
  ]
54
+ },
55
+ "download": {
56
+ "graphs": {
57
+ "checks": "basic"
58
+ }
53
59
  }
54
60
  }
55
61
 
@@ -102,6 +108,17 @@ class Rule(models.Model, rm.RestModel):
102
108
  return True
103
109
  return False
104
110
 
111
+ def set_checks(self, value):
112
+ if not isinstance(value, list):
113
+ return
114
+ exclude = ["id", "pk", "created", "modified", "parent"]
115
+ for item in value:
116
+ nval = {key: item[key] for key in item if key not in exclude and RuleCheck.hasField(key)}
117
+ nval["parent"] = self.id
118
+ rh.debug(nval)
119
+ obj = RuleCheck.createFromDict(self.getActiveRequest(), nval)
120
+ rh.debug("created", obj)
121
+
105
122
 
106
123
  class RuleCheck(models.Model, rm.RestModel):
107
124
  class RestMeta:
metrics/tq.py CHANGED
@@ -16,7 +16,7 @@ def checkDomains(task):
16
16
  alarm_date = now + timedelta(days=30)
17
17
  result = ssl_check.check(*settings.DOMAIN_WATCH)
18
18
  for key, value in result.items():
19
- if value is None:
19
+ if value is None or isinstance(value, str):
20
20
  continue
21
21
  if value < alarm_date:
22
22
  task.log(f"{key} is expiring soon")
rest/models/base.py CHANGED
@@ -394,6 +394,8 @@ class RestModel(object):
394
394
  return
395
395
  if not hasattr(self, "_field_names__"):
396
396
  self._field_names__ = [f.name for f in self._meta.get_fields()]
397
+ # if not hasattr(self, "_related_field_names__"):
398
+ # self._related_field_names__ = self.get_related_name_fields()
397
399
  # print "saving field: {0} = {1}".format(fieldname, value)
398
400
  if fieldname in RestModel.__RestMeta__.NO_SAVE_FIELDS:
399
401
  return
@@ -479,8 +481,6 @@ class RestModel(object):
479
481
  if hasattr(self, fieldname) and getattr(self, fieldname) != value:
480
482
  self._changed__[fieldname] = getattr(self, fieldname)
481
483
  setattr(self, fieldname, value)
482
- # else:
483
- # print "does not have field: {0}".format(fieldname)
484
484
 
485
485
  def saveFromRequest(self, request, **kwargs):
486
486
  if "files" not in kwargs:
@@ -1064,6 +1064,10 @@ class RestModel(object):
1064
1064
  if hasattr(cls.RestMeta, "FORMATS"):
1065
1065
  fields = cls.RestMeta.FORMATS.get(format, fields)
1066
1066
  if len(fields) == 0:
1067
+ if format == "json":
1068
+ g = cls.getGraph("default")
1069
+ if "fields" in g:
1070
+ return g["fields"]
1067
1071
  no_show_fields = RestModel.__RestMeta__.NO_SHOW_FIELDS
1068
1072
  if hasattr(cls.RestMeta, "NO_SHOW_FIELDS"):
1069
1073
  no_show_fields = cls.RestMeta.NO_SHOW_FIELDS
@@ -1077,12 +1081,17 @@ class RestModel(object):
1077
1081
  def on_rest_list_format(cls, request, format, qset):
1078
1082
  if format in ["summary", "summary_only"]:
1079
1083
  return cls.on_rest_list_summary(request, qset)
1080
- fields = cls.getRestFormatFields(format)
1084
+ fields = None
1085
+ if format == "json":
1086
+ g = cls.getGraph(request.DATA.get("graph", "download"))
1087
+ if g and "fields" in g:
1088
+ fields = g["fields"]
1089
+ if fields is None:
1090
+ fields = cls.getRestFormatFields(format)
1081
1091
  if fields or format == "json":
1082
1092
  name = request.DATA.get("format_filename", None)
1083
1093
  format_size = request.DATA.get("format_size", 10000)
1084
1094
  localize = request.DATA.get("localize", None, field_type=dict)
1085
- rh.debug("localize", localize)
1086
1095
  if name is None:
1087
1096
  ext = format
1088
1097
  if "_" in ext:
@@ -1157,6 +1166,14 @@ class RestModel(object):
1157
1166
  output[lbl] = rh.getMax(act_qset, field)
1158
1167
  return GRAPH_HELPERS.restGet(request, output)
1159
1168
 
1169
+ @classmethod
1170
+ def getRestBatchCreateFilter(cls, item, exclude=["id", "pk", "created", "modified"], include=None):
1171
+ if isinstance(include, list):
1172
+ return {key: item[key] for key in include if key in item}
1173
+ # ignore related fields
1174
+ rfs = cls.get_related_name_fields()
1175
+ return {key: item[key] for key in item if key not in exclude and cls.hasField(key) and key not in rfs}
1176
+
1160
1177
  @classmethod
1161
1178
  def on_rest_batch(cls, request, action):
1162
1179
  # this method is called when rest_batch='somme action'
@@ -1185,19 +1202,38 @@ class RestModel(object):
1185
1202
  count = qset.update(**update_fields)
1186
1203
  return GRAPH_HELPERS.restStatus(request, True, error="updated {} items".format(count))
1187
1204
  elif action == "create":
1188
- batch_data = request.DATA.getlist("batch_data", [])
1189
- items = []
1190
- for item in batch_data:
1191
- try:
1192
- obj = cls.ro_objects().filter(**item).last()
1193
- if not obj:
1194
- obj.checkPermsAndSave(request, item)
1195
- items.append(obj)
1196
- except Exception:
1197
- pass
1198
- return GRAPH_HELPERS.restList(request, items)
1205
+ cls.on_rest_batch_create(request)
1199
1206
  return GRAPH_HELPERS.restStatus(request, False, error="not implemented")
1200
1207
 
1208
+ @classmethod
1209
+ def on_rest_batch_create(cls, request):
1210
+ batch_data = request.DATA.get("batch_data")
1211
+ if isinstance(batch_data, str):
1212
+ batch_data = objict.fromJSON(batch_data)
1213
+ if isinstance(batch_data, dict):
1214
+ if "data" in batch_data:
1215
+ batch_data = batch_data["data"]
1216
+ items = []
1217
+ for item in batch_data:
1218
+ obj = cls.createFromBatch(item)
1219
+ if obj:
1220
+ items.append(obj)
1221
+ return GRAPH_HELPERS.restList(request, items)
1222
+
1223
+ @classmethod
1224
+ def createFromBatch(cls, item, request=None):
1225
+ obj = None
1226
+ try:
1227
+ rh.debug("batch item", item)
1228
+ item_filter = cls.getRestBatchCreateFilter(item)
1229
+ rh.debug("batch filters", item_filter)
1230
+ obj = cls.ro_objects().filter(**item_filter).last()
1231
+ if obj is None:
1232
+ obj = cls.createFromDict(request, item)
1233
+ except Exception:
1234
+ rh.log_exception(item)
1235
+ return obj
1236
+
1201
1237
  @classmethod
1202
1238
  def on_rest_create(cls, request, pk=None):
1203
1239
  # permissions are checked in the save routine
@@ -1487,7 +1523,7 @@ class RestModel(object):
1487
1523
  qset = qset.filter(**q)
1488
1524
  return qset
1489
1525
 
1490
- ALLOWED_QUERY_OPERATORS = ["gt", "gte", "lt", "lte", "contains", "icontains", "in", "startswith", "endswith"]
1526
+ ALLOWED_QUERY_OPERATORS = ["isnull", "gt", "gte", "lt", "lte", "contains", "icontains", "in", "startswith", "endswith"]
1491
1527
 
1492
1528
  @classmethod
1493
1529
  def queryFromRequest(cls, request, qset):
@@ -1526,6 +1562,7 @@ class RestModel(object):
1526
1562
  <field_name>=<operator>:value
1527
1563
  """
1528
1564
  q = {}
1565
+ exq = {}
1529
1566
  field_names = cls.rest_getQueryFields()
1530
1567
  query_keys = request.DATA.keys()
1531
1568
  special_keys = getattr(cls.RestMeta, "QUERY_FIELDS_SPECIAL", {})
@@ -1597,6 +1634,8 @@ class RestModel(object):
1597
1634
  if oper == "in":
1598
1635
  if isinstance(value, str) and ',' in value:
1599
1636
  value = [a.strip() for a in value.split(',')]
1637
+ elif oper == "isnull":
1638
+ value = value in [True, "true", 1, "1"]
1600
1639
  elif oper is None and value in ["null", "None"]:
1601
1640
  key = "{}__isnull".format(key)
1602
1641
  value = True
@@ -1607,8 +1646,12 @@ class RestModel(object):
1607
1646
  value = rh.toInteger(value)
1608
1647
  q[key] = value
1609
1648
  if bool(q):
1610
- # rh.debug("queryFromRequest", q, "default_filters:", request.default_rest_filters)
1611
- qset = qset.filter(**q)
1649
+ exq = {key[:-4]: q[key] for key in q if key.endswith("__ne")}
1650
+ q = {key: q[key] for key in q if not key.endswith("__ne")}
1651
+ if bool(exq):
1652
+ qset = qset.exclude(**exq)
1653
+ if bool(q):
1654
+ qset = qset.filter(**q)
1612
1655
  return qset
1613
1656
 
1614
1657
  @classmethod
@@ -1671,6 +1714,10 @@ class RestModel(object):
1671
1714
  '''returns the internal field type'''
1672
1715
  return [field.name for field in cls._meta.fields if field.get_internal_type() == "ForeignKey"]
1673
1716
 
1717
+ @classmethod
1718
+ def get_related_name_fields(cls):
1719
+ return [f.related_name for f in cls._meta.related_objects]
1720
+
1674
1721
  @classmethod
1675
1722
  def get_fk_model(cls, fieldname):
1676
1723
  '''returns None if not foreignkey, otherswise the relevant model'''