Changeset 8215 for django/branches/gis/django/contrib/admin
- Timestamp:
- 08/05/08 12:15:33 (4 months ago)
- Files:
-
- django/branches/gis (modified) (1 prop)
- django/branches/gis/django/contrib/admin/__init__.py (modified) (1 diff)
- django/branches/gis/django/contrib/admin/media/css/rtl.css (modified) (1 diff)
- django/branches/gis/django/contrib/admin/options.py (modified) (23 diffs)
- django/branches/gis/django/contrib/admin/sites.py (modified) (15 diffs)
- django/branches/gis/django/contrib/admin/templates/admin_doc/model_detail.html (modified) (1 diff)
- django/branches/gis/django/contrib/admin/templates/admin/includes/fieldset.html (modified) (1 diff)
- django/branches/gis/django/contrib/admin/templates/registration/password_reset_complete.html (copied) (copied from django/trunk/django/contrib/admin/templates/registration/password_reset_complete.html)
- django/branches/gis/django/contrib/admin/templates/registration/password_reset_confirm.html (copied) (copied from django/trunk/django/contrib/admin/templates/registration/password_reset_confirm.html)
- django/branches/gis/django/contrib/admin/templates/registration/password_reset_done.html (modified) (1 diff)
- django/branches/gis/django/contrib/admin/templates/registration/password_reset_email.html (modified) (2 diffs)
- django/branches/gis/django/contrib/admin/templates/registration/password_reset_form.html (modified) (1 diff)
- django/branches/gis/django/contrib/admin/validation.py (modified) (3 diffs)
- django/branches/gis/django/contrib/admin/views/decorators.py (modified) (5 diffs)
- django/branches/gis/django/contrib/admin/views/main.py (modified) (1 diff)
- django/branches/gis/django/contrib/admin/widgets.py (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
- Modified
- Copied
- Moved
django/branches/gis
- Property svnmerge-integrated changed from /django/trunk:1-7978 to /django/trunk:1-8214
django/branches/gis/django/contrib/admin/__init__.py
r7979 r8215 9 9 may want. 10 10 """ 11 import imp 11 12 from django.conf import settings 12 13 for app in settings.INSTALLED_APPS: 13 14 try: 14 __import__("%s.admin" % app)15 imp.find_module("admin", __import__(app, {}, {}, [app.split(".")[-1]]).__path__) 15 16 except ImportError: 16 pass 17 # there is no app admin.py, skip it 18 continue 19 __import__("%s.admin" % app) django/branches/gis/django/contrib/admin/media/css/rtl.css
r3415 r8215 45 45 .selector { float: right;} 46 46 .selector .selector-filter { text-align: right;} 47 48 /* x unsorted */ 49 .inline-related h2 { text-align:right } django/branches/gis/django/contrib/admin/options.py
r7979 r8215 1 from django import oldforms, template 2 from django import forms 1 from django import forms, template 3 2 from django.forms.formsets import all_valid 4 3 from django.forms.models import modelform_factory, inlineformset_factory … … 16 15 from django.utils.translation import ugettext as _ 17 16 from django.utils.encoding import force_unicode 18 import sets 17 try: 18 set 19 except NameError: 20 from sets import Set as set # Python 2.3 fallback 19 21 20 22 HORIZONTAL, VERTICAL = 1, 2 … … 91 93 92 94 def errors(self): 93 return mark_safe(u'\n'.join([self.form[f].errors.as_ul() for f in self.fields]) )95 return mark_safe(u'\n'.join([self.form[f].errors.as_ul() for f in self.fields]).strip('\n')) 94 96 95 97 class AdminField(object): … … 131 133 If kwargs are given, they're passed to the form Field's constructor. 132 134 """ 135 136 # If the field specifies choices, we don't need to look for special 137 # admin widgets - we just need to use a select widget of some kind. 138 if db_field.choices: 139 if db_field.name in self.radio_fields: 140 # If the field is named as a radio_field, use a RadioSelect 141 kwargs['widget'] = widgets.AdminRadioSelect( 142 choices=db_field.get_choices(include_blank=db_field.blank, 143 blank_choice=[('', _('None'))]), 144 attrs={ 145 'class': get_ul_class(self.radio_fields[db_field.name]), 146 } 147 ) 148 else: 149 # Otherwise, use the default select widget. 150 return db_field.formfield(**kwargs) 151 133 152 # For DateTimeFields, use a special field and widget. 134 153 if isinstance(db_field, models.DateTimeField): … … 163 182 else: 164 183 if isinstance(db_field, models.ManyToManyField): 165 if db_field.name in self.raw_id_fields: 184 # If it uses an intermediary model, don't show field in admin. 185 if db_field.rel.through is not None: 186 return None 187 elif db_field.name in self.raw_id_fields: 166 188 kwargs['widget'] = widgets.ManyToManyRawIdWidget(db_field.rel) 167 189 kwargs['help_text'] = '' 168 elif db_field.name in ( self.filter_vertical + self.filter_horizontal):190 elif db_field.name in (list(self.filter_vertical) + list(self.filter_horizontal)): 169 191 kwargs['widget'] = widgets.FilteredSelectMultiple(db_field.verbose_name, (db_field.name in self.filter_vertical)) 170 192 # Wrap the widget's render() method with a method that adds … … 175 197 formfield.widget = widgets.RelatedFieldWidgetWrapper(formfield.widget, db_field.rel, self.admin_site) 176 198 return formfield 177 178 if db_field.choices and db_field.name in self.radio_fields:179 kwargs['widget'] = widgets.AdminRadioSelect(180 choices=db_field.get_choices(include_blank=db_field.blank,181 blank_choice=[('', _('None'))]),182 attrs={183 'class': get_ul_class(self.radio_fields[db_field.name]),184 }185 )186 199 187 200 # For any other type of field, just call its formfield() method. … … 211 224 ordering = None 212 225 inlines = [] 213 226 214 227 # Custom templates (designed to be over-ridden in subclasses) 215 228 change_form_template = None … … 262 275 if self.filter_vertical or self.filter_horizontal: 263 276 js.extend(['js/SelectBox.js' , 'js/SelectFilter2.js']) 264 277 265 278 return forms.Media(js=['%s%s' % (settings.ADMIN_MEDIA_PREFIX, url) for url in js]) 266 279 media = property(_media) … … 346 359 pk_value = new_object._get_pk_val() 347 360 LogEntry.objects.log_action(request.user.id, ContentType.objects.get_for_model(self.model).id, pk_value, force_unicode(new_object), ADDITION) 348 msg = _('The %(name)s "%(obj)s" was added successfully.') % {'name': opts.verbose_name, 'obj': new_object}361 msg = _('The %(name)s "%(obj)s" was added successfully.') % {'name': force_unicode(opts.verbose_name), 'obj': new_object} 349 362 # Here, we distinguish between different save types by checking for 350 363 # the presence of keys in request.POST. … … 360 373 (escape(pk_value), escape(new_object))) 361 374 elif request.POST.has_key("_addanother"): 362 request.user.message_set.create(message=msg + ' ' + (_("You may add another %s below.") % opts.verbose_name))375 request.user.message_set.create(message=msg + ' ' + (_("You may add another %s below.") % force_unicode(opts.verbose_name))) 363 376 return HttpResponseRedirect(request.path) 364 377 else: … … 379 392 380 393 `form` is a bound Form instance that's verified to be valid. 381 394 382 395 `formsets` is a sequence of InlineFormSet instances that are verified to be valid. 383 396 """ … … 395 408 if form.changed_data: 396 409 change_message.append(_('Changed %s.') % get_text_list(form.changed_data, _('and'))) 397 410 398 411 if formsets: 399 412 for formset in formsets: 400 413 for added_object in formset.new_objects: 401 change_message.append(_('Added %(name)s "%(object)s".') 414 change_message.append(_('Added %(name)s "%(object)s".') 402 415 % {'name': added_object._meta.verbose_name, 403 416 'object': added_object}) 404 417 for changed_object, changed_fields in formset.changed_objects: 405 change_message.append(_('Changed %(list)s for %(name)s "%(object)s".') 406 % {'list': get_text_list(changed_fields, _('and')), 407 'name': changed_object._meta.verbose_name, 418 change_message.append(_('Changed %(list)s for %(name)s "%(object)s".') 419 % {'list': get_text_list(changed_fields, _('and')), 420 'name': changed_object._meta.verbose_name, 408 421 'object': changed_object}) 409 422 for deleted_object in formset.deleted_objects: 410 change_message.append(_('Deleted %(name)s "%(object)s".') 423 change_message.append(_('Deleted %(name)s "%(object)s".') 411 424 % {'name': deleted_object._meta.verbose_name, 412 425 'object': deleted_object}) … … 416 429 LogEntry.objects.log_action(request.user.id, ContentType.objects.get_for_model(self.model).id, pk_value, force_unicode(new_object), CHANGE, change_message) 417 430 418 msg = _('The %(name)s "%(obj)s" was changed successfully.') % {'name': opts.verbose_name, 'obj': new_object}431 msg = _('The %(name)s "%(obj)s" was changed successfully.') % {'name': force_unicode(opts.verbose_name), 'obj': new_object} 419 432 if request.POST.has_key("_continue"): 420 433 request.user.message_set.create(message=msg + ' ' + _("You may edit it again below.")) … … 424 437 return HttpResponseRedirect(request.path) 425 438 elif request.POST.has_key("_saveasnew"): 426 request.user.message_set.create(message=_('The %(name)s "%(obj)s" was added successfully. You may edit it again below.') % {'name': opts.verbose_name, 'obj': new_object})439 request.user.message_set.create(message=_('The %(name)s "%(obj)s" was added successfully. You may edit it again below.') % {'name': force_unicode(opts.verbose_name), 'obj': new_object}) 427 440 return HttpResponseRedirect("../%s/" % pk_value) 428 441 elif request.POST.has_key("_addanother"): 429 request.user.message_set.create(message=msg + ' ' + (_("You may add another %s below.") % opts.verbose_name))442 request.user.message_set.create(message=msg + ' ' + (_("You may add another %s below.") % force_unicode(opts.verbose_name))) 430 443 return HttpResponseRedirect("../add/") 431 444 else: … … 505 518 506 519 context = { 507 'title': _('Add %s') % opts.verbose_name,520 'title': _('Add %s') % force_unicode(opts.verbose_name), 508 521 'adminform': adminForm, 509 522 'is_popup': request.REQUEST.has_key('_popup'), … … 535 548 536 549 if obj is None: 537 raise Http404('%s object with primary key %r does not exist.' % ( opts.verbose_name, escape(object_id)))550 raise Http404('%s object with primary key %r does not exist.' % (force_unicode(opts.verbose_name), escape(object_id))) 538 551 539 552 if request.POST and request.POST.has_key("_saveasnew"): … … 558 571 adminForm = AdminForm(form, self.get_fieldsets(request, obj), self.prepopulated_fields) 559 572 media = self.media + adminForm.media 560 for fs in inline_formsets:561 media = media + fs.media562 573 563 574 inline_admin_formsets = [] … … 566 577 inline_admin_formset = InlineAdminFormSet(inline, formset, fieldsets) 567 578 inline_admin_formsets.append(inline_admin_formset) 579 media = media + inline_admin_formset.media 568 580 569 581 context = { 570 'title': _('Change %s') % opts.verbose_name,582 'title': _('Change %s') % force_unicode(opts.verbose_name), 571 583 'adminform': adminForm, 572 584 'object_id': object_id, … … 600 612 return render_to_response('admin/invalid_setup.html', {'title': _('Database error')}) 601 613 return HttpResponseRedirect(request.path + '?' + ERROR_FLAG + '=1') 602 614 603 615 context = { 604 616 'title': cl.title, … … 633 645 634 646 if obj is None: 635 raise Http404('%s object with primary key %r does not exist.' % ( opts.verbose_name, escape(object_id)))647 raise Http404('%s object with primary key %r does not exist.' % (force_unicode(opts.verbose_name), escape(object_id))) 636 648 637 649 # Populate deleted_objects, a data structure of all related objects that 638 650 # will also be deleted. 639 651 deleted_objects = [mark_safe(u'%s: <a href="../../%s/">%s</a>' % (escape(force_unicode(capfirst(opts.verbose_name))), quote(object_id), escape(obj))), []] 640 perms_needed = set s.Set()652 perms_needed = set() 641 653 get_deleted_objects(deleted_objects, perms_needed, request.user, obj, opts, 1, self.admin_site) 642 654 … … 651 663 return HttpResponseRedirect("../../../../") 652 664 return HttpResponseRedirect("../../") 653 665 654 666 context = { 655 667 "title": _("Are you sure?"), 656 "object_name": opts.verbose_name,668 "object_name": force_unicode(opts.verbose_name), 657 669 "object": obj, 658 670 "deleted_objects": deleted_objects, … … 682 694 'title': _('Change history: %s') % force_unicode(obj), 683 695 'action_list': action_list, 684 'module_name': capfirst( opts.verbose_name_plural),696 'module_name': capfirst(force_unicode(opts.verbose_name_plural)), 685 697 'object': obj, 686 698 'root_path': self.admin_site.root_path, … … 762 774 yield self.formset.form.base_fields[field_name] 763 775 776 def _media(self): 777 media = self.formset.media 778 for fs in self: 779 media = media + fs.media 780 return media 781 media = property(_media) 782 764 783 class InlineAdminForm(AdminForm): 765 784 """ django/branches/gis/django/contrib/admin/sites.py
r7979 r8215 1 import base64 2 import cPickle as pickle 3 import re 4 1 5 from django import http, template 2 6 from django.contrib.admin import ModelAdmin … … 9 13 from django.views.decorators.cache import never_cache 10 14 from django.conf import settings 11 import base64 12 import cPickle as pickle 13 import datetime 14 import md5 15 import re 15 from django.utils.hashcompat import md5_constructor 16 16 17 17 ERROR_MESSAGE = ugettext_lazy("Please enter a correct username and password. Note that both fields are case-sensitive.") … … 27 27 28 28 def _encode_post_data(post_data): 29 from django.conf import settings30 29 pickled = pickle.dumps(post_data) 31 pickled_md5 = md5 .new(pickled + settings.SECRET_KEY).hexdigest()30 pickled_md5 = md5_constructor(pickled + settings.SECRET_KEY).hexdigest() 32 31 return base64.encodestring(pickled + pickled_md5) 33 32 34 33 def _decode_post_data(encoded_data): 35 from django.conf import settings36 34 encoded_data = base64.decodestring(encoded_data) 37 35 pickled, tamper_check = encoded_data[:-32], encoded_data[-32:] 38 if md5 .new(pickled + settings.SECRET_KEY).hexdigest() != tamper_check:36 if md5_constructor(pickled + settings.SECRET_KEY).hexdigest() != tamper_check: 39 37 from django.core.exceptions import SuspiciousOperation 40 38 raise SuspiciousOperation, "User may have tampered with session cookie." … … 48 46 that presents a full admin interface for the collection of registered models. 49 47 """ 50 48 51 49 index_template = None 52 50 login_template = None 53 51 54 52 def __init__(self): 55 53 self._registry = {} # model_class class -> admin_class instance … … 67 65 If a model is already registered, this will raise AlreadyRegistered. 68 66 """ 69 do_validate = admin_class and settings.DEBUG 70 if do_validate: 71 # don't import the humongous validation code unless required 67 # Don't import the humongous validation code unless required 68 if admin_class and settings.DEBUG: 72 69 from django.contrib.admin.validation import validate 73 admin_class = admin_class or ModelAdmin 74 # TODO: Handle options 70 else: 71 validate = lambda model, adminclass: None 72 73 if not admin_class: 74 admin_class = ModelAdmin 75 75 if isinstance(model_or_iterable, ModelBase): 76 76 model_or_iterable = [model_or_iterable] … … 78 78 if model in self._registry: 79 79 raise AlreadyRegistered('The model %s is already registered' % model.__name__) 80 if do_validate: 81 validate(admin_class, model) 80 81 # If we got **options then dynamically construct a subclass of 82 # admin_class with those **options. 83 if options: 84 # For reasons I don't quite understand, without a __module__ 85 # the created class appears to "live" in the wrong place, 86 # which causes issues later on. 87 options['__module__'] = __name__ 88 admin_class = type("%sAdmin" % model.__name__, (admin_class,), options) 89 90 # Validate (which might be a no-op) 91 validate(admin_class, model) 92 93 # Instantiate the admin class to save in the registry 82 94 self._registry[model] = admin_class(model, self) 83 95 … … 103 115 104 116 def root(self, request, url): 105 """ 117 """ 106 118 Handles main URL routing for the admin app. 107 119 … … 110 122 if request.method == 'GET' and not request.path.endswith('/'): 111 123 return http.HttpResponseRedirect(request.path + '/') 112 124 113 125 # Figure out the admin base URL path and stash it for later use 114 126 self.root_path = re.sub(re.escape(url) + '$', '', request.path) 115 127 116 128 url = url.rstrip('/') # Trim trailing slash, if it exists. 117 129 … … 119 131 if url == 'logout': 120 132 return self.logout(request) 121 133 122 134 # Check permission to continue or display login form. 123 135 if not self.has_permission(request): … … 140 152 if match: 141 153 return self.user_change_password(request, match.group(1)) 142 154 143 155 if '/' in url: 144 156 return self.model_page(request, *url.split('/', 2)) … … 190 202 generated JavaScript will be leaner and faster. 191 203 """ 192 from django.conf import settings193 204 if settings.USE_I18N: 194 205 from django.views.i18n import javascript_catalog … … 250 261 if user.is_active and user.is_staff: 251 262 login(request, user) 252 # TODO: set last_login with an event.253 user.last_login = datetime.datetime.now()254 user.save()255 263 if request.POST.has_key('post_data'): 256 264 post_data = _decode_post_data(request.POST['post_data']) … … 309 317 for app in app_list: 310 318 app['models'].sort(lambda x, y: cmp(x['name'], y['name'])) 311 319 312 320 context = { 313 321 'title': _('Site administration'), … … 316 324 } 317 325 context.update(extra_context or {}) 318 return render_to_response(self.index_template or 'admin/index.html', context, 326 return render_to_response(self.index_template or 'admin/index.html', context, 319 327 context_instance=template.RequestContext(request) 320 328 ) … … 331 339 else: 332 340 post_data = _encode_post_data({}) 333 341 334 342 context = { 335 343 'title': _('Log in'), django/branches/gis/django/contrib/admin/templates/admin_doc/model_detail.html
r7354 r8215 35 35 <td>{{ field.name }}</td> 36 36 <td>{{ field.data_type }}</td> 37 <td>{% if field.verbose %}{{ field.verbose }}{% endif %}{% if field.help_text %} - {{ field.help_text }}{% endif %}</td>37 <td>{% if field.verbose %}{{ field.verbose }}{% endif %}{% if field.help_text %} - {{ field.help_text|safe }}{% endif %}</td> 38 38 </tr> 39 39 {% endfor %} django/branches/gis/django/contrib/admin/templates/admin/includes/fieldset.html
r7979 r8215 1 1 <fieldset class="module aligned {{ fieldset.classes }}"> 2 2 {% if fieldset.name %}<h2>{{ fieldset.name }}</h2>{% endif %} 3 {% if fieldset.description %}<div class="description">{{ fieldset.description }}</div>{% endif %}3 {% if fieldset.description %}<div class="description">{{ fieldset.description|safe }}</div>{% endif %} 4 4 {% for line in fieldset %} 5 5 <div class="form-row{% if line.errors %} errors{% endif %} {% for field in line %}{{ field.field.name }} {% endfor %} "> django/branches/gis/django/contrib/admin/templates/registration/password_reset_done.html
r7354 r8215 10 10 <h1>{% trans 'Password reset successful' %}</h1> 11 11 12 <p>{% trans "We've e-mailed a newpassword to the e-mail address you submitted. You should be receiving it shortly." %}</p>12 <p>{% trans "We've e-mailed you instructions for setting your password to the e-mail address you submitted. You should be receiving it shortly." %}</p> 13 13 14 14 {% endblock %} django/branches/gis/django/contrib/admin/templates/registration/password_reset_email.html
r7354 r8215 1 {% load i18n %} 1 {% load i18n %}{% autoescape off %} 2 2 {% trans "You're receiving this e-mail because you requested a password reset" %} 3 3 {% blocktrans %}for your user account at {{ site_name }}{% endblocktrans %}. 4 4 5 {% blocktrans %}Your new password is: {{ new_password }}{% endblocktrans %} 6 7 {% trans "Feel free to change this password by going to this page:" %} 8 9 http://{{ domain }}/password_change/ 10 5 {% trans "Please go to the following page and choose a new password:" %} 6 {% block reset_link %} 7 {{ protocol }}://{{ domain }}/reset/{{ uid }}-{{ token }}/ 8 {% endblock %} 11 9 {% trans "Your username, in case you've forgotten:" %} {{ user.username }} 12 10 … … 14 12 15 13 {% blocktrans %}The {{ site_name }} team{% endblocktrans %} 14 15 {% endautoescape %} django/branches/gis/django/contrib/admin/templates/registration/password_reset_form.html
r7979 r8215 10 10 <h1>{% trans "Password reset" %}</h1> 11 11 12 <p>{% trans "Forgotten your password? Enter your e-mail address below, and we'll reset your password and e-mail the new one to you." %}</p>12 <p>{% trans "Forgotten your password? Enter your e-mail address below, and we'll e-mail instructions for setting a new one." %}</p> 13 13 14 14 <form action="" method="post"> django/branches/gis/django/contrib/admin/validation.py
r7979 r8215 1 try: 2 set 3 except NameError: 4 from sets import Set as set # Python 2.3 fallback 1 5 2 6 from django.core.exceptions import ImproperlyConfigured … … 166 170 if cls.fieldsets: 167 171 raise ImproperlyConfigured('Both fieldsets and fields are specified in %s.' % cls.__name__) 172 if len(cls.fields) > len(set(cls.fields)): 173 raise ImproperlyConfigured('There are duplicate field(s) in %s.fields' % cls.__name__) 168 174 169 175 # fieldsets … … 180 186 "%s.fieldsets[%d][1] field options dict." 181 187 % (cls.__name__, idx)) 182 for field in flatten_fieldsets(cls.fieldsets): 188 flattened_fieldsets = flatten_fieldsets(cls.fieldsets) 189 if len(flattened_fieldsets) > len(set(flattened_fieldsets)): 190 raise ImproperlyConfigured('There are duplicate field(s) in %s.fieldsets' % cls.__name__) 191 for field in flattened_fieldsets: 183 192 _check_form_field_existsw("fieldsets[%d][1]['fields']" % idx, field) 184 193 django/branches/gis/django/contrib/admin/views/decorators.py
r7979 r8215 1 1 import base64 2 import md53 2 import cPickle as pickle 4 3 try: … … 13 12 from django.shortcuts import render_to_response 14 13 from django.utils.translation import ugettext_lazy, ugettext as _ 14 from django.utils.hashcompat import md5_constructor 15 15 16 16 ERROR_MESSAGE = ugettext_lazy("Please enter a correct username and password. Note that both fields are case-sensitive.") … … 36 36 def _encode_post_data(post_data): 37 37 pickled = pickle.dumps(post_data) 38 pickled_md5 = md5 .new(pickled + settings.SECRET_KEY).hexdigest()38 pickled_md5 = md5_constructor(pickled + settings.SECRET_KEY).hexdigest() 39 39 return base64.encodestring(pickled + pickled_md5) 40 40 … … 42 42 encoded_data = base64.decodestring(encoded_data) 43 43 pickled, tamper_check = encoded_data[:-32], encoded_data[-32:] 44 if md5 .new(pickled + settings.SECRET_KEY).hexdigest() != tamper_check:44 if md5_constructor(pickled + settings.SECRET_KEY).hexdigest() != tamper_check: 45 45 from django.core.exceptions import SuspiciousOperation 46 46 raise SuspiciousOperation, "User may have tampered with session cookie." … … 88 88 message = _("Your e-mail address is not your username. Try '%s' instead.") % users[0].username 89 89 else: 90 # Either we cannot find the user, or if more than 1 90 # Either we cannot find the user, or if more than 1 91 91 # we cannot guess which user is the correct one. 92 92 message = _("Usernames cannot contain the '@' character.") django/branches/gis/django/contrib/admin/views/main.py
r7979 r8215 7 7 from django.utils.encoding import force_unicode, smart_str 8 8 from django.utils.translation import ugettext 9 from django.utils.safestring import mark_safe10 9 from django.utils.http import urlencode 11 10 import operator django/branches/gis/django/contrib/admin/widgets.py
r7979 r8215 8 8 from django.forms.widgets import RadioFieldRenderer 9 9 from django.forms.util import flatatt 10 from django.utils.datastructures import MultiValueDict 11 from django.utils.text import capfirst, truncate_words 10 from django.utils.text import truncate_words 12 11 from django.utils.translation import ugettext as _ 13 12 from django.utils.safestring import mark_safe
