Django Form Wizard Skip Multiple Steps with Conditional Logic - django

Is there some way to skip multiple steps in Django's Form Wizard (i.e. SessionWizardView)?
I know it is possible to use condition_dict to produce the following form display logic:
Page 1 -> Page 3
#urls.py
path('main/', MyWizard.as_view(set_of_forms, condition_dict={'1': view_condition} ))
What I would like to do is the following:
Page 1 --> Page 4
Presumably, adding a condition_dict based on the content of Page 1 should work to skip Page 3, but it doesn't work. For example:
#urls.py
path('main/', MyWizard.as_view(set_of_forms, condition_dict={'1': view_condition, '2': view_condition2,} ))
I am truly stumped on how to crack this nut. Any guidance that you can provide would be GREATLY appreciated.

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)
# 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 MyWizard(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 = MyWizard.as_view(MyWizard._form_list,condition_dict= MyWizard._condition_dict)
urlpatterns = patterns('',
...
url(r'^form_wizard_url/$', your_form_wizard, name='my-form-wizard',)
)

Thank you, Paulo, for your answer! This is exactly what is needed to easily skip form pages. Building on Paulo's answer, three changes need to be made to skip multiple steps simultaneously:
# 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 MyWizard(SessionWizardView):
# Your form wizard itself; will not be called directly by urls.py, but rather wrapped in a function that provide the condition_dictionary
# **Change1**: functions need to be stated before the dictionary
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
# **Change 2:** Only proceed with the logic if the step is valid
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 != None:
if step_1_info == some_condition:
return True # show step 2
else: return False # or don't
# **Change 3**: a condition must be added to skip an additional form
_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: check_step_two, # conditional callable for verifying whether to show step three
STEP_FOUR: 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),
(STEP_THREE, your_forms.StepFourForm),
]
...
''' urls.py '''
your_form_wizard = MyWizard.as_view(MyWizard._form_list,condition_dict= MyWizard._condition_dict)
urlpatterns = patterns('',
...
url(r'^form_wizard_url/$', your_form_wizard, name='my-form-wizard',)
)

Related

How to perform a query by using URL with question mark in Django?

It seems like the original URL querying function has been removed from Django 3.1. Does anyone know how to do it with a new package?
The url.py:
urlpatterns = [
re_path(r'^portfolio/(?P<title>[\w-]+)/$' , BlogApp_View.displayPortfolio, name='displayPortfolio'),
path('portfolio/', BlogApp_View.selectPortfolio, name='selectPortfolio'),]
The view.py
def displayPortfolio(request):
title = request.GET.get('title')
portfolio = Article.objects.filter(articleType__name__contains = "Portfolio", title=title)
print(title)
DICT = {}
return render(request, 'Article/', DICT)
The problem is now if I visit http://127.0.0.1:8000/Blog/portfolio/?title=A_UAV_Positioning_Approach_Using_LoRa/, it will skip the re_path shows in url.py.
Instead, it goes to the path one.
I have tried str:title method but that is actually not what I want. I prefer using the question mark pattern to finish the query.
The part after the questionmark is the querystring [wiki] and is not part of the path. This thus means that regardless what patterns you write, you can not distinguish on this, since the path patterns, regardless whether it is a path or re_path, are never matched against a URL with a query string.
You thus should write a single view, and inspect the request.GET query dict (which is a dictionary-like representation of the query string and see if it contains a value for title.
Your urlpatterns thus look like:
urlpatterns = [
path('portfolio/', BlogApp_View.selectPortfolio, name='selectPortfolio'),
]
and in the view, you can see if it contains a title:
def selectPortfolio(request):
if 'title' in request.GET:
# contains a ?title=…
title = request.GET.get('title')
portfolio = Article.objects.filter(
articleType__name__contains='Portfolio',
title=title
)
data = {'portfolio': portfolio}
return render(request, 'some_template.html', data)
else:
# contains no ?title=…
# …
return …

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

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

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())

How can I access URL parameters from within a BasePermission?

I'm trying to write a custom rest_framework Permission to prevent users from querying information that's not of the same company as them. Unfortunately, I can't seem to access any of the URL's parameters from within has_permission() or has_object_permissions().
Here's the beginning of my router:
# Create a basic router
router = routers.SimpleRouter()
# Establish some variables to assist with nested routes
root_elem = 'companies'
root_elem_id = '/(?P<company_id>[0-9]+)'
loca_elem = '/locations'
loca_elem_id = '/(?P<location_id>[0-9]+)'
# Companies will be the root from which all other relations branch
router.register(r'' + root_elem, views.CompanyViewSet)
router.register(r'' + root_elem + root_elem_id + loca_elem,
views.LocationViewSet)
Here's my custom permission:
# Only permit actions originating from location managers or company admins
class IsLocationManagerOrHigher(BasePermission):
# Checked when displaying lists of records
def has_permission(self, request, *args, **kwargs):
is_correct_level = False
# Admins can see every location if their location_id
# matches a location that's a child of the company
# specified in the URL
if request.employee.is_admin:
is_correct_level = True
return request.user and is_correct_level
# Checked when viewing specific records
def has_object_permission(self, request, view, obj):
is_correct_level = False
# Admins can see location details if their location's company_id
# matches a Location's company_id
if request.employee.is_admin:
is_correct_level = True
# Managers can see location details if it's their location
elif obj.id == request.employee.location_id and request.employee.is_manager:
is_correct_level = True
return request.user and is_correct_level
Right now checking request.employee.is_admin is only half of what I need - I also need to access the company_id from the URL and make sure it matches the admin's location's company_id:
# Pseudocode
try:
user_location = Location.objects.get(id=request.employee.location_id)
return user_location.company_id == kwargs['company_id']
except ObjectDoesNotExist:
pass
I've yet to figure out how to pass these parameters into the Permission so that it can perform this extra step. Or perhaps there's a better way of accomplishing what I'm trying to do?
If you can't pass them in directly (which would be preferable), they are available on the request object:
company_id = request.resolver_match.kwargs.get('company_id')
request.resolver_match.args and request.resolver_match.kwargs contain the positional/keyword arguments captured in your url.
As an alternative to the correct response posted by knbk, you can also get the URL parameters using the view object passed to has_permission method. Like this:
company_id = view.kwargs.get('company_id')

Need clarification on using Django 1.4 Form Wizards, specifically pre-filling and saving

We are building a wizard using Django 1.4's new form wizard functionality.
The docs on this are very terse and we can't find any advanced examples. We are using a named step wizard (needed to support a listview/datagrid we use) and a session backend.
The wizard is meant to edit roles and linked rights and is built to provide both add and edit functionality. We do this by asking the user in the first step if he/she wants to add or edit.
The next step depends on that choice;
If the user wants to edit, there is a search screen, followed by a listview/datagrid that displays results. The user can then select one of the results and goes to a details-screen, followed by a FilteredSelectMultiple page, allowing him/her to link rights to this role.
If the user wants to add a new role, the search and results screens are skipped and the user goes directly to the details screen, followed by the link-screen.
It all works pretty well, using a condition_dict in urls.py, but we are wondering a couple of things about the general functionality:
When a specific pre-existing role is selected, how can we fill the details and the link-screen with the corresponding data?
Do we instantiate a roles-object and pass it somehow to the two forms, if so, where do we instantiate it and do we need to do that for every form separately (which seems a bit over the top)?
When saving, is it common practice to create another instance of a role object, add the form data to it and save, or can we re-use the object used in the forms somehow?
We have tried overloading get_form_instance to return instances of roles, and we have looked at instance_dict in the docs, but it feels like the wrong approach and there are no examples to be found online, and we're not even sure these are used to pre-fill data or even if we're on the right track.
Logically, I would say in the step that selects an existing role, I need to fill the wizard-variables using an instance of the chosen object, and these get displayed in the forms. At the end of the wizard we reverse the process and get all data from the wizard-variables and add them to a newly instantiated roles-object and save it. Ideally this instance will determine itself if it needs to perform an INSERT or an UPDATE, depending on whether or not the promary key is filled.
If anyone can provide an example, or a nudge in the right direction, it would be very much appreciated.
The code of the wizardview class in views.py is below:
class RolesWizard(NamedUrlSessionWizardView):
def get_template_names(self):
# get template for each step...
if self.steps.current == 'choice':
return 'clubassistant/wizard_neworeditrole.html'
if self.steps.current == 'search':
return 'clubassistant/wizard_searchrole.html'
if self.steps.current == 'results':
return 'clubassistant/wizard_pickrole.html'
if self.steps.current == 'details':
return 'clubassistant/wizard_detailsrole.html'
elif self.steps.current == 'rights':
return 'clubassistant/wizard_roles.html'
def get_context_data(self, form, **kwargs):
# get context data to be passed to the respective templates
context = super(RolesWizard, self).get_context_data(form=form, **kwargs)
# add the listview in the results screen
if self.steps.current == 'results':
# get search text from previous step
cleaned_data = self.get_cleaned_data_for_step('search')
table = RolesTable(Roles.objects.filter(
role_name__contains=cleaned_data['searchrole'])
)
RequestConfig(self.request, paginate={
"per_page": 4,
}).configure(table)
# add the listview with results
context.update({'table': table})
# add a role instance based on the chosen primary key
if self.steps.current == 'rights':
cleaned_data = self.get_cleaned_data_for_step('results')
role_id = cleaned_data['role_uuid']
role = get_object_or_404(Roles, pk=role_id)
context.update({'role': role})
return context
def done(self, form_list, **kwargs):
# this code is executed when the wizard needs to be completed
# combine all forms into a single dictionary
wizard = self.get_all_cleaned_data()
if wizard.get("neworeditrole")=="add":
role = Roles()
else:
role = get_object_or_404(Roles, pk=wizard.get("role_uuid"))
# many-to-many rights/roles
role.role_rights_new_style.clear()
for each_right in wizard.get('role_rights_new_style'):
RightsRoles.objects.create(role=role, right=each_right,)
# other properties
for field, value in self.get_cleaned_data_for_step('details'):
setattr(role, field, value)
role.save()
# return to first page of wizard...
return HttpResponseRedirect('/login/maintenance/roles/wizard/choice/')
For future googlers:
I had some success with using get_form() because it is called before a form is rendered. Start with a couple of ModelForms:
class Wizard1(models.ModelForm):
class Meta:
model = MyModel
fields = ('field0', 'model0')
class Wizard2(models.ModelForm):
class Meta:
model = MyModel
excludes = ('field0', 'model0')
Then, in your SessionWizardView:
class MyWizard(SessionWizardView):
def get_form(self, step=None, data=None, files=None):
form = super(ExtensionCreationWizard, self).get_form(step, data, files)
if step is not None and data is not None:
# get_form is called for validation by get_cleaned_data_for_step()
return form
if step == "0":
# you can set initial values or tweak fields here
elif step == "1":
data = self.get_cleaned_data_for_step('0')
if data is not None:
form.fields['field1'].initial = data.get('field0')
form.fields['field2'].widget.attrs['readonly'] = True
form.fields['field3'].widget.attrs['disabled'] = True
form.fields['model1'].queryset = Model1.objects.filter(name="foo")
return form
The action is all in step 1. You request validated data from step 0 (which triggers another call to get_form() for step 0, so be careful) and then you can access any values that were set in step 0.
I threw in a couple of examples of settings you can change on the fields. You can update a queryset to limit the values in a ChoiceField, or re-display a value again but make it read-only. One caveat I noticed... readonly does not work on ChoiceField. You can make it disabled, but then the value is not propagated when you submit the form.
Let's see if I can help. I did a form wizard that adds steps depending on the answers. At each step I save all forms in a session variable, like so:
def process_step(self, request, form, step):
request.session['form_list'] = self.form_list
request.session['initial'] = self.initial
Then, each time that view is rendered, I instantiate a new form wizard with all the previous data:
def dynamic_wizard(request):
if not request.session.get('form_list'):
form = Wizard([Form1])
else:
form = Wizard(request.session.get('form_list'), initial = request.session['initial'])
return form(context=RequestContext(request), request=request)