How change the Connection Arguments (after, before) in graphene-python (relay)? - django

Using:
Django 3.x [ Django-Filters 2.2.0, graphene-django 2.8.0, graphql-relay 2.0.1 ]
Vue 2.x [ Vue-Apollo ]
After applying some filters (iContains etc.) on my graphQL search i tried to change or manipulate the connection_args like firstor after. I can fetch a Dictionary on my resolver like {'first': 2, 'name__icontains': 'eagle'} with values i put in the IDE. As you can see (Example 1 /def resolve_all_birds2) i use that already for a logic. But i do not understand where do manipulate the GraphQLArgument states of the before. after first. last function which comes with relay?
Example 1
class ExtendedConnection(Connection):
class Meta:
abstract = True
total_count = Int()
edge_count = Int()
def resolve_total_count(root, info, **kwargs):
return root.length
def resolve_edge_count(root, info, **kwargs):
return len(root.edges)
class Birds2Node(DjangoObjectType):
class Meta:
model = Birds
filter_fields = {
'id': ['exact', 'icontains'],
'name': ['exact', 'icontains', 'istartswith', 'iendswith'],
}
interfaces = (relay.Node, )
connection_class = ExtendedConnection
# --- CUSTOM FIELDS -->
# pkey = _db primary key
pKey = Int()
def resolve_pKey(parent, info):
return parent.pk
# qRank = Item Rank in Edge Array
qRank = Int()
def resolve_qRank(parent, info, **kwargs):
return info.path[2]
class Birds2Query(ObjectType):
birds2 = relay.Node.Field(Birds2Node)
all_birds2 = DjangoFilterConnectionField(Birds2Node)
def resolve_all_birds2(self, info, **kwargs):
if 'name__icontains' in kwargs:
nameIcon = kwargs['name__icontains']
nameIconBool = bool(nameIcon.strip()) # if blanks turns False
if nameIconBool == False: # has blanks
return Birds.objects.filter(name=None)
pass
if 'name__istartswith' in kwargs:
nameIsta = kwargs['name__istartswith']
nameIstaBool = bool(nameIsta.strip()) # if blanks turns False
if nameIstaBool == False: # has blanks
return Birds.objects.filter(name=None)
pass
return
For example, in my IDE i declare allBirds2(first: 2, name_Icontains: "a")... i can fetch these values with my resolver as a Dictionary via **kwargs`` or via args def resolve_all_birds2(self, info, first, name_icontains): so far so good, i can manipulate my ModelQuery and it returned only 2 per Edge.
But Imagine i want to change first: 2 to first: 10 in my BackEnd? Can i update the Dictionary? The Documentation means yes, but it seems strict related to the ObjectTypes (Fields) you resolve.
For Example i tried this...
Example 2
def resolve_all_birds2(self, info, **kwargs):
<...>
return {'first': '20', 'name__icontains': 'd' }
Output IDE: "message": "'dict' object has no attribute 'model'"
Example 3
def resolve_all_birds2(self, info, first, **kwargs):
<...>
return f'20, {first}!'
Output IDE: "message": "name 'first' is not defined",
Question
Unfortunately i found only parameter manipulation on the modelquery in the graphene-python docs.
So my Question is how can i manipulate - in my backend - the Values of the Fields before. after first. last, that relay offers and that are already useable in my IDE. Do i have to declare them extra in my DjangoObjectType or create a custom Node to manipulate and change the values after a user sends a request?

Adding a middleware would probably allow changing the input values after the request is made and before running the query. Graphene has an example at: https://docs.graphene-python.org/en/latest/execution/middleware/
However, it's not clear (to me) from the documentation which of the mentioned parameters would contain the first field you want to manipulate.
The middleware approach does not seem to be highly recommended, though, because this is an undesirable side effect: https://github.com/graphql-python/graphene/issues/1285

Related

Display possible values (choices) of SlugRelatedField in drf-yasg OpenAPI and Swagger views

