From 5f0794bd287ef9514d8645242017245fc6508641 Mon Sep 17 00:00:00 2001 From: Tushar Athare Date: Wed, 20 Nov 2024 13:25:11 +0530 Subject: [PATCH] Add non required fields filter and CustomDateTime --- graphene_django_extras/base_types.py | 61 +++++-- graphene_django_extras/converter.py | 243 ++++++++++++++++++--------- graphene_django_extras/mutation.py | 3 + graphene_django_extras/types.py | 5 +- 4 files changed, 216 insertions(+), 96 deletions(-) diff --git a/graphene_django_extras/base_types.py b/graphene_django_extras/base_types.py index 411360a9..a9ca9eab 100644 --- a/graphene_django_extras/base_types.py +++ b/graphene_django_extras/base_types.py @@ -3,12 +3,13 @@ import binascii import datetime +from ast import literal_eval import graphene -from graphene.types.datetime import Date, DateTime, Time +from graphene.types.datetime import Date, Time, DateTime from graphene.utils.str_converters import to_camel_case from graphql.language import ast - +import logging def factory_type(operation, _type, *args, **kwargs): if operation == "output": @@ -30,6 +31,7 @@ class Meta: description = "Auto generated Type for {} model".format( kwargs.get("model").__name__ ) + non_required_fields=kwargs.get("non_required_fields") return GenericType @@ -39,7 +41,8 @@ class GenericInputType(_type): class Meta: model = kwargs.get("model") name = kwargs.get("name") or to_camel_case( - "{}_{}_Generic_Type".format(kwargs.get("model").__name__, args[0]) + "{}_{}_Generic_Type".format( + kwargs.get("model").__name__, args[0]) ) only_fields = kwargs.get("only_fields") exclude_fields = kwargs.get("exclude_fields") @@ -50,6 +53,7 @@ class Meta: description = "Auto generated InputType for {} model".format( kwargs.get("model").__name__ ) + non_required_fields=kwargs.get("non_required_fields") return GenericInputType @@ -154,9 +158,9 @@ def serialize(time): if isinstance(time, datetime.datetime): time = time.time() - assert isinstance(time, datetime.time), 'Received not compatible time "{}"'.format( - repr(time) - ) + assert isinstance( + time, datetime.time + ), 'Received not compatible time "{}"'.format(repr(time)) return time.isoformat() @@ -168,19 +172,44 @@ def serialize(date): if isinstance(date, datetime.datetime): date = date.date() - assert isinstance(date, datetime.date), 'Received not compatible date "{}"'.format( - repr(date) - ) + assert isinstance( + date, datetime.date + ), 'Received not compatible date "{}"'.format(repr(date)) return date.isoformat() -class CustomDateTime(DateTime): +class CustomDateTime(graphene.Scalar): + """ + This method is wrote to accept epoch and datetime formats + """ @staticmethod def serialize(dt): - if isinstance(dt, CustomDateFormat): - return dt.date_str + return int(dt.timestamp() * 1000.) - assert isinstance( - dt, (datetime.datetime, datetime.date) - ), 'Received not compatible datetime "{}"'.format(repr(dt)) - return dt.isoformat() + @staticmethod + def parse_value(value): + if isinstance(value, str): + value = int(value) + return datetime.datetime.fromtimestamp(value / 1000.0).strftime('%Y-%m-%d %H:%M:%S.%f') + + +class CustomDict(graphene.Scalar): + + @staticmethod + def serialize(dt): + return dt + + @staticmethod + def parse_literal(node): + if node.fields: + eval_node = literal_eval(node.value) + if isinstance(eval_node, dict): + return eval_node + else: + raise ValueError("For instance '{'key':'value'}'") + logging.info("INFO ::: JSON/HStore value passed empty") + return None + + @staticmethod + def parse_value(value): + return value \ No newline at end of file diff --git a/graphene_django_extras/converter.py b/graphene_django_extras/converter.py index 86cc5199..74594209 100644 --- a/graphene_django_extras/converter.py +++ b/graphene_django_extras/converter.py @@ -1,42 +1,62 @@ # -*- coding: utf-8 -*- import re from collections import OrderedDict -from functools import singledispatch from django.conf import settings -from django.contrib.contenttypes.fields import GenericForeignKey, GenericRel, GenericRelation +from django.contrib.contenttypes.fields import ( + GenericForeignKey, + GenericRelation, + GenericRel, +) from django.db import models -from django.utils.encoding import force_str -from graphene import ID, UUID, Boolean, Dynamic, Enum, Field, Float, Int, List, NonNull, String -from graphene.types.json import JSONString +from django.utils.encoding import force_text +from graphene import ( + Field, + ID, + Boolean, + Dynamic, + Enum, + Float, + Int, + List, + NonNull, + String, + UUID, +) from graphene.utils.str_converters import to_camel_case -from graphene_django.compat import ArrayField, HStoreField, JSONField, RangeField +from graphene_django.compat import ArrayField,RangeField +from functools import singledispatch from graphene_django.utils.str_converters import to_const from .base_types import ( - Binary, - CustomDate, + GenericForeignKeyType, + GenericForeignKeyInputType, CustomDateTime, CustomTime, - GenericForeignKeyInputType, - GenericForeignKeyType, + CustomDate, + Binary, + CustomDict + ) from .fields import DjangoFilterListField, DjangoListField -from .utils import get_model_fields, get_related_model, is_required +from .utils import is_required, get_model_fields, get_related_model +from netfields import InetAddressField +from django.contrib.postgres.fields import HStoreField +from graphene_file_upload.scalars import Upload NAME_PATTERN = r"^[_a-zA-Z][_a-zA-Z0-9]*$" COMPILED_NAME_PATTERN = re.compile(NAME_PATTERN) def assert_valid_name(name): - """Helper to assert that provided names are valid.""" - assert COMPILED_NAME_PATTERN.match(name), 'Names must match /{}/ but "{}" does not.'.format( - NAME_PATTERN, name - ) + """ Helper to assert that provided names are valid. """ + assert COMPILED_NAME_PATTERN.match( + name + ), 'Names must match /{}/ but "{}" does not.'.format(NAME_PATTERN, name) def convert_choice_name(name): - name = to_const(force_str(name)) + name = to_const(force_text(name)) try: assert_valid_name(name) except AssertionError: @@ -59,8 +79,13 @@ def get_choices(choices): yield name, value, description -def convert_django_field_with_choices(field, registry=None, input_flag=None, nested_field=False): +def convert_django_field_with_choices( + field, registry=None, input_flag=None, nested_field=False, non_required_fields=None,field_name=None +): choices = getattr(field, "choices", None) + non_required_fields_list = [] + if non_required_fields: + non_required_fields_list = [field.name for field in non_required_fields] if choices: meta = field.model._meta @@ -80,20 +105,22 @@ class EnumWithDescriptionsType(object): def description(self): return named_choices_descriptions[self.name] - enum = Enum(name, list(named_choices), type=EnumWithDescriptionsType) + enum = Enum(name, list(named_choices), + type=EnumWithDescriptionsType) registry.register_enum(name, enum) - if type(field).__name__ == "MultiSelectField": return DjangoListField( enum, description=field.help_text or field.verbose_name, - required=is_required(field) and input_flag == "create", + required=is_required( + field) if not field_name in non_required_fields_list else False and input_flag == "create", ) return enum( description=field.help_text or field.verbose_name, - required=is_required(field) and input_flag == "create", + required=is_required( + field) if not field_name in non_required_fields_list else False and input_flag == "create", ) - return convert_django_field(field, registry, input_flag, nested_field) + return convert_django_field(field, registry, input_flag, nested_field, non_required_fields_list,field_name) def construct_fields( @@ -104,19 +131,22 @@ def construct_fields( exclude_fields, input_flag=None, nested_fields=(), + non_required_fields=None, ): _model_fields = get_model_fields(model) - if settings.DEBUG: if input_flag == "create": - _model_fields = sorted(_model_fields, key=lambda f: (not is_required(f[1]), f[0])) + _model_fields = sorted( + _model_fields, key=lambda f: (not is_required(f[1]), f[0]) + ) elif not input_flag: _model_fields = sorted(_model_fields, key=lambda f: f[0]) fields = OrderedDict() - if input_flag == "delete": - converted = convert_django_field_with_choices(dict(_model_fields)["id"], registry) + converted = convert_django_field_with_choices( + dict(_model_fields)["id"], registry + ) fields["id"] = converted else: for name, field in _model_fields: @@ -126,7 +156,9 @@ def construct_fields( nested_field = name in nested_fields is_not_in_only = only_fields and name not in only_fields # is_already_created = name in options.fields - is_excluded = exclude_fields and name in exclude_fields # or is_already_created + is_excluded = ( + exclude_fields and name in exclude_fields + ) # or is_already_created # https://docs.djangoproject.com/en/1.10/ref/models/fields/#django.db.models.ForeignKey.related_query_name is_no_backref = str(name).endswith("+") # if is_not_in_only or is_excluded or is_no_backref: @@ -139,20 +171,25 @@ def construct_fields( input_flag and not field.editable and not isinstance( - field, (models.fields.related.ForeignObjectRel, GenericForeignKey) + field, (models.fields.related.ForeignObjectRel, + GenericForeignKey) ) ): continue - - converted = convert_django_field_with_choices(field, registry, input_flag, nested_field) + converted = convert_django_field_with_choices( + field, registry, input_flag, nested_field, non_required_fields,name + ) fields[name] = converted + return fields @singledispatch -def convert_django_field(field, registry=None, input_flag=None, nested_field=False): +def convert_django_field(field, registry=None, input_flag=None, nested_field=False,non_required_fields_list=[],field_name=None): raise Exception( - "Don't know how to convert the Django field {} ({})".format(field, field.__class__) + "Don't know how to convert the Django field {} ({})".format( + field, field.__class__ + ) ) @@ -162,16 +199,17 @@ def convert_django_field(field, registry=None, input_flag=None, nested_field=Fal @convert_django_field.register(models.SlugField) @convert_django_field.register(models.URLField) @convert_django_field.register(models.GenericIPAddressField) -@convert_django_field.register(models.FileField) -def convert_field_to_string(field, registry=None, input_flag=None, nested_field=False): +@convert_django_field.register(InetAddressField) +def convert_field_to_string(field, registry=None, input_flag=None, nested_field=False, non_required_fields_list=[],field_name=None): return String( description=field.help_text or field.verbose_name, - required=is_required(field) and input_flag == "create", + required=is_required( + field) if not field_name in non_required_fields_list else False and input_flag == "create", ) @convert_django_field.register(models.AutoField) -def convert_field_to_id(field, registry=None, input_flag=None, nested_field=False): +def convert_field_to_id(field, registry=None, input_flag=None, nested_field=False,non_required_fields_list=[],field_name=None): if input_flag: return ID( description=field.help_text or "Django object unique identification field", @@ -184,10 +222,11 @@ def convert_field_to_id(field, registry=None, input_flag=None, nested_field=Fals @convert_django_field.register(models.UUIDField) -def convert_field_to_uuid(field, registry=None, input_flag=None, nested_field=False): +def convert_field_to_uuid(field, registry=None, input_flag=None, nested_field=False, non_required_fields_list=[],field_name=None): return UUID( description=field.help_text or field.verbose_name, - required=is_required(field) and input_flag == "create", + required=is_required( + field) if not field_name in non_required_fields_list else False and input_flag == "create", ) @@ -196,74 +235,86 @@ def convert_field_to_uuid(field, registry=None, input_flag=None, nested_field=Fa @convert_django_field.register(models.SmallIntegerField) @convert_django_field.register(models.BigIntegerField) @convert_django_field.register(models.IntegerField) -def convert_field_to_int(field, registry=None, input_flag=None, nested_field=False): +def convert_field_to_int(field, registry=None, input_flag=None, nested_field=False, non_required_fields_list=[],field_name=None): return Int( description=field.help_text or field.verbose_name, - required=is_required(field) and input_flag == "create", + required=is_required( + field) if not field_name in non_required_fields_list else False and input_flag == "create", ) @convert_django_field.register(models.BooleanField) -def convert_field_to_boolean(field, registry=None, input_flag=None, nested_field=False): - required = is_required(field) and input_flag == "create" +def convert_field_to_boolean(field, registry=None, input_flag=None, nested_field=False, non_required_fields_list=[],field_name=None): + required = is_required( + field) if not field_name in non_required_fields_list else False and input_flag == "create" if required: return NonNull(Boolean, description=field.help_text or field.verbose_name) return Boolean(description=field.help_text) @convert_django_field.register(models.NullBooleanField) -def convert_field_to_nullboolean(field, registry=None, input_flag=None, nested_field=False): +def convert_field_to_nullboolean( + field, registry=None, input_flag=None, nested_field=False, non_required_fields_list=[],field_name=None +): return Boolean( description=field.help_text or field.verbose_name, - required=is_required(field) and input_flag == "create", + required=is_required( + field) if not field_name in non_required_fields_list else False and input_flag == "create", ) @convert_django_field.register(models.BinaryField) -def convert_binary_to_string(field, registry=None, input_flag=None, nested_field=False): +def convert_binary_to_string(field, registry=None, input_flag=None, nested_field=False, non_required_fields_list=[],field_name=None): return Binary( description=field.help_text or field.verbose_name, - required=is_required(field) and input_flag == "create", + required=is_required( + field) if not field_name in non_required_fields_list else False and input_flag == "create", ) @convert_django_field.register(models.DecimalField) @convert_django_field.register(models.FloatField) @convert_django_field.register(models.DurationField) -def convert_field_to_float(field, registry=None, input_flag=None, nested_field=False): +def convert_field_to_float(field, registry=None, input_flag=None, nested_field=False, non_required_fields_list=[],field_name=None): return Float( description=field.help_text or field.verbose_name, - required=is_required(field) and input_flag == "create", + required=is_required( + field) if not field_name in non_required_fields_list else False and input_flag == "create", ) @convert_django_field.register(models.DateField) -def convert_date_to_string(field, registry=None, input_flag=None, nested_field=False): +def convert_date_to_string(field, registry=None, input_flag=None, nested_field=False, non_required_fields_list=[],field_name=None): return CustomDate( description=field.help_text or field.verbose_name, - required=is_required(field) and input_flag == "create", + required=is_required( + field) if not field_name in non_required_fields_list else False and input_flag == "create", ) @convert_django_field.register(models.DateTimeField) -def convert_datetime_to_string(field, registry=None, input_flag=None, nested_field=False): +def convert_datetime_to_string( + field, registry=None, input_flag=None, nested_field=False, non_required_fields_list=[],field_name=None +): return CustomDateTime( description=field.help_text or field.verbose_name, - required=is_required(field) and input_flag == "create", + required=is_required( + field) if not field_name in non_required_fields_list else False and input_flag == "create", ) @convert_django_field.register(models.TimeField) -def convert_time_to_string(field, registry=None, input_flag=None, nested_field=False): +def convert_time_to_string(field, registry=None, input_flag=None, nested_field=False, non_required_fields_list=[],field_name=None): return CustomTime( description=field.help_text or field.verbose_name, - required=is_required(field) and input_flag == "create", + required=is_required( + field) if not field_name in non_required_fields_list else False and input_flag == "create", ) @convert_django_field.register(models.OneToOneRel) def convert_onetoone_field_to_djangomodel( - field, registry=None, input_flag=None, nested_field=False + field, registry=None, input_flag=None, nested_field=False, non_required_fields_list=[],field_name=None ): model = field.related_model @@ -273,20 +324,23 @@ def dynamic_type(): _type = registry.get_type_for_model(model, for_input=input_flag) if not _type: return - return Field(_type, required=is_required(field) and input_flag == "create") + return Field(_type, required=is_required(field) if not field_name in non_required_fields_list else False and input_flag == "create") return Dynamic(dynamic_type) @convert_django_field.register(models.ManyToManyField) -def convert_field_to_list_or_connection(field, registry=None, input_flag=None, nested_field=False): +def convert_field_to_list_or_connection( + field, registry=None, input_flag=None, nested_field=False, non_required_fields_list=[],field_name=None +): model = get_related_model(field) def dynamic_type(): if input_flag and not nested_field: return DjangoListField( ID, - required=is_required(field) and input_flag == "create", + required=is_required( + field) if not field_name in non_required_fields_list else False and input_flag == "create", description=field.help_text or field.verbose_name, ) else: @@ -298,14 +352,16 @@ def dynamic_type(): elif _type._meta.filter_fields or _type._meta.filterset_class: return DjangoFilterListField( _type, - required=is_required(field) and input_flag == "create", + required=is_required( + field) if not field_name in non_required_fields_list else False and input_flag == "create", description=field.help_text or field.verbose_name, filterset_class=_type._meta.filterset_class, ) else: return DjangoListField( _type, - required=is_required(field) and input_flag == "create", + required=is_required( + field) if not field_name in non_required_fields_list else False and input_flag == "create", description=field.help_text or field.verbose_name, ) @@ -315,7 +371,9 @@ def dynamic_type(): @convert_django_field.register(GenericRel) @convert_django_field.register(models.ManyToManyRel) @convert_django_field.register(models.ManyToOneRel) -def convert_many_rel_to_djangomodel(field, registry=None, input_flag=None, nested_field=False): +def convert_many_rel_to_djangomodel( + field, registry=None, input_flag=None, nested_field=False, non_required_fields_list=[],field_name=None +): model = field.related_model def dynamic_type(): @@ -330,12 +388,14 @@ def dynamic_type(): elif _type._meta.filter_fields or _type._meta.filterset_class: return DjangoFilterListField( _type, - required=is_required(field) and input_flag == "create", + required=is_required( + field) if not field_name in non_required_fields_list else False and input_flag == "create", filterset_class=_type._meta.filterset_class, ) else: return DjangoListField( - _type, required=is_required(field) and input_flag == "create" + _type, required=is_required( + field) if not field_name in non_required_fields_list else False and input_flag == "create" ) return Dynamic(dynamic_type) @@ -343,17 +403,21 @@ def dynamic_type(): @convert_django_field.register(models.OneToOneField) @convert_django_field.register(models.ForeignKey) -def convert_field_to_djangomodel(field, registry=None, input_flag=None, nested_field=False): +def convert_field_to_djangomodel( + field, registry=None, input_flag=None, nested_field=False, non_required_fields_list=[],field_name=None +): model = get_related_model(field) - def dynamic_type(): # Avoid create field for auto generate OneToOneField product of an inheritance - if isinstance(field, models.OneToOneField) and issubclass(field.model, field.related_model): + if isinstance(field, models.OneToOneField) and issubclass( + field.model, field.related_model + ): return if input_flag and not nested_field: return ID( description=field.help_text or field.verbose_name, - required=is_required(field) and input_flag == "create", + required=is_required( + field) if not field_name in non_required_fields_list else False and input_flag == "create", ) _type = registry.get_type_for_model(model, for_input=input_flag) @@ -363,7 +427,8 @@ def dynamic_type(): return Field( _type, description=field.help_text or field.verbose_name, - required=is_required(field) and input_flag == "create", + required=is_required( + field) if not field_name in non_required_fields_list else False and input_flag == "create", ) return Dynamic(dynamic_type) @@ -371,7 +436,7 @@ def dynamic_type(): @convert_django_field.register(GenericForeignKey) def convert_generic_foreign_key_to_object( - field, registry=None, input_flag=None, nested_field=False + field, registry=None, input_flag=None, nested_field=False, non_required_fields_list=[],field_name=None ): def dynamic_type(): key = "{}_{}".format(field.name, field.model.__name__.lower()) @@ -392,7 +457,11 @@ def dynamic_type(): break if ct_field is not None and fk_field is not None: - required = (is_required(ct_field) and is_required(fk_field)) or required + if field_name in non_required_fields_list: + required = False + else: + required = (is_required(ct_field) + and is_required(fk_field)) or required if input_flag: return GenericForeignKeyInputType( @@ -416,7 +485,7 @@ def dynamic_type(): @convert_django_field.register(GenericRelation) def convert_generic_relation_to_object_list( - field, registry=None, input_flag=None, nested_field=False + field, registry=None, input_flag=None, nested_field=False, non_required_fields_list=[],field_name = None ): model = field.related_model @@ -433,33 +502,49 @@ def dynamic_type(): @convert_django_field.register(ArrayField) -def convert_postgres_array_to_list(field, registry=None, input_flag=None, nested_field=False): +def convert_postgres_array_to_list( + field, registry=None, input_flag=None, nested_field=False, non_required_fields_list=[],field_name=None +): base_type = convert_django_field(field.base_field) if not isinstance(base_type, (List, NonNull)): base_type = type(base_type) return List( base_type, description=field.help_text or field.verbose_name, - required=is_required(field) and input_flag == "create", + required=is_required( + field) if not field_name in non_required_fields_list else False and input_flag == "create", ) @convert_django_field.register(HStoreField) -@convert_django_field.register(JSONField) -def convert_postgres_field_to_string(field, registry=None, input_flag=None, nested_field=False): - return JSONString( +@convert_django_field.register(models.JSONField) +def convert_postgres_field_to_string( + field, registry=None, input_flag=None, nested_field=False, non_required_fields_list=[],field_name=None +): + return CustomDict( description=field.help_text or field.verbose_name, - required=is_required(field) and input_flag == "create", + required=is_required( + field) if not field_name in non_required_fields_list else False and input_flag == "create", ) @convert_django_field.register(RangeField) -def convert_postgres_range_to_string(field, registry=None, input_flag=None, nested_field=False): +def convert_postgres_range_to_string( + field, registry=None, input_flag=None, nested_field=False, non_required_fields_list=[],field_name=None +): inner_type = convert_django_field(field.base_field) if not isinstance(inner_type, (List, NonNull)): inner_type = type(inner_type) return List( inner_type, description=field.help_text or field.verbose_name, - required=is_required(field) and input_flag == "create", + required=is_required( + field) if not field_name in non_required_fields_list else False and input_flag == "create", ) + +@convert_django_field.register(models.FileField) +def convert_field_to_string(field, registry=None, input_flag=None, nested_field=False): + return Upload( + description=field.help_text or field.verbose_name, + required=is_required(field) and input_flag == "create", + ) \ No newline at end of file diff --git a/graphene_django_extras/mutation.py b/graphene_django_extras/mutation.py index f83cac93..cfc06453 100644 --- a/graphene_django_extras/mutation.py +++ b/graphene_django_extras/mutation.py @@ -49,6 +49,8 @@ def __init_subclass_with_meta__( nested_fields=(), **options, ): + non_required_fields = options["non_required_fields"] + del options["non_required_fields"] if not serializer_class: raise Exception("serializer_class is required on all DjangoSerializerMutation") @@ -85,6 +87,7 @@ def __init_subclass_with_meta__( "nested_fields": nested_fields, "registry": registry, "skip_registry": False, + "non_required_fields": non_required_fields } output_type = registry.get_type_for_model(model) diff --git a/graphene_django_extras/types.py b/graphene_django_extras/types.py index c0504a0b..46b2d37c 100644 --- a/graphene_django_extras/types.py +++ b/graphene_django_extras/types.py @@ -74,6 +74,7 @@ def __init_subclass_with_meta__( filter_fields=None, interfaces=(), filterset_class=None, + non_required_fields = None, **options, ): assert is_valid_django_model(model), ( @@ -93,7 +94,7 @@ def __init_subclass_with_meta__( ) django_fields = yank_fields_from_attrs( - construct_fields(model, registry, only_fields, include_fields, exclude_fields), + construct_fields(model, registry, only_fields, include_fields, exclude_fields,non_required_fields), _as=Field, ) @@ -152,6 +153,7 @@ def __init_subclass_with_meta__( filter_fields=None, input_for="create", nested_fields=(), + non_required_fields=None, **options, ): assert is_valid_django_model(model), ( @@ -183,6 +185,7 @@ def __init_subclass_with_meta__( exclude_fields, input_for, nested_fields, + non_required_fields ), _as=InputField, sort=False,