def create_user_state_config(class_config): """ Creates the UserStateType based on specific class_config :param class_config: A dict containing class configurations. The default is: dict( settings=dict( model_class=Settings, graphene_class=SettingsType, graphene_fields=settings_fields, query=SettingsQuery, mutation=SettingsMutation ), region=dict( model_class=Region, graphene_class=RegionType, graphene_fields=region_fields, query=RegionQuery, mutation=RegionMutation ), project=dict( model_class=Project, graphene_class=ProjectType, graphene_fields=project_fields, query=ProjectQuery, mutation=ProjectMutation ), resource=dict( model_class=Resource, graphene_class=ResourceType, graphene_fields=resource_fields, query=ResourceQuery, mutation=ResourceMutation ), location=get_location_schema(), user_search=get_user_search_data_schema(), search_location=get_search_location_schema() # additional_user_scope_schemas and additional_user_scopes # are passed in from a calling app # these are a dict of properties that need to go on user_regions and user_projects # at the same level as userSearch. For instance, a user's saved app selections could go here # additional_user_scope_schemas = dict( # userDesignFeatureLayers=dict( # graphene_class=UserDesignFeatureDataType, # graphene_fields=user_design_feature_data_fields # ) # additional_user_scopes explains the path to Django models within additional_user_scope_schemas # additional_django_model_user_scopes = dict( # userDesignFeatureLayers=dict( # designFeature=True # ) # Would match the list of some django DesignFeature model instances ) :return: """ class UserStateType(DjangoObjectType, DjangoObjectTypeRevisionedMixin): """ UserStateType models UserState, which represents the settings both imposed upon and chosen by the user """ id = graphene.Int(source='pk') class Meta: model = UserState # Modify data field to use the resolver. # I guess there's no way to specify a resolver upon field creation, since graphene just reads the underlying # Django model to generate the fields UserStateType._meta.fields['data'] = Field( UserStateDataType(class_config), resolver=resolver_for_data_field ) user_state_fields = merge_with_django_properties(UserStateType, dict( id=dict(create=DENY, update=REQUIRE), # This is a Foreign Key. Graphene generates these relationships for us, but we need it here to # support our Mutation subclasses and query_argument generation # For simplicity we limit fields to id. Mutations can only use id, and a query doesn't need other # details of the User--it can query separately for that user=dict(graphene_type=UserType, fields=user_fields), # This refers to the UserState, which is a representation of all the json fields of UserState.data data=dict(graphene_type=UserStateDataType(class_config), fields=user_state_data_fields(class_config), default=lambda: dict()), **reversion_and_safe_delete_types )) user_state_mutation_config = dict( class_name='UserState', crud={ CREATE: 'createUserState', UPDATE: 'updateUserState' }, resolve=guess_update_or_create ) additional_django_model_user_scopes = R.prop('additional_django_model_user_scopes', class_config) \ if R.prop_or(None, 'additional_django_model_user_scopes', class_config) else {} additional_user_scope_schemas = R.prop('additional_user_scope_schemas', class_config) \ if R.prop_or(None, 'additional_user_scope_schemas', class_config) else {} # The scope instance types expected in user_state.data user_state_scope_instances_config = R.concat([ # dict(region=True) means search all userRegions for that dict dict(pick=dict(userRegions=dict(region=True)), key='region', model=get_region_model() ), # dict(project=True) means search all userProjects for that dict dict( pick=dict(userProjects=dict(project=True)), key='project', model=get_project_model(), # This is currently just needed for the field key's unique_with function field_config=project_fields, # Projects can be modified when userState is mutated can_mutate_related=True ), dict( pick=dict( userRegions=[ dict( userSearch=dict( # dict(searchLocation=True) means search all userSearchLocations for that dict userSearchLocations=dict( searchLocation=True, ) ) ) ], userProjects=[ dict( userSearch=dict( # dict(searchLocation=True) means search all userSearchLocations for that dict userSearchLocations=dict(searchLocation=True) ) ) ] ), key='searchLocation', model=get_search_location_schema()['model_class'], # These can be modified when userState is mutated can_mutate_related=True ), ], # Map each additional_django_model_user_scopes to a scope config R.map_with_obj_to_values( lambda field_name, additional_django_model_user_scope: dict( pick=dict( userRegions=[ {field_name: additional_django_model_user_scope} ], userProjects=[ {field_name: additional_django_model_user_scope} ] ), # Assume the scope object is the deepest field key=list(R.keys(R.flatten_dct(additional_django_model_user_scope, '.')))[0].split('.')[-1], # model isn't needed unless can_mutate_related is true model=additional_user_scope_schemas[field_name]['model'], # These can be modified when userState is mutated can_mutate_related=R.prop_or(False, 'can_mutate_related', additional_django_model_user_scope) ), additional_django_model_user_scopes ) ) class UpsertUserState(Mutation): """ Abstract base class for mutation """ user_state = Field(UserStateType) def mutate(self, info, user_state_data=None): """ Update or create the user state :param info: :param user_state_data: :return: """ # Check that all the scope instances in user_state.data exist. We permit deleted instances for now. new_data = R.prop_or({}, 'data', user_state_data) # Copy since Graphene reuses this data copied_new_data = copy.deepcopy(new_data) old_user_state_data = UserState.objects.get( id=user_state_data['id'] ).data if R.prop_or(None, 'id', user_state_data) else None # Inspect the data and find all scope instances within UserState.data # This includes userRegions[*].region, userProject[*].project and within userRegions and userProjects # userSearch.userSearchLocations[*].search_location and whatever the implementing libraries define # in addition updated_new_data = validate_and_mutate_scope_instances( user_state_scope_instances_config, copied_new_data ) # If either userProjects or userRegions are null, it means those scope instances aren't part # of the update, so merge in the old values if R.prop_or(None, 'id', user_state_data) and R.any_satisfy( lambda user_scope_key: not R.prop_or(None, user_scope_key, updated_new_data), ['userProjects', 'userRegions'] ): # The special update case where one userScope collection is null, # indicates that we are only updating one userScope object. The rest # should remain the same and not be removed for user_scope_key in ['userProjects', 'userRegions']: # Database values old_user_scopes_by_id = user_scope_instances_by_id( user_scope_key, old_user_state_data ) # New values with updates applied new_user_scopes_by_id = user_scope_instances_by_id( user_scope_key, updated_new_data ) # Prefer the old over the new, merging all objects but overriding lists # We override lists because a non-null list always replaces the old list in the database updated_new_data[user_scope_key] = R.values(R.merge_deep( old_user_scopes_by_id, new_user_scopes_by_id, MyMerger( # pass in a list of tuples,with the # strategies you are looking to apply # to each type. [ (list, ["override_non_null"]), (dict, ["merge"]) ], # next, choose the fallback strategies, # applied to all other types: ["override"], # finally, choose the strategies in # the case where the types conflict: ["override"] ) )) # Update user_state_data the updated data modified_user_state_data = R.merge(user_state_data, dict(data=updated_new_data)) # id or user.id can be used to identify the existing instance id_props = R.compact_dict( dict( id=R.prop_or(None, 'id', modified_user_state_data), user_id=R.item_str_path_or(None, 'user.id', modified_user_state_data) ) ) def fetch_and_merge(modified_user_state_data, props): existing = UserState.objects.filter(**props) # If the user doesn't have a user state yet if not R.length(existing): return modified_user_state_data return merge_data_fields_on_update( ['data'], R.head(existing), # Merge existing's id in case it wasn't in user_state_data R.merge(modified_user_state_data, R.pick(['id'], existing)) ) modified_data = R.if_else( R.compose(R.length, R.keys), lambda props: fetch_and_merge(modified_user_state_data, props), lambda _: modified_user_state_data )(id_props) update_or_create_values = input_type_parameters_for_update_or_create( user_state_fields, # Make sure that all props are unique that must be, either by modifying values or erring. enforce_unique_props( user_state_fields, modified_data) ) user_state, created = update_or_create_with_revision(UserState, update_or_create_values) return UpsertUserState(user_state=user_state) class CreateUserState(UpsertUserState): """ Create UserState mutation class """ class Arguments: user_state_data = type('CreateUserStateInputType', (InputObjectType,), input_type_fields(user_state_fields, CREATE, UserStateType) )(required=True) class UpdateUserState(UpsertUserState): """ Update UserState mutation class """ class Arguments: user_state_data = type('UpdateUserStateInputType', (InputObjectType,), input_type_fields(user_state_fields, UPDATE, UserStateType))(required=True) graphql_update_or_create_user_state = graphql_update_or_create(user_state_mutation_config, user_state_fields) graphql_query_user_states = graphql_query(UserStateType, user_state_fields, 'userStates') return dict( model_class=UserState, graphene_class=UserStateType, graphene_fields=user_state_fields, create_mutation_class=CreateUserState, update_mutation_class=UpdateUserState, graphql_mutation=graphql_update_or_create_user_state, graphql_query=graphql_query_user_states )
def create_group_state_config(class_config): """ Creates the GroupStateType based on specific class_config :param class_config: A dict containing class configurations. Right now it's only region in the form dict( region=dict( model_class=..., graphene_class=..., fields=... ) ) :return: """ class GroupStateType(DjangoObjectType, DjangoObjectTypeRevisionedMixin): """ GroupStateType models GroupState, which represents the settings both imposed upon and chosen by the group """ id = graphene.Int(source='pk') class Meta: model = GroupState # Modify data field to use the resolver. # I guess there's no way to specify a resolver upon field creation, since graphene just reads the underlying # Django model to generate the fields GroupStateType._meta.fields['data'] = Field( GroupStateDataType(class_config), resolver=resolver_for_dict_field ) group_state_fields = merge_with_django_properties(GroupStateType, dict( id=dict(create=DENY, update=REQUIRE), data=dict(graphene_type=GroupStateDataType(class_config), fields=group_state_data_fields(class_config), default=lambda: dict()), **reversion_and_safe_delete_types )) group_state_mutation_config = dict( class_name='GroupState', crud={ CREATE: 'createGroupState', UPDATE: 'updateGroupState' }, resolve=guess_update_or_create ) class UpsertGroupState(Mutation): """ Abstract base class for mutation """ group_state = Field(GroupStateType) def mutate(self, info, group_state_data=None): """ Update or create the group state :param info: :param group_state_data: :return: """ update_or_create_values = input_type_parameters_for_update_or_create(group_state_fields, group_state_data) # We can do update_or_create since we have a unique group_id in addition to the unique id group_state, created = update_or_create_with_revision(GroupState, update_or_create_values) return UpsertGroupState(group_state=group_state) class CreateGroupState(UpsertGroupState): """ Create GroupState mutation class """ class Arguments: group_state_data = type('CreateGroupStateInputType', (InputObjectType,), input_type_fields(group_state_fields, CREATE, GroupStateType))(required=True) class UpdateGroupState(UpsertGroupState): """ Update GroupState mutation class """ class Arguments: group_state_data = type('UpdateGroupStateInputType', (InputObjectType,), input_type_fields(group_state_fields, UPDATE, GroupStateType))(required=True) graphql_update_or_create_group_state = graphql_update_or_create(group_state_mutation_config, group_state_fields) graphql_query_group_states = graphql_query(GroupStateType, group_state_fields, 'groupStates') return dict( model_class=GroupState, graphene_class=GroupStateType, graphene_fields=group_state_fields, create_mutation_class=CreateGroupState, update_mutation_class=UpdateGroupState, graphql_mutation=graphql_update_or_create_group_state, graphql_query=graphql_query_group_states )
""" Create Settings mutation class """ class Arguments: settings_data = type( 'CreateSettingsInputType', (InputObjectType, ), input_type_fields(settings_fields, CREATE, SettingsType))(required=True) class UpdateSettings(UpsertSettings): """ Update Settings mutation class """ class Arguments: settings_data = type( 'UpdateSettingsInputType', (InputObjectType, ), input_type_fields(settings_fields, UPDATE, SettingsType))(required=True) class SettingsMutation(graphene.ObjectType): create_settings = CreateSettings.Field() update_settings = UpdateSettings.Field() graphql_update_or_create_settings = graphql_update_or_create( settings_mutation_config, settings_fields) graphql_query_settings = graphql_query(SettingsType, settings_fields, 'settings')
search_location_data = type( 'UpdateSearchLocationInputType', (InputObjectType, ), input_type_fields(search_location_fields, UPDATE, SearchLocationType))(required=True) class SearchLocationMutation(graphene.ObjectType): create_search_location = CreateSearchLocation.Field() update_search_location = UpdateSearchLocation.Field() graphql_update_or_create_search_location = graphql_update_or_create( search_location_mutation_config, search_location_fields) # Just use location fields here, the search fields will be added automatically graphql_query_search_locations = graphql_query(SearchLocationType, search_location_fields, 'searchLocations') def graphql_query_locations_limited(search_location_fields): return graphql_query(SearchLocationType, search_location_fields, 'searchLocations') # This must be referenced in settings.py search_location_schema_config = dict(model_class=SearchLocation, graphene_class=SearchLocationType, graphene_fields=search_location_fields, query=SearchLocationQuery, mutation=SearchLocationMutation)
def graphql_query_locations_limited(search_location_fields): return graphql_query(SearchLocationType, search_location_fields, 'searchLocations')
class UpdateRegion(UpsertRegion): """ Update Region mutation class """ class Arguments: region_data = type( 'UpdateRegionInputType', (InputObjectType, ), input_type_fields(region_fields, UPDATE, RegionType))(required=True) class RegionMutation(graphene.ObjectType): create_region = CreateRegion.Field() update_region = UpdateRegion.Field() graphql_update_or_create_region = graphql_update_or_create( region_mutation_config, region_fields) graphql_query_regions = graphql_query(RegionType, region_fields, 'regions') def graphql_query_regions_limited(region_fields): return graphql_query(RegionType, region_fields, 'regions') graphql_query_regions_paginated = graphql_query(RegionPaginatedType, region_paginated_fields, 'regionsPaginated')
def graphql_query_regions_limited(region_fields): return graphql_query(RegionType, region_fields, 'regions')
class UpdateSearchJurisdiction(UpsertSearchJurisdiction): """ Update SearchJurisdiction mutation class """ class Arguments: search_jurisdiction_data = type( 'UpdateSearchJurisdictionInputType', (InputObjectType, ), input_type_fields(search_jurisdiction_fields, UPDATE, SearchJurisdictionType, with_filter_fields=False))(required=True) class SearchJurisdictionMutation(graphene.ObjectType): create_search_jurisdiction = CreateSearchJurisdiction.Field() update_search_jurisdiction = UpdateSearchJurisdiction.Field() graphql_update_or_create_jurisdiction = graphql_update_or_create( search_jurisdiction_mutation_config, search_jurisdiction_fields) # Just use jurisdiction_fields, the search fields will be added automatically graphql_query_jurisdictions = graphql_query(SearchJurisdictionType, search_jurisdiction_fields, 'searchJurisdictions') def graphql_query_jurisdictions_limited(search_jurisdiction_fields): return graphql_query(SearchJurisdictionType, search_jurisdiction_fields, 'searchJurisdictions')
def graphql_query_jurisdictions_limited(search_jurisdiction_fields): return graphql_query(SearchJurisdictionType, search_jurisdiction_fields, 'searchJurisdictions')
""" Update Location mutation class """ class Arguments: location_data = type('UpdateLocationInputType', (InputObjectType,), input_type_fields(location_fields, UPDATE, LocationType))(required=True) class LocationMutation(graphene.ObjectType): create_location = CreateLocation.Field() update_location = UpdateLocation.Field() graphql_update_or_create_location = graphql_update_or_create(location_mutation_config, location_fields) graphql_query_locations = graphql_query(LocationType, location_fields, 'locations') # This must be referenced in settings.py location_schema_config = dict( model_class=Location, graphene_class=LocationType, graphene_fields=location_fields, query=LocationQuery, mutation=LocationMutation ) graphql_query_locations_paginated = graphql_query( LocationPaginatedType, location_paginated_fields, 'locationsPaginated' )
def graphql_query_jurisdictions_limited(jurisdiction_fields): return graphql_query(JurisdictionType, jurisdiction_fields, 'jurisdictions')
class Arguments: jurisdiction_data = type( 'UpdateJurisdictionInputType', (InputObjectType, ), input_type_fields(jurisdiction_fields, UPDATE, JurisdictionType))(required=True) class JurisdictionMutation(graphene.ObjectType): create_jurisdiction = CreateJurisdiction.Field() update_jurisdiction = UpdateJurisdiction.Field() graphql_update_or_create_jurisdiction = graphql_update_or_create( jurisdiction_mutation_config, jurisdiction_fields) graphql_query_jurisdictions = graphql_query(JurisdictionType, jurisdiction_fields, 'jurisdictions') def graphql_query_jurisdictions_limited(jurisdiction_fields): return graphql_query(JurisdictionType, jurisdiction_fields, 'jurisdictions') graphql_query_jurisdictions_paginated = graphql_query( JurisdictionPaginatedType, jurisdiction_paginated_fields, 'jurisdictionsPaginated') graphql_query_jurisdictions_versioned = graphql_query( JurisdictionVersionedType, jurisdiction_versioned_fields, 'jurisdictionsVersioned')
if R.prop_or(False, 'id', update_or_create_values): resource, created = Resource.objects.update_or_create(**update_or_create_values) else: resource = Resource(**update_or_create_values['defaults']) resource.save() return UpsertResource(resource=resource) class CreateResource(UpsertResource): """ Create Resource mutation class """ class Arguments: resource_data = type('CreateResourceInputType', (InputObjectType,), input_type_fields(resource_fields, CREATE, ResourceType))(required=True) class UpdateResource(UpsertResource): """ Update Resource mutation class """ class Arguments: resource_data = type('UpdateResourceInputType', (InputObjectType,), input_type_fields(resource_fields, UPDATE, ResourceType))(required=True) graphql_update_or_create_resource = graphql_update_or_create(resource_mutation_config, resource_fields) graphql_query_resources = graphql_query('resources', resource_fields)
""" Create Resource mutation class """ class Arguments: resource_data = type( 'CreateResourceInputType', (InputObjectType, ), input_type_fields(resource_fields, CREATE, ResourceType))(required=True) class UpdateResource(UpsertResource): """ Update Resource mutation class """ class Arguments: resource_data = type( 'UpdateResourceInputType', (InputObjectType, ), input_type_fields(resource_fields, UPDATE, ResourceType))(required=True) class ResourceMutation(graphene.ObjectType): create_resource = CreateResource.Field() update_resource = UpdateResource.Field() graphql_update_or_create_resource = graphql_update_or_create( resource_mutation_config, resource_fields) graphql_query_resources = graphql_query(ResourceType, resource_fields, 'resources')