Sunday, 1 September 2013

Using Django with SQLServer reporting

If you have a requirement to launch a SQLServer Reporting Services report from a Django site, here is the method I used.  It isn't as straight forward as it might first seem:

First if you need to pass data from Django to the report, passing the parameters on the URL isn't always a good idea as it allows an end user to potentially view data they shouldn't see by changing the URL parameters manually.  I used a SQL table to store the parameters and generated a GUID to pass to the report.  The report can then retrieve the parameters using a SQL query using the GUID as a key.   A request params row needs to be populated in a table, so a form is needed to request some of the request parameters while others may be calculated.

The model for the request parameters table is:

class params(models.Model):
    guid=models.CharField(null= False,max_length= 40, editable= False)
    report_type = models.ForeignKey( 'report_type', editable=False )
    start_date = models.DateTimeField()
    end_date = models.DateTimeField()
#... add any other parameters you need

    def __unicode__(self):
        return self.description


A ModelForm is then used with settings to include only the fields that require user input:

from django.forms import ModelForm
from .models import params
import logging
log = logging.getLogger(__name__)

   
class paramsForm(ModelForm):   
    class Meta:
        model = params
        fields = ( 'start_date', 'end_date' )


Two urls are defined, one to direct the user to the view that displays the model form template and another to direct the user to a separate view whose only job is to redirect to the SQLServer report.  Because of HTTP restrictions this redirect can't be carried out in the first view because a POST (which comes back from the Model Form template) can't be redirected:
 

from django.conf.urls import patterns, url
from xgenca_report import views

urlpatterns = patterns('',
    url(r'notification_report/', views.notification_report, name='notification_report' ),
    url(r'showreport/', views.show_report, name= 'show_report')
)

The two views are shown below.  The non user editable parameters are set up in the first view and the URL to the SQLServer report is stored in the session.  The second view then redirects to this URL.
 

from .models import report_type
from myapp.forms import paramsForm
from django.shortcuts import redirect, render_to_response
from django.contrib.auth.decorators import login_required
from django.template import RequestContext

import hashlib
import random

@login_required
def notification_report(request, *args, **kwargs):
    form = paramsForm(request.POST or None)
    if request.method == 'POST':
        #if form.instance.start_date and form.instance.end_date:
        if form.is_valid():
            obj = form.save(commit= False)
 
#... set other parameters here if needed
            obj.report_type = report_type.objects.get(report_description= "Notification" )
            guid = hashlib.sha1(str(random.random())).hexdigest()
            obj.guid = guid
            obj.save()           
            url = 'http://reportserver/ReportServer/Pages/ReportViewer.aspx?%2fMYPROJECT%2fNotificationsReport&rs:Command=Render&report_guid=' + guid + '&report_guid=' + guid
            request.session[ '_reporturl'] = url
            return redirect( '/report/showreport')
   
    return render_to_response( 'myapp/notification_report.html' , {'form': form,}, context_instance=RequestContext(request))

# View used to redirect the user to the report URL (this can't be done in the original view because of HTTP restrictions that prevent a POST being redirected
def show_report(request):
    return redirect(request.session.get( '_reporturl'))



The template looks like:
 
 
{% extends "base.html" %}
{% block content %}
<h3 align="center">{% block title %}Notification Report{%endblock%}</h3>
<table width="100%">
<tr>
<td width="33%">
</td>
<td width="33%">
<form method="post" action= "">
{% csrf_token %}
    {{ form.as_p }}
    <input type="submit" value= "View Report"/> <input type="button" name="Cancel" value="Cancel" onclick="window.location = '/' " />
</form>
</td>
<td width="33%">
</td>
</tr>
</table>
{%endblock%}
 
 

Configuring Apache for Basic Authentication to the SQLServer Reporting Services Server

One final problem is that when the user is redirected to the report server they may be asked to log in.   I got round this problem by configuring my Apache web server for basic authentication against the report server as follows:
 
 
LoadModule proxy_html_module modules/mod_proxy_html/mod_proxy_html.so
LoadModule xml2enc_module modules/mod_proxy_html/mod_xml2enc.so
Include modules/mod_proxy_html/proxy_html.conf

# Set up proxy for SQL Server reports
<Location /ReportServer/>
  SetEnv force-proxy-request-1.0  1
  SetEnv proxy-nokeepalive        1
  SetEnv proxy-initial-not-pooled 1
  ProxyHTMLEnable On
  ProxyHTMLURLMap http://yourreportserver/ReportServer/ /ReportServer/
  # Base64 encoding of "yourdomain\youreportserveruser:yourreportserveruserpassword"
  RequestHeader set Authorization "Basic base64string"
</Location>
 
Substitute the italic strings above with the information specific to your environment.

mod_proxy_html is a third party module downloaded from http://www.apachelounge.com/download/win32/modules-2.2/mod_proxy_html-3.1.2-win32.zip - open the zip and dump the mod_proxy_html folder under Program Files (x86)\Apache Software Foundation\Apache2.2\modules.  The necessary config is included in the above section.  (It may be included in your version of Apache.)

The three SetEnv directives prevent hanging when proxying IIS pages over Apache – they essentially force the use of HTTP 1.0 instead of 1.1 between corvos and endor.

The string in the RequestHeader section can be produced with any base64 encoder – you can find some JavaScript ones online.  Alternatively, log into the report server using suitable credentials and use a browser debugging tool to snoop your own request headers (for Firefox, Live HTTP Headers or Firebug’s Net panel can do this.)

Saturday, 31 August 2013

Serving Static Files in Django

This post shows the steps for setting up static files in Django.   Static files are things like images, logo's etc that won't change.  They are held within your Django project tree and then copied to a static files folder held outside your project tree, normally within the web server root.  The copy is kicked off by the collectstatic command shown below.

1. Make sure that django.contrib.staticfiles is included in your INSTALLED_APPS.


2. Settings.py:

# Absolute path to the directory static files should be collected to.
# Don't put anything in this directory yourself; store your static files
# in apps' "static/" subdirectories and in STATICFILES_DIRS.
# Example: "/home/media/media.lawrence.com/static/"
STATIC_ROOT = 'C:/inetpub/wwwroot/static/'

# URL prefix for static files.
# Example: "http://media.lawrence.com/static/"
STATIC_URL = '/static/'

# Additional locations of static files
STATICFILES_DIRS = (
  os.path.join(SITE_ROOT, 'static/'),
)
#STATICFILES_DIRS = (

3. Template:

    <img style= "width: 306px; height: 79px;" alt="my logo" src="{{ STATIC_URL }}logo.png" >

4.  Run python manage.py collectstatic to copy the static files into the STATICFILES_DIRS.

5.  In main urls.py:

    url(r'^static/(?P<path>.*)$', 'django.views.static.serve',
    {'document_root':settings.STATIC_ROOT}

6. For debug mode only add the following url to the urls.py file for the app (storing the static file folder):

if settings.DEBUG:
    urlpatterns += patterns( 'django.contrib.staticfiles.views' ,
        url(r '^static/(?P<path>.*)$', 'serve' ),
    )

Sunday, 25 August 2013

Django Model Forms Example

The Django documentation on Model Forms is thorough, but sometimes you want a simple example to explain the concept.   Here's one:

Model forms are used for the majority of forms in Django.  They're forms based on a model.  

Model on which the ModelForm is based:

class notification(models.Model):
    report_type = models.ForeignKey( 'report_type')
    event_type = models.ForeignKey( 'event_type')
    client_type = models.ForeignKey( 'client_type')
    email_addresses = models.TextField( 'Email Addresses (; separated)', null=True , max_length=500)

ModelForm:

from django.forms import ModelForm
from myapp.models import notification
from django.contrib.auth.models import User
from django.db.models import Q
from django.core.exceptions import ValidationError
import logging
log = logging.getLogger(__name__)

   
class notificationForm(ModelForm):   
    class Meta:
        model = Notification
    def clean_email_addresses(self):
        # check the email addresses are all for a registered user
        emails = self.cleaned_data['email_addresses']
        emaillist = emails.split( ";")
        if emaillist is not None:
            for e in emaillist:
                users = User.objects.filter(Q(email=e))
                if not users:
                    raise ValidationError( 'Each email address entered must be for a registered user of X-GenCA')
        return emails



View using the ModelForm:

# Create your views here.
from notification.forms import notificatioForm
from myapp.models import Notification
from django.shortcuts import redirect, render
from django.contrib.auth.decorators import login_required
import logging
log = logging.getLogger(__name__)
#command line debugging:
#import pdb; pdb.set_trace()

@login_required
def create_notification(request, template_name='notification/save.html' ):
    form = notificationForm(request.POST or None)
    if form.is_valid():
        form.save()
        return redirect( '/')
   
    return render(request, template_name, {'form':form,'notification' :Notification})


Template (save.html):

{% extends "base.html" %}
<h1 align="center">Create Notification</h1>
<form method="post" action= ".">
{% csrf_token %}
<div style="text-align: center;">
    {{ form.as_p }}
    <input type="submit" value= "Submit Form"/>
    </div>
</form>


URL pointing to View:

url(r'^create/$', views.create_notification), 
 
 
 
 The clean methods in the ModelForm can be used to add any business logic you require to validate the fields.  In the example above it checks the email addresses are those for registered users of the system.