2015-11-30

ReportLab Chinese Support

I need to generate a PDF with Chinese characters in a Django project. Generating a PDF is straightforward as ReportLab does the job well. The hard part is to make sure the user can read the Chinese characters in the PDF.
At the end I settled in using the Chinese font provided by the Adobe's Asian Language Packs. That way, if no usable Chinese font is available, the user can download and install the Chinese font on demand automatically. This approach offers good performance since nothing is embedded in the PDF.
The following is the code snippet in registering the Chinese font for ReportLab to use.
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.cidfonts import UnicodeCIDFont
font_chinese = 'STSong-Light' # from Adobe's Asian Language Packs
pdfmetrics.registerFont(UnicodeCIDFont(font_chinese))
...
canvas.setFont(font_chinese)

2015-11-22

Django Form - Confirm Before Saving (Part 2)

In Part 1 of this post, we create a form which conditionally shows a checkbox for user confirmation. The implementation is to raise an validation error when a condition is met. That does not work well with multiple confirmations, as the checking stops when the first condition is met. We need a way to check all conditions without raising an validation error, with one checkbox to confirm them all. We can do that with Form.add_error()
Instead of raising an validation error like in Part 1 of this post:
class InvoiceForm(forms.ModelForm):
    ...
    def clean(self):
        super().clean()
        ...
       if all((key in self.cleaned_data for key in ['date', 'confirm'])):
           if self.cleaned_data['date'].weekday() == 6 and not self.cleaned_data['confirm']:
                raise forms.ValidationError(MSG001)
We add an validation error when a condition is met:
class InvoiceForm(forms.ModelForm):
    ...
    def clean(self):
        super().clean()
        ...
        if 'confirm' in self.cleaned_data and not self.cleaned_data['confirm']:
            if 'date' in self.cleaned_data:
                d = self.cleaned_data['date']
                if d.weekday() == 6:
                    self.add_error(None, forms.ValidationError(CONFIRM_MSG[1]))
                if (datetime.date.today() - d).days > 14:
                    self.add_error(None, forms.ValidationError(CONFIRM_MSG[2]))
In the example above, a confirmation is required if either the invoice date falls on a Sunday or the invoice date is more than 14 days in the past. CONFIRM_MSG is a dict of confirmation messages such as the following:
CONFIRM_MSG = {
    1: "The invoice date does not fall on a Sunday.  Please check confirm to proceed.",
    2: "The invoice date is more than 14 days in the past.  Please check confirm to proceed.",
}
In the Form.__init__() method, we check for existence of any of the confirmation messages to show the checkbox. Note the use of the Form.non_field_errors() method to retrieve all non-field errors, such as those added in the Form.clean().
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        ...
        confirm_set = set(CONFIRM_MSG.values())
        if confirm_set.intersection(self.non_field_errors()):
            self.fields['confirm'].widget = forms.CheckboxInput()

2015-11-17

Django Form - Confirm Before Saving (Part 1)

How can I ask the user to confirm before saving a form if the form meets certain conditions? I tried different approaches and settled on the following.
This example is about saving an invoice. I would like to ask the user to confirm (by checking a checkbox) before saving an invoice if the invoice date falls on a Sunday. The checkbox will only appear if confirmation is required.
Step 1: Add a checkbox in the form as a hidden input.
class InvoiceForm(forms.ModelForm):
    confirm = forms.BooleanField(initial=False, required=False, widget=forms.HiddenInput)
    class Meta:
        model = Invoice
        fields = ('data', ..., 'confirm',)
Step 2: Check for the condition in the form.clean() method. The required data are retrieved from the form cleaned_data dictionary. Note that it is a straightly a read only access. If the condition is met, raise a validation error thereby invalidating the form.
MSG001 is a string constant used as the error message. For example something like "The invoice date does not fall on a Sunday. Please check confirm to proceed."
class InvoiceForm(forms.ModelForm):
    ...
    def clean(self):
        super().clean()
        ...
        if all((key in self.cleaned_data for key in ['date', 'confirm'])):
            if self.cleaned_data['date'].weekday() == 6 and not self.cleaned_data['confirm']:
                raise forms.ValidationError(MSG001)
Step 3: Check for the error condition in the __init__() method of the form, and dynamically change the checkbox from a hidden input to a regular checkbox on the fly. This effectively adds a checkbox to the form and unless it is checked, the form is invalid.
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        ...
        if any(e == MSG001 for e in self.errors.get('__all__', [])):
            self.fields['confirm'].widget = forms.CheckboxInput()
Step 4: In the HTML template, add conditional logic to show the checkbox either as a hidden input or as a checkbox as appropriate.
{% if form.confirm in form.hidden_fields %}
  {{ form.confirm }}
{% else %}
  {{ form.confirm.label_tag }}{{ form.confirm }}
{% endif %}
Final thoughts:
This approach does not require AJAX or any third party packages. The reason is to minimize Javascript knowledge and external dependency.
This approach uses validation error as a signal to add a checkbox to the form.
This approach modifies a form field dynamically in the form.__init__() method.
Also, in another approach, I tried to to update the form.cleaned_data dict without success. The changes I made are lost somewhere in the call chain. It is evident that I do not have sufficient knowledge in how Django forms work.
Continue to Part 2 of this blog for working with multiple confirmations.