I have several models that I use as enums (basically, the model is just a name and a slug), like currencies and countries etc, and I'm trying to show the available choices in drf-yasg without success.
My last attempt was adding this to the serializer's Meta class:
swagger_schema_fields = {
'currency': {'enum': list(Currency.objects.values_list('slug', flat=True))}
}
But of course it failed miserably - not only it didn't show the enum values, it also broke the serializer (because it used the strings instead of the actual model).
Is there any way of doing this?
Eventually I added a new field class that does exactly what I needed. I can't promise it's the most efficient way of solving this (retrieving the swagger page seems to take longer) but it does the job:
# choices_slug_field.py
from drf_yasg.inspectors import RelatedFieldInspector
from rest_framework.metadata import SimpleMetadata
from rest_framework.relations import SlugRelatedField
from rest_framework.serializers import ManyRelatedField, RelatedField
class ShowChoicesMetadata(SimpleMetadata):
def get_field_info(self, field):
field_info = super().get_field_info(field)
if (not field_info.get('read_only') and
isinstance(field, (ManyRelatedField, RelatedField)) and
hasattr(field, 'choices') and
getattr(field, 'show_choices', False)):
field_info['choices'] = [
{
'value': choice_value,
'display_name': str(choice_name)
}
for choice_value, choice_name in field.choices.items()
]
return field_info
class ShowChoicesMixin:
show_choices = True
class ChoicesSlugRelatedField(ShowChoicesMixin, SlugRelatedField):
pass
class ShowChoicesFieldInspector(RelatedFieldInspector):
def field_to_swagger_object(self, field, swagger_object_type, use_references, **kwargs):
dataobj = super().field_to_swagger_object(field, swagger_object_type, use_references, **kwargs)
if (isinstance(field, ChoicesSlugRelatedField) and hasattr(field, 'choices')
and getattr(field, 'show_choices', False) and 'enum' not in dataobj):
dataobj['enum'] = [k for k, v in field.choices.items()]
return dataobj

skipping step x to step y and validate step x data

I have actually a big problem on a django wizard form.
I have 3 steps. The second step can contains data or not. The last step is a file upload step.
In the WizardForm class, i overrided the get_context_data method and include this in it :
if self.steps.current == 'against_indication':
questions = None
try:
# get the machine
machine_id = self.kwargs['pk']
machine = Machine.objects.get(pk=int(machine_id))
# check if there is against indications
if machine.type_question is False:
questions = YhappsQuestion.objects.filter(type_modalite=machine.type)
else:
questions = CustomQuestion.objects.filter(machine=machine)
except Machine.DoesNotExist:
pass
if len(questions) == 0:
# we modify the form wizard to skip against indication step
self.render_next_step(form, **kwargs)
#self.render_goto_step(step='against_indication', goto_step='prescription', **kwargs)
As you see, if there is no questions, i skip the second step (against_indication) to go into the next step (prescription).
The problem appears here. When the last step is rendered, there is not enough data in the wizard form. In the ddt's request there is it :
with skip step.
So if i upload the file, it gonna fill the against_indication datas instead of prescription datas, and re-renderer me the last step...
I tried to do all of this without skip the second step, and see how look the ddt's request :
without skip step.
Someone has a solution to permit have the right datas when i skip step, plz ?
Thanks for your further answers
I don't think get_context_data is the correct method to do this in; FormWizard is a very specific class that restricts where you can perform different functions.
The typical way to specify when FormWizard skips a step is to use a condition_dictionary. Django uses the structure to only include the form for a step when the conditions (set as callables) return True. If not, then that step's form doesn't force form.is_valid() to be called, bypassing the validation of that step. This also assures that all hidden management info for the form is created for each step.
Here's a example of how this can work:
# I always specify index values for steps so that all functions can share them
STEP_ONE = u'0'
STEP_TWO = u'1'
STEP_THREE = u'2'
def YourFormWizard(SessionWizardView):
# Your form wizard itself; will not be called directly by urls.py, but rather wrapped in a function that provide the condition_dictionary
_condition_dict = { # a dictionary with key=step, value=callable function that return True to show step and False to not
STEP_ONE: return_true, # callable function that says to always show this step
STEP_TWO: check_step_two, # conditional callable for verifying whether to show step two
STEP_THREE: return_true, # callable function that says to always show this step
}
_form_list = [ # a list of forms used per step
(STEP_ONE,your_forms.StepOneForm),
(STEP_TWO, your_forms.StepTwoForm),
(STEP_THREE, your_forms.StepThreeForm),
]
...
def return_true(wizard): # callable function called in _condition_dict
return True # a condition that is always True, for when you always want form seen
def check_step_two(wizard): # callable function called in _condition_dict
step_1_info = wizard.get_cleaned_data_for_step(STEP_ONE)
# do something with info; can retrieve for any prior steps
if step_1_info == some_condition:
return True # show step 2
else: return False # or don't
''' urls.py '''
your_form_wizard = YourFormWizard.as_view(YourFormWizard._form_list,condition_dict= YourFormWizard._condition_dict)
urlpatterns = patterns('',
...
url(r'^form_wizard_url/$', your_form_wizard, name='my-form-wizard',)
)
Based on great Ian Price answer, I tried to make some improvements because I noticed structure could help over time, especially if you try to change order of your steps:
I used a dataclass to centralize data about a step wizard, then generating required data for urls later.
I used lambdas for returning True and giving callable is optional
I extracted STEP_X in a const.py files to be re-usable
I tried as possible to gather data within the class view itself rather than in functions
Here is the code (Python 3.7+):
const.py
STEP_0 = '0'
STEP_1 = '1'
STEP_2 = '2'
views.py
from dataclasses import dataclass
from typing import Optional, Callable, Sequence
#dataclass
class WizardStepData:
step: str
form_class: any
trigger_condition: Optional[Callable] = None
def __init__(self, step, form_class, trigger_condition=None):
""" if trigger_condition is not provided, we return a Callable that returns True """
self.step = step
self.form_class = form_class
self.trigger_condition = trigger_condition if trigger_condition else lambda _: True
def YourFormWizard(SessionWizardView):
#staticmethod
def check_step_one(wizard) -> bool:
pass # ...
#classmethod
def get_wizard_data_list(cls) -> Sequence:
return [
WizardStepData(step=STEP_0,
form_class=StepZeroForm),
WizardStepData(step=STEP_1,
form_class=StepOneForm,
trigger_condition=cls.check_step_one),
WizardStepData(step=STEP_2,
form_class=StepTwoForm),
]
#classmethod
def _condition_dict(cls) -> dict:
return {data.step: data.trigger_condition for data in cls.get_wizard_data_list()}
#classmethod
def _form_list(cls) -> list:
return [(data.step, data.form_class) for data in cls.get_wizard_data_list()]
urls.py
# ...
your_form_wizard = YourFormWizard.as_view(form_list=YourFormWizard._form_list(),
condition_dict=YourFormWizard._condition_dict())

mongoengine know when to delete document

New to django. I'm doing my best to implement CRUD using Django, mongodb, and mongoengine. I'm able to query the database and render my page with the correct information from the database. I'm also able to change some document fields using javascript and do an Ajax POST back to the original Django View class with the correct csrf token.
The data payload I'm sending back and forth is a list of each Document Model (VirtualPageModel) serialized to json (each element contains ObjectId string along with the other specific fields from the Model.)
This is where it starts getting murky. In order to update the original document in my View Class post function I do an additional query using the object id and loop through the dictionary items, setting the respective fields each time. I then call save and any new data is pushed to the Mongo collection correctly.
I'm not sure if what I'm doing to update existing documents is correct or in the spirit of django's abstracted database operations. The deeper I get the more I feel like I'm not using some fundamental facility earlier on (provided by either django or mongoengine) and because of this I'm having to make things up further downstream.
The way my code is now I would not be able to create a new document (although that's easy enough to fix). However what I'm really curious about is how I would know when to delete a document which existed in the initial query, but was removed by the user/javascript code? Am I overthinking things and the contents of my POST should contain a list of ObjectIds to delete (sounds like a security risk although this would be an internal tool.)
I was assuming that my View Class might maintain either the original document objects (or simply ObjectIds) it queried and I could do my comparisions off of that set, but I can't seem to get that information to persist (as a class variable in VolumeSplitterView) from its inception to when I received the POST at the end.
I would appreciate if anyone could take a look at my code. It really seems like the "ease of use" facilities of Django start to break when paired with Mongo and/or a sufficiently complex Model schema which needs to be directly available to javascript as opposed to simple Forms.
I was going to use this dev work to become django battle-hardened in order to tackle a future app which will be much more complicated and important. I can hack on this thing all day and make it functional, but what I'm really interested in is anyone's experience in using Django + MongoDB + MongoEngine to implement CRUD on a Database Schema which is not vary Form-centric (think more nested metadata).
Thanks.
model.py: uses mongoengine Field types.
class MongoEncoder(JSONEncoder):
def default(self, o):
if isinstance(o, VirtualPageModel):
data_dict = (o.to_mongo()).to_dict()
if isinstance(data_dict.get('_id'), ObjectId):
data_dict.update({'_id': str(data_dict.get('_id'))})
return data_dict
else:
return JSONEncoder.default(self, o)
class SubTypeModel(EmbeddedDocument):
filename = StringField(max_length=200, required=True)
page_num = IntField(required=True)
class VirtualPageModel(Document):
volume = StringField(max_length=200, required=True)
start_physical_page_num = IntField()
physical_pages = ListField(EmbeddedDocumentField(SubTypeModel),
default=list)
error_msg = ListField(StringField(),
default=list)
def save(self, *args, **kwargs):
print('In save: {}'.format(kwargs))
for k, v in kwargs.items():
if k == 'physical_pages':
self.physical_pages = []
for a_page in v:
tmp_pp = SubTypeModel()
for p_k, p_v in a_page.items():
setattr(tmp_pp, p_k, p_v)
self.physical_pages.append(tmp_pp)
else:
setattr(self, k, v)
return super(VirtualPageModel, self).save(*args, **kwargs)
views.py: My attempt at a view
class VolumeSplitterView(View):
#initial = {'key': 'value'}
template_name = 'click_model/index.html'
vol = None
start = 0
end = 20
def get(self, request, *args, **kwargs):
self.vol = self.kwargs.get('vol', None)
records = self.get_records()
records = records[self.start:self.end]
vp_json_list = []
img_filepaths = []
for vp in records:
vp_json = json.dumps(vp, cls=MongoEncoder)
vp_json_list.append(vp_json)
for pp in vp.physical_pages:
filepath = get_file_path(vp, pp.filename)
img_filepaths.append(filepath)
data_dict = {
'img_filepaths': img_filepaths,
'vp_json_list': vp_json_list
}
return render_to_response(self.template_name,
{'data_dict': data_dict},
RequestContext(request))
def get_records(self):
return VirtualPageModel.objects(volume=self.vol)
def post(self, request, *args, **kwargs):
if request.is_ajax:
vp_dict_list = json.loads(request.POST.get('data', []))
for vp_dict in vp_dict_list:
o_id = vp_dict.pop('_id')
original_doc = VirtualPageModel.objects.get(id=o_id)
try:
original_doc.save(**vp_dict)
except Exception:
print(traceback.format_exc())

Convert POST to PUT with Tastypie

Full Disclosure: Cross posted to Tastypie Google Group
I have a situation where I have limited control over what is being sent to my api. Essentially there are two webservices that I need to be able to accept POST data from. Both use plain POST actions with urlencoded data (basic form submission essentially).
Thinking about it in "curl" terms it's like:
curl --data "id=1&foo=2" http://path/to/api
My problem is that I can't update records using POST. So I need to adjust the model resource (I believe) such that if an ID is specified, the POST acts as a PUT instead of a POST.
api.py
class urlencodeSerializer(Serializer):
formats = ['json', 'jsonp', 'xml', 'yaml', 'html', 'plist', 'urlencoded']
content_types = {
'json': 'application/json',
'jsonp': 'text/javascript',
'xml': 'application/xml',
'yaml': 'text/yaml',
'html': 'text/html',
'plist': 'application/x-plist',
'urlencoded': 'application/x-www-form-urlencoded',
}
# cheating
def to_urlencoded(self,content):
pass
# this comes from an old patch on github, it was never implemented
def from_urlencoded(self, data,options=None):
""" handles basic formencoded url posts """
qs = dict((k, v if len(v)>1 else v[0] )
for k, v in urlparse.parse_qs(data).iteritems())
return qs
class FooResource(ModelResource):
class Meta:
queryset = Foo.objects.all() # "id" = models.AutoField(primary_key=True)
resource_name = 'foo'
authorization = Authorization() # only temporary, I know.
serializer = urlencodeSerializer()
urls.py
foo_resource = FooResource
...
url(r'^api/',include(foo_resource.urls)),
)
In #tastypie on Freenode, Ghost[], suggested that I overwrite post_list() by creating a function in the model resource like so, however, I have not been successful in using this as yet.
def post_list(self, request, **kwargs):
if request.POST.get('id'):
return self.put_detail(request,**kwargs)
else:
return super(YourResource, self).post_list(request,**kwargs)
Unfortunately this method isn't working for me. I'm hoping the larger community could provide some guidance or a solution for this problem.
Note: I cannot overwrite the headers that come from the client (as per: http://django-tastypie.readthedocs.org/en/latest/resources.html#using-put-delete-patch-in-unsupported-places)
I had a similar problem on user creation where I wasn't able to check if the record already existed. I ended up creating a custom validation method which validated if the user didn't exist in which case post would work fine. If the user did exist I updated the record from the validation method. The api still returns a 400 response but the record is updated. It feels a bit hacky but...
from tastypie.validation import Validation
class MyValidation(Validation):
def is_valid(self, bundle, request=None):
errors = {}
#if this dict is empty validation passes.
my_foo = foo.objects.filter(id=1)
if not len(my_foo) == 0: #if object exists
foo[0].foo = 'bar' #so existing object updated
errors['status'] = 'object updated' #this will be returned in the api response
return errors
#so errors is empty if object does not exist and validation passes. Otherwise object
#updated and response notifies you of this
class FooResource(ModelResource):
class Meta:
queryset = Foo.objects.all() # "id" = models.AutoField(primary_key=True)
validation = MyValidation()
With Cathal's recommendation I was able to utilize a validation function to update the records I needed. While this does not return a valid code... it works.
from tastypie.validation import Validation
import string # wrapping in int() doesn't work
class Validator(Validation):
def __init__(self,**kwargs):
pass
def is_valid(self,bundle,request=None):
if string.atoi(bundle.data['id']) in Foo.objects.values_list('id',flat=True):
# ... update code here
else:
return {}
Make sure you specify the validation = Validator() in the ModelResource meta.

How to write a request filter / preprocessor in Django

I am writing an application in Django, which uses [year]/[month]/[title-text] in the url to identitfy news items. To manage the items I have defined a number of urls, each starting with the above prefix.
urlpatterns = patterns('msite.views',
(r'^(?P<year>[\d]{4})/(?P<month>[\d]{1,2})/(?P<slug>[\w]+)/edit/$', 'edit'),
(r'^(?P<year>[\d]{4})/(?P<month>[\d]{1,2})/(?P<slug>[\w]+)/$', 'show'),
(r'^(?P<year>[\d]{4})/(?P<month>[\d]{1,2})/(?P<slug>[\w]+)/save$', 'save'),
)
I was wondering, if there is a mechanism in Django, which allows me to preprocess a given request to the views edit, show and save. It could parse the parameters e.g. year=2010, month=11, slug='this-is-a-title' and extract a model object out of them.
The benefit would be, that I could define my views as
def show(news_item):
'''does some stuff with the news item, doesn't have to care
about how to extract the item from request data'''
...
instead of
def show(year, month, slug):
'''extract the model instance manually inside this method'''
...
What is the Django way of solving this?
Or in a more generic way, is there some mechanism to implement request filters / preprocessors such as in JavaEE and Ruby on Rails?
You need date based generic views and create/update/delete generic views maybe?
One way of doing this is to write a custom decorator. I tested this in one of my projects and it worked.
First, a custom decorator. This one will have to accept other arguments beside the function, so we declare another decorator to make it so.
decorator_with_arguments = lambda decorator: lambda * args, **kwargs: lambda func: decorator(func, *args, **kwargs)
Now the actual decorator:
#decorator_with_arguments
def parse_args_and_create_instance(function, klass, attr_names):
def _function(request, *args, **kwargs):
model_attributes_and_values = dict()
for name in attr_names:
value = kwargs.get(name, None)
if value: model_attributes_and_values[name] = value
model_instance = klass.objects.get(**model_attributes_and_values)
return function(model_instance)
return _function
This decorator expects two additional arguments besides the function it is decorating. These are respectively the model class for which the instance is to be prepared and injected and the names of the attributes to be used to prepare the instance. In this case the decorator uses the attributes to get the instance from the database.
And now, a "generic" view making use of a show function.
def show(model_instance):
return HttpResponse(model_instance.some_attribute)
show_order = parse_args_and_create_instance(Order, ['order_id'])(show)
And another:
show_customer = parse_args_and_create_instance(Customer, ['id'])(show)
In order for this to work the URL configuration parameters must contain the same key words as the attributes. Of course you can customize this by tweaking the decorator.
# urls.py
...
url(r'^order/(?P<order_id>\d+)/$', 'show_order', {}, name = 'show_order'),
url(r'^customer/(?P<id>\d+)/$', 'show_customer', {}, name = 'show_customer'),
...
Update
As #rebus correctly pointed out you also need to investigate Django's generic views.
Django is python after all, so you can easily do this:
def get_item(*args, **kwargs):
year = kwargs['year']
month = kwargs['month']
slug = kwargs['slug']
# return item based on year, month, slug...
def show(request, *args, **kwargs):
item = get_item(request, *args, **kwargs)
# rest of your logic using item
# return HttpResponse...