karrio-server-graph 2025.5rc1__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.
- karrio/server/graph/__init__.py +1 -0
- karrio/server/graph/admin.py +3 -0
- karrio/server/graph/apps.py +5 -0
- karrio/server/graph/forms.py +59 -0
- karrio/server/graph/management/__init__.py +0 -0
- karrio/server/graph/management/commands/__init__.py +0 -0
- karrio/server/graph/management/commands/export_schema.py +9 -0
- karrio/server/graph/migrations/0001_initial.py +37 -0
- karrio/server/graph/migrations/0002_auto_20210512_1353.py +22 -0
- karrio/server/graph/migrations/__init__.py +0 -0
- karrio/server/graph/models.py +44 -0
- karrio/server/graph/schema.py +46 -0
- karrio/server/graph/schemas/__init__.py +2 -0
- karrio/server/graph/schemas/base/__init__.py +367 -0
- karrio/server/graph/schemas/base/inputs.py +582 -0
- karrio/server/graph/schemas/base/mutations.py +871 -0
- karrio/server/graph/schemas/base/types.py +1365 -0
- karrio/server/graph/serializers.py +388 -0
- karrio/server/graph/templates/graphql/graphiql.html +142 -0
- karrio/server/graph/templates/karrio/email_change_email.html +13 -0
- karrio/server/graph/templates/karrio/email_change_email.txt +13 -0
- karrio/server/graph/templates/karrio/password_reset_email.html +14 -0
- karrio/server/graph/tests/__init__.py +9 -0
- karrio/server/graph/tests/base.py +124 -0
- karrio/server/graph/tests/test_carrier_connections.py +219 -0
- karrio/server/graph/tests/test_metafield.py +404 -0
- karrio/server/graph/tests/test_rate_sheets.py +348 -0
- karrio/server/graph/tests/test_templates.py +677 -0
- karrio/server/graph/tests/test_user_info.py +71 -0
- karrio/server/graph/urls.py +10 -0
- karrio/server/graph/utils.py +304 -0
- karrio/server/graph/views.py +93 -0
- karrio/server/settings/graph.py +7 -0
- karrio_server_graph-2025.5rc1.dist-info/METADATA +29 -0
- karrio_server_graph-2025.5rc1.dist-info/RECORD +37 -0
- karrio_server_graph-2025.5rc1.dist-info/WHEEL +5 -0
- karrio_server_graph-2025.5rc1.dist-info/top_level.txt +2 -0
@@ -0,0 +1,871 @@
|
|
1
|
+
import strawberry
|
2
|
+
import typing
|
3
|
+
import logging
|
4
|
+
import datetime
|
5
|
+
from strawberry.types import Info
|
6
|
+
from rest_framework import exceptions
|
7
|
+
from django.utils.http import urlsafe_base64_decode
|
8
|
+
from django.contrib.contenttypes.models import ContentType
|
9
|
+
from django_email_verification import confirm as email_verification
|
10
|
+
from django_otp.plugins.otp_email import models as otp
|
11
|
+
from django.utils.translation import gettext_lazy as _
|
12
|
+
from django.db import transaction
|
13
|
+
|
14
|
+
from karrio.server.core.utils import ConfirmationToken, send_email
|
15
|
+
from karrio.server.user.serializers import TokenSerializer
|
16
|
+
from karrio.server.conf import settings
|
17
|
+
from karrio.server.serializers import (
|
18
|
+
save_many_to_many_data,
|
19
|
+
process_dictionaries_mutations,
|
20
|
+
)
|
21
|
+
import karrio.server.providers.serializers as providers_serializers
|
22
|
+
import karrio.server.manager.serializers as manager_serializers
|
23
|
+
import karrio.server.graph.schemas.base.inputs as inputs
|
24
|
+
import karrio.server.graph.schemas.base.types as types
|
25
|
+
import karrio.server.graph.serializers as serializers
|
26
|
+
import karrio.server.providers.models as providers
|
27
|
+
import karrio.server.manager.models as manager
|
28
|
+
import karrio.server.user.forms as user_forms
|
29
|
+
import karrio.server.core.gateway as gateway
|
30
|
+
import karrio.server.graph.models as graph
|
31
|
+
import karrio.server.graph.forms as forms
|
32
|
+
import karrio.server.graph.utils as utils
|
33
|
+
import karrio.server.user.models as auth
|
34
|
+
import karrio.server.iam.models as iam
|
35
|
+
import karrio.server.core.models as core
|
36
|
+
import karrio.lib as lib
|
37
|
+
|
38
|
+
logger = logging.getLogger(__name__)
|
39
|
+
|
40
|
+
|
41
|
+
@strawberry.type
|
42
|
+
class UserUpdateMutation(utils.BaseMutation):
|
43
|
+
user: typing.Optional[types.UserType] = None
|
44
|
+
|
45
|
+
@staticmethod
|
46
|
+
@utils.authentication_required
|
47
|
+
@utils.authorization_required()
|
48
|
+
def mutate(info: Info, **input: inputs.UpdateUserInput) -> "UserUpdateMutation":
|
49
|
+
instance = types.User.objects.get(id=info.context.request.user.id)
|
50
|
+
|
51
|
+
serializer = serializers.UserModelSerializer(
|
52
|
+
instance,
|
53
|
+
partial=True,
|
54
|
+
data=input,
|
55
|
+
context=info.context.request,
|
56
|
+
)
|
57
|
+
|
58
|
+
serializer.is_valid(raise_exception=True)
|
59
|
+
|
60
|
+
return UserUpdateMutation(user=serializer.save()) # type:ignore
|
61
|
+
|
62
|
+
|
63
|
+
@strawberry.type
|
64
|
+
class WorkspaceConfigMutation(utils.BaseMutation):
|
65
|
+
workspace_config: typing.Optional[types.WorkspaceConfigType] = None
|
66
|
+
|
67
|
+
@staticmethod
|
68
|
+
@utils.authentication_required
|
69
|
+
@utils.authorization_required(["manage_team"])
|
70
|
+
def mutate(
|
71
|
+
info: Info, **input: inputs.WorkspaceConfigMutationInput
|
72
|
+
) -> "WorkspaceConfigMutation":
|
73
|
+
data = dict(config=input.copy())
|
74
|
+
workspace = auth.WorkspaceConfig.access_by(info.context.request).first()
|
75
|
+
|
76
|
+
serializer = serializers.WorkspaceConfigModelSerializer(
|
77
|
+
workspace,
|
78
|
+
partial=workspace is not None,
|
79
|
+
data=process_dictionaries_mutations(["config"], data, workspace),
|
80
|
+
context=info.context.request,
|
81
|
+
)
|
82
|
+
|
83
|
+
serializer.is_valid(raise_exception=True)
|
84
|
+
|
85
|
+
return WorkspaceConfigMutation(
|
86
|
+
workspace_config=serializer.save()
|
87
|
+
) # type:ignore
|
88
|
+
|
89
|
+
|
90
|
+
@strawberry.type
|
91
|
+
class TokenMutation(utils.BaseMutation):
|
92
|
+
token: typing.Optional[types.TokenType] = None
|
93
|
+
|
94
|
+
@staticmethod
|
95
|
+
@utils.authentication_required
|
96
|
+
@utils.authorization_required()
|
97
|
+
def mutate(
|
98
|
+
info: Info,
|
99
|
+
key: str = None,
|
100
|
+
refresh: bool = None,
|
101
|
+
password: str = None,
|
102
|
+
) -> "UserUpdateMutation":
|
103
|
+
tokens = auth.Token.access_by(info.context.request).filter(key=key)
|
104
|
+
|
105
|
+
if refresh:
|
106
|
+
if len(password or "") == 0:
|
107
|
+
raise exceptions.ValidationError(
|
108
|
+
{"password": "Password is required to refresh token"}
|
109
|
+
)
|
110
|
+
|
111
|
+
if not info.context.request.user.check_password(password):
|
112
|
+
raise exceptions.ValidationError({"password": "Invalid password"})
|
113
|
+
|
114
|
+
if any(tokens):
|
115
|
+
tokens.delete()
|
116
|
+
|
117
|
+
else:
|
118
|
+
return TokenMutation(token=tokens.first()) # type:ignore
|
119
|
+
|
120
|
+
token = (
|
121
|
+
TokenSerializer.map(data={}, context=info.context.request).save().instance
|
122
|
+
)
|
123
|
+
|
124
|
+
return TokenMutation(token=token) # type:ignore
|
125
|
+
|
126
|
+
|
127
|
+
@strawberry.type
|
128
|
+
class CreateAPIKeyMutation(utils.BaseMutation):
|
129
|
+
api_key: typing.Optional[types.APIKeyType] = None
|
130
|
+
|
131
|
+
@staticmethod
|
132
|
+
@transaction.atomic
|
133
|
+
@utils.authentication_required
|
134
|
+
@utils.authorization_required()
|
135
|
+
@utils.password_required
|
136
|
+
def mutate(
|
137
|
+
info: Info, password: str, **input: inputs.CreateAPIKeyMutationInput
|
138
|
+
) -> "CreateAPIKeyMutation":
|
139
|
+
context = info.context.request
|
140
|
+
data = input.copy()
|
141
|
+
permissions = data.pop("permissions", [])
|
142
|
+
api_key = TokenSerializer.map(data=data, context=context).save().instance
|
143
|
+
|
144
|
+
if any(permissions):
|
145
|
+
_auth_ctx = (
|
146
|
+
context.token
|
147
|
+
if hasattr(getattr(info.context.request, "token", None), "permissions")
|
148
|
+
else context.user
|
149
|
+
)
|
150
|
+
_ctx_permissions = getattr(_auth_ctx, "permissions", [])
|
151
|
+
_invalid_permissions = [_ for _ in permissions if _ not in _ctx_permissions]
|
152
|
+
|
153
|
+
if any(_invalid_permissions):
|
154
|
+
raise exceptions.ValidationError({"permissions": "Invalid permissions"})
|
155
|
+
|
156
|
+
_ctx = iam.ContextPermission.objects.create(
|
157
|
+
object_pk=api_key.pk,
|
158
|
+
content_object=api_key,
|
159
|
+
content_type=ContentType.objects.get_for_model(api_key),
|
160
|
+
)
|
161
|
+
_ctx.groups.set(auth.Group.objects.filter(name__in=permissions))
|
162
|
+
|
163
|
+
return CreateAPIKeyMutation(
|
164
|
+
api_key=auth.Token.access_by(context).get(key=api_key.key)
|
165
|
+
) # type:ignore
|
166
|
+
|
167
|
+
|
168
|
+
@strawberry.type
|
169
|
+
class DeleteAPIKeyMutation(utils.BaseMutation):
|
170
|
+
label: typing.Optional[str] = None
|
171
|
+
|
172
|
+
@staticmethod
|
173
|
+
@utils.authentication_required
|
174
|
+
@utils.authorization_required()
|
175
|
+
@utils.password_required
|
176
|
+
def mutate(
|
177
|
+
info: Info, password: str, **input: inputs.DeleteAPIKeyMutationInput
|
178
|
+
) -> "DeleteAPIKeyMutation":
|
179
|
+
api_key = auth.Token.access_by(info.context.request).get(**input)
|
180
|
+
label = api_key.label
|
181
|
+
api_key.delete()
|
182
|
+
|
183
|
+
return DeleteAPIKeyMutation(label=label) # type:ignore
|
184
|
+
|
185
|
+
|
186
|
+
@strawberry.type
|
187
|
+
class RequestEmailChangeMutation(utils.BaseMutation):
|
188
|
+
user: typing.Optional[types.UserType] = None
|
189
|
+
|
190
|
+
@staticmethod
|
191
|
+
@utils.authentication_required
|
192
|
+
@utils.authorization_required()
|
193
|
+
@utils.password_required
|
194
|
+
def mutate(
|
195
|
+
info: Info, email: str, password: str, redirect_url: str
|
196
|
+
) -> "RequestEmailChangeMutation":
|
197
|
+
try:
|
198
|
+
token = ConfirmationToken.for_data(
|
199
|
+
user=info.context.request.user,
|
200
|
+
data=dict(new_email=email),
|
201
|
+
)
|
202
|
+
|
203
|
+
send_email(
|
204
|
+
emails=[email],
|
205
|
+
subject="Confirm your new email address",
|
206
|
+
email_template="karrio/email_change_email.html",
|
207
|
+
text_template="karrio/email_change_email.txt",
|
208
|
+
context=dict(
|
209
|
+
token=token,
|
210
|
+
link=redirect_url,
|
211
|
+
),
|
212
|
+
expiry=(datetime.datetime.now() + datetime.timedelta(hours=2)),
|
213
|
+
)
|
214
|
+
except Exception as e:
|
215
|
+
logger.exception(e)
|
216
|
+
raise e
|
217
|
+
|
218
|
+
return RequestEmailChangeMutation(user=info.context.request.user) # type:ignore
|
219
|
+
|
220
|
+
|
221
|
+
@strawberry.type
|
222
|
+
class ConfirmEmailChangeMutation(utils.BaseMutation):
|
223
|
+
user: typing.Optional[types.UserType] = None
|
224
|
+
|
225
|
+
@staticmethod
|
226
|
+
@utils.authentication_required
|
227
|
+
@utils.authorization_required()
|
228
|
+
def mutate(info: Info, token: str) -> "ConfirmEmailChangeMutation":
|
229
|
+
validated_token = ConfirmationToken(token)
|
230
|
+
user = info.context.request.user
|
231
|
+
|
232
|
+
if user.id != validated_token["user_id"]:
|
233
|
+
raise exceptions.ValidationError(
|
234
|
+
{"token": "auth.Token is invalid or expired"}
|
235
|
+
)
|
236
|
+
|
237
|
+
if user.email == validated_token["new_email"]:
|
238
|
+
raise exceptions.APIException("Email is already confirmed")
|
239
|
+
|
240
|
+
user.email = validated_token["new_email"]
|
241
|
+
user.save()
|
242
|
+
|
243
|
+
return ConfirmEmailChangeMutation(
|
244
|
+
user=types.User.objects.get(id=validated_token["user_id"])
|
245
|
+
) # type:ignore
|
246
|
+
|
247
|
+
|
248
|
+
@strawberry.type
|
249
|
+
class RegisterUserMutation(utils.BaseMutation):
|
250
|
+
user: typing.Optional[types.UserType] = None
|
251
|
+
|
252
|
+
@staticmethod
|
253
|
+
def mutate(
|
254
|
+
info: Info, **input: inputs.RegisterUserMutationInput
|
255
|
+
) -> "RegisterUserMutation":
|
256
|
+
if settings.ALLOW_SIGNUP == False:
|
257
|
+
raise Exception(
|
258
|
+
"Signup is not allowed. "
|
259
|
+
"Please contact your administrator to create an account."
|
260
|
+
)
|
261
|
+
|
262
|
+
try:
|
263
|
+
form = user_forms.SignUpForm(input)
|
264
|
+
user = form.save()
|
265
|
+
|
266
|
+
return RegisterUserMutation(user=user) # type:ignore
|
267
|
+
except Exception as e:
|
268
|
+
logger.exception(e)
|
269
|
+
raise e
|
270
|
+
|
271
|
+
|
272
|
+
@strawberry.type
|
273
|
+
class ConfirmEmailMutation(utils.BaseMutation):
|
274
|
+
success: bool = False
|
275
|
+
|
276
|
+
@staticmethod
|
277
|
+
def mutate(info: Info, token: str) -> "ConfirmEmailMutation":
|
278
|
+
try:
|
279
|
+
success, _ = email_verification.verify_token(token)
|
280
|
+
|
281
|
+
return ConfirmEmailMutation(success=success) # type:ignore
|
282
|
+
except Exception as e:
|
283
|
+
logger.exception(e)
|
284
|
+
raise e
|
285
|
+
|
286
|
+
|
287
|
+
@strawberry.type
|
288
|
+
class ChangePasswordMutation(utils.BaseMutation):
|
289
|
+
user: typing.Optional[types.UserType] = None
|
290
|
+
|
291
|
+
@staticmethod
|
292
|
+
@utils.authentication_required
|
293
|
+
@utils.authorization_required()
|
294
|
+
def mutate(
|
295
|
+
info: Info, **input: inputs.ChangePasswordMutationInput
|
296
|
+
) -> "ChangePasswordMutation":
|
297
|
+
form = forms.PasswordChangeForm(info.context.request.user, data=input)
|
298
|
+
if form.is_valid():
|
299
|
+
user = form.save()
|
300
|
+
return ChangePasswordMutation(user=user) # type:ignore
|
301
|
+
else:
|
302
|
+
raise exceptions.ValidationError(form.errors)
|
303
|
+
|
304
|
+
|
305
|
+
@strawberry.type
|
306
|
+
class RequestPasswordResetMutation(utils.BaseMutation):
|
307
|
+
email: str = strawberry.UNSET
|
308
|
+
redirect_url: str = strawberry.UNSET
|
309
|
+
|
310
|
+
@staticmethod
|
311
|
+
def mutate(
|
312
|
+
info: Info, **input: inputs.RequestPasswordResetMutationInput
|
313
|
+
) -> "RequestPasswordResetMutation":
|
314
|
+
form = forms.ResetPasswordRequestForm(data=input)
|
315
|
+
|
316
|
+
if form.is_valid():
|
317
|
+
form.save(request=info.context.request)
|
318
|
+
return RequestPasswordResetMutation(**form.cleaned_data) # type:ignore
|
319
|
+
else:
|
320
|
+
raise exceptions.ValidationError(form.errors)
|
321
|
+
|
322
|
+
|
323
|
+
@strawberry.type
|
324
|
+
class ConfirmPasswordResetMutation(utils.BaseMutation):
|
325
|
+
user: typing.Optional[types.UserType] = None
|
326
|
+
|
327
|
+
@staticmethod
|
328
|
+
def get_user(uidb64):
|
329
|
+
try:
|
330
|
+
uid = urlsafe_base64_decode(uidb64).decode()
|
331
|
+
user = types.User._default_manager.get(pk=uid)
|
332
|
+
except (
|
333
|
+
TypeError,
|
334
|
+
ValueError,
|
335
|
+
OverflowError,
|
336
|
+
types.User.DoesNotExist,
|
337
|
+
exceptions.ValidationError,
|
338
|
+
):
|
339
|
+
user = None
|
340
|
+
return user
|
341
|
+
|
342
|
+
@staticmethod
|
343
|
+
def mutate(
|
344
|
+
info: Info, **input: inputs.ConfirmPasswordResetMutationInput
|
345
|
+
) -> "ConfirmPasswordResetMutation":
|
346
|
+
uuid = input.get("uid")
|
347
|
+
user = ConfirmPasswordResetMutation.get_user(uuid) # type:ignore
|
348
|
+
form = forms.ConfirmPasswordResetForm(user, data=input)
|
349
|
+
|
350
|
+
if form.is_valid():
|
351
|
+
return ConfirmPasswordResetMutation(user=form.save()) # type:ignore
|
352
|
+
else:
|
353
|
+
raise exceptions.ValidationError(form.errors)
|
354
|
+
|
355
|
+
|
356
|
+
@strawberry.type
|
357
|
+
class EnableMultiFactorMutation(utils.BaseMutation):
|
358
|
+
user: typing.Optional[types.UserType] = None
|
359
|
+
|
360
|
+
@staticmethod
|
361
|
+
@utils.authentication_required
|
362
|
+
@utils.authorization_required()
|
363
|
+
def mutate(
|
364
|
+
info: Info, **input: inputs.EnableMultiFactorMutationInput
|
365
|
+
) -> "EnableMultiFactorMutation":
|
366
|
+
# Retrieve a default device or create a new one.
|
367
|
+
device = otp.EmailDevice.objects.filter(
|
368
|
+
user__id=info.context.request.user.id
|
369
|
+
).first()
|
370
|
+
if device is None:
|
371
|
+
device = otp.EmailDevice.objects.create(
|
372
|
+
name="default",
|
373
|
+
confirmed=False,
|
374
|
+
user=info.context.request.user,
|
375
|
+
)
|
376
|
+
|
377
|
+
# Send and email challenge if the device isn't confirmed yet.
|
378
|
+
if device.confirmed == False:
|
379
|
+
device.generate_challenge()
|
380
|
+
|
381
|
+
return EnableMultiFactorMutation( # type:ignore
|
382
|
+
user=types.User.objects.get(pk=info.context.request.user.id)
|
383
|
+
)
|
384
|
+
|
385
|
+
|
386
|
+
@strawberry.type
|
387
|
+
class ConfirmMultiFactorMutation(utils.BaseMutation):
|
388
|
+
user: typing.Optional[types.UserType] = None
|
389
|
+
|
390
|
+
@staticmethod
|
391
|
+
@utils.authentication_required
|
392
|
+
@utils.authorization_required()
|
393
|
+
def mutate(
|
394
|
+
info: Info, **input: inputs.ConfirmMultiFactorMutationInput
|
395
|
+
) -> "ConfirmMultiFactorMutation":
|
396
|
+
token = input.get("token")
|
397
|
+
# Retrieve a default device or create a new one.
|
398
|
+
device = otp.EmailDevice.objects.filter(
|
399
|
+
user__id=info.context.request.user.id
|
400
|
+
).first()
|
401
|
+
|
402
|
+
if device is None:
|
403
|
+
raise exceptions.APIException(
|
404
|
+
_("You need to enable Two factor auth first."), code="2fa_disabled"
|
405
|
+
)
|
406
|
+
|
407
|
+
# check if token is valid
|
408
|
+
if device.verify_token(token) is False:
|
409
|
+
raise exceptions.ValidationError(
|
410
|
+
{"otp_token": _("Invalid or Expired OTP token")}, code="otp_invalid"
|
411
|
+
)
|
412
|
+
|
413
|
+
device.confirmed = True
|
414
|
+
device.save()
|
415
|
+
|
416
|
+
return ConfirmMultiFactorMutation( # type:ignore
|
417
|
+
user=types.User.objects.get(pk=info.context.request.user.id)
|
418
|
+
)
|
419
|
+
|
420
|
+
|
421
|
+
@strawberry.type
|
422
|
+
class DisableMultiFactorMutation(utils.BaseMutation):
|
423
|
+
user: typing.Optional[types.UserType] = None
|
424
|
+
|
425
|
+
@staticmethod
|
426
|
+
@utils.authentication_required
|
427
|
+
@utils.authorization_required()
|
428
|
+
def mutate(
|
429
|
+
info: Info, **input: inputs.DisableMultiFactorMutationInput
|
430
|
+
) -> "DisableMultiFactorMutation":
|
431
|
+
# Retrieve a default device or create a new one.
|
432
|
+
device = otp.EmailDevice.objects.filter(
|
433
|
+
user__id=info.context.request.user.id
|
434
|
+
).first()
|
435
|
+
if device is not None:
|
436
|
+
device.delete()
|
437
|
+
|
438
|
+
return DisableMultiFactorMutation( # type:ignore
|
439
|
+
user=types.User.objects.get(pk=info.context.request.user.id)
|
440
|
+
)
|
441
|
+
|
442
|
+
|
443
|
+
@strawberry.type
|
444
|
+
class MetadataMutation(utils.BaseMutation):
|
445
|
+
id: str = strawberry.UNSET
|
446
|
+
metadata: utils.JSON = strawberry.UNSET
|
447
|
+
|
448
|
+
@staticmethod
|
449
|
+
@utils.authentication_required
|
450
|
+
@utils.authorization_required()
|
451
|
+
def mutate(
|
452
|
+
info: Info,
|
453
|
+
id: str,
|
454
|
+
object_type: utils.MetadataObjectTypeEnum,
|
455
|
+
added_values: dict = {},
|
456
|
+
discarded_keys: list = [],
|
457
|
+
) -> "MetadataMutation":
|
458
|
+
object_model = utils.MetadataObjectType[object_type].value
|
459
|
+
instance = object_model.access_by(info.context.request).get(id=id)
|
460
|
+
instance.metadata = {
|
461
|
+
key: value
|
462
|
+
for key, value in (instance.metadata or {}).items()
|
463
|
+
if key not in discarded_keys
|
464
|
+
}
|
465
|
+
instance.metadata.update(added_values)
|
466
|
+
instance.save(update_fields=["metadata"])
|
467
|
+
|
468
|
+
return MetadataMutation(id=id, metadata=instance.metadata) # type:ignore
|
469
|
+
|
470
|
+
|
471
|
+
@strawberry.type
|
472
|
+
class DeleteMutation(utils.BaseMutation):
|
473
|
+
id: str = strawberry.UNSET
|
474
|
+
|
475
|
+
@staticmethod
|
476
|
+
@utils.authentication_required
|
477
|
+
@utils.authorization_required()
|
478
|
+
def mutate(
|
479
|
+
info: Info,
|
480
|
+
model,
|
481
|
+
validator: typing.Callable = None,
|
482
|
+
**input: inputs.DeleteMutationInput,
|
483
|
+
) -> "DeleteMutation":
|
484
|
+
id = input.get("id")
|
485
|
+
queryset = (
|
486
|
+
model.access_by(info.context.request)
|
487
|
+
if hasattr(model, "access_by")
|
488
|
+
else model.objects
|
489
|
+
)
|
490
|
+
instance = queryset.get(id=id)
|
491
|
+
|
492
|
+
if validator:
|
493
|
+
validator(instance, context=info.context)
|
494
|
+
|
495
|
+
shipment = getattr(instance, "shipment", None)
|
496
|
+
instance.delete(keep_parents=True)
|
497
|
+
|
498
|
+
if shipment is not None:
|
499
|
+
manager_serializers.reset_related_shipment_rates(shipment)
|
500
|
+
|
501
|
+
return DeleteMutation(id=id) # type:ignore
|
502
|
+
|
503
|
+
|
504
|
+
@strawberry.type
|
505
|
+
class CreateRateSheetMutation(utils.BaseMutation):
|
506
|
+
rate_sheet: typing.Optional[types.RateSheetType] = None
|
507
|
+
|
508
|
+
@staticmethod
|
509
|
+
@transaction.atomic
|
510
|
+
@utils.authentication_required
|
511
|
+
@utils.authorization_required(["manage_carriers"])
|
512
|
+
def mutate(
|
513
|
+
info: Info, **input: inputs.CreateRateSheetMutationInput
|
514
|
+
) -> "CreateRateSheetMutation":
|
515
|
+
data = input.copy()
|
516
|
+
carriers = data.pop("carriers", [])
|
517
|
+
slug = f"{input.get('name', '').lower()}_sheet".replace(" ", "").lower()
|
518
|
+
serializer = serializers.RateSheetModelSerializer(
|
519
|
+
data={**data, "slug": slug},
|
520
|
+
context=info.context.request,
|
521
|
+
)
|
522
|
+
|
523
|
+
serializer.is_valid(raise_exception=True)
|
524
|
+
rate_sheet = serializer.save()
|
525
|
+
|
526
|
+
if "services" in data:
|
527
|
+
save_many_to_many_data(
|
528
|
+
"services",
|
529
|
+
serializers.ServiceLevelModelSerializer,
|
530
|
+
rate_sheet,
|
531
|
+
payload=data,
|
532
|
+
context=info.context.request,
|
533
|
+
)
|
534
|
+
|
535
|
+
if any(carriers):
|
536
|
+
_carriers = gateway.Carriers.list(
|
537
|
+
context=info.context.request,
|
538
|
+
carrier_name=rate_sheet.carrier_name,
|
539
|
+
).filter(id__in=carriers)
|
540
|
+
for _ in _carriers:
|
541
|
+
_.settings.rate_sheet = rate_sheet
|
542
|
+
_.settings.save(update_fields=["rate_sheet"])
|
543
|
+
|
544
|
+
return CreateRateSheetMutation(rate_sheet=rate_sheet)
|
545
|
+
|
546
|
+
|
547
|
+
@strawberry.type
|
548
|
+
class UpdateRateSheetMutation(utils.BaseMutation):
|
549
|
+
rate_sheet: typing.Optional[types.RateSheetType] = None
|
550
|
+
|
551
|
+
@staticmethod
|
552
|
+
@transaction.atomic
|
553
|
+
@utils.authentication_required
|
554
|
+
@utils.authorization_required(["manage_carriers"])
|
555
|
+
def mutate(
|
556
|
+
info: Info, **input: inputs.UpdateRateSheetMutationInput
|
557
|
+
) -> "UpdateRateSheetMutation":
|
558
|
+
instance = providers.RateSheet.access_by(info.context.request).get(
|
559
|
+
id=input["id"]
|
560
|
+
)
|
561
|
+
serializer = serializers.RateSheetModelSerializer(
|
562
|
+
instance,
|
563
|
+
data=input,
|
564
|
+
context=info.context.request,
|
565
|
+
partial=True,
|
566
|
+
)
|
567
|
+
|
568
|
+
serializer.is_valid(raise_exception=True)
|
569
|
+
rate_sheet = serializer.save()
|
570
|
+
|
571
|
+
return UpdateRateSheetMutation(
|
572
|
+
rate_sheet=providers.RateSheet.objects.get(id=input["id"])
|
573
|
+
)
|
574
|
+
|
575
|
+
|
576
|
+
@strawberry.type
|
577
|
+
class PartialShipmentMutation(utils.BaseMutation):
|
578
|
+
shipment: typing.Optional[types.ShipmentType] = None
|
579
|
+
|
580
|
+
@staticmethod
|
581
|
+
@utils.authentication_required
|
582
|
+
@utils.authorization_required(["manage_shipments"])
|
583
|
+
def mutate(
|
584
|
+
info: Info, **input: inputs.PartialShipmentMutationInput
|
585
|
+
) -> "PartialShipmentMutation":
|
586
|
+
try:
|
587
|
+
id = input["id"]
|
588
|
+
shipment = manager.Shipment.access_by(info.context.request).get(id=id)
|
589
|
+
manager_serializers.can_mutate_shipment(
|
590
|
+
shipment, update=True, payload=input
|
591
|
+
)
|
592
|
+
|
593
|
+
manager_serializers.ShipmentSerializer.map(
|
594
|
+
shipment,
|
595
|
+
context=info.context.request,
|
596
|
+
data=process_dictionaries_mutations(["options"], input, shipment),
|
597
|
+
).save()
|
598
|
+
|
599
|
+
# refetch the shipment to get the updated state with signals processed
|
600
|
+
update = manager.Shipment.access_by(info.context.request).get(id=id)
|
601
|
+
|
602
|
+
return PartialShipmentMutation(errors=None, shipment=update) # type:ignore
|
603
|
+
except Exception as e:
|
604
|
+
logger.exception(e)
|
605
|
+
raise e
|
606
|
+
|
607
|
+
|
608
|
+
@strawberry.type
|
609
|
+
class ChangeShipmentStatusMutation(utils.BaseMutation):
|
610
|
+
shipment: typing.Optional[types.ShipmentType] = None
|
611
|
+
|
612
|
+
@staticmethod
|
613
|
+
@utils.authentication_required
|
614
|
+
@utils.authorization_required(["manage_shipments"])
|
615
|
+
def mutate(
|
616
|
+
info: Info, **input: inputs.ChangeShipmentStatusMutationInput
|
617
|
+
) -> "ChangeShipmentStatusMutation":
|
618
|
+
shipment = manager.Shipment.access_by(info.context.request).get(id=input["id"])
|
619
|
+
|
620
|
+
if shipment.status in [
|
621
|
+
utils.ShipmentStatusEnum.draft.name,
|
622
|
+
utils.ShipmentStatusEnum.cancelled.name,
|
623
|
+
]:
|
624
|
+
raise exceptions.ValidationError(
|
625
|
+
_(f"{shipment.status} shipment cannot be changed to {input['status']}"),
|
626
|
+
code="invalid_operation",
|
627
|
+
)
|
628
|
+
|
629
|
+
if getattr(shipment, "tracker", None) is not None:
|
630
|
+
raise exceptions.ValidationError(
|
631
|
+
_(f"this shipment is tracked automatically by API"),
|
632
|
+
code="invalid_operation",
|
633
|
+
)
|
634
|
+
|
635
|
+
shipment.status = input["status"]
|
636
|
+
shipment.save(update_fields=["status"])
|
637
|
+
|
638
|
+
return ChangeShipmentStatusMutation(shipment=shipment) # type:ignore
|
639
|
+
|
640
|
+
|
641
|
+
def create_template_mutation(name: str, template_type: str) -> typing.Type:
|
642
|
+
_type: typing.Any = dict(
|
643
|
+
address=types.AddressTemplateType,
|
644
|
+
customs=types.CustomsTemplateType,
|
645
|
+
parcel=types.ParcelTemplateType,
|
646
|
+
).get(template_type)
|
647
|
+
|
648
|
+
@strawberry.type
|
649
|
+
class _Mutation(utils.BaseMutation):
|
650
|
+
template: typing.Optional[_type] = None
|
651
|
+
|
652
|
+
@staticmethod
|
653
|
+
@utils.authentication_required
|
654
|
+
@utils.authorization_required()
|
655
|
+
@transaction.atomic
|
656
|
+
def mutate(info: Info, **input) -> name: # type:ignore
|
657
|
+
data = input.copy()
|
658
|
+
instance = (
|
659
|
+
graph.Template.access_by(info.context.request).get(id=input["id"])
|
660
|
+
if "id" in input
|
661
|
+
else None
|
662
|
+
)
|
663
|
+
customs_data = data.get("customs", {})
|
664
|
+
|
665
|
+
if "commodities" in customs_data and instance is not None:
|
666
|
+
save_many_to_many_data(
|
667
|
+
"commodities",
|
668
|
+
serializers.CommodityModelSerializer,
|
669
|
+
getattr(instance, "customs", None),
|
670
|
+
payload=customs_data,
|
671
|
+
context=info.context.request,
|
672
|
+
)
|
673
|
+
|
674
|
+
serializer = serializers.TemplateModelSerializer(
|
675
|
+
instance,
|
676
|
+
data=data,
|
677
|
+
context=info.context.request,
|
678
|
+
partial=(instance is not None),
|
679
|
+
)
|
680
|
+
|
681
|
+
serializer.is_valid(raise_exception=True)
|
682
|
+
template = serializer.save()
|
683
|
+
|
684
|
+
return _Mutation(template=template) # type:ignore
|
685
|
+
|
686
|
+
return strawberry.type(type(name, (_Mutation,), {}))
|
687
|
+
|
688
|
+
|
689
|
+
CreateAddressTemplateMutation = create_template_mutation(
|
690
|
+
"CreateAddressTemplateMutation", "address"
|
691
|
+
)
|
692
|
+
UpdateAddressTemplateMutation = create_template_mutation(
|
693
|
+
"UpdateAddressTemplateMutation",
|
694
|
+
"address",
|
695
|
+
)
|
696
|
+
CreateCustomsTemplateMutation = create_template_mutation(
|
697
|
+
"CreateCustomsTemplateMutation", "customs"
|
698
|
+
)
|
699
|
+
UpdateCustomsTemplateMutation = create_template_mutation(
|
700
|
+
"UpdateCustomsTemplateMutation", "customs"
|
701
|
+
)
|
702
|
+
CreateParcelTemplateMutation = create_template_mutation(
|
703
|
+
"CreateParcelTemplateMutation", "parcel"
|
704
|
+
)
|
705
|
+
UpdateParcelTemplateMutation = create_template_mutation(
|
706
|
+
"UpdateParcelTemplateMutation", "parcel"
|
707
|
+
)
|
708
|
+
|
709
|
+
|
710
|
+
@strawberry.type
|
711
|
+
class CreateCarrierConnectionMutation(utils.BaseMutation):
|
712
|
+
connection: types.CarrierConnectionType = None
|
713
|
+
|
714
|
+
@staticmethod
|
715
|
+
@utils.error_wrapper
|
716
|
+
@utils.authentication_required
|
717
|
+
@utils.authorization_required(["manage_carriers"])
|
718
|
+
def mutate(info: Info, **input) -> "CreateCarrierConnectionMutation":
|
719
|
+
data = input.copy()
|
720
|
+
|
721
|
+
connection = lib.identity(
|
722
|
+
providers_serializers.CarrierConnectionModelSerializer.map(
|
723
|
+
data=providers_serializers.CarrierConnectionData.map(data=data).data,
|
724
|
+
context=info.context.request,
|
725
|
+
)
|
726
|
+
.save()
|
727
|
+
.instance
|
728
|
+
)
|
729
|
+
|
730
|
+
return CreateCarrierConnectionMutation( # type:ignore
|
731
|
+
connection=connection
|
732
|
+
)
|
733
|
+
|
734
|
+
|
735
|
+
@strawberry.type
|
736
|
+
class UpdateCarrierConnectionMutation(utils.BaseMutation):
|
737
|
+
connection: types.CarrierConnectionType = None
|
738
|
+
|
739
|
+
@staticmethod
|
740
|
+
@utils.error_wrapper
|
741
|
+
@utils.authentication_required
|
742
|
+
@utils.authorization_required(["manage_carriers"])
|
743
|
+
def mutate(info: Info, **input) -> "UpdateCarrierConnectionMutation":
|
744
|
+
data = input.copy()
|
745
|
+
id = data.get("id")
|
746
|
+
instance = providers.Carrier.access_by(info.context.request).get(id=id)
|
747
|
+
connection = lib.identity(
|
748
|
+
providers_serializers.CarrierConnectionModelSerializer.map(
|
749
|
+
instance,
|
750
|
+
data=data,
|
751
|
+
context=info.context.request,
|
752
|
+
)
|
753
|
+
.save()
|
754
|
+
.instance
|
755
|
+
)
|
756
|
+
|
757
|
+
return UpdateCarrierConnectionMutation( # type:ignore
|
758
|
+
connection=connection
|
759
|
+
)
|
760
|
+
|
761
|
+
|
762
|
+
@strawberry.type
|
763
|
+
class SystemCarrierMutation(utils.BaseMutation):
|
764
|
+
carrier: typing.Optional[types.SystemConnectionType] = None
|
765
|
+
|
766
|
+
@staticmethod
|
767
|
+
@utils.error_wrapper
|
768
|
+
@utils.authentication_required
|
769
|
+
@utils.authorization_required(["manage_carriers"])
|
770
|
+
def mutate(
|
771
|
+
info: Info, **input: inputs.SystemCarrierMutationInput
|
772
|
+
) -> "SystemCarrierMutation":
|
773
|
+
pk = input.get("id")
|
774
|
+
context = info.context.request
|
775
|
+
carrier = providers.Carrier.system_carriers.get(pk=pk)
|
776
|
+
|
777
|
+
if "enable" in input:
|
778
|
+
if input.get("enable"):
|
779
|
+
if hasattr(carrier, "active_orgs"):
|
780
|
+
carrier.active_orgs.add(info.context.request.org)
|
781
|
+
else:
|
782
|
+
carrier.active_users.add(info.context.request.user)
|
783
|
+
else:
|
784
|
+
if hasattr(carrier, "active_orgs"):
|
785
|
+
carrier.active_orgs.remove(info.context.request.org)
|
786
|
+
else:
|
787
|
+
carrier.active_users.remove(info.context.request.user)
|
788
|
+
|
789
|
+
if "config" in input:
|
790
|
+
config = providers.Carrier.resolve_config(carrier, is_user_config=True)
|
791
|
+
serializers.CarrierConfigModelSerializer.map(
|
792
|
+
instance=config,
|
793
|
+
context=context,
|
794
|
+
data={
|
795
|
+
"carrier": carrier.pk,
|
796
|
+
"config": process_dictionaries_mutations(
|
797
|
+
["config"], (input["config"] or {}), config
|
798
|
+
),
|
799
|
+
},
|
800
|
+
).save()
|
801
|
+
|
802
|
+
return SystemCarrierMutation(
|
803
|
+
carrier=providers.Carrier.system_carriers.get(pk=pk)
|
804
|
+
) # type: ignore
|
805
|
+
|
806
|
+
|
807
|
+
@strawberry.type
|
808
|
+
class UpdateServiceZoneMutation(utils.BaseMutation):
|
809
|
+
rate_sheet: typing.Optional[types.RateSheetType] = None
|
810
|
+
|
811
|
+
@staticmethod
|
812
|
+
@transaction.atomic
|
813
|
+
@utils.authentication_required
|
814
|
+
@utils.authorization_required(["manage_carriers"])
|
815
|
+
def mutate(
|
816
|
+
info: Info, **input: inputs.UpdateServiceZoneMutationInput
|
817
|
+
) -> "UpdateServiceZoneMutation":
|
818
|
+
rate_sheet = providers.RateSheet.access_by(info.context.request).get(
|
819
|
+
id=input["id"]
|
820
|
+
)
|
821
|
+
service = rate_sheet.services.get(id=input["service_id"])
|
822
|
+
|
823
|
+
serializer = serializers.ServiceLevelModelSerializer(
|
824
|
+
service,
|
825
|
+
context=info.context.request,
|
826
|
+
)
|
827
|
+
serializer.update_zone(input["zone_index"], input["zone"])
|
828
|
+
|
829
|
+
return UpdateServiceZoneMutation(rate_sheet=rate_sheet) # type:ignore
|
830
|
+
|
831
|
+
|
832
|
+
@strawberry.type
|
833
|
+
class CreateMetafieldMutation(utils.BaseMutation):
|
834
|
+
metafield: typing.Optional[types.MetafieldType] = None
|
835
|
+
|
836
|
+
@staticmethod
|
837
|
+
@utils.authentication_required
|
838
|
+
@utils.authorization_required()
|
839
|
+
def mutate(
|
840
|
+
info: Info, **input: inputs.CreateMetafieldInput
|
841
|
+
) -> "CreateMetafieldMutation":
|
842
|
+
data = input.copy()
|
843
|
+
|
844
|
+
metafield = serializers.MetafieldModelSerializer.map(
|
845
|
+
data=data,
|
846
|
+
context=info.context.request,
|
847
|
+
).save().instance
|
848
|
+
|
849
|
+
return CreateMetafieldMutation(metafield=metafield)
|
850
|
+
|
851
|
+
|
852
|
+
@strawberry.type
|
853
|
+
class UpdateMetafieldMutation(utils.BaseMutation):
|
854
|
+
metafield: typing.Optional[types.MetafieldType] = None
|
855
|
+
|
856
|
+
@staticmethod
|
857
|
+
@utils.authentication_required
|
858
|
+
@utils.authorization_required()
|
859
|
+
def mutate(
|
860
|
+
info: Info, **input: inputs.UpdateMetafieldInput
|
861
|
+
) -> "UpdateMetafieldMutation":
|
862
|
+
data = input.copy()
|
863
|
+
instance = core.Metafield.access_by(info.context.request).get(id=data.get("id"))
|
864
|
+
|
865
|
+
metafield = serializers.MetafieldModelSerializer.map(
|
866
|
+
instance,
|
867
|
+
data=data,
|
868
|
+
context=info.context.request,
|
869
|
+
).save().instance
|
870
|
+
|
871
|
+
return UpdateMetafieldMutation(metafield=metafield)
|