Commits

Anonymous committed ea7deae

[project @ 445]
Finished unicode-isation, and fixed bug in bbcode

Comments (0)

Files changed (12)

cciw/cciwmain/templatetags/bbcode.py

             ret = u'<%s/>' % opening 
         else:
             if len(node.children) > 0:
-                ret = u'<%s>%s</%s>' % (opening, + node.render_children_xhtml(), self.html_equiv)
+                ret = u'<%s>%s</%s>' % (opening, node.render_children_xhtml(), self.html_equiv)
             else:
                 ret = u''
         return ret
             url = ESV_BROWSE_URL + "?" + urllib.urlencode({'q':node.parameter})
             output += u'<div class="biblequote"><a href="%s" title="Browse %s in the ESV">%s:</a></div>' % \
                 (escape(url), escape(node.parameter), escape(node.parameter))
-        output += u'<blockquote class="bible">%s</blockquote>' + node.render_children_xhtml()
-        return ''.join(output)
+        output += u'<blockquote class="bible">%s</blockquote>' % node.render_children_xhtml()
+        return u''.join(output)
 
 ###### DATA ######
 

cciw/cciwmain/templatetags/test_bbcode.py

+# # -*- coding: utf-8 -*-
 import sys
 import os
 
         '<ul>\n<li>Newlines not discarded, or converted when brs would be illegal</li></ul>'),
     ('[quote]\nNewlines not discarded at beginning of quote', 
         '<blockquote>\n<div>Newlines not discarded at beginning of quote</div></blockquote>'),
-    ('[list]Text in root of list tag is moved outside[*]and put in a div[/list]',
-        '<div>Text in root of list tag is moved outside<ul><li>and put in a div</li></ul></div>'),
+    (u'[list]Text in root of list tag is moved outside[*]and put in a div é[/list]',
+        u'<div>Text in root of list tag is moved outside<ul><li>and put in a div é</li></ul></div>'),
     (':-) :bosh:',
         '<div><img src="' + bbcode.EMOTICONS_ROOT + 'smile.gif" alt=":-)" /> <img src="' + bbcode.EMOTICONS_ROOT + 'mallet1.gif" alt=":bosh:" /></div>' ),
     ('0:-)',
     ('[emoticon]hello[/emoticon]',
         '<div></div>'),
     # escaping:
-    ('[[b]] [[/b]] [[quote=foo]] [[[b]]]',
-        '<div>[b] [/b] [quote=foo] [[b]]</div>'),
+    (u'[[b]] [[/b]] [[quote=fooé]] [[[b]]]',
+        u'<div>[b] [/b] [quote=fooé] [[b]]</div>'),
     # text that is accidentally similar to escaping:
     ('[[b]Just some bold text in square brackets[/b]]',
         '<div>[<b>Just some bold text in square brackets</b>]</div>'),
     
     # non-existant tags come through as literals
-    ('[nonexistanttag]',    
-        '<div>[nonexistanttag]</div>'),
+    (u'[nonéxistanttag]',    
+        u'<div>[nonéxistanttag]</div>'),
     # empty string should return nothing
     ('',
         ''),
     for bb, xhtml in tests:
         yield check_correction, bb
 
+def test_unicode():
+    # Should always return unicode objects
+    for bb, xhtml in tests:
+        yield check_unicode, bb
+
+def check_unicode(bb):
+    assert type(bbcode.bb2xhtml(bb)) is unicode
+
 def test_correct_preserves_whitespace():
     # These examples are correct bbcode with whitespace
     # in various places, and 'correct' shouldn't mess with our whitespace!

cciw/cciwmain/views/camps.py

             List of all Camp objects (or all Camp objects in the specified year).
     """
     c = standard_extra_context()
-    c['title'] ="Camp forums and photos"
+    c['title'] = u"Camp forums and photos"
     all_camps = Camp.objects.filter(end_date__lte=datetime.datetime.today())
     if (year == None):
         camps = all_camps.order_by('-year', 'number')
     c['title'] = camp.nice_name
     
     if camp.is_past():
-        c['breadcrumb'] = create_breadcrumb(year_forum_breadcrumb(str(camp.year)) + [camp.nice_name])    
+        c['breadcrumb'] = create_breadcrumb(year_forum_breadcrumb(unicode(camp.year)) + [camp.nice_name])
     else:
-        c['breadcrumb'] = create_breadcrumb([standard_subs('<a href="/thisyear/">Camps {{thisyear}}</a>'), "Camp " + number])
+        c['breadcrumb'] = create_breadcrumb([standard_subs(u'<a href="/thisyear/">Camps {{thisyear}}</a>'), "Camp " + number])
     return render_to_response('cciw/camps/detail.html', context_instance=c)
 
 def thisyear(request):
-    c = RequestContext(request, standard_extra_context(title="Camps " + str(get_thisyear())))
+    c = RequestContext(request,
+                       standard_extra_context(title=u"Camps %d" % get_thisyear()))
     c['camps'] = Camp.objects.filter(year=get_thisyear()).order_by('number')
     return render_to_response('cciw/camps/thisyear.html', context_instance=c)
 
     if number == 'all':
         camp = None
         forum = _get_forum_for_path_and_year(request.path[1:], int(year))
-        title = "General forum " + str(year)
+        title = u"General forum %s" % year
         breadcrumb_extra = year_forum_breadcrumb(year)
         
     else:
         forum = get_forum_for_camp(camp)
         if forum is None:
             raise Http404
-        title = camp.nice_name + " - Forum"
+        title = u"%s - Forum" % camp.nice_name
         breadcrumb_extra = camp_forum_breadcrumb(camp)
 
     c = standard_extra_context(title=title)
     """Displays a topic for a camp."""
     camp, breadcrumb_extra = _get_camp_and_breadcrumb(year, number)
     
-    return forums_views.topic(request, topicid=topicnumber, title_start='Topic',
+    return forums_views.topic(request, topicid=topicnumber, title_start=u'Topic',
         template_name='cciw/forums/topic.html', breadcrumb_extra=breadcrumb_extra)        
 
 @member_required
     except Photo.DoesNotExist:
         raise Http404
     
-    ec = standard_extra_context(title="Photos: " + camp.nice_name)
+    ec = standard_extra_context(title=u"Photos: %s" % camp.nice_name)
     
     return forums_views.photo(request, photo, ec, breadcrumb_extra)
 
     except Photo.DoesNotExist:
         raise Http404
     
-    ec = standard_extra_context(title=utils.unslugify(year+", " + galleryname) + " - Photos")    
+    ec = standard_extra_context(title=u"%s, %s - Photos" %
+                                (utils.unslugify(year), utils.unslugify(galleryname)))
     return forums_views.photo(request, photo, ec, breadcrumb_extra)
 
 def camp_forum_breadcrumb(camp):
-    return ['<a href="/camps/">Forums and photos</a>', '<a href="/camps/#year' + str(camp.year) + '">' + str(camp.year) + '</a>', camp.get_link()]
+    return [u'<a href="/camps/">Forums and photos</a>',
+            u'<a href="/camps/#year%d">%d</a>' % (camp.year, camp.year),
+            camp.get_link()]
     
 def year_forum_breadcrumb(year):
-    return ['<a href="/camps/">Forums and photos</a>', '<a href="/camps/#year' + year + '">' + utils.unslugify(year) + '</a>']
+    # NB: 'year' may be a string like 'Ancient'
+    return [u'<a href="/camps/">Forums and photos</a>',
+            u'<a href="/camps/#year%s">%s</a>' % (year, utils.unslugify(year)) ]

cciw/cciwmain/views/forums.py

 
 # Utility functions for breadcrumbs
 def topicindex_breadcrumb(forum):
-    return ["Topics"]
+    return [u"Topics"]
 
 def photoindex_breadcrumb(gallery):
-    return ["Photos"]
+    return [u"Photos"]
 
 def topic_breadcrumb(forum, topic):
-    return ['<a href="' + forum.get_absolute_url() + '">Topics</a>']
+    return [u'<a href="%s">Topics</a>' % forum.get_absolute_url()]
 
 def photo_breadcrumb(gallery, photo):
-    prev_and_next = ''
+    prev_and_next = u''
     try:
         previous_photo = Photo.objects.filter(id__lt=photo.id, \
             gallery__id__exact = photo.gallery_id).order_by('-id')[0]
-        prev_and_next += '<a href="%s" title="Previous photo">&laquo;</a> ' % previous_photo.get_absolute_url() 
+        prev_and_next += u'<a href="%s" title="Previous photo">&laquo;</a> ' % previous_photo.get_absolute_url() 
     except IndexError:
-        prev_and_next += '&laquo; '
+        prev_and_next += u'&laquo; '
         
     try:
         next_photo = Photo.objects.filter(id__gt=photo.id, \
             gallery__id__exact = photo.gallery_id).order_by('id')[0]
-        prev_and_next += '<a href="%s" title="Next photo">&raquo;</a> ' % next_photo.get_absolute_url()
+        prev_and_next += u'<a href="%s" title="Next photo">&raquo;</a> ' % next_photo.get_absolute_url()
     except IndexError:
-        prev_and_next += '&raquo; '
+        prev_and_next += u'&raquo; '
         
-    return ['<a href="' + gallery.get_absolute_url() + '">Photos</a>', str(photo.id), prev_and_next]
+    return [u'<a href="%s">Photos</a>' % gallery.get_absolute_url(), unicode(photo.id), prev_and_next]
     
 # Called directly as a view for /news/ and /website/forum/, and used by other views
 def topicindex(request, title=None, extra_context=None, forum=None,
         extra_context = standard_extra_context(title=title)
     
     extra_context['forum'] = forum
-    extra_context['atom_feed_title'] = "Atom feed for new topics on this board."
+    extra_context['atom_feed_title'] = u"Atom feed for new topics on this board."
     
     ### BREADCRUMB ###
     if breadcrumb_extra is None:
     context = RequestContext(request, standard_extra_context(title='Add topic'))
     
     if not forum.open:
-        context['message'] = 'This forum is closed - new topics cannot be added.'
+        context['message'] = u'This forum is closed - new topics cannot be added.'
     else:
         context['forum'] = forum
         context['show_form'] = True
         msg_text = request.POST.get('message', '').strip()
         
         if subject == '':
-            errors.append('You must enter a subject')
+            errors.append(u'You must enter a subject')
             
         if msg_text == '':
-            errors.append('You must enter a message.')
+            errors.append(u'You must enter a message.')
         
         context['message_text'] = bbcode.correct(msg_text)
         context['subject_text'] = subject
         subject = request.POST.get('subject', '').strip()
         msg_text = request.POST.get('message', '').strip()
         
-        if subject == '':
-            errors.append('You must enter a subject.')
+        if subject == u'':
+            errors.append(u'You must enter a subject.')
             
-        if msg_text == '':
-            errors.append('You must enter the short news item.')
+        if msg_text == u'':
+            errors.append(u'You must enter the short news item.')
         
         context['message_text'] = bbcode.correct(msg_text)
         context['subject_text'] = subject
     l = [opt for opt in map(string.strip, polloptions.strip().split("\n")) if len(opt) > 0]
     
     if len(l) == 0:
-        raise validators.ValidationError, "At least one option must be entered"
+        raise validators.ValidationError(u"At least one option must be entered")
     
     maxlength = PollOption._meta.get_field('text').maxlength
     if len([opt for opt in l if len(opt) > maxlength]) > 0:
-        raise validators.ValidationError, "Options may not be more than %s chars long" % maxlength
+        raise validators.ValidationError(u"Options may not be more than %s chars long" % maxlength)
         
     return l
     
     forum = _get_forum_or_404(request.path, suffix)
     
     if poll_id is not None:
-        title = "Edit poll"
+        title = u"Edit poll"
     else:
-        title = "Create poll"
+        title = u"Create poll"
     c = standard_extra_context(title=title)
     
     cur_member = get_current_member()
     if not poll.can_anyone_vote():
         # Only get here if the poll was closed 
         # while they were voting
-        errors.append('This poll is closed for voting, sorry.')
+        errors.append(u'This poll is closed for voting, sorry.')
         context['voting_errors'] = errors
         return
     
     if not poll.can_vote(cur_member):
-        errors.append('You cannot vote on this poll.  Please check the voting rules.')
+        errors.append(u'You cannot vote on this poll.  Please check the voting rules.')
         context['voting_errors'] = errors
     
     if not polloption_id in (po.id for po in poll.poll_options.all()):
-        errors.append('Invalid option chosen')
+        errors.append(u'Invalid option chosen')
         context['voting_errors'] = errors
     
     if not errors:
                             member=cur_member,
                             date=datetime.datetime.now())
         voteinfo.save()
-        context['voting_message'] = 'Vote registered, thank you.'
+        context['voting_message'] = u'Vote registered, thank you.'
 
 @member_required_for_post
 def topic(request, title_start=None, template_name='cciw/forums/topic.html', topicid=0,
     # Add additional title
     title = topic.subject[0:40]
     if len(title_start) > 0:
-        title = title_start + ": " + title
+        title = title_start + u": " + title
     extra_context = standard_extra_context(title=title)
 
     if breadcrumb_extra is None:
     if introtext:
         extra_context['introtext'] = introtext
 
-    extra_context['atom_feed_title'] = "Atom feed for posts in this topic."
+    extra_context['atom_feed_title'] = u"Atom feed for posts in this topic."
 
     ### PROCESSING ###
     # Process any message that they added.
     
     ### FEED ###
     resp = feeds.handle_feed_request(request, 
-        feeds.gallery_photo_feed("CCIW - " + extra_context['title']), query_set=photos)
+        feeds.gallery_photo_feed(u"CCIW - %s" % extra_context['title']), query_set=photos)
     if resp is not None: return resp
     
-    extra_context['atom_feed_title'] = "Atom feed for photos in this gallery."
+    extra_context['atom_feed_title'] = u"Atom feed for photos in this gallery."
     extra_context['gallery'] = gallery    
     extra_context['breadcrumb'] =   create_breadcrumb(breadcrumb_extra + photoindex_breadcrumb(gallery))
 
     resp = feeds.handle_feed_request(request, feeds.photo_post_feed(photo), query_set=posts)
     if resp: return resp
     
-    extra_context['atom_feed_title'] = "Atom feed for posts on this photo."
+    extra_context['atom_feed_title'] = u"Atom feed for posts on this photo."
     
     extra_context['breadcrumb'] = create_breadcrumb(breadcrumb_extra + photo_breadcrumb(photo.gallery, photo))
     extra_context['photo'] = photo
         paginate_by=settings.FORUM_PAGINATE_POSTS_BY, allow_empty=True)
 
 def all_posts(request):
-    context = standard_extra_context(title="Recent posts")
+    context = standard_extra_context(title=u"Recent posts")
     posts = Post.objects.exclude(posted_at__isnull=True).order_by('-posted_at')
     
     resp = feeds.handle_feed_request(request, feeds.PostFeed, query_set=posts)
     if resp: return resp
     
-    context['atom_feed_title'] = "Atom feed for all posts on CCIW message boards."
+    context['atom_feed_title'] = u"Atom feed for all posts on CCIW message boards."
 
     return list_detail.object_list(request, posts,
         extra_context=context, template_name='cciw/forums/posts.html',
     return HttpResponseRedirect(url)
 
 def all_topics(request):
-    context = standard_extra_context(title="Recent new topics")
+    context = standard_extra_context(title=u"Recent new topics")
     topics = Topic.objects.exclude(created_at__isnull=True).order_by('-created_at')
     
     resp = feeds.handle_feed_request(request, feeds.TopicFeed, query_set=topics)
     if resp: return resp
     
-    context['atom_feed_title'] = "Atom feed for all new topics."
+    context['atom_feed_title'] = u"Atom feed for all new topics."
 
     return list_detail.object_list(request, topics,
         extra_context=context, template_name='cciw/forums/topics.html',

cciw/cciwmain/views/memberadmin.py

     # Use every other character to make it shorter and friendlier
     return md5.new(settings.SECRET_KEY + email + user_name).hexdigest()[::2]
 
-
 def validate_email_username_and_hash(email, user_name, hash):
     if email_address_used(email):
         return (False, """The e-mail address is already in use.""")
         
         new_email = new_data['email']
         
-        errors = manipulator.get_validation_errors(new_data)
-        
+        errors = manipulator.get_validation_errors(new_data)        
         
         if not errors:
             # E-mail changes require verification, so fix it here

cciw/cciwmain/views/members.py

     if (request.GET.has_key('online')):
         members = members.filter(last_seen__gte=(datetime.now() - timedelta(minutes=3)))
     
-    extra_context = standard_extra_context(title='Members')
+    extra_context = standard_extra_context(title=u'Members')
     order_by = get_order_option(
         {'adj': ('date_joined',),
         'ddj': ('-date_joined',),
     except KeyError:
         pass
 
-    extra_context['atom_feed_title'] = "Atom feed for new members."
+    extra_context['atom_feed_title'] = u"Atom feed for new members."
     
     return list_detail.object_list(request, members,
         extra_context=extra_context, 
                 pass
         
     c = RequestContext(request, 
-        standard_extra_context(title="Member: " + member.user_name))
+        standard_extra_context(title=u"Member: %s" % member.user_name))
     c['member'] = member
     c['awards'] = member.personal_awards.all()
     return render_to_response('cciw/members/detail.html', context_instance=c)
     message_text = None
     
     no_messages = False
-    to_name = ''
+    to_name = u''
 
     to = None
     if current_member.user_name != member.user_name:
     if request.POST:
         # Recipient
         if to is None:
-            to_name = request.POST.get('to', '').strip()
-            if to_name == '':
+            to_name = request.POST.get('to', u'').strip()
+            if to_name == u'':
                 errors.append('No user name given.')
             else:
                 try:
                     to = Member.objects.get(user_name=to_name)
                 except Member.DoesNotExist:
-                    errors.append('The user %s could not be found' % to_name)
+                    errors.append(u'The user %s could not be found' % to_name)
 
         if to is not None and to.message_option == Member.MESSAGES_NONE:
-            errors.append('This user has chosen not to receive any messages.')
+            errors.append(u'This user has chosen not to receive any messages.')
         else:
             # Message
-            message_text = request.POST.get('message', '').strip()
-            if message_text == '':
-                errors.append('No message entered.')
+            message_text = request.POST.get('message', u'').strip()
+            if message_text == u'':
+                errors.append(u'No message entered.')
             
             # Always do a preview (for 'preview' and 'send')
             preview = bbcode.bb2xhtml(message_text)
             if len(errors) == 0 and request.POST.has_key('send'):
                 Message.send_message(to, current_member, message_text)
                 message_sent = True
-                message_text = '' # don't persist.
+                message_text = u'' # don't persist.
             else:
                 # Persist text entered, but corrected:
                 message_text = bbcode.correct(message_text)
     crumbs = [get_member_link(user_name)]
     if current_member.user_name == member.user_name:
         mode = 'send'
-        title = "Send a message"
-        crumbs.append('Messages &lt; Send | <a href="inbox/">Inbox</a> | <a href="archived/">Archived</a> &gt;')
+        title = u"Send a message"
+        crumbs.append(u'Messages &lt; Send | <a href="inbox/">Inbox</a> | <a href="archived/">Archived</a> &gt;')
         # to_name = to_name (from POST)
     else:
         mode = 'leave'
-        title = "Leave a message for %s" % member.user_name
-        crumbs.append('Send message')
+        title = u"Leave a message for %s" % member.user_name
+        crumbs.append(u'Send message')
         to_name = user_name
 
     c = RequestContext(request, standard_extra_context(title=title))    
 def _msg_del(msg):
     msg.delete()
 
+
+_id_vars_re = re.compile('msg_(\d+)')
+
 @same_member_required(1)
 def message_list(request, user_name, box):
     """View function to display inbox or archived messages."""
         
     # Deal with moves/deletes:
     if request.POST:
-        id_vars_re = re.compile('msg_\d+')
-        ids = [int(var[4:]) for var in request.POST.keys() if id_vars_re.match(var)]
+        ids = [int(m.groups()[0]) for m in map(_id_vars_re.match, request.POST.keys()) if m is not None]
         actions = {
             'delete': _msg_del,
             'inbox': _msg_move_inbox,
     extra_context = standard_extra_context()
     crumbs = [get_member_link(user_name)]
     if box == Message.MESSAGE_BOX_INBOX:
-        extra_context['title'] = "%s: Inbox" % user_name
-        crumbs.append('Messages &lt; <a href="../">Send</a> | Inbox | <a href="../archived/">Archived</a> &gt;')
+        extra_context['title'] = u"%s: Inbox" % user_name
+        crumbs.append(u'Messages &lt; <a href="../">Send</a> | Inbox | <a href="../archived/">Archived</a> &gt;')
         extra_context['show_archive_button'] = True
     else:
-        extra_context['title'] = "%s: Archived messages" % user_name
-        crumbs.append('Messages &lt; <a href="../">Send</a> | <a href="../inbox/">Inbox</a> | Archived &gt;')
+        extra_context['title'] = u"%s: Archived messages" % user_name
+        crumbs.append(u'Messages &lt; <a href="../">Send</a> | <a href="../inbox/">Inbox</a> | Archived &gt;')
         extra_context['show_move_inbox_button'] = True
      
     extra_context['show_delete_button'] = True
                                      query_set=posts)
     if resp: return resp
     
-    context = standard_extra_context(title="Recent posts by %s" % user_name)
+    context = standard_extra_context(title=u"Recent posts by %s" % user_name)
     context['member'] = member
-    crumbs = [get_member_link(user_name), 'Recent posts']
+    crumbs = [get_member_link(user_name), u'Recent posts']
     context['breadcrumb'] = create_breadcrumb(crumbs)
-    context['atom_feed_title'] = "Atom feed for posts by %s." % user_name
+    context['atom_feed_title'] = u"Atom feed for posts by %s." % user_name
 
     return list_detail.object_list(request, posts,
         extra_context=context, template_name='cciw/members/posts.html',

cciw/cciwmain/views/misc.py

         )
 
 def feedback(request):
-    c = standard_extra_context(title="Contact us")
+    c = standard_extra_context(title=u"Contact us")
     
     manipulator = FeedbackManipulator()
     
         if not errors:
             manipulator.do_html2python(new_data)
             send_feedback(new_data['email'], new_data['name'], new_data['message'])
-            c['message'] = "Thank you, your message has been sent."
+            c['message'] = u"Thank you, your message has been sent."
     else:
         errors = new_data = {}
     

cciw/cciwmain/views/tagging.py

     extra_context['showtagtext'] = True
     extra_context['showtaggedby'] = True
     extra_context['showtagtarget'] = True
-    extra_context['tag_href_prefix'] = "/tags/"
-    extra_context['atom_feed_title'] = "Atom feed for all tags."
+    extra_context['tag_href_prefix'] = u"/tags/"
+    extra_context['atom_feed_title'] = u"Atom feed for all tags."
     
     def feed_handler(request, queryset):
         return feeds.handle_feed_request(request, feeds.TagFeed, query_set=queryset)
 
 def tag_target(request, model_name, object_id):
     obj = _object_for_model_name_and_id(model_name, object_id)
-    extra_context = standard_extra_context(title='Tags for %s' % obj)
+    extra_context = standard_extra_context(title=u'Tags for %s' % obj)
     extra_context['showtagtext'] = True
     extra_context['showtaggedby'] = True
     extra_context['showtagtarget'] = False

cciw/officers/email_utils.py

     Get the email address plus name of the user, formatted for
     use in sending an email, or 'None' if no email address available
     """
-    name = (user.first_name + " " + user.last_name).strip().replace('"', '')
+    name = (u"%s %s" % (user.first_name, user.last_name)).strip().replace(u'"', u'')
     email = user.email.strip()
     if len(email) == 0:
         return None
     elif len(name) > 0:
-        return '"%s" <%s>' % (name, email)
+        return u'"%s" <%s>' % (name, email)
     else:
         return email

cciw/officers/hooks.py

     # Email to the officer
     user_email = formatted_email(application.officer)
     user_msg = (
-"""%s,
+u"""%s,
 
 For your records, here is a copy of the application you have submitted
 to CCIW. It is also attached to this email as an RTF file.
 def send_leader_email(leader_emails, application, application_text, rtf_attachment):
     subject = "CCIW application form from %s" % application.full_name
     body = \
-"""The following application form has been submitted via the
+u"""The following application form has been submitted via the
 CCIW website.  It is also attached to this email as an RTF file.
 
 """ + application_text

cciw/officers/models.py

     validators = list(kwargs.get('validator_list', ()))
     validators.append(yyyy_mm_validator)
     kwargs['validator_list'] = validators
-    kwargs['help_text'] = 'Enter the date in YYYY/MM format.'
+    kwargs['help_text'] = u'Enter the date in YYYY/MM format.'
     return models.CharField(*args, **kwargs)
 
 def AddressField(*args, **kwargs):
-    kwargs['help_text'] ='Full address, including post code and country'
+    kwargs['help_text'] = u'Full address, including post code and country'
     return models.TextField(*args, **kwargs)
 
 class ExplicitBooleanField(models.NullBooleanField):

cciw/officers/views.py

         rtf_attachment = (application_rtf_filename(app), application_rtf, 'text/rtf')
 
         msg = \
-"""Dear %s,
+u"""Dear %s,
 
 Please find attached a copy of the application you requested
  -- in plain text below and an RTF version attached.
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.