Commit a3869982 authored by Leonidas Poulopoulos's avatar Leonidas Poulopoulos
Browse files

Implemented alt login for users and institutions overview. Closes #3120

Implemented alternate login for Helpdesk and Management overview on
Users and Institutions. It is based on a permission set by admin.
The overview directory should be protected the same manner admin is
parents 34051323 b2f8faba
# encoding: utf-8
import datetime
from south.db import db
from south.v2 import DataMigration
from django.db import models
class Migration(DataMigration):
def forwards(self, orm):
"Write your forwards methods here."
ct, created = orm['contenttypes.ContentType'].objects.get_or_create(
model='userprofile', app_label='accounts') # model must be lowercase!
perm, created = orm['auth.permission'].objects.get_or_create(
content_type=ct, codename='overview', defaults=dict(name=u'Helpdesk/Managerial Overview'))
def backwards(self, orm):
"Write your backwards methods here."
models = {
'accounts.userprofile': {
'Meta': {'object_name': 'UserProfile'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'institution': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['edumanage.Institution']"}),
'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True'})
},
'auth.group': {
'Meta': {'object_name': 'Group'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
'auth.message': {
'Meta': {'object_name': 'Message'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'message': ('django.db.models.fields.TextField', [], {}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'_message_set'", 'to': "orm['auth.User']"})
},
'auth.permission': {
'Meta': {'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
'auth.user': {
'Meta': {'object_name': 'User'},
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'blank': 'True'}),
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'})
},
'contenttypes.contenttype': {
'Meta': {'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
},
'edumanage.contact': {
'Meta': {'object_name': 'Contact'},
'email': ('django.db.models.fields.CharField', [], {'max_length': '80', 'db_column': "'contact_email'"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_column': "'contact_name'"}),
'phone': ('django.db.models.fields.CharField', [], {'max_length': '80', 'db_column': "'contact_phone'"})
},
'edumanage.institution': {
'Meta': {'object_name': 'Institution'},
'ertype': ('django.db.models.fields.PositiveIntegerField', [], {'max_length': '1', 'db_column': "'type'"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'realmid': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['edumanage.Realm']"})
},
'edumanage.name_i18n': {
'Meta': {'object_name': 'Name_i18n'},
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']", 'null': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'lang': ('django.db.models.fields.CharField', [], {'max_length': '5'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '80'}),
'object_id': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'})
},
'edumanage.realm': {
'Meta': {'object_name': 'Realm'},
'address_city': ('django.db.models.fields.CharField', [], {'max_length': '24'}),
'address_street': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
'contact': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['edumanage.Contact']", 'symmetrical': 'False'}),
'country': ('django.db.models.fields.CharField', [], {'max_length': '2'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'stype': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0', 'max_length': '1'}),
'ts': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'})
},
'edumanage.url_i18n': {
'Meta': {'object_name': 'URL_i18n'},
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']", 'null': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'lang': ('django.db.models.fields.CharField', [], {'max_length': '5'}),
'object_id': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}),
'url': ('django.db.models.fields.CharField', [], {'max_length': '180', 'db_column': "'URL'"}),
'urltype': ('django.db.models.fields.CharField', [], {'max_length': '10', 'db_column': "'type'"})
}
}
complete_apps = ['contenttypes', 'auth', 'accounts']
......@@ -7,6 +7,11 @@ class UserProfile(models.Model):
user = models.OneToOneField(User)
institution = models.ForeignKey(Institution)
class Meta:
permissions = (
("overview", "Can see registered user and respective institutions"),
)
def __unicode__(self):
return "%s:%s" %(self.user.username, self.institution)
# -*- coding: utf-8 -*- vim:encoding=utf-8:
# vim: tabstop=4:shiftwidth=4:softtabstop=4:expandtab
import ldap
from django.contrib.auth.models import User, UserManager, Permission, Group
from django.conf import settings
class ldapBackend:
def authenticate(self, username=None, password=None):
ldap_settings = settings.LDAP_AUTH_SETTINGS
# Authenticate the base user so we can search
# Go through servers using their corresponding DNs
for ldap_setting in ldap_settings:
uri = ldap_setting['url']
base = ldap_setting['base']
try:
l = ldap.initialize(uri)
l.start_tls_s()
except ldap.LDAPError:
continue
else:
l.protocol_version = ldap.VERSION3
l.simple_bind_s()
myUser = self._auth_user(base, username, password, l)
if not myUser:
continue
return myUser
def _auth_user(self, base, username, password, l):
scope = ldap.SCOPE_SUBTREE
filter = "uid=" + username
ret = ['dn', 'mail', 'givenName', 'sn']
try:
result_id = l.search(base, scope, filter, ret)
result_type, result_data = l.result(result_id, 0)
# If the user does not exist in LDAP, Fail.
if (len(result_data) != 1):
return None
# We prevent a situation where binding could raise an exception with empty password
# Plus security...
if (len(password) == 0):
return None
# Attempt to bind to the user's DN
l.simple_bind_s(result_data[0][0], password)
# Checking to see if user has an e-mail
try:
mail = result_data[0][1]['mail'][0]
except:
mail = ''
# The user existed and authenticated. Get the user record
try:
user = User.objects.get(username__exact=username)
user.email = mail
user.first_name = result_data[0][1]['givenName'][0]
user.last_name = result_data[0][1]['sn'][0]
user.is_active = True
user.save()
# The user did not exist. Create one with no privileges
except:
user = User.objects.create_user(username, mail, None)
user.first_name = result_data[0][1]['givenName'][0]
user.last_name = result_data[0][1]['sn'][0]
user.is_staff = settings.LDAP_AUTH_IS_STAFF
user.is_superuser = False
user.is_active = True
if settings.LDAP_AUTH_GROUP:
try:
g = Group.objects.get(name=settings.LDAP_AUTH_GROUP)
user.groups.add(g)
user.save()
except:
pass
return user
except ldap.INVALID_CREDENTIALS:
return None
def get_user(self, user_id):
try:
return User.objects.get(pk=user_id)
except User.DoesNotExist:
return None
......@@ -914,6 +914,24 @@ def get_service_points(request):
else:
return HttpResponseNotFound('<h1>Something went really wrong</h1>')
@never_cache
def overview(request):
user = request.user
if user.is_authenticated():
if user.has_perm('accounts.overview'):
users = User.objects.all()
institutions = Institution.objects.all()
return render_to_response('overview/index.html', {'users': users, 'institutions': institutions},
context_instance=RequestContext(request))
else:
violation=True
return render_to_response('overview/index.html', {'violation': violation},
context_instance=RequestContext(request))
else:
return HttpResponseRedirect(reverse("altlogin"))
@never_cache
def get_all_services(request):
lang = request.LANGUAGE_CODE
......
......@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: 0.8\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2012-12-14 12:17+0200\n"
"POT-Creation-Date: 2012-12-14 12:36+0200\n"
"PO-Revision-Date: 2012-10-23 11:29+0300\n"
"Last-Translator: Leonidas Poulopoulos <leopoul@noc.grnet.gr>\n"
"Language-Team: Greek <leopoul@noc.grnet.gr>\n"
......@@ -148,23 +148,23 @@ msgid ""
"%(numid)s"
msgstr ""
#: edumanage/views.py:984
#: edumanage/views.py:1002
msgid ""
"Your idP should release the eduPersonPrincipalName attribute towards this "
"service<br>"
msgstr ""
#: edumanage/views.py:986
#: edumanage/views.py:1004
msgid ""
"Your idP should release an appropriate eduPersonEntitlement attribute "
"towards this service<br>"
msgstr ""
#: edumanage/views.py:988
#: edumanage/views.py:1006
msgid "Your idP should release the mail attribute towards this service"
msgstr ""
#: edumanage/views.py:1015 edumanage/views.py:1059
#: edumanage/views.py:1033 edumanage/views.py:1077
#, python-format
msgid ""
"User account <strong>%s</strong> is pending activation. Administrators have "
......@@ -178,19 +178,19 @@ msgstr ""
"μεγάλο<br>χρονικό διάστημα, επικοινωνήστε με τον τεχνικό σας υπεύθυνο ή το "
"Helpdesk της ΕΔΕΤ"
#: edumanage/views.py:1019
#: edumanage/views.py:1037
#, python-format
msgid ""
"Something went wrong during user authentication. Contact your administrator "
"%s"
msgstr ""
#: edumanage/views.py:1023
#: edumanage/views.py:1041
#, python-format
msgid "Invalid login procedure. Error: %s"
msgstr "Λανθασμένη διαδικασία εισαγωγής στη διαχείριση. Σφάλμα: %s"
#: edumanage/views.py:1049
#: edumanage/views.py:1067
msgid ""
"Violation warning: User account is already associated with an institution."
"The event has been logged and our administrators will be notified about it"
......@@ -302,11 +302,12 @@ msgstr "επικοινωνήστε με το Helpdesk του ΕΔΕΤ"
#: templates/edumanage/institution_edit.html:87
#: templates/edumanage/servers.html:118 templates/edumanage/services.html:114
#: templates/edumanage/services_edit.html:375 templates/front/index.html:170
#: templates/overview/index.html:170 templates/overview/index.html.py:194
msgid "Name"
msgstr "Όνομα"
#: templates/edumanage/add_user.html:14 templates/edumanage/contacts.html:115
#: templates/edumanage/contacts_edit.html:41
#: templates/edumanage/contacts_edit.html:41 templates/overview/index.html:169
msgid "Email"
msgstr ""
......@@ -321,21 +322,23 @@ msgstr "Τηλέφωνο"
#: templates/edumanage/institution_edit.html:115
#: templates/edumanage/service_details.html:137
#: templates/edumanage/services_edit.html:414
#: templates/edumanage/welcome.html:217
#: templates/edumanage/welcome.html:217 templates/overview/index.html:196
msgid "Contacts"
msgstr "Επαφές"
#: templates/edumanage/contacts.html:35
#: templates/edumanage/instrealmmons.html:39
#: templates/edumanage/realms.html:32 templates/edumanage/servers.html:38
#: templates/edumanage/services.html:41
#: templates/edumanage/services.html:41 templates/overview/index.html:42
#: templates/overview/index.html.py:86
msgid "Display"
msgstr "Προβολή"
#: templates/edumanage/contacts.html:35
#: templates/edumanage/instrealmmons.html:39
#: templates/edumanage/realms.html:32 templates/edumanage/servers.html:38
#: templates/edumanage/services.html:41
#: templates/edumanage/services.html:41 templates/overview/index.html:42
#: templates/overview/index.html.py:86
msgid "All"
msgstr "Όλα"
......@@ -346,49 +349,56 @@ msgstr "επαφές"
#: templates/edumanage/contacts.html:37
#: templates/edumanage/instrealmmons.html:41
#: templates/edumanage/realms.html:34 templates/edumanage/servers.html:40
#: templates/edumanage/services.html:43
#: templates/edumanage/services.html:43 templates/overview/index.html:44
#: templates/overview/index.html.py:88
msgid "No records to display"
msgstr "Δεν υπάρχουν δεδομένα για προβολή"
#: templates/edumanage/contacts.html:39
#: templates/edumanage/instrealmmons.html:43
#: templates/edumanage/realms.html:36 templates/edumanage/servers.html:42
#: templates/edumanage/services.html:45
#: templates/edumanage/services.html:45 templates/overview/index.html:46
#: templates/overview/index.html.py:90
msgid "Showing 0 to 0 of 0 entries"
msgstr "Προβολή 0 έως 0 από 0 εγγραφές"
#: templates/edumanage/contacts.html:42
#: templates/edumanage/instrealmmons.html:46
#: templates/edumanage/realms.html:39 templates/edumanage/servers.html:45
#: templates/edumanage/services.html:48
#: templates/edumanage/services.html:48 templates/overview/index.html:49
#: templates/overview/index.html.py:93
msgid "Search:"
msgstr "Αναζήτηση"
#: templates/edumanage/contacts.html:45
#: templates/edumanage/instrealmmons.html:49
#: templates/edumanage/realms.html:42 templates/edumanage/servers.html:48
#: templates/edumanage/services.html:51
#: templates/edumanage/services.html:51 templates/overview/index.html:52
#: templates/overview/index.html.py:96
msgid "First"
msgstr "Πρώτη"
#: templates/edumanage/contacts.html:46
#: templates/edumanage/instrealmmons.html:50
#: templates/edumanage/realms.html:43 templates/edumanage/servers.html:49
#: templates/edumanage/services.html:52
#: templates/edumanage/services.html:52 templates/overview/index.html:53
#: templates/overview/index.html.py:97
msgid "Previous"
msgstr "Προηγούμενο"
#: templates/edumanage/contacts.html:47
#: templates/edumanage/instrealmmons.html:51
#: templates/edumanage/realms.html:44 templates/edumanage/servers.html:50
#: templates/edumanage/services.html:53
#: templates/edumanage/services.html:53 templates/overview/index.html:54
#: templates/overview/index.html.py:98
msgid "Next"
msgstr "Επόμενο"
#: templates/edumanage/contacts.html:48
#: templates/edumanage/instrealmmons.html:52
#: templates/edumanage/realms.html:45 templates/edumanage/servers.html:51
#: templates/edumanage/services.html:54
#: templates/edumanage/services.html:54 templates/overview/index.html:55
#: templates/overview/index.html.py:99
msgid "Last"
msgstr "Τελευταία"
......@@ -481,7 +491,7 @@ msgstr "Εφαρμογή"
#: templates/edumanage/institution.html:13
#: templates/edumanage/institution.html:18
#: templates/edumanage/institution_edit.html:6
#: templates/edumanage/welcome.html:200
#: templates/edumanage/welcome.html:200 templates/overview/index.html:172
#: templates/registration/activate_edit.html:36
#: templates/registration/select_institution.html:46
msgid "Institution"
......@@ -492,7 +502,7 @@ msgstr "Φορέας"
#: templates/edumanage/instrealmmons.html:148
#: templates/edumanage/server_details.html:31
#: templates/edumanage/servers.html:119
#: templates/edumanage/servers_edit.html:34
#: templates/edumanage/servers_edit.html:34 templates/overview/index.html:195
msgid "Type"
msgstr "Τύπος"
......@@ -641,6 +651,7 @@ msgid "EAP2 Method"
msgstr ""
#: templates/edumanage/monlocauthpar_edit.html:59
#: templates/overview/index.html:168
msgid "Username"
msgstr "Όνομα Χρήστη"
......@@ -765,6 +776,7 @@ msgstr "Όνομα Τοποθεσίας"
#: templates/edumanage/service_details.html:122
#: templates/edumanage/services.html:115 templates/front/index.html:173
#: templates/overview/index.html:197
msgid "Address"
msgstr "Διεύθυνση"
......@@ -875,7 +887,8 @@ msgstr "Στοιχεία"
msgid "More..."
msgstr "Περισσότερα"
#: templates/edumanage/welcome.html:187
#: templates/edumanage/welcome.html:187 templates/overview/index.html:122
#: templates/overview/index.html.py:134
msgid "Logout"
msgstr "Έξοδος"
......@@ -1037,6 +1050,50 @@ msgstr ""
msgid "Eduroam Wolrdwide"
msgstr ""
#: templates/overview/index.html:42
msgid "users"
msgstr ""
#: templates/overview/index.html:86
msgid "institutions"
msgstr "φορείς"
#: templates/overview/index.html:119 templates/overview/index.html.py:131
msgid "Overview"
msgstr "Επισκόπηση"
#: templates/overview/index.html:150
msgid "Users - Institutions"
msgstr "Χρήστες - Φορείς"
#: templates/overview/index.html:157
msgid "Users"
msgstr "Χρήστες"
#: templates/overview/index.html:158
msgid "Institutions"
msgstr "Φορείς"
#: templates/overview/index.html:171
msgid "Status"
msgstr "Κατάσταση"
#: templates/overview/index.html:181
msgid "Active"
msgstr "Ενεργός"
#: templates/overview/index.html:181
msgid "Inactive"
msgstr "Ανενεργός"
#: templates/overview/index.html:181
msgid "Key Expired"
msgstr ""
#: templates/overview/index.html:198
msgid "Admin Users"
msgstr "Διαχειριστές"
#: templates/registration/activate.html:3
#: templates/registration/activation_complete.html:3
#: templates/registration/activation_complete.html:27
......
......@@ -77,6 +77,7 @@ SECRET_KEY = 'x)gmuyo2h=l@tmpyh4b(-!gki%@u$-=3^@z+vf&&!ci7$*!+k9'
AUTHENTICATION_BACKENDS = (
'eduroam.djangobackends.shibauthBackend.shibauthBackend',
'django.contrib.auth.backends.ModelBackend',
'grnet.djangobackends.ldapBackend.ldapBackend',
)
......@@ -167,3 +168,13 @@ TINYMCE_DEFAULT_CONFIG = {
CACHE_BACKEND = 'memcached://127.0.0.1:11211/?timeout=5184000'
# Overview LDAP
LDAP_AUTH_SETTINGS = (
{'url': 'ldap://ds.example.com', 'base': 'dc=dept,dc=example,dc=com'},
)
# If defined as a string new users will belong in this group. Group must exist
LDAP_AUTH_GROUP = None
# Whether new users will have admin access
LDAP_AUTH_IS_STAFF = False
{% extends "base.html"%}
{% load i18n %}
{% load tolocale %}
{% load i18n %}
{% block extrahead %}
<script type="text/javascript" src="/static/js/jquery.dataTables.min.js"></script>
<script type="text/javascript" src="/static/js/datatables_bootstrap.js"></script>
<style>
.tab-content {
overflow: visible;
}
</style>
<script type="text/javascript">
$(document).ready(function(){
var oTableU = $('#usertable').dataTable({
"sPaginationType": "bootstrap",
"sDom": "<'row-fluid'<'span6'l><'span6'f>r>t<'row-fluid'<'span6'i><'span6'p>>",
"aoColumns": [{
"bSearchable": true,
"bSortable": true
}, {
"bSearchable": true,
"bSortable": true
}, {
"bSearchable": true,
"bSortable": true
}, {
"bSearchable": true,
"bSortable": true
}, {
"bSearchable": true,
"bSortable": true
}],
"aaSorting": [[0, 'desc']],
"iDisplayLength": 25,
"oSearch": {"bSmart": false, "bRegex":true},
"oLanguage": {
"sLengthMenu": '{% trans "Display" %} <select><option value="25">25</option><option value="50">50</option><option value="-1">{% trans "All" %}</option></select> {% trans "users" %}',
"sProcessing": "Processing...",
"sZeroRecords": '{% trans "No records to display" %}',
"sInfo": "Showing _START_ to _END_ of _TOTAL_ entries",
"sInfoEmpty": "{% trans "Showing 0 to 0 of 0 entries" %}",
"sInfoFiltered": "(filtered from _MAX_ total entries)",
"sInfoPostFix": "",
"sSearch": '{% trans "Search:" %}',
"sUrl": "",
"oPaginate": {
"sFirst": '{% trans "First" %}',
"sPrevious": '{% trans "Previous" %}',
"sNext": '{% trans "Next" %}',
"sLast": '{% trans "Last" %}'
}
}
});
oTableU.fnDraw();
var oTableI = $('#insttable').dataTable({
"sPaginationType": "bootstrap",
"sDom": "<'row-fluid'<'span6'l><'span6'f>r>t<'row-fluid'<'span6'i><'span6'p>>",
"aoColumns": [{
"bSearchable": true,
"bSortable": true
}, {
"bSearchable": true,
"bSortable": true
}, {
"bSearchable": true,
"bSortable": true
}, {
"bSearchable": true,
"bSortable": true
}, {
"bSearchable": true,
"bSortable": true
}],
"aaSorting": [[0, 'desc']],
"iDisplayLength": 25,
"oSearch": {"bSmart": false, "bRegex":true},
"oLanguage": {
"sLengthMenu": '{% trans "Display" %} <select><option value="25">25</option><option value="50">50</option><option value="-1">{% trans "All" %}</option></select> {% trans "institutions" %}',
"sProcessing": "Processing...",
"sZeroRecords": '{% trans "No records to display" %}',
"sInfo": "Showing _START_ to _END_ of _TOTAL_ entries",
"sInfoEmpty": "{% trans "Showing 0 to 0 of 0 entries" %}",
"sInfoFiltered": "(filtered from _MAX_ total entries)",
"sInfoPostFix": "",
"sSearch": '{% trans "Search:" %}',
"sUrl": "",
"oPaginate": {
"sFirst": '{% trans "First" %}',
"sPrevious": '{% trans "Previous" %}',
"sNext": '{% trans "Next" %}',
"sLast": '{% trans "Last" %}'
}