Carlos Ble

Carlos Ble

I am a professional software developer, I solve problems.

I also teach and mentor developers to build better software.

Developing software since 2001.

Can I help you?

  • Do you need high quality tailor-made software?
  • Need training on TDD, clean code or refactoring?
  • Do you need a technical consultant?
  • May I pair with you to write better code?

Events

Upcoming training courses:

  1. TDD - [en Español] - 6 y 7 Octubre
    Gran Canaria
  2. TDD - [in English] - October 20, 21 & 22
    London, UK
  3. TDD - [en Español] - 29, 30 y 31 de Octubre.
    Madrid, Spain

Conferences:

  1. I'll be at the Agile Testing Days 2014
  2. I'll be at the London Test Gathering Workshops.

Archive for May, 2009



Very important update: DO NOT USE THE FORMWIZARD for dynamic steps as described below. Django stores the FormWizard as a Singleton so concurrent sessions produce crazy behavior on the wizard. Thanks to my friends at Galotecnia.com because they found the problem. Use the FormWizard just as described in Django documentation and always stateless.

---------------------------
Django FormWizard is really useful. But when it comes to variable steps (I mean, depending on the choosen answer the user gets different forms) you can't just initialize the FormWizard with a bunch of Forms as you don't know which ones. Using polymorphism makes it possible to save lots of code lines by letting each form decide which one is next.

Instead of define your form extending Form, use this base class:

  1.  
  2. class WizardStepForm(forms.Form):
  3. step_order = 0
  4. confirmQuestion = "Proceed?"
  5.  
  6. def process_dynamic_step(self, form_list):
  7. raise NotImplementedError("Please implement this")
  8.  
  9. def get_field_name(self):
  10. raise NotImplementedError("Please implement this")
  11.  
  12. def get_field_name_for_summary(self):
  13. fieldname = self.get_field_name()
  14. firstUpper = fieldname.upper()[0] + fieldname[1:]
  15. return firstUpper + ":"
  16.  
  17. def append_end(self, form_list):
  18. return "", form_list
  19.  

Now we assume our FormWizard will work with WizardStepForms instead of regular Forms:

  1.  
  2. class MyWizard(FormWizard):
  3. def get_summary(self, form_list):
  4. """
  5. To show the user the summary at the last step
  6. """
  7. summary = ""
  8. for i in form_list:
  9. if hasattr(i, "cleaned_data"):
  10. if isinstance(i, WizardStepForm):
  11. model = i.cleaned_data[i.cleaned_data.keys()[0]]
  12. append_text = i.get_field_name_for_summary()
  13. append_text = append_text + " " + str(model)
  14. summary = summary + append_text + "<br>"
  15. return summary
  16.  
  17. def get_hidden_summary(self, form_list):
  18. """
  19. Collect the information from all the steps which is in the input hidden field
  20. """
  21. summary = ""
  22. for i in form_list:
  23. if hasattr(i, "cleaned_data"):
  24. model = i.cleaned_data[i.cleaned_data.keys()[0]]
  25. if hasattr(model, "id"):
  26. summary = summary + str(model.id) + ":"
  27. else:
  28. summary = summary + str(model) + ":"
  29. summary = summary[:-1]
  30. return summary
  31.  
  32. def done(self, request, form_list):
  33. summary = self.get_summary(form_list)
  34. hidden_summary = self.get_hidden_summary(form_list)
  35. return render_to_response('confirm.html',
  36. {
  37. 'summary': summary,
  38. 'hidden_summary': hidden_summary,
  39. 'form': ConfirmationForm(), # a regular Form
  40. },
  41. RequestContext(request))
  42.  
  43. def get_template(self, step):
  44. return 'wizard.html'
  45.  
  46. def process_step(self, request, form, step):
  47. self.extra_context = {'info': ''}
  48. if hasattr(form, "cleaned_data"):
  49. info, self.form_list = form.process_dynamic_step(self.form_list)
  50. self.extra_context = {'info': info}
  51.  

Now two forms for the wizard (two steps):

  1.  
  2. class SearchWhateverForm(WizardStepForm):
  3. step_order = 1
  4.  
  5. someChoice = forms.ChoiceField(choices=WHATEVER_CHOICES)
  6.  
  7. @classmethod
  8. def get_field_name(self):
  9. return "whatever"
  10.  
  11. def process_dynamic_step(self, form_list):
  12. form_list = form_list[:self.step_order]
  13. form_list.append(WhatToGenerateForm)
  14. return "How many items do you need to generate?", form_list
  15.  
  16. class WhatToGenerateForm(WizardStepForm):
  17. step_order = 2
  18. generate = forms.ChoiceField(choices=GENERATION_CHOICES)
  19.  
  20. @classmethod
  21. def get_field_name(self):
  22. return "generate"
  23.  
  24. def process_dynamic_step(self, form_list):
  25. form_list = form_list[:self.step_order]
  26. generate = self.cleaned_data['generate']
  27. if generate == GENERATE_EXACT or generate == GENERATE_EXACT_90:
  28. form_list.append(HowManyToGenerateForm)
  29. return "Please set number", form_list
  30. else:
  31. return self.append_end(form_list)
  32.  

File urls.py would be like this:

  1.  
  2. url(r'^wizard/$', MyWizard([SearchWhateverForm])
  3.  

Only one template is needed for all the steps:

  1.  
  2. <form action="." method="post">
  3. <table>
  4. {{ form }}
  5. </table>
  6. <input type="hidden" name="{{ step_field }}" value="{{ step0 }}" />
  7. {{ previous_fields|safe }}
  8. <p>
  9. <input class="button" type="submit" value="{% trans "Next Step" %}" />
  10. </p>
  11. </form>
  12.  

So, How could you test drive the wizard?

As you don't need views or templates to run the sequence, it is easy:

  1.  
  2. def test_StepsAreOk(self):
  3. form_list = []
  4. form_list.append(SearchWhateverForm)
  5. # define somedata
  6. form_list[-1].cleaned_data = {SearchWhateverForm.get_field_name(): somedata}
  7. info, form_list = form_list[-1]().process_dynamic_step(form_list)
  8. self.assertEqual(form_list[-1].get_field_name(), WhatToGenerateForm.get_field_name())
  9.  

How can I populate the choices of one form dynamically?

The forms_list of the wizard is not populated with instances but data types so you've got to dynamically create a form type once you know which choices will it have available. One solution is to extend from one of your forms:

  1.  
  2. class SomeWizardStepForm(WizardStepForm):
  3. step_order = 1
  4.  
  5. #field is not defined
  6.  
  7. @classmethod
  8. def get_field_name(self):
  9. return "whatever"
  10.  
  11. def process_dynamic_step(self, form_list):
  12. form_list = form_list[:self.step_order]
  13. form_list.append(TheNextForm)
  14. return "whatever", form_list
  15.  
  16.  
  17. def createDynamicForm(self, someInput):
  18. class SomeDynamicForm(SomeWizardStepForm):
  19. field = forms.ModelChoiceField(queryset=someInput.get_whatever())
  20. return SomeDynamicForm # returning a datatype not an instance.
  21.  

Method createDynamicForm can be inserted in MyWizard class or in any WizardStepForm, in its process_dynamic_step method, so once the datatype is returned the form_list can hold the new form.

What if I need to populate the first step form with an incoming variable?

Initialize the FormWizard with an empty list:

  1.  
  2. url(r'^wizard/(?P<someInput>\d+)/$', MyWizard([]),
  3.  

Add logic to MyWizard to populate and place the first form:

  1.  
  2. class MyWizard(FormWizard):
  3.  
  4. someInput = None # this is a model in my actual code
  5.  
  6. def _initialize(self, id):
  7. self.someInput = SomeModel.objects.get(id=id)
  8. self.extra_context = {'someInput': self.someInput}
  9. self.form_list = []
  10. self.form_list.append(self.createDynamicForm(self.someInput))
  11.  
  12. def parse_params(self, request, *args, **kwargs):
  13. """
  14. Get the parameters sent through the URL
  15. """
  16. if self.someInput is None: # first time
  17. self._initialize(kwargs['someInput'])
  18.  
  19. ....
  20.  

It the code snippets are not clear enough let me know.

Developers needed

Do you want to work (part time) on development projects using Django?. We're looking for candidates all over the planet, we'll work over the internet. If you've got:

  • Interest on test driven development and other XP techniques
  • Good english level (at least as bad as mine)
  • Python experience
  • Subversion experience

Django knowledge is also interesting. Remember this is just part time, not any permanent position.
Hackers please send me an email with your CV, or just your blog and your rates. Thanks :-)

He tenido la suerte de entrar en el equipo de desarrollo de Novasoft Canarias en el proyecto de software que usa el Servicio Canario de Empleo. Muy buenas condiciones laborales y muy buen ambiente. La verdad que es una oportunidad estupenda. Esta vez estoy con Java. Ahora toca compaginar mi trabajo en ésta gran empresa con mis colaboraciones particulares con otras empresas que han tenido a bien confiar en mí para desarrollos varios. Se preveen meses de mucho trabajo y nuevos posts sobre cuestiones relativas a Java.

Por cierto, estoy buscando colaboradores con conocimientos en Django, HTML, CSS, y Javascript que puedan trabajar via internet a tiempo parcial. Si alguien esta interesado puede encontrar mi email en el enlace de Contacto.