Browse Source

Advanced search

Rafał Pitoń 11 years ago
parent
commit
6ffa44fad5
70 changed files with 3099 additions and 185 deletions
  1. 51 21
      misago/apps/search/forms.py
  2. 4 1
      misago/apps/search/urls.py
  3. 189 77
      misago/apps/search/views.py
  4. 2 1
      misago/forms/__init__.py
  5. 12 1
      misago/forms/fields.py
  6. 6 18
      misago/forms/forms.py
  7. 23 7
      misago/search.py
  8. 5 1
      static/cranefly/css/cranefly.css
  9. 1 0
      static/cranefly/css/cranefly.less
  10. 30 6
      static/cranefly/css/cranefly/search.less
  11. 481 0
      static/cranefly/css/datepicker.css
  12. 0 1
      static/cranefly/css/style.css
  13. 1395 0
      static/cranefly/js/bootstrap-datepicker.js
  14. 15 0
      static/cranefly/js/locales/bootstrap-datepicker.ar.js
  15. 14 0
      static/cranefly/js/locales/bootstrap-datepicker.bg.js
  16. 14 0
      static/cranefly/js/locales/bootstrap-datepicker.ca.js
  17. 15 0
      static/cranefly/js/locales/bootstrap-datepicker.cs.js
  18. 14 0
      static/cranefly/js/locales/bootstrap-datepicker.cy.js
  19. 14 0
      static/cranefly/js/locales/bootstrap-datepicker.da.js
  20. 17 0
      static/cranefly/js/locales/bootstrap-datepicker.de.js
  21. 13 0
      static/cranefly/js/locales/bootstrap-datepicker.el.js
  22. 14 0
      static/cranefly/js/locales/bootstrap-datepicker.es.js
  23. 14 0
      static/cranefly/js/locales/bootstrap-datepicker.et.js
  24. 16 0
      static/cranefly/js/locales/bootstrap-datepicker.fi.js
  25. 17 0
      static/cranefly/js/locales/bootstrap-datepicker.fr.js
  26. 15 0
      static/cranefly/js/locales/bootstrap-datepicker.he.js
  27. 13 0
      static/cranefly/js/locales/bootstrap-datepicker.hr.js
  28. 16 0
      static/cranefly/js/locales/bootstrap-datepicker.hu.js
  29. 15 0
      static/cranefly/js/locales/bootstrap-datepicker.id.js
  30. 14 0
      static/cranefly/js/locales/bootstrap-datepicker.is.js
  31. 16 0
      static/cranefly/js/locales/bootstrap-datepicker.it.js
  32. 15 0
      static/cranefly/js/locales/bootstrap-datepicker.ja.js
  33. 15 0
      static/cranefly/js/locales/bootstrap-datepicker.ka.js
  34. 13 0
      static/cranefly/js/locales/bootstrap-datepicker.kr.js
  35. 16 0
      static/cranefly/js/locales/bootstrap-datepicker.lt.js
  36. 16 0
      static/cranefly/js/locales/bootstrap-datepicker.lv.js
  37. 14 0
      static/cranefly/js/locales/bootstrap-datepicker.mk.js
  38. 14 0
      static/cranefly/js/locales/bootstrap-datepicker.ms.js
  39. 14 0
      static/cranefly/js/locales/bootstrap-datepicker.nb.js
  40. 16 0
      static/cranefly/js/locales/bootstrap-datepicker.nl-BE.js
  41. 14 0
      static/cranefly/js/locales/bootstrap-datepicker.nl.js
  42. 15 0
      static/cranefly/js/locales/bootstrap-datepicker.no.js
  43. 15 0
      static/cranefly/js/locales/bootstrap-datepicker.pl.js
  44. 15 0
      static/cranefly/js/locales/bootstrap-datepicker.pt-BR.js
  45. 16 0
      static/cranefly/js/locales/bootstrap-datepicker.pt.js
  46. 15 0
      static/cranefly/js/locales/bootstrap-datepicker.ro.js
  47. 14 0
      static/cranefly/js/locales/bootstrap-datepicker.rs-latin.js
  48. 14 0
      static/cranefly/js/locales/bootstrap-datepicker.rs.js
  49. 15 0
      static/cranefly/js/locales/bootstrap-datepicker.ru.js
  50. 15 0
      static/cranefly/js/locales/bootstrap-datepicker.sk.js
  51. 14 0
      static/cranefly/js/locales/bootstrap-datepicker.sl.js
  52. 15 0
      static/cranefly/js/locales/bootstrap-datepicker.sq.js
  53. 16 0
      static/cranefly/js/locales/bootstrap-datepicker.sv.js
  54. 15 0
      static/cranefly/js/locales/bootstrap-datepicker.sw.js
  55. 14 0
      static/cranefly/js/locales/bootstrap-datepicker.th.js
  56. 16 0
      static/cranefly/js/locales/bootstrap-datepicker.tr.js
  57. 15 0
      static/cranefly/js/locales/bootstrap-datepicker.ua.js
  58. 14 0
      static/cranefly/js/locales/bootstrap-datepicker.uk.js
  59. 16 0
      static/cranefly/js/locales/bootstrap-datepicker.zh-CN.js
  60. 17 0
      static/cranefly/js/locales/bootstrap-datepicker.zh-TW.js
  61. 6 6
      templates/cranefly/layout.html
  62. 0 26
      templates/cranefly/macros.html
  63. 0 17
      templates/cranefly/search/home.html
  64. 15 0
      templates/cranefly/search/layout.html
  65. 10 2
      templates/cranefly/search/results.html
  66. 37 0
      templates/cranefly/search/search_form.html
  67. 47 0
      templates/cranefly/search/search_forums.html
  68. 39 0
      templates/cranefly/search/search_private_threads.html
  69. 41 0
      templates/cranefly/search/search_reports.html
  70. 6 0
      templates/forms.html

+ 51 - 21
misago/apps/search/forms.py

@@ -1,36 +1,29 @@
 from django.utils import timezone
 from django.utils.translation import ungettext_lazy, ugettext_lazy as _
 import floppyforms as forms
-from misago.forms import Form
+from misago.forms import Form, ForumMultipleChoiceField
+from misago.models import Forum
 
-class QuickSearchForm(Form):
-    search_query = forms.CharField(max_length=255, required=False)
+class SearchFormBase(Form):
+    search_query = forms.CharField(label=_("Search Phrases"), max_length=255)
+    search_thread_titles = forms.BooleanField(label=_("Search Thread Titles"), required=False)
+    search_thread = forms.CharField(label=_("Thread Name or Link"),
+                                    help_text=_("Limit search to specified thread by entering it's name or link here."),
+                                    max_length=255,
+                                    required=False)
+    search_author = forms.CharField(label=_("Author Name"),
+                                    help_text=_("Limit search to specified user by entering his or her name here."),
+                                    max_length=255,
+                                    required=False)
 
     def clean_search_query(self):
         data = self.cleaned_data['search_query']
         if len(data) < 3:
             raise forms.ValidationError(_("Search query should be at least 3 characters long."))
-
-        self.mode = None
-
-        if data[0:6].lower() == 'forum:':
-            forum_name = data[6:].strip()
-            if len(forum_name) < 2:
-                raise forms.ValidationError(_("In order to jump to forum, You have to enter full forum name or first few characters of it."))
-            self.mode = 'forum'
-            self.target = forum_name
-
-        if data[0:5].lower() == 'user:':
-            username = data[5:].strip()
-            if len(username) < 2:
-                raise forms.ValidationError(_("In order to jump to user profile, You have to enter full user name or first few characters of it."))
-            self.mode = 'user'
-            self.target = username
-
         return data
 
     def clean(self):
-        cleaned_data = super(QuickSearchForm, self).clean()
+        cleaned_data = super(SearchFormBase, self).clean()
         if self.request.user.is_authenticated():
             self.check_flood_user()
         if self.request.user.is_anonymous():
@@ -73,3 +66,40 @@ class QuickSearchForm(Form):
                         })
 
 
+class QuickSearchForm(SearchFormBase):
+    pass
+
+
+class AdvancedSearchForm(SearchFormBase):
+    search_before = forms.DateField(label=_("Posted Before"),
+                                    help_text=_("Exclude posts made before specified date from search. Use YYYY-MM-DD format, for example 2013-11-23."),
+                                    required=False)
+    search_after = forms.DateField(label=_("Posted After"),
+                                   help_text=_("Exclude posts made after specified date from search. Use YYYY-MM-DD format, for example 2013-11-23."),
+                                   required=False)
+
+
+class ForumsSearchForm(AdvancedSearchForm):
+    def finalize_form(self):
+        self.add_field('search_forums', ForumMultipleChoiceField(label=_("Search Forums"),
+                                                                 help_text=_("If you want, you can limit search to specified forums."),
+                                                                 queryset=Forum.objects.get(special='root').get_descendants().filter(pk__in=self.request.acl.forums.acl['can_browse']),
+                                                                 required=False))
+        self.add_field('search_forums_childs', forms.BooleanField(label=_("Include Children Forums"), required=False))
+
+
+class PrivateThreadsSearchForm(AdvancedSearchForm):
+    pass
+
+
+class ReportsSearchForm(AdvancedSearchForm):
+    search_weight = forms.TypedMultipleChoiceField(label=_("Report Types"),
+                                                   help_text=_("Limit search to certain report types."),
+                                                   choices=(
+                                                            (2, _("Open")),
+                                                            (1, _("Resolved")),
+                                                            (0, _("Bogus")),
+                                                           ),
+                                                   coerce=int,
+                                                   widget=forms.CheckboxSelectMultiple,
+                                                   required=False)

+ 4 - 1
misago/apps/search/urls.py

@@ -1,7 +1,10 @@
 from django.conf.urls import patterns, url
 
 urlpatterns = patterns('misago.apps.search.views',
-    url(r'^$', 'QuickSearchView', name="search"),
+    url(r'^$', 'SearchForumsView', name="search_forums"),
+    url(r'^quick/$', 'QuickSearchView', name="search_quick"),
+    url(r'^private-threads/$', 'SearchPrivateThreadsView', name="search_private_threads"),
+    url(r'^reports/$', 'SearchReportsView', name="search_reports"),
     url(r'^results/$', 'SearchResultsView', name="search_results"),
     url(r'^results/(?P<page>[1-9]([0-9]+)?)/$', 'SearchResultsView', name="search_results"),
 )

+ 189 - 77
misago/apps/search/views.py

@@ -1,4 +1,5 @@
-from django.core.urlresolvers import reverse
+from urlparse import urlparse
+from django.core.urlresolvers import reverse, resolve
 from django.http import Http404
 from django.shortcuts import redirect
 from django.template import RequestContext
@@ -9,46 +10,81 @@ from haystack.query import SearchQuerySet, RelatedSearchQuerySet
 from misago.acl.exceptions import ACLError403, ACLError404
 from misago.conf import settings
 from misago.decorators import block_crawlers
+from misago.messages import Message
 from misago.models import Forum, Thread, Post, User
 from misago.search import SearchException, MisagoSearchQuerySet
 from misago.shortcuts import render_to_response
 from misago.utils.pagination import make_pagination
 from misago.apps.errors import error403, error404
 from misago.apps.profiles.views import list as users_list
-from misago.apps.search.forms import QuickSearchForm
+from misago.apps.search.forms import (QuickSearchForm, ForumsSearchForm,
+                                      PrivateThreadsSearchForm, ReportsSearchForm)
 
 class ViewBase(object):
     search_route = 'search'
+    search_form = None
 
     def check_acl(self):
         pass
 
-    def make_query(self, search_query):
+    def make_query(self, search_data):
         try:
             sqs = MisagoSearchQuerySet(self.request.user, self.request.acl)
-
-            if self.request.POST.get('search_in') == 'thread':
-                thread_id = int(self.request.POST.get('search_thread'))
-                sqs.search_in(Thread.objects.get(id=thread_id))
-            elif self.request.POST.get('search_in') == 'private_threads':
-                sqs.search_in(Forum.objects.special_model('private_threads'))
-            elif self.request.POST.get('search_in') == 'reports':
-                sqs.search_in(Forum.objects.special_model('reports'))
+            if self.search_route == 'search_private_threads':
+                sqs.in_forums([Forum.objects.special_pk('private_threads')])
+            elif self.search_route == 'search_reports':
+                sqs.in_forums([Forum.objects.special_pk('reports')])
             else:
-                sqs.search_in(Forum.objects.special_model('root'))
+                if search_data.get('search_forums'):
+                    if search_data.get('search_forums_childs'):
+                        forums_tree = Forum.objects.forums_tree
+                        readable_forums = Forum.objects.readable_forums(self.request.acl)
+
+                        ranges = []
+                        for forum in search_data.get('search_forums'):
+                            if not ranges or ranges[-1][1] < forum.rght:
+                                ranges.append((forum.lft, forum.rght))
+
+                        forums = []
+                        for rang in ranges:
+                            for pk, forum in forums_tree.items():
+                                if pk in readable_forums:
+                                    if forum.lft >= rang[0] and forum.rght <= rang[1]:
+                                        forums.append(pk)
+                                    if forum.lft > rang[1]:
+                                        break
+
+                        sqs.in_forums(forums)
+                    else:
+                        sqs.in_forums([f.pk for f in search_data.get('search_forums')])
+                else:
+                    sqs.in_forums(Forum.objects.readable_forums(self.request.acl))
 
-            if self.request.POST.get('search_thread_titles'):
-                sqs.search_thread_name(search_query)
+            if search_data.get('search_thread_titles'):
+                sqs.search_thread_name(search_data.get('search_query'))
+                sqs.search_thread_titles()
             else:
-                sqs.search_content(search_query)
+                sqs.search_content(search_data.get('search_query'))
+
+            if search_data.get('search_thread'):
+                sqs.search_thread_name_link(search_data.get('search_thread'))
+
+            if search_data.get('search_author'):
+                sqs.search_user_name(search_data.get('search_author'))
+
+            if search_data.get('search_before'):
+                sqs.search_before(search_data.get('search_before'))
+
+            if search_data.get('search_after'):
+                sqs.search_after(search_data.get('search_after'))
 
-            if self.request.POST.get('search_author'):
-                sqs.search_user_name(search_query)
             return sqs
         except Thread.DoesNotExist:
             raise ACLError404()
 
     def render_to_response(self, template, form, context):
+        context['search_route'] = self.search_route
+        context['form'] = form
         for i in ('search_query', 'search_in', 'search_author', 'search_thread_titles'):
             if self.request.POST.get(i):
                 context[i] = self.request.POST.get(i)
@@ -60,56 +96,61 @@ class ViewBase(object):
                                   context,
                                   context_instance=RequestContext(self.request))
 
-    def __new__(cls, request, **kwargs):
-        obj = super(ViewBase, cls).__new__(cls)
-        return obj(request, **kwargs)
-
-    def __call__(self, request, **kwargs):
-        try:
-            if request.user.is_crawler():
-                raise ACLError404()
-            self.check_acl()
-            if not request.acl.search.can_search():
-                raise ACLError403(_("You don't have permission to search community."))
-            self.request = request
-            return self.call(**kwargs)
-        except ACLError403 as e:
-            return error403(request, unicode(e))
-        except ACLError404 as e:
-            return error404(request, unicode(e))
+    def draw_form(self, request):
+        search_form_data = self.request.session.get('search_form_data')
+        if search_form_data and search_form_data['form'] == self.search_route:
+            form = self.search_form(request=self.request, initial=search_form_data['data'])
+        else:
+            form = self.search_form(request=self.request)
+        return self.render_to_response(self.search_route, form,
+                                       {'search_result': self.request.session.get('search_results')})
 
+    def search(self, request):
+        self.request.session['search_form_data'] = None
+        message = None
 
-class QuickSearchView(ViewBase):
-    def call(self, **kwargs):
-        form_type = QuickSearchForm
-        if self.request.method != "POST":
-            form = QuickSearchForm(request=self.request)
-            return self.render_to_response('home', form,
-                                           {'search_result': self.request.session.get('search_results')})
+        # Hackish interception of quick search form
+        if self.search_route == 'search_quick':
+            if self.request.POST.get('search_in') == 'thread':
+                try:
+                    link = resolve(urlparse(self.request.POST.get('search_thread')).path)
+                    search_thread = Thread.objects.get(pk=link.kwargs['thread'])
+                    self.request.acl.threads.allow_thread_view(self.request.user, search_thread)
+                    if search_thread.forum_id == Forum.objects.special_pk('private_threads'):
+                        self.search_route = 'search_private_threads'
+                        self.search_form = PrivateThreadsSearchForm
+                    elif search_thread.forum_id == Forum.objects.special_pk('reports'):
+                        self.search_route = 'search_reports'
+                        self.search_form = ReportsSearchForm
+                    else:
+                        self.search_route = 'search_forums'
+                        self.search_form = ForumsSearchForm
+                except (Http404, KeyError, Thread.DoesNotExist):
+                    raise ACLError404()
+            elif self.request.POST.get('search_in') in ('forums', 'private_threads', 'reports'):
+                if self.request.POST.get('search_in') == 'forums':
+                    self.search_route = 'search_forums'
+                    self.search_form = ForumsSearchForm
+                elif self.request.POST.get('search_in') == 'private_threads':
+                    self.search_route = 'search_private_threads'
+                    self.search_form = PrivateThreadsSearchForm
+                elif self.request.POST.get('search_in') == 'reports':
+                    self.search_route = 'search_reports'
+                    self.search_form = ReportsSearchForm
 
+        form = self.search_form(self.request.POST, request=self.request)
         try:
-            form = QuickSearchForm(self.request.POST, request=self.request)
             if form.is_valid():
-                if form.mode == 'forum':
-                    jump_to = Forum.objects.forum_by_name(form.target, self.request.acl)
-                    if jump_to:
-                        if jump_to.level == 1:
-                            return redirect(reverse('index') + ('#%s' % jump_to.slug))
-                        return redirect(jump_to.url)
-                    else:
-                        raise SearchException(_('Forum "%(forum)s" could not be found.') % {'forum': form.target})
-                if form.mode == 'user':
-                    self.request.POST = self.request.POST.copy()
-                    self.request.POST['username'] = form.target
-                    return users_list(self.request)
-
-                sqs = self.make_query(form.cleaned_data['search_query']).query.load_all()[:120]
+                sqs = self.make_query(form.cleaned_data).query.load_all()[:120]
                 results = []
+                search_weight = form.cleaned_data.get('search_weight')
                 for p in sqs:
                     post = p.object
+                    if search_weight and post.thread.weight not in search_weight:
+                        continue
                     try:
                         self.request.acl.threads.allow_post_view(self.request.user, post.thread, post)
-                        results.append(post)
+                        results.append(post.pk)
                     except ACLError404:
                         pass
 
@@ -119,11 +160,14 @@ class QuickSearchView(ViewBase):
                 if self.request.user.is_anonymous():
                     self.request.session['last_search'] = timezone.now()
 
+                self.request.session['search_form_data'] = {'form': self.search_route, 'data': form.cleaned_data}
+
                 if not results:
                     raise SearchException(_("Search returned no results. Change search query and try again."))
 
                 self.request.session['search_results'] = {
                                                           'search_query': form.cleaned_data['search_query'],
+                                                          'search_route': self.search_route,
                                                           'search_in': self.request.POST.get('search_in'),
                                                           'search_author': self.request.POST.get('search_author'),
                                                           'search_thread_titles': self.request.POST.get('search_thread_titles'),
@@ -135,21 +179,88 @@ class QuickSearchView(ViewBase):
                     pass
                 return redirect(reverse('search_results'))
             else:
-                if 'search_query' in form.errors:
-                    raise SearchException(form.errors['search_query'][0])
                 raise SearchException(form.errors['__all__'][0])
         except SearchException as e:
-            return self.render_to_response('error', form,
-                                           {'message': unicode(e)})
+            message = Message(e)
+
+        return self.render_to_response(self.search_route, form,
+                                       {
+                                        'message': message,
+                                        'search_result': self.request.session.get('search_results')
+                                       })
 
 
-class SearchResultsView(ViewBase):
+    def __new__(cls, request, **kwargs):
+        obj = super(ViewBase, cls).__new__(cls)
+        return obj(request, **kwargs)
+
+    def __call__(self, request, **kwargs):
+        try:
+            self.request = request
+            if request.user.is_crawler():
+                raise ACLError404()
+            self.check_acl()
+            if not request.acl.search.can_search():
+                raise ACLError403(_("You don't have permission to search community."))
+            if self.request.method == "POST":
+                return self.search(request)
+            return self.draw_form(request)
+        except ACLError403 as e:
+            return error403(request, unicode(e))
+        except ACLError404 as e:
+            return error404(request, unicode(e))
+
+
+class QuickSearchView(ViewBase):
+    search_route = 'search_quick'
+    search_form = QuickSearchForm
+
+
+class SearchForumsView(ViewBase):
+    search_route = 'search_forums'
+    search_form = ForumsSearchForm
+
+
+class SearchPrivateThreadsView(ViewBase):
+    search_route = 'search_private_threads'
+    search_form = PrivateThreadsSearchForm
+
+    def check_acl(self):
+        if not self.request.acl.private_threads.can_participate():
+            raise ACLError404()
+
+
+class SearchReportsView(ViewBase):
+    search_route = 'search_reports'
+    search_form = ReportsSearchForm
+
+    def check_acl(self):
+        if not self.request.acl.reports.can_handle():
+            raise ACLError404()
+
+
+class SearchResultsView(object):
+    def __new__(cls, request, **kwargs):
+        obj = super(SearchResultsView, cls).__new__(cls)
+        return obj(request, **kwargs)
+
+    def __call__(self, request, **kwargs):
+        try:
+            if request.user.is_crawler():
+                raise ACLError404()
+            if not request.acl.search.can_search():
+                raise ACLError403(_("You don't have permission to search community."))
+            self.request = request
+            return self.call(**kwargs)
+        except ACLError403 as e:
+            return error403(request, unicode(e))
+        except ACLError404 as e:
+            return error404(request, unicode(e))
+
     def call(self, **kwargs):
         result = self.request.session.get('search_results')
         if not result:
-            form = QuickSearchForm(request=self.request)
-            return self.render_to_response('error', form,
-                                           {'message': _("No search results were found.")})
+            return error404(self.request, _("No search results were found."))
 
         items = result['search_results']
         items_total = len(items);
@@ -158,15 +269,16 @@ class SearchResultsView(ViewBase):
         except Http404:
             return redirect(reverse('search_results'))
 
-        form = QuickSearchForm(request=self.request, initial={'search_query': result['search_query']})
-        return self.render_to_response('results', form,
-                                       {
-                                        'search_query': result['search_query'],
-                                        'search_in': result.get('search_in'),
-                                        'search_author': result.get('search_author'),
-                                        'search_thread_titles': result.get('search_thread_titles'),
-                                        'search_thread': result.get('search_thread'),
-                                        'results': items[pagination['start']:pagination['stop']],
-                                        'items_total': items_total,
-                                        'pagination': pagination,
-                                       })
+        return render_to_response('search/results.html',
+                                  {
+                                   'search_in': result.get('search_in'),
+                                   'search_route': result.get('search_route'),
+                                   'search_query': result['search_query'],
+                                   'search_author': result.get('search_author'),
+                                   'search_thread_titles': result.get('search_thread_titles'),
+                                   'search_thread': result.get('search_thread'),
+                                   'results': Post.objects.filter(id__in=items).select_related('forum', 'thread', 'user')[pagination['start']:pagination['stop']],
+                                   'items_total': items_total,
+                                   'pagination': pagination,
+                                  },
+                                  context_instance=RequestContext(self.request))

+ 2 - 1
misago/forms/__init__.py

@@ -1,4 +1,5 @@
-from misago.forms.fields import ForumChoiceField, ReCaptchaField, QACaptchaField
+from misago.forms.fields import (ForumChoiceField, ForumMultipleChoiceField,
+                                 ReCaptchaField, QACaptchaField)
 from misago.forms.forms import Form
 from misago.forms.iterators import FormIterator
 from misago.forms.widgets import ReCaptchaWidget, YesNoSwitch, ForumTOS

+ 12 - 1
misago/forms/fields.py

@@ -1,7 +1,7 @@
 from django.utils.html import conditional_escape, mark_safe
 from django.utils.translation import ugettext_lazy as _
 from floppyforms import fields, widgets
-from mptt.forms import TreeNodeChoiceField
+from mptt.forms import TreeNodeChoiceField, TreeNodeMultipleChoiceField
 from misago.forms.widgets import ReCaptchaWidget
 
 class ForumChoiceField(TreeNodeChoiceField):
@@ -19,6 +19,17 @@ class ForumChoiceField(TreeNodeChoiceField):
         return mark_safe(conditional_escape(self.level_indicator) * (level - 1))
 
 
+class ForumMultipleChoiceField(TreeNodeMultipleChoiceField):
+    widget = widgets.CheckboxSelectMultiple
+
+    def __init__(self, *args, **kwargs):
+        kwargs['level_indicator'] = u'- - '
+        super(ForumMultipleChoiceField, self).__init__(*args, **kwargs)
+
+    def _get_level_indicator(self, obj):
+        level = getattr(obj, obj._mptt_meta.level_attr)
+        return mark_safe(conditional_escape(self.level_indicator) * (level - 1))
+
 class ReCaptchaField(fields.CharField):
     widget = ReCaptchaWidget
     api_error = None

+ 6 - 18
misago/forms/forms.py

@@ -66,24 +66,12 @@ class Form(forms.Form):
         """
         self.ensure_finalization()
         self.data = self.data.copy()
-        for key, field in self.fields.iteritems():
-            try:
-                if field.__class__.__name__ in ['ModelChoiceField', 'TreeForeignKey'] and self.data[key]:
-                    self.data[key] = int(self.data[key])
-                elif field.__class__.__name__ == 'ModelMultipleChoiceField':
-                    self.data.setlist(key, [int(x) for x in self.data.getlist(key, [])])
-                elif field.__class__.__name__ not in ['DateField', 'DateTimeField']:
-                    if not key in self.dont_strip:
-                        if field.__class__.__name__ in ['MultipleChoiceField', 'TypedMultipleChoiceField']:
-                            self.data.setlist(key, [x.strip() for x in self.data.getlist(key, [])])
-                        else:
-                            self.data[key] = self.data[key].strip()
-                    if field.__class__.__name__ in ['MultipleChoiceField', 'TypedMultipleChoiceField']:
-                        self.data.setlist(key, [x.replace("\r\n", '') for x in self.data.getlist(key, [])])
-                    elif not field.widget.__class__.__name__ in ['Textarea']:
-                        self.data[key] = self.data[key].replace("\r\n", '')
-            except (KeyError, AttributeError):
-                pass
+        for name, field in self.fields.iteritems():
+            if field.__class__ == forms.CharField:
+                try:
+                    self.data[name] = self.data[name].strip()
+                except KeyError:
+                    pass
         super(Form, self).full_clean()
 
     def clean(self):

+ 23 - 7
misago/search.py

@@ -1,4 +1,7 @@
+from urlparse import urlparse
+from django.core.urlresolvers import resolve
 from django.db.models import Q
+from django.http import Http404
 from django.utils.translation import ugettext_lazy as _
 from haystack.inputs import AutoQuery
 from haystack.query import SearchQuerySet, RelatedSearchQuerySet
@@ -97,6 +100,7 @@ class SearchQuery(object):
 class MisagoSearchQuerySet(object):
     def __init__(self, user, acl):
         self._content = None
+        self._thread_start = None
         self._thread_name = None
         self._user_name = None
         self._after = None
@@ -135,7 +139,7 @@ class MisagoSearchQuerySet(object):
             if thread.pk in self._threads:
                 self._threads = [thread.pk]
             else:
-                raise ACLError403()
+                raise ACLError404()
         self._threads = [thread.pk]
 
     def search_content(self, query):
@@ -144,21 +148,31 @@ class MisagoSearchQuerySet(object):
     def restrict_threads(self, threads=None):
         self._threads = threads
 
+    def search_thread_name_link(self, query):
+        try:
+            link = resolve(urlparse(query).path)
+            thread = Thread.objects.get(pk=link.kwargs['thread'])
+            self.allow_thread_search(thread)
+        except (Http404, KeyError, Thread.DoesNotExist):
+            self._thread_name = query
+
+    def search_thread_titles(self, value=True):
+        self._thread_start = value
+
     def search_thread_name(self, query):
         self._thread_name = query
 
     def search_user_name(self, query):
         self._user_name = query
 
-    def after(self, datetime):
+    def search_after(self, datetime):
         self._after = datetime
 
-    def before(self, datetime):
+    def search_before(self, datetime):
         self._before = datetime
 
-    def in_forum(self, forum, children=False):
-        self._forum = forum
-        self._children = children
+    def in_forums(self, forums):
+        self._forums = forums
 
     @property
     def query(self):
@@ -173,7 +187,9 @@ class MisagoSearchQuerySet(object):
             sqs = sqs.auto_query(self._content)
 
         if self._thread_name:
-            sqs = sqs.filter(thread_name=AutoQuery(self._thread_name), start_post=1)
+            sqs = sqs.filter(thread_name=AutoQuery(self._thread_name))
+            if self._thread_start:
+                sqs = sqs.filter(start_post=1)
 
         if self._user_name:
             sqs = sqs.filter(username=self._user_name)

+ 5 - 1
static/cranefly/css/cranefly.css

@@ -1,5 +1,6 @@
 @import "jquery.atwho.css";
 @import "jquery.Jcrop.min.css";
+@import "datepicker.css";
 @import "font-awesome.min.css";
 article,aside,details,figcaption,figure,footer,header,hgroup,nav,section{display:block}
 audio,canvas,video{display:inline-block;*display:inline;*zoom:1}
@@ -1106,7 +1107,10 @@ a.btn-link:hover,a.btn-link:active,a.btn-link:focus{color:#333;text-decoration:n
 .search-suggestion{overflow:auto}.search-suggestion p,.search-suggestion form{float:left}
 .search-suggestion .lead{color:#7b7b7b}
 .search-suggestion form .btn-link{margin-top:1px;color:#3c85a3;font-size:21px;font-style:italic;font-weight:200;text-decoration:underline}.search-suggestion form .btn-link:hover,.search-suggestion form .btn-link:active{color:#3c85a3;text-decoration:underline !important}
-.search-resume .muted{color:#7b7b7b}.search-resume .muted a{color:#333}
+.search-resume a:link,.search-resume a:visited{color:#999}.search-resume a:link strong,.search-resume a:visited strong{color:#555}
+.search-resume a:hover,.search-resume a:active{color:#555}.search-resume a:hover strong,.search-resume a:active strong{color:#333}
+.search-results h2{margin-bottom:30px;color:#999;font-weight:normal}.search-results h2 strong{color:#333}
+.search-results h2 .btn{position:relative;top:6px}
 .search-results .results-list .result{border-bottom:1px solid #eee;margin-bottom:10px;padding-bottom:10px}.search-results .results-list .result .result-avatar{float:left}.search-results .results-list .result .result-avatar img{-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;width:70px;height:70px}
 .search-results .results-list .result .result-content{margin-left:84px}.search-results .results-list .result .result-content h3{margin:0;line-height:20px}.search-results .results-list .result .result-content h3 a:link,.search-results .results-list .result .result-content h3 a:visited{color:#555;font-weight:normal;font-size:18.2px;text-decoration:underline}
 .search-results .results-list .result .result-content h3 a:hover,.search-results .results-list .result .result-content h3 a:active{color:#333}

+ 1 - 0
static/cranefly/css/cranefly.less

@@ -97,6 +97,7 @@
 // Extra css
 @import "jquery.atwho.css";
 @import "jquery.Jcrop.min.css";
+@import "datepicker.css";
 @import "font-awesome.min.css";
 
 // Keep ranks last for easy overrides!

+ 30 - 6
static/cranefly/css/cranefly/search.less

@@ -31,16 +31,40 @@
 }
 
 .search-resume {
-  .muted {
-    color: lighten(@gray, 15%);
+	a:link, a:visited {
+		color: @grayLight;
 
-    a {
-      color: @textColor;
-    }
-  }
+		strong {
+			color: @gray;
+		}
+	}
+
+	a:hover, a:active {
+		color: @gray;
+
+		strong {
+			color: @textColor;
+		}
+	}
 }
 
 .search-results {
+	h2 {
+		margin-bottom: @baseLineHeight * 1.5;
+
+		color: @grayLight;
+		font-weight: normal;
+
+		strong {
+			color: @textColor;
+		}
+
+		.btn {
+			position: relative;
+			top: 6px;
+		}
+	}
+
   .results-list {
     .result {
       border-bottom: 1px solid darken(@bodyBackground, 5%);

+ 481 - 0
static/cranefly/css/datepicker.css

@@ -0,0 +1,481 @@
+/*!
+ * Datepicker for Bootstrap
+ *
+ * Copyright 2012 Stefan Petre
+ * Improvements by Andrew Rowls
+ * Licensed under the Apache License v2.0
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ */
+.datepicker {
+  padding: 4px;
+  -webkit-border-radius: 4px;
+  -moz-border-radius: 4px;
+  border-radius: 4px;
+  direction: ltr;
+  /*.dow {
+		border-top: 1px solid #ddd !important;
+	}*/
+
+}
+.datepicker-inline {
+  width: 220px;
+}
+.datepicker.datepicker-rtl {
+  direction: rtl;
+}
+.datepicker.datepicker-rtl table tr td span {
+  float: right;
+}
+.datepicker-dropdown {
+  top: 0;
+  left: 0;
+}
+.datepicker-dropdown:before {
+  content: '';
+  display: inline-block;
+  border-left: 7px solid transparent;
+  border-right: 7px solid transparent;
+  border-bottom: 7px solid #ccc;
+  border-top: 0;
+  border-bottom-color: rgba(0, 0, 0, 0.2);
+  position: absolute;
+}
+.datepicker-dropdown:after {
+  content: '';
+  display: inline-block;
+  border-left: 6px solid transparent;
+  border-right: 6px solid transparent;
+  border-bottom: 6px solid #ffffff;
+  border-top: 0;
+  position: absolute;
+}
+.datepicker-dropdown.datepicker-orient-left:before {
+  left: 6px;
+}
+.datepicker-dropdown.datepicker-orient-left:after {
+  left: 7px;
+}
+.datepicker-dropdown.datepicker-orient-right:before {
+  right: 6px;
+}
+.datepicker-dropdown.datepicker-orient-right:after {
+  right: 7px;
+}
+.datepicker-dropdown.datepicker-orient-top:before {
+  top: -7px;
+}
+.datepicker-dropdown.datepicker-orient-top:after {
+  top: -6px;
+}
+.datepicker-dropdown.datepicker-orient-bottom:before {
+  bottom: -7px;
+  border-bottom: 0;
+  border-top: 7px solid #999;
+}
+.datepicker-dropdown.datepicker-orient-bottom:after {
+  bottom: -6px;
+  border-bottom: 0;
+  border-top: 6px solid #ffffff;
+}
+.datepicker > div {
+  display: none;
+}
+.datepicker.days div.datepicker-days {
+  display: block;
+}
+.datepicker.months div.datepicker-months {
+  display: block;
+}
+.datepicker.years div.datepicker-years {
+  display: block;
+}
+.datepicker table {
+  margin: 0;
+  -webkit-touch-callout: none;
+  -webkit-user-select: none;
+  -khtml-user-select: none;
+  -moz-user-select: none;
+  -ms-user-select: none;
+  user-select: none;
+}
+.datepicker td,
+.datepicker th {
+  text-align: center;
+  width: 20px;
+  height: 20px;
+  -webkit-border-radius: 4px;
+  -moz-border-radius: 4px;
+  border-radius: 4px;
+  border: none;
+}
+.table-striped .datepicker table tr td,
+.table-striped .datepicker table tr th {
+  background-color: transparent;
+}
+.datepicker table tr td.day:hover {
+  background: #eeeeee;
+  cursor: pointer;
+}
+.datepicker table tr td.old,
+.datepicker table tr td.new {
+  color: #999999;
+}
+.datepicker table tr td.disabled,
+.datepicker table tr td.disabled:hover {
+  background: none;
+  color: #999999;
+  cursor: default;
+}
+.datepicker table tr td.today,
+.datepicker table tr td.today:hover,
+.datepicker table tr td.today.disabled,
+.datepicker table tr td.today.disabled:hover {
+  background-color: #fde19a;
+  background-image: -moz-linear-gradient(top, #fdd49a, #fdf59a);
+  background-image: -ms-linear-gradient(top, #fdd49a, #fdf59a);
+  background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#fdd49a), to(#fdf59a));
+  background-image: -webkit-linear-gradient(top, #fdd49a, #fdf59a);
+  background-image: -o-linear-gradient(top, #fdd49a, #fdf59a);
+  background-image: linear-gradient(top, #fdd49a, #fdf59a);
+  background-repeat: repeat-x;
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fdd49a', endColorstr='#fdf59a', GradientType=0);
+  border-color: #fdf59a #fdf59a #fbed50;
+  border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
+  filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);
+  color: #000;
+}
+.datepicker table tr td.today:hover,
+.datepicker table tr td.today:hover:hover,
+.datepicker table tr td.today.disabled:hover,
+.datepicker table tr td.today.disabled:hover:hover,
+.datepicker table tr td.today:active,
+.datepicker table tr td.today:hover:active,
+.datepicker table tr td.today.disabled:active,
+.datepicker table tr td.today.disabled:hover:active,
+.datepicker table tr td.today.active,
+.datepicker table tr td.today:hover.active,
+.datepicker table tr td.today.disabled.active,
+.datepicker table tr td.today.disabled:hover.active,
+.datepicker table tr td.today.disabled,
+.datepicker table tr td.today:hover.disabled,
+.datepicker table tr td.today.disabled.disabled,
+.datepicker table tr td.today.disabled:hover.disabled,
+.datepicker table tr td.today[disabled],
+.datepicker table tr td.today:hover[disabled],
+.datepicker table tr td.today.disabled[disabled],
+.datepicker table tr td.today.disabled:hover[disabled] {
+  background-color: #fdf59a;
+}
+.datepicker table tr td.today:active,
+.datepicker table tr td.today:hover:active,
+.datepicker table tr td.today.disabled:active,
+.datepicker table tr td.today.disabled:hover:active,
+.datepicker table tr td.today.active,
+.datepicker table tr td.today:hover.active,
+.datepicker table tr td.today.disabled.active,
+.datepicker table tr td.today.disabled:hover.active {
+  background-color: #fbf069 \9;
+}
+.datepicker table tr td.today:hover:hover {
+  color: #000;
+}
+.datepicker table tr td.today.active:hover {
+  color: #fff;
+}
+.datepicker table tr td.range,
+.datepicker table tr td.range:hover,
+.datepicker table tr td.range.disabled,
+.datepicker table tr td.range.disabled:hover {
+  background: #eeeeee;
+  -webkit-border-radius: 0;
+  -moz-border-radius: 0;
+  border-radius: 0;
+}
+.datepicker table tr td.range.today,
+.datepicker table tr td.range.today:hover,
+.datepicker table tr td.range.today.disabled,
+.datepicker table tr td.range.today.disabled:hover {
+  background-color: #f3d17a;
+  background-image: -moz-linear-gradient(top, #f3c17a, #f3e97a);
+  background-image: -ms-linear-gradient(top, #f3c17a, #f3e97a);
+  background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#f3c17a), to(#f3e97a));
+  background-image: -webkit-linear-gradient(top, #f3c17a, #f3e97a);
+  background-image: -o-linear-gradient(top, #f3c17a, #f3e97a);
+  background-image: linear-gradient(top, #f3c17a, #f3e97a);
+  background-repeat: repeat-x;
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#f3c17a', endColorstr='#f3e97a', GradientType=0);
+  border-color: #f3e97a #f3e97a #edde34;
+  border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
+  filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);
+  -webkit-border-radius: 0;
+  -moz-border-radius: 0;
+  border-radius: 0;
+}
+.datepicker table tr td.range.today:hover,
+.datepicker table tr td.range.today:hover:hover,
+.datepicker table tr td.range.today.disabled:hover,
+.datepicker table tr td.range.today.disabled:hover:hover,
+.datepicker table tr td.range.today:active,
+.datepicker table tr td.range.today:hover:active,
+.datepicker table tr td.range.today.disabled:active,
+.datepicker table tr td.range.today.disabled:hover:active,
+.datepicker table tr td.range.today.active,
+.datepicker table tr td.range.today:hover.active,
+.datepicker table tr td.range.today.disabled.active,
+.datepicker table tr td.range.today.disabled:hover.active,
+.datepicker table tr td.range.today.disabled,
+.datepicker table tr td.range.today:hover.disabled,
+.datepicker table tr td.range.today.disabled.disabled,
+.datepicker table tr td.range.today.disabled:hover.disabled,
+.datepicker table tr td.range.today[disabled],
+.datepicker table tr td.range.today:hover[disabled],
+.datepicker table tr td.range.today.disabled[disabled],
+.datepicker table tr td.range.today.disabled:hover[disabled] {
+  background-color: #f3e97a;
+}
+.datepicker table tr td.range.today:active,
+.datepicker table tr td.range.today:hover:active,
+.datepicker table tr td.range.today.disabled:active,
+.datepicker table tr td.range.today.disabled:hover:active,
+.datepicker table tr td.range.today.active,
+.datepicker table tr td.range.today:hover.active,
+.datepicker table tr td.range.today.disabled.active,
+.datepicker table tr td.range.today.disabled:hover.active {
+  background-color: #efe24b \9;
+}
+.datepicker table tr td.selected,
+.datepicker table tr td.selected:hover,
+.datepicker table tr td.selected.disabled,
+.datepicker table tr td.selected.disabled:hover {
+  background-color: #9e9e9e;
+  background-image: -moz-linear-gradient(top, #b3b3b3, #808080);
+  background-image: -ms-linear-gradient(top, #b3b3b3, #808080);
+  background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#b3b3b3), to(#808080));
+  background-image: -webkit-linear-gradient(top, #b3b3b3, #808080);
+  background-image: -o-linear-gradient(top, #b3b3b3, #808080);
+  background-image: linear-gradient(top, #b3b3b3, #808080);
+  background-repeat: repeat-x;
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#b3b3b3', endColorstr='#808080', GradientType=0);
+  border-color: #808080 #808080 #595959;
+  border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
+  filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);
+  color: #fff;
+  text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
+}
+.datepicker table tr td.selected:hover,
+.datepicker table tr td.selected:hover:hover,
+.datepicker table tr td.selected.disabled:hover,
+.datepicker table tr td.selected.disabled:hover:hover,
+.datepicker table tr td.selected:active,
+.datepicker table tr td.selected:hover:active,
+.datepicker table tr td.selected.disabled:active,
+.datepicker table tr td.selected.disabled:hover:active,
+.datepicker table tr td.selected.active,
+.datepicker table tr td.selected:hover.active,
+.datepicker table tr td.selected.disabled.active,
+.datepicker table tr td.selected.disabled:hover.active,
+.datepicker table tr td.selected.disabled,
+.datepicker table tr td.selected:hover.disabled,
+.datepicker table tr td.selected.disabled.disabled,
+.datepicker table tr td.selected.disabled:hover.disabled,
+.datepicker table tr td.selected[disabled],
+.datepicker table tr td.selected:hover[disabled],
+.datepicker table tr td.selected.disabled[disabled],
+.datepicker table tr td.selected.disabled:hover[disabled] {
+  background-color: #808080;
+}
+.datepicker table tr td.selected:active,
+.datepicker table tr td.selected:hover:active,
+.datepicker table tr td.selected.disabled:active,
+.datepicker table tr td.selected.disabled:hover:active,
+.datepicker table tr td.selected.active,
+.datepicker table tr td.selected:hover.active,
+.datepicker table tr td.selected.disabled.active,
+.datepicker table tr td.selected.disabled:hover.active {
+  background-color: #666666 \9;
+}
+.datepicker table tr td.active,
+.datepicker table tr td.active:hover,
+.datepicker table tr td.active.disabled,
+.datepicker table tr td.active.disabled:hover {
+  background-color: #006dcc;
+  background-image: -moz-linear-gradient(top, #0088cc, #0044cc);
+  background-image: -ms-linear-gradient(top, #0088cc, #0044cc);
+  background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0044cc));
+  background-image: -webkit-linear-gradient(top, #0088cc, #0044cc);
+  background-image: -o-linear-gradient(top, #0088cc, #0044cc);
+  background-image: linear-gradient(top, #0088cc, #0044cc);
+  background-repeat: repeat-x;
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#0088cc', endColorstr='#0044cc', GradientType=0);
+  border-color: #0044cc #0044cc #002a80;
+  border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
+  filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);
+  color: #fff;
+  text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
+}
+.datepicker table tr td.active:hover,
+.datepicker table tr td.active:hover:hover,
+.datepicker table tr td.active.disabled:hover,
+.datepicker table tr td.active.disabled:hover:hover,
+.datepicker table tr td.active:active,
+.datepicker table tr td.active:hover:active,
+.datepicker table tr td.active.disabled:active,
+.datepicker table tr td.active.disabled:hover:active,
+.datepicker table tr td.active.active,
+.datepicker table tr td.active:hover.active,
+.datepicker table tr td.active.disabled.active,
+.datepicker table tr td.active.disabled:hover.active,
+.datepicker table tr td.active.disabled,
+.datepicker table tr td.active:hover.disabled,
+.datepicker table tr td.active.disabled.disabled,
+.datepicker table tr td.active.disabled:hover.disabled,
+.datepicker table tr td.active[disabled],
+.datepicker table tr td.active:hover[disabled],
+.datepicker table tr td.active.disabled[disabled],
+.datepicker table tr td.active.disabled:hover[disabled] {
+  background-color: #0044cc;
+}
+.datepicker table tr td.active:active,
+.datepicker table tr td.active:hover:active,
+.datepicker table tr td.active.disabled:active,
+.datepicker table tr td.active.disabled:hover:active,
+.datepicker table tr td.active.active,
+.datepicker table tr td.active:hover.active,
+.datepicker table tr td.active.disabled.active,
+.datepicker table tr td.active.disabled:hover.active {
+  background-color: #003399 \9;
+}
+.datepicker table tr td span {
+  display: block;
+  width: 23%;
+  height: 54px;
+  line-height: 54px;
+  float: left;
+  margin: 1%;
+  cursor: pointer;
+  -webkit-border-radius: 4px;
+  -moz-border-radius: 4px;
+  border-radius: 4px;
+}
+.datepicker table tr td span:hover {
+  background: #eeeeee;
+}
+.datepicker table tr td span.disabled,
+.datepicker table tr td span.disabled:hover {
+  background: none;
+  color: #999999;
+  cursor: default;
+}
+.datepicker table tr td span.active,
+.datepicker table tr td span.active:hover,
+.datepicker table tr td span.active.disabled,
+.datepicker table tr td span.active.disabled:hover {
+  background-color: #006dcc;
+  background-image: -moz-linear-gradient(top, #0088cc, #0044cc);
+  background-image: -ms-linear-gradient(top, #0088cc, #0044cc);
+  background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0044cc));
+  background-image: -webkit-linear-gradient(top, #0088cc, #0044cc);
+  background-image: -o-linear-gradient(top, #0088cc, #0044cc);
+  background-image: linear-gradient(top, #0088cc, #0044cc);
+  background-repeat: repeat-x;
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#0088cc', endColorstr='#0044cc', GradientType=0);
+  border-color: #0044cc #0044cc #002a80;
+  border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
+  filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);
+  color: #fff;
+  text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
+}
+.datepicker table tr td span.active:hover,
+.datepicker table tr td span.active:hover:hover,
+.datepicker table tr td span.active.disabled:hover,
+.datepicker table tr td span.active.disabled:hover:hover,
+.datepicker table tr td span.active:active,
+.datepicker table tr td span.active:hover:active,
+.datepicker table tr td span.active.disabled:active,
+.datepicker table tr td span.active.disabled:hover:active,
+.datepicker table tr td span.active.active,
+.datepicker table tr td span.active:hover.active,
+.datepicker table tr td span.active.disabled.active,
+.datepicker table tr td span.active.disabled:hover.active,
+.datepicker table tr td span.active.disabled,
+.datepicker table tr td span.active:hover.disabled,
+.datepicker table tr td span.active.disabled.disabled,
+.datepicker table tr td span.active.disabled:hover.disabled,
+.datepicker table tr td span.active[disabled],
+.datepicker table tr td span.active:hover[disabled],
+.datepicker table tr td span.active.disabled[disabled],
+.datepicker table tr td span.active.disabled:hover[disabled] {
+  background-color: #0044cc;
+}
+.datepicker table tr td span.active:active,
+.datepicker table tr td span.active:hover:active,
+.datepicker table tr td span.active.disabled:active,
+.datepicker table tr td span.active.disabled:hover:active,
+.datepicker table tr td span.active.active,
+.datepicker table tr td span.active:hover.active,
+.datepicker table tr td span.active.disabled.active,
+.datepicker table tr td span.active.disabled:hover.active {
+  background-color: #003399 \9;
+}
+.datepicker table tr td span.old,
+.datepicker table tr td span.new {
+  color: #999999;
+}
+.datepicker th.datepicker-switch {
+  width: 145px;
+}
+.datepicker thead tr:first-child th,
+.datepicker tfoot tr th {
+  cursor: pointer;
+}
+.datepicker thead tr:first-child th:hover,
+.datepicker tfoot tr th:hover {
+  background: #eeeeee;
+}
+.datepicker .cw {
+  font-size: 10px;
+  width: 12px;
+  padding: 0 2px 0 5px;
+  vertical-align: middle;
+}
+.datepicker thead tr:first-child th.cw {
+  cursor: default;
+  background-color: transparent;
+}
+.input-append.date .add-on i,
+.input-prepend.date .add-on i {
+  display: block;
+  cursor: pointer;
+  width: 16px;
+  height: 16px;
+}
+.input-daterange input {
+  text-align: center;
+}
+.input-daterange input:first-child {
+  -webkit-border-radius: 3px 0 0 3px;
+  -moz-border-radius: 3px 0 0 3px;
+  border-radius: 3px 0 0 3px;
+}
+.input-daterange input:last-child {
+  -webkit-border-radius: 0 3px 3px 0;
+  -moz-border-radius: 0 3px 3px 0;
+  border-radius: 0 3px 3px 0;
+}
+.input-daterange .add-on {
+  display: inline-block;
+  width: auto;
+  min-width: 16px;
+  height: 18px;
+  padding: 4px 5px;
+  font-weight: normal;
+  line-height: 18px;
+  text-align: center;
+  text-shadow: 0 1px 0 #ffffff;
+  vertical-align: middle;
+  background-color: #eeeeee;
+  border: 1px solid #ccc;
+  margin-left: -5px;
+  margin-right: -5px;
+}

+ 0 - 1
static/cranefly/css/style.css

@@ -1 +0,0 @@
-lessc: ENOENT, open '/Users/rafapiton/Documents/misago/static/cranefly/css/style.less'

+ 1395 - 0
static/cranefly/js/bootstrap-datepicker.js

@@ -0,0 +1,1395 @@
+/* =========================================================
+ * bootstrap-datepicker.js
+ * http://www.eyecon.ro/bootstrap-datepicker
+ * =========================================================
+ * Copyright 2012 Stefan Petre
+ * Improvements by Andrew Rowls
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ========================================================= */
+
+(function( $ ) {
+
+	var $window = $(window);
+
+	function UTCDate(){
+		return new Date(Date.UTC.apply(Date, arguments));
+	}
+	function UTCToday(){
+		var today = new Date();
+		return UTCDate(today.getUTCFullYear(), today.getUTCMonth(), today.getUTCDate());
+	}
+
+
+	// Picker object
+
+	var Datepicker = function(element, options) {
+		var that = this;
+
+		this._process_options(options);
+
+		this.element = $(element);
+		this.isInline = false;
+		this.isInput = this.element.is('input');
+		this.component = this.element.is('.date') ? this.element.find('.add-on, .btn') : false;
+		this.hasInput = this.component && this.element.find('input').length;
+		if(this.component && this.component.length === 0)
+			this.component = false;
+
+		this.picker = $(DPGlobal.template);
+		this._buildEvents();
+		this._attachEvents();
+
+		if(this.isInline) {
+			this.picker.addClass('datepicker-inline').appendTo(this.element);
+		} else {
+			this.picker.addClass('datepicker-dropdown dropdown-menu');
+		}
+
+		if (this.o.rtl){
+			this.picker.addClass('datepicker-rtl');
+			this.picker.find('.prev i, .next i')
+						.toggleClass('icon-arrow-left icon-arrow-right');
+		}
+
+
+		this.viewMode = this.o.startView;
+
+		if (this.o.calendarWeeks)
+			this.picker.find('tfoot th.today')
+						.attr('colspan', function(i, val){
+							return parseInt(val) + 1;
+						});
+
+		this._allow_update = false;
+
+		this.setStartDate(this._o.startDate);
+		this.setEndDate(this._o.endDate);
+		this.setDaysOfWeekDisabled(this.o.daysOfWeekDisabled);
+
+		this.fillDow();
+		this.fillMonths();
+
+		this._allow_update = true;
+
+		this.update();
+		this.showMode();
+
+		if(this.isInline) {
+			this.show();
+		}
+	};
+
+	Datepicker.prototype = {
+		constructor: Datepicker,
+
+		_process_options: function(opts){
+			// Store raw options for reference
+			this._o = $.extend({}, this._o, opts);
+			// Processed options
+			var o = this.o = $.extend({}, this._o);
+
+			// Check if "de-DE" style date is available, if not language should
+			// fallback to 2 letter code eg "de"
+			var lang = o.language;
+			if (!dates[lang]) {
+				lang = lang.split('-')[0];
+				if (!dates[lang])
+					lang = defaults.language;
+			}
+			o.language = lang;
+
+			switch(o.startView){
+				case 2:
+				case 'decade':
+					o.startView = 2;
+					break;
+				case 1:
+				case 'year':
+					o.startView = 1;
+					break;
+				default:
+					o.startView = 0;
+			}
+
+			switch (o.minViewMode) {
+				case 1:
+				case 'months':
+					o.minViewMode = 1;
+					break;
+				case 2:
+				case 'years':
+					o.minViewMode = 2;
+					break;
+				default:
+					o.minViewMode = 0;
+			}
+
+			o.startView = Math.max(o.startView, o.minViewMode);
+
+			o.weekStart %= 7;
+			o.weekEnd = ((o.weekStart + 6) % 7);
+
+			var format = DPGlobal.parseFormat(o.format);
+			if (o.startDate !== -Infinity) {
+				if (!!o.startDate) {
+					if (o.startDate instanceof Date)
+						o.startDate = this._local_to_utc(this._zero_time(o.startDate));
+					else
+						o.startDate = DPGlobal.parseDate(o.startDate, format, o.language);
+				} else {
+					o.startDate = -Infinity;
+				}
+			}
+			if (o.endDate !== Infinity) {
+				if (!!o.endDate) {
+					if (o.endDate instanceof Date)
+						o.endDate = this._local_to_utc(this._zero_time(o.endDate));
+					else
+						o.endDate = DPGlobal.parseDate(o.endDate, format, o.language);
+				} else {
+					o.endDate = Infinity;
+				}
+			}
+
+			o.daysOfWeekDisabled = o.daysOfWeekDisabled||[];
+			if (!$.isArray(o.daysOfWeekDisabled))
+				o.daysOfWeekDisabled = o.daysOfWeekDisabled.split(/[,\s]*/);
+			o.daysOfWeekDisabled = $.map(o.daysOfWeekDisabled, function (d) {
+				return parseInt(d, 10);
+			});
+
+			var plc = String(o.orientation).toLowerCase().split(/\s+/g),
+				_plc = o.orientation.toLowerCase();
+			plc = $.grep(plc, function(word){
+				return (/^auto|left|right|top|bottom$/).test(word);
+			});
+			o.orientation = {x: 'auto', y: 'auto'};
+			if (!_plc || _plc === 'auto')
+				; // no action
+			else if (plc.length === 1){
+				switch(plc[0]){
+					case 'top':
+					case 'bottom':
+						o.orientation.y = plc[0];
+						break;
+					case 'left':
+					case 'right':
+						o.orientation.x = plc[0];
+						break;
+				}
+			}
+			else {
+				_plc = $.grep(plc, function(word){
+					return (/^left|right$/).test(word);
+				});
+				o.orientation.x = _plc[0] || 'auto';
+
+				_plc = $.grep(plc, function(word){
+					return (/^top|bottom$/).test(word);
+				});
+				o.orientation.y = _plc[0] || 'auto';
+			}
+		},
+		_events: [],
+		_secondaryEvents: [],
+		_applyEvents: function(evs){
+			for (var i=0, el, ev; i<evs.length; i++){
+				el = evs[i][0];
+				ev = evs[i][1];
+				el.on(ev);
+			}
+		},
+		_unapplyEvents: function(evs){
+			for (var i=0, el, ev; i<evs.length; i++){
+				el = evs[i][0];
+				ev = evs[i][1];
+				el.off(ev);
+			}
+		},
+		_buildEvents: function(){
+			if (this.isInput) { // single input
+				this._events = [
+					[this.element, {
+						focus: $.proxy(this.show, this),
+						keyup: $.proxy(this.update, this),
+						keydown: $.proxy(this.keydown, this)
+					}]
+				];
+			}
+			else if (this.component && this.hasInput){ // component: input + button
+				this._events = [
+					// For components that are not readonly, allow keyboard nav
+					[this.element.find('input'), {
+						focus: $.proxy(this.show, this),
+						keyup: $.proxy(this.update, this),
+						keydown: $.proxy(this.keydown, this)
+					}],
+					[this.component, {
+						click: $.proxy(this.show, this)
+					}]
+				];
+			}
+			else if (this.element.is('div')) {  // inline datepicker
+				this.isInline = true;
+			}
+			else {
+				this._events = [
+					[this.element, {
+						click: $.proxy(this.show, this)
+					}]
+				];
+			}
+
+			this._secondaryEvents = [
+				[this.picker, {
+					click: $.proxy(this.click, this)
+				}],
+				[$(window), {
+					resize: $.proxy(this.place, this)
+				}],
+				[$(document), {
+					'mousedown touchstart': $.proxy(function (e) {
+						// Clicked outside the datepicker, hide it
+						if (!(
+							this.element.is(e.target) ||
+							this.element.find(e.target).length ||
+							this.picker.is(e.target) ||
+							this.picker.find(e.target).length
+						)) {
+							this.hide();
+						}
+					}, this)
+				}]
+			];
+		},
+		_attachEvents: function(){
+			this._detachEvents();
+			this._applyEvents(this._events);
+		},
+		_detachEvents: function(){
+			this._unapplyEvents(this._events);
+		},
+		_attachSecondaryEvents: function(){
+			this._detachSecondaryEvents();
+			this._applyEvents(this._secondaryEvents);
+		},
+		_detachSecondaryEvents: function(){
+			this._unapplyEvents(this._secondaryEvents);
+		},
+		_trigger: function(event, altdate){
+			var date = altdate || this.date,
+				local_date = this._utc_to_local(date);
+
+			this.element.trigger({
+				type: event,
+				date: local_date,
+				format: $.proxy(function(altformat){
+					var format = altformat || this.o.format;
+					return DPGlobal.formatDate(date, format, this.o.language);
+				}, this)
+			});
+		},
+
+		show: function(e) {
+			if (!this.isInline)
+				this.picker.appendTo('body');
+			this.picker.show();
+			this.height = this.component ? this.component.outerHeight() : this.element.outerHeight();
+			this.place();
+			this._attachSecondaryEvents();
+			if (e) {
+				e.preventDefault();
+			}
+			this._trigger('show');
+		},
+
+		hide: function(e){
+			if(this.isInline) return;
+			if (!this.picker.is(':visible')) return;
+			this.picker.hide().detach();
+			this._detachSecondaryEvents();
+			this.viewMode = this.o.startView;
+			this.showMode();
+
+			if (
+				this.o.forceParse &&
+				(
+					this.isInput && this.element.val() ||
+					this.hasInput && this.element.find('input').val()
+				)
+			)
+				this.setValue();
+			this._trigger('hide');
+		},
+
+		remove: function() {
+			this.hide();
+			this._detachEvents();
+			this._detachSecondaryEvents();
+			this.picker.remove();
+			delete this.element.data().datepicker;
+			if (!this.isInput) {
+				delete this.element.data().date;
+			}
+		},
+
+		_utc_to_local: function(utc){
+			return new Date(utc.getTime() + (utc.getTimezoneOffset()*60000));
+		},
+		_local_to_utc: function(local){
+			return new Date(local.getTime() - (local.getTimezoneOffset()*60000));
+		},
+		_zero_time: function(local){
+			return new Date(local.getFullYear(), local.getMonth(), local.getDate());
+		},
+		_zero_utc_time: function(utc){
+			return new Date(Date.UTC(utc.getUTCFullYear(), utc.getUTCMonth(), utc.getUTCDate()));
+		},
+
+		getDate: function() {
+			return this._utc_to_local(this.getUTCDate());
+		},
+
+		getUTCDate: function() {
+			return this.date;
+		},
+
+		setDate: function(d) {
+			this.setUTCDate(this._local_to_utc(d));
+		},
+
+		setUTCDate: function(d) {
+			this.date = d;
+			this.setValue();
+		},
+
+		setValue: function() {
+			var formatted = this.getFormattedDate();
+			if (!this.isInput) {
+				if (this.component){
+					this.element.find('input').val(formatted).change();
+				}
+			} else {
+				this.element.val(formatted).change();
+			}
+		},
+
+		getFormattedDate: function(format) {
+			if (format === undefined)
+				format = this.o.format;
+			return DPGlobal.formatDate(this.date, format, this.o.language);
+		},
+
+		setStartDate: function(startDate){
+			this._process_options({startDate: startDate});
+			this.update();
+			this.updateNavArrows();
+		},
+
+		setEndDate: function(endDate){
+			this._process_options({endDate: endDate});
+			this.update();
+			this.updateNavArrows();
+		},
+
+		setDaysOfWeekDisabled: function(daysOfWeekDisabled){
+			this._process_options({daysOfWeekDisabled: daysOfWeekDisabled});
+			this.update();
+			this.updateNavArrows();
+		},
+
+		place: function(){
+						if(this.isInline) return;
+			var calendarWidth = this.picker.outerWidth(),
+				calendarHeight = this.picker.outerHeight(),
+				visualPadding = 10,
+				windowWidth = $window.width(),
+				windowHeight = $window.height(),
+				scrollTop = $window.scrollTop();
+
+			var zIndex = parseInt(this.element.parents().filter(function() {
+							return $(this).css('z-index') != 'auto';
+						}).first().css('z-index'))+10;
+			var offset = this.component ? this.component.parent().offset() : this.element.offset();
+			var height = this.component ? this.component.outerHeight(true) : this.element.outerHeight(false);
+			var width = this.component ? this.component.outerWidth(true) : this.element.outerWidth(false);
+			var left = offset.left,
+				top = offset.top;
+
+			this.picker.removeClass(
+				'datepicker-orient-top datepicker-orient-bottom '+
+				'datepicker-orient-right datepicker-orient-left'
+			);
+
+			if (this.o.orientation.x !== 'auto') {
+				this.picker.addClass('datepicker-orient-' + this.o.orientation.x);
+				if (this.o.orientation.x === 'right')
+					left -= calendarWidth - width;
+			}
+			// auto x orientation is best-placement: if it crosses a window
+			// edge, fudge it sideways
+			else {
+				// Default to left
+				this.picker.addClass('datepicker-orient-left');
+				if (offset.left < 0)
+					left -= offset.left - visualPadding;
+				else if (offset.left + calendarWidth > windowWidth)
+					left = windowWidth - calendarWidth - visualPadding;
+			}
+
+			// auto y orientation is best-situation: top or bottom, no fudging,
+			// decision based on which shows more of the calendar
+			var yorient = this.o.orientation.y,
+				top_overflow, bottom_overflow;
+			if (yorient === 'auto') {
+				top_overflow = -scrollTop + offset.top - calendarHeight;
+				bottom_overflow = scrollTop + windowHeight - (offset.top + height + calendarHeight);
+				if (Math.max(top_overflow, bottom_overflow) === bottom_overflow)
+					yorient = 'top';
+				else
+					yorient = 'bottom';
+			}
+			this.picker.addClass('datepicker-orient-' + yorient);
+			if (yorient === 'top')
+				top += height;
+			else
+				top -= calendarHeight + parseInt(this.picker.css('padding-top'));
+
+			this.picker.css({
+				top: top,
+				left: left,
+				zIndex: zIndex
+			});
+		},
+
+		_allow_update: true,
+		update: function(){
+			if (!this._allow_update) return;
+
+			var oldDate = new Date(this.date),
+				date, fromArgs = false;
+			if(arguments && arguments.length && (typeof arguments[0] === 'string' || arguments[0] instanceof Date)) {
+				date = arguments[0];
+				if (date instanceof Date)
+					date = this._local_to_utc(date);
+				fromArgs = true;
+			} else {
+				date = this.isInput ? this.element.val() : this.element.data('date') || this.element.find('input').val();
+				delete this.element.data().date;
+			}
+
+			this.date = DPGlobal.parseDate(date, this.o.format, this.o.language);
+
+			if (fromArgs) {
+				// setting date by clicking
+				this.setValue();
+			} else if (date) {
+				// setting date by typing
+				if (oldDate.getTime() !== this.date.getTime())
+					this._trigger('changeDate');
+			} else {
+				// clearing date
+				this._trigger('clearDate');
+			}
+
+			if (this.date < this.o.startDate) {
+				this.viewDate = new Date(this.o.startDate);
+				this.date = new Date(this.o.startDate);
+			} else if (this.date > this.o.endDate) {
+				this.viewDate = new Date(this.o.endDate);
+				this.date = new Date(this.o.endDate);
+			} else {
+				this.viewDate = new Date(this.date);
+				this.date = new Date(this.date);
+			}
+			this.fill();
+		},
+
+		fillDow: function(){
+			var dowCnt = this.o.weekStart,
+			html = '<tr>';
+			if(this.o.calendarWeeks){
+				var cell = '<th class="cw">&nbsp;</th>';
+				html += cell;
+				this.picker.find('.datepicker-days thead tr:first-child').prepend(cell);
+			}
+			while (dowCnt < this.o.weekStart + 7) {
+				html += '<th class="dow">'+dates[this.o.language].daysMin[(dowCnt++)%7]+'</th>';
+			}
+			html += '</tr>';
+			this.picker.find('.datepicker-days thead').append(html);
+		},
+
+		fillMonths: function(){
+			var html = '',
+			i = 0;
+			while (i < 12) {
+				html += '<span class="month">'+dates[this.o.language].monthsShort[i++]+'</span>';
+			}
+			this.picker.find('.datepicker-months td').html(html);
+		},
+
+		setRange: function(range){
+			if (!range || !range.length)
+				delete this.range;
+			else
+				this.range = $.map(range, function(d){ return d.valueOf(); });
+			this.fill();
+		},
+
+		getClassNames: function(date){
+			var cls = [],
+				year = this.viewDate.getUTCFullYear(),
+				month = this.viewDate.getUTCMonth(),
+				currentDate = this.date.valueOf(),
+				today = new Date();
+			if (date.getUTCFullYear() < year || (date.getUTCFullYear() == year && date.getUTCMonth() < month)) {
+				cls.push('old');
+			} else if (date.getUTCFullYear() > year || (date.getUTCFullYear() == year && date.getUTCMonth() > month)) {
+				cls.push('new');
+			}
+			// Compare internal UTC date with local today, not UTC today
+			if (this.o.todayHighlight &&
+				date.getUTCFullYear() == today.getFullYear() &&
+				date.getUTCMonth() == today.getMonth() &&
+				date.getUTCDate() == today.getDate()) {
+				cls.push('today');
+			}
+			if (date.valueOf() == currentDate) {
+				cls.push('active');
+			}
+			if (date.valueOf() < this.o.startDate || date.valueOf() > this.o.endDate ||
+				$.inArray(date.getUTCDay(), this.o.daysOfWeekDisabled) !== -1) {
+				cls.push('disabled');
+			}
+			if (this.range){
+				if (date > this.range[0] && date < this.range[this.range.length-1]){
+					cls.push('range');
+				}
+				if ($.inArray(date.valueOf(), this.range) != -1){
+					cls.push('selected');
+				}
+			}
+			return cls;
+		},
+
+		fill: function() {
+			var d = new Date(this.viewDate),
+				year = d.getUTCFullYear(),
+				month = d.getUTCMonth(),
+				startYear = this.o.startDate !== -Infinity ? this.o.startDate.getUTCFullYear() : -Infinity,
+				startMonth = this.o.startDate !== -Infinity ? this.o.startDate.getUTCMonth() : -Infinity,
+				endYear = this.o.endDate !== Infinity ? this.o.endDate.getUTCFullYear() : Infinity,
+				endMonth = this.o.endDate !== Infinity ? this.o.endDate.getUTCMonth() : Infinity,
+				currentDate = this.date && this.date.valueOf(),
+				tooltip;
+			this.picker.find('.datepicker-days thead th.datepicker-switch')
+						.text(dates[this.o.language].months[month]+' '+year);
+			this.picker.find('tfoot th.today')
+						.text(dates[this.o.language].today)
+						.toggle(this.o.todayBtn !== false);
+			this.picker.find('tfoot th.clear')
+						.text(dates[this.o.language].clear)
+						.toggle(this.o.clearBtn !== false);
+			this.updateNavArrows();
+			this.fillMonths();
+			var prevMonth = UTCDate(year, month-1, 28,0,0,0,0),
+				day = DPGlobal.getDaysInMonth(prevMonth.getUTCFullYear(), prevMonth.getUTCMonth());
+			prevMonth.setUTCDate(day);
+			prevMonth.setUTCDate(day - (prevMonth.getUTCDay() - this.o.weekStart + 7)%7);
+			var nextMonth = new Date(prevMonth);
+			nextMonth.setUTCDate(nextMonth.getUTCDate() + 42);
+			nextMonth = nextMonth.valueOf();
+			var html = [];
+			var clsName;
+			while(prevMonth.valueOf() < nextMonth) {
+				if (prevMonth.getUTCDay() == this.o.weekStart) {
+					html.push('<tr>');
+					if(this.o.calendarWeeks){
+						// ISO 8601: First week contains first thursday.
+						// ISO also states week starts on Monday, but we can be more abstract here.
+						var
+							// Start of current week: based on weekstart/current date
+							ws = new Date(+prevMonth + (this.o.weekStart - prevMonth.getUTCDay() - 7) % 7 * 864e5),
+							// Thursday of this week
+							th = new Date(+ws + (7 + 4 - ws.getUTCDay()) % 7 * 864e5),
+							// First Thursday of year, year from thursday
+							yth = new Date(+(yth = UTCDate(th.getUTCFullYear(), 0, 1)) + (7 + 4 - yth.getUTCDay())%7*864e5),
+							// Calendar week: ms between thursdays, div ms per day, div 7 days
+							calWeek =  (th - yth) / 864e5 / 7 + 1;
+						html.push('<td class="cw">'+ calWeek +'</td>');
+
+					}
+				}
+				clsName = this.getClassNames(prevMonth);
+				clsName.push('day');
+
+				if (this.o.beforeShowDay !== $.noop){
+					var before = this.o.beforeShowDay(this._utc_to_local(prevMonth));
+					if (before === undefined)
+						before = {};
+					else if (typeof(before) === 'boolean')
+						before = {enabled: before};
+					else if (typeof(before) === 'string')
+						before = {classes: before};
+					if (before.enabled === false)
+						clsName.push('disabled');
+					if (before.classes)
+						clsName = clsName.concat(before.classes.split(/\s+/));
+					if (before.tooltip)
+						tooltip = before.tooltip;
+				}
+
+				clsName = $.unique(clsName);
+				html.push('<td class="'+clsName.join(' ')+'"' + (tooltip ? ' title="'+tooltip+'"' : '') + '>'+prevMonth.getUTCDate() + '</td>');
+				if (prevMonth.getUTCDay() == this.o.weekEnd) {
+					html.push('</tr>');
+				}
+				prevMonth.setUTCDate(prevMonth.getUTCDate()+1);
+			}
+			this.picker.find('.datepicker-days tbody').empty().append(html.join(''));
+			var currentYear = this.date && this.date.getUTCFullYear();
+
+			var months = this.picker.find('.datepicker-months')
+						.find('th:eq(1)')
+							.text(year)
+							.end()
+						.find('span').removeClass('active');
+			if (currentYear && currentYear == year) {
+				months.eq(this.date.getUTCMonth()).addClass('active');
+			}
+			if (year < startYear || year > endYear) {
+				months.addClass('disabled');
+			}
+			if (year == startYear) {
+				months.slice(0, startMonth).addClass('disabled');
+			}
+			if (year == endYear) {
+				months.slice(endMonth+1).addClass('disabled');
+			}
+
+			html = '';
+			year = parseInt(year/10, 10) * 10;
+			var yearCont = this.picker.find('.datepicker-years')
+								.find('th:eq(1)')
+									.text(year + '-' + (year + 9))
+									.end()
+								.find('td');
+			year -= 1;
+			for (var i = -1; i < 11; i++) {
+				html += '<span class="year'+(i == -1 ? ' old' : i == 10 ? ' new' : '')+(currentYear == year ? ' active' : '')+(year < startYear || year > endYear ? ' disabled' : '')+'">'+year+'</span>';
+				year += 1;
+			}
+			yearCont.html(html);
+		},
+
+		updateNavArrows: function() {
+			if (!this._allow_update) return;
+
+			var d = new Date(this.viewDate),
+				year = d.getUTCFullYear(),
+				month = d.getUTCMonth();
+			switch (this.viewMode) {
+				case 0:
+					if (this.o.startDate !== -Infinity && year <= this.o.startDate.getUTCFullYear() && month <= this.o.startDate.getUTCMonth()) {
+						this.picker.find('.prev').css({visibility: 'hidden'});
+					} else {
+						this.picker.find('.prev').css({visibility: 'visible'});
+					}
+					if (this.o.endDate !== Infinity && year >= this.o.endDate.getUTCFullYear() && month >= this.o.endDate.getUTCMonth()) {
+						this.picker.find('.next').css({visibility: 'hidden'});
+					} else {
+						this.picker.find('.next').css({visibility: 'visible'});
+					}
+					break;
+				case 1:
+				case 2:
+					if (this.o.startDate !== -Infinity && year <= this.o.startDate.getUTCFullYear()) {
+						this.picker.find('.prev').css({visibility: 'hidden'});
+					} else {
+						this.picker.find('.prev').css({visibility: 'visible'});
+					}
+					if (this.o.endDate !== Infinity && year >= this.o.endDate.getUTCFullYear()) {
+						this.picker.find('.next').css({visibility: 'hidden'});
+					} else {
+						this.picker.find('.next').css({visibility: 'visible'});
+					}
+					break;
+			}
+		},
+
+		click: function(e) {
+			e.preventDefault();
+			var target = $(e.target).closest('span, td, th');
+			if (target.length == 1) {
+				switch(target[0].nodeName.toLowerCase()) {
+					case 'th':
+						switch(target[0].className) {
+							case 'datepicker-switch':
+								this.showMode(1);
+								break;
+							case 'prev':
+							case 'next':
+								var dir = DPGlobal.modes[this.viewMode].navStep * (target[0].className == 'prev' ? -1 : 1);
+								switch(this.viewMode){
+									case 0:
+										this.viewDate = this.moveMonth(this.viewDate, dir);
+										this._trigger('changeMonth', this.viewDate);
+										break;
+									case 1:
+									case 2:
+										this.viewDate = this.moveYear(this.viewDate, dir);
+										if (this.viewMode === 1)
+											this._trigger('changeYear', this.viewDate);
+										break;
+								}
+								this.fill();
+								break;
+							case 'today':
+								var date = new Date();
+								date = UTCDate(date.getFullYear(), date.getMonth(), date.getDate(), 0, 0, 0);
+
+								this.showMode(-2);
+								var which = this.o.todayBtn == 'linked' ? null : 'view';
+								this._setDate(date, which);
+								break;
+							case 'clear':
+								var element;
+								if (this.isInput)
+									element = this.element;
+								else if (this.component)
+									element = this.element.find('input');
+								if (element)
+									element.val("").change();
+								this._trigger('changeDate');
+								this.update();
+								if (this.o.autoclose)
+									this.hide();
+								break;
+						}
+						break;
+					case 'span':
+						if (!target.is('.disabled')) {
+							this.viewDate.setUTCDate(1);
+							if (target.is('.month')) {
+								var day = 1;
+								var month = target.parent().find('span').index(target);
+								var year = this.viewDate.getUTCFullYear();
+								this.viewDate.setUTCMonth(month);
+								this._trigger('changeMonth', this.viewDate);
+								if (this.o.minViewMode === 1) {
+									this._setDate(UTCDate(year, month, day,0,0,0,0));
+								}
+							} else {
+								var year = parseInt(target.text(), 10)||0;
+								var day = 1;
+								var month = 0;
+								this.viewDate.setUTCFullYear(year);
+								this._trigger('changeYear', this.viewDate);
+								if (this.o.minViewMode === 2) {
+									this._setDate(UTCDate(year, month, day,0,0,0,0));
+								}
+							}
+							this.showMode(-1);
+							this.fill();
+						}
+						break;
+					case 'td':
+						if (target.is('.day') && !target.is('.disabled')){
+							var day = parseInt(target.text(), 10)||1;
+							var year = this.viewDate.getUTCFullYear(),
+								month = this.viewDate.getUTCMonth();
+							if (target.is('.old')) {
+								if (month === 0) {
+									month = 11;
+									year -= 1;
+								} else {
+									month -= 1;
+								}
+							} else if (target.is('.new')) {
+								if (month == 11) {
+									month = 0;
+									year += 1;
+								} else {
+									month += 1;
+								}
+							}
+							this._setDate(UTCDate(year, month, day,0,0,0,0));
+						}
+						break;
+				}
+			}
+		},
+
+		_setDate: function(date, which){
+			if (!which || which == 'date')
+				this.date = new Date(date);
+			if (!which || which  == 'view')
+				this.viewDate = new Date(date);
+			this.fill();
+			this.setValue();
+			this._trigger('changeDate');
+			var element;
+			if (this.isInput) {
+				element = this.element;
+			} else if (this.component){
+				element = this.element.find('input');
+			}
+			if (element) {
+				element.change();
+			}
+			if (this.o.autoclose && (!which || which == 'date')) {
+				this.hide();
+			}
+		},
+
+		moveMonth: function(date, dir){
+			if (!dir) return date;
+			var new_date = new Date(date.valueOf()),
+				day = new_date.getUTCDate(),
+				month = new_date.getUTCMonth(),
+				mag = Math.abs(dir),
+				new_month, test;
+			dir = dir > 0 ? 1 : -1;
+			if (mag == 1){
+				test = dir == -1
+					// If going back one month, make sure month is not current month
+					// (eg, Mar 31 -> Feb 31 == Feb 28, not Mar 02)
+					? function(){ return new_date.getUTCMonth() == month; }
+					// If going forward one month, make sure month is as expected
+					// (eg, Jan 31 -> Feb 31 == Feb 28, not Mar 02)
+					: function(){ return new_date.getUTCMonth() != new_month; };
+				new_month = month + dir;
+				new_date.setUTCMonth(new_month);
+				// Dec -> Jan (12) or Jan -> Dec (-1) -- limit expected date to 0-11
+				if (new_month < 0 || new_month > 11)
+					new_month = (new_month + 12) % 12;
+			} else {
+				// For magnitudes >1, move one month at a time...
+				for (var i=0; i<mag; i++)
+					// ...which might decrease the day (eg, Jan 31 to Feb 28, etc)...
+					new_date = this.moveMonth(new_date, dir);
+				// ...then reset the day, keeping it in the new month
+				new_month = new_date.getUTCMonth();
+				new_date.setUTCDate(day);
+				test = function(){ return new_month != new_date.getUTCMonth(); };
+			}
+			// Common date-resetting loop -- if date is beyond end of month, make it
+			// end of month
+			while (test()){
+				new_date.setUTCDate(--day);
+				new_date.setUTCMonth(new_month);
+			}
+			return new_date;
+		},
+
+		moveYear: function(date, dir){
+			return this.moveMonth(date, dir*12);
+		},
+
+		dateWithinRange: function(date){
+			return date >= this.o.startDate && date <= this.o.endDate;
+		},
+
+		keydown: function(e){
+			if (this.picker.is(':not(:visible)')){
+				if (e.keyCode == 27) // allow escape to hide and re-show picker
+					this.show();
+				return;
+			}
+			var dateChanged = false,
+				dir, day, month,
+				newDate, newViewDate;
+			switch(e.keyCode){
+				case 27: // escape
+					this.hide();
+					e.preventDefault();
+					break;
+				case 37: // left
+				case 39: // right
+					if (!this.o.keyboardNavigation) break;
+					dir = e.keyCode == 37 ? -1 : 1;
+					if (e.ctrlKey){
+						newDate = this.moveYear(this.date, dir);
+						newViewDate = this.moveYear(this.viewDate, dir);
+						this._trigger('changeYear', this.viewDate);
+					} else if (e.shiftKey){
+						newDate = this.moveMonth(this.date, dir);
+						newViewDate = this.moveMonth(this.viewDate, dir);
+						this._trigger('changeMonth', this.viewDate);
+					} else {
+						newDate = new Date(this.date);
+						newDate.setUTCDate(this.date.getUTCDate() + dir);
+						newViewDate = new Date(this.viewDate);
+						newViewDate.setUTCDate(this.viewDate.getUTCDate() + dir);
+					}
+					if (this.dateWithinRange(newDate)){
+						this.date = newDate;
+						this.viewDate = newViewDate;
+						this.setValue();
+						this.update();
+						e.preventDefault();
+						dateChanged = true;
+					}
+					break;
+				case 38: // up
+				case 40: // down
+					if (!this.o.keyboardNavigation) break;
+					dir = e.keyCode == 38 ? -1 : 1;
+					if (e.ctrlKey){
+						newDate = this.moveYear(this.date, dir);
+						newViewDate = this.moveYear(this.viewDate, dir);
+						this._trigger('changeYear', this.viewDate);
+					} else if (e.shiftKey){
+						newDate = this.moveMonth(this.date, dir);
+						newViewDate = this.moveMonth(this.viewDate, dir);
+						this._trigger('changeMonth', this.viewDate);
+					} else {
+						newDate = new Date(this.date);
+						newDate.setUTCDate(this.date.getUTCDate() + dir * 7);
+						newViewDate = new Date(this.viewDate);
+						newViewDate.setUTCDate(this.viewDate.getUTCDate() + dir * 7);
+					}
+					if (this.dateWithinRange(newDate)){
+						this.date = newDate;
+						this.viewDate = newViewDate;
+						this.setValue();
+						this.update();
+						e.preventDefault();
+						dateChanged = true;
+					}
+					break;
+				case 13: // enter
+					this.hide();
+					e.preventDefault();
+					break;
+				case 9: // tab
+					this.hide();
+					break;
+			}
+			if (dateChanged){
+				this._trigger('changeDate');
+				var element;
+				if (this.isInput) {
+					element = this.element;
+				} else if (this.component){
+					element = this.element.find('input');
+				}
+				if (element) {
+					element.change();
+				}
+			}
+		},
+
+		showMode: function(dir) {
+			if (dir) {
+				this.viewMode = Math.max(this.o.minViewMode, Math.min(2, this.viewMode + dir));
+			}
+			/*
+				vitalets: fixing bug of very special conditions:
+				jquery 1.7.1 + webkit + show inline datepicker in bootstrap popover.
+				Method show() does not set display css correctly and datepicker is not shown.
+				Changed to .css('display', 'block') solve the problem.
+				See https://github.com/vitalets/x-editable/issues/37
+
+				In jquery 1.7.2+ everything works fine.
+			*/
+			//this.picker.find('>div').hide().filter('.datepicker-'+DPGlobal.modes[this.viewMode].clsName).show();
+			this.picker.find('>div').hide().filter('.datepicker-'+DPGlobal.modes[this.viewMode].clsName).css('display', 'block');
+			this.updateNavArrows();
+		}
+	};
+
+	var DateRangePicker = function(element, options){
+		this.element = $(element);
+		this.inputs = $.map(options.inputs, function(i){ return i.jquery ? i[0] : i; });
+		delete options.inputs;
+
+		$(this.inputs)
+			.datepicker(options)
+			.bind('changeDate', $.proxy(this.dateUpdated, this));
+
+		this.pickers = $.map(this.inputs, function(i){ return $(i).data('datepicker'); });
+		this.updateDates();
+	};
+	DateRangePicker.prototype = {
+		updateDates: function(){
+			this.dates = $.map(this.pickers, function(i){ return i.date; });
+			this.updateRanges();
+		},
+		updateRanges: function(){
+			var range = $.map(this.dates, function(d){ return d.valueOf(); });
+			$.each(this.pickers, function(i, p){
+				p.setRange(range);
+			});
+		},
+		dateUpdated: function(e){
+			var dp = $(e.target).data('datepicker'),
+				new_date = dp.getUTCDate(),
+				i = $.inArray(e.target, this.inputs),
+				l = this.inputs.length;
+			if (i == -1) return;
+
+			if (new_date < this.dates[i]){
+				// Date being moved earlier/left
+				while (i>=0 && new_date < this.dates[i]){
+					this.pickers[i--].setUTCDate(new_date);
+				}
+			}
+			else if (new_date > this.dates[i]){
+				// Date being moved later/right
+				while (i<l && new_date > this.dates[i]){
+					this.pickers[i++].setUTCDate(new_date);
+				}
+			}
+			this.updateDates();
+		},
+		remove: function(){
+			$.map(this.pickers, function(p){ p.remove(); });
+			delete this.element.data().datepicker;
+		}
+	};
+
+	function opts_from_el(el, prefix){
+		// Derive options from element data-attrs
+		var data = $(el).data(),
+			out = {}, inkey,
+			replace = new RegExp('^' + prefix.toLowerCase() + '([A-Z])'),
+			prefix = new RegExp('^' + prefix.toLowerCase());
+		for (var key in data)
+			if (prefix.test(key)){
+				inkey = key.replace(replace, function(_,a){ return a.toLowerCase(); });
+				out[inkey] = data[key];
+			}
+		return out;
+	}
+
+	function opts_from_locale(lang){
+		// Derive options from locale plugins
+		var out = {};
+		// Check if "de-DE" style date is available, if not language should
+		// fallback to 2 letter code eg "de"
+		if (!dates[lang]) {
+			lang = lang.split('-')[0]
+			if (!dates[lang])
+				return;
+		}
+		var d = dates[lang];
+		$.each(locale_opts, function(i,k){
+			if (k in d)
+				out[k] = d[k];
+		});
+		return out;
+	}
+
+	var old = $.fn.datepicker;
+	$.fn.datepicker = function ( option ) {
+		var args = Array.apply(null, arguments);
+		args.shift();
+		var internal_return,
+			this_return;
+		this.each(function () {
+			var $this = $(this),
+				data = $this.data('datepicker'),
+				options = typeof option == 'object' && option;
+			if (!data) {
+				var elopts = opts_from_el(this, 'date'),
+					// Preliminary otions
+					xopts = $.extend({}, defaults, elopts, options),
+					locopts = opts_from_locale(xopts.language),
+					// Options priority: js args, data-attrs, locales, defaults
+					opts = $.extend({}, defaults, locopts, elopts, options);
+				if ($this.is('.input-daterange') || opts.inputs){
+					var ropts = {
+						inputs: opts.inputs || $this.find('input').toArray()
+					};
+					$this.data('datepicker', (data = new DateRangePicker(this, $.extend(opts, ropts))));
+				}
+				else{
+					$this.data('datepicker', (data = new Datepicker(this, opts)));
+				}
+			}
+			if (typeof option == 'string' && typeof data[option] == 'function') {
+				internal_return = data[option].apply(data, args);
+				if (internal_return !== undefined)
+					return false;
+			}
+		});
+		if (internal_return !== undefined)
+			return internal_return;
+		else
+			return this;
+	};
+
+	var defaults = $.fn.datepicker.defaults = {
+		autoclose: false,
+		beforeShowDay: $.noop,
+		calendarWeeks: false,
+		clearBtn: false,
+		daysOfWeekDisabled: [],
+		endDate: Infinity,
+		forceParse: true,
+		format: 'mm/dd/yyyy',
+		keyboardNavigation: true,
+		language: 'en',
+		minViewMode: 0,
+		orientation: "auto",
+		rtl: false,
+		startDate: -Infinity,
+		startView: 0,
+		todayBtn: false,
+		todayHighlight: false,
+		weekStart: 0
+	};
+	var locale_opts = $.fn.datepicker.locale_opts = [
+		'format',
+		'rtl',
+		'weekStart'
+	];
+	$.fn.datepicker.Constructor = Datepicker;
+	var dates = $.fn.datepicker.dates = {
+		en: {
+			days: ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"],
+			daysShort: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"],
+			daysMin: ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"],
+			months: ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"],
+			monthsShort: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"],
+			today: "Today",
+			clear: "Clear"
+		}
+	};
+
+	var DPGlobal = {
+		modes: [
+			{
+				clsName: 'days',
+				navFnc: 'Month',
+				navStep: 1
+			},
+			{
+				clsName: 'months',
+				navFnc: 'FullYear',
+				navStep: 1
+			},
+			{
+				clsName: 'years',
+				navFnc: 'FullYear',
+				navStep: 10
+		}],
+		isLeapYear: function (year) {
+			return (((year % 4 === 0) && (year % 100 !== 0)) || (year % 400 === 0));
+		},
+		getDaysInMonth: function (year, month) {
+			return [31, (DPGlobal.isLeapYear(year) ? 29 : 28), 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month];
+		},
+		validParts: /dd?|DD?|mm?|MM?|yy(?:yy)?/g,
+		nonpunctuation: /[^ -\/:-@\[\u3400-\u9fff-`{-~\t\n\r]+/g,
+		parseFormat: function(format){
+			// IE treats \0 as a string end in inputs (truncating the value),
+			// so it's a bad format delimiter, anyway
+			var separators = format.replace(this.validParts, '\0').split('\0'),
+				parts = format.match(this.validParts);
+			if (!separators || !separators.length || !parts || parts.length === 0){
+				throw new Error("Invalid date format.");
+			}
+			return {separators: separators, parts: parts};
+		},
+		parseDate: function(date, format, language) {
+			if (date instanceof Date) return date;
+			if (typeof format === 'string')
+				format = DPGlobal.parseFormat(format);
+			if (/^[\-+]\d+[dmwy]([\s,]+[\-+]\d+[dmwy])*$/.test(date)) {
+				var part_re = /([\-+]\d+)([dmwy])/,
+					parts = date.match(/([\-+]\d+)([dmwy])/g),
+					part, dir;
+				date = new Date();
+				for (var i=0; i<parts.length; i++) {
+					part = part_re.exec(parts[i]);
+					dir = parseInt(part[1]);
+					switch(part[2]){
+						case 'd':
+							date.setUTCDate(date.getUTCDate() + dir);
+							break;
+						case 'm':
+							date = Datepicker.prototype.moveMonth.call(Datepicker.prototype, date, dir);
+							break;
+						case 'w':
+							date.setUTCDate(date.getUTCDate() + dir * 7);
+							break;
+						case 'y':
+							date = Datepicker.prototype.moveYear.call(Datepicker.prototype, date, dir);
+							break;
+					}
+				}
+				return UTCDate(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), 0, 0, 0);
+			}
+			var parts = date && date.match(this.nonpunctuation) || [],
+				date = new Date(),
+				parsed = {},
+				setters_order = ['yyyy', 'yy', 'M', 'MM', 'm', 'mm', 'd', 'dd'],
+				setters_map = {
+					yyyy: function(d,v){ return d.setUTCFullYear(v); },
+					yy: function(d,v){ return d.setUTCFullYear(2000+v); },
+					m: function(d,v){
+						if (isNaN(d))
+							return d;
+						v -= 1;
+						while (v<0) v += 12;
+						v %= 12;
+						d.setUTCMonth(v);
+						while (d.getUTCMonth() != v)
+							d.setUTCDate(d.getUTCDate()-1);
+						return d;
+					},
+					d: function(d,v){ return d.setUTCDate(v); }
+				},
+				val, filtered, part;
+			setters_map['M'] = setters_map['MM'] = setters_map['mm'] = setters_map['m'];
+			setters_map['dd'] = setters_map['d'];
+			date = UTCDate(date.getFullYear(), date.getMonth(), date.getDate(), 0, 0, 0);
+			var fparts = format.parts.slice();
+			// Remove noop parts
+			if (parts.length != fparts.length) {
+				fparts = $(fparts).filter(function(i,p){
+					return $.inArray(p, setters_order) !== -1;
+				}).toArray();
+			}
+			// Process remainder
+			if (parts.length == fparts.length) {
+				for (var i=0, cnt = fparts.length; i < cnt; i++) {
+					val = parseInt(parts[i], 10);
+					part = fparts[i];
+					if (isNaN(val)) {
+						switch(part) {
+							case 'MM':
+								filtered = $(dates[language].months).filter(function(){
+									var m = this.slice(0, parts[i].length),
+										p = parts[i].slice(0, m.length);
+									return m == p;
+								});
+								val = $.inArray(filtered[0], dates[language].months) + 1;
+								break;
+							case 'M':
+								filtered = $(dates[language].monthsShort).filter(function(){
+									var m = this.slice(0, parts[i].length),
+										p = parts[i].slice(0, m.length);
+									return m == p;
+								});
+								val = $.inArray(filtered[0], dates[language].monthsShort) + 1;
+								break;
+						}
+					}
+					parsed[part] = val;
+				}
+				for (var i=0, _date, s; i<setters_order.length; i++){
+					s = setters_order[i];
+					if (s in parsed && !isNaN(parsed[s])){
+						_date = new Date(date);
+						setters_map[s](_date, parsed[s]);
+						if (!isNaN(_date))
+							date = _date;
+					}
+				}
+			}
+			return date;
+		},
+		formatDate: function(date, format, language){
+			if (typeof format === 'string')
+				format = DPGlobal.parseFormat(format);
+			var val = {
+				d: date.getUTCDate(),
+				D: dates[language].daysShort[date.getUTCDay()],
+				DD: dates[language].days[date.getUTCDay()],
+				m: date.getUTCMonth() + 1,
+				M: dates[language].monthsShort[date.getUTCMonth()],
+				MM: dates[language].months[date.getUTCMonth()],
+				yy: date.getUTCFullYear().toString().substring(2),
+				yyyy: date.getUTCFullYear()
+			};
+			val.dd = (val.d < 10 ? '0' : '') + val.d;
+			val.mm = (val.m < 10 ? '0' : '') + val.m;
+			var date = [],
+				seps = $.extend([], format.separators);
+			for (var i=0, cnt = format.parts.length; i <= cnt; i++) {
+				if (seps.length)
+					date.push(seps.shift());
+				date.push(val[format.parts[i]]);
+			}
+			return date.join('');
+		},
+		headTemplate: '<thead>'+
+							'<tr>'+
+								'<th class="prev">&laquo;</th>'+
+								'<th colspan="5" class="datepicker-switch"></th>'+
+								'<th class="next">&raquo;</th>'+
+							'</tr>'+
+						'</thead>',
+		contTemplate: '<tbody><tr><td colspan="7"></td></tr></tbody>',
+		footTemplate: '<tfoot><tr><th colspan="7" class="today"></th></tr><tr><th colspan="7" class="clear"></th></tr></tfoot>'
+	};
+	DPGlobal.template = '<div class="datepicker">'+
+							'<div class="datepicker-days">'+
+								'<table class=" table-condensed">'+
+									DPGlobal.headTemplate+
+									'<tbody></tbody>'+
+									DPGlobal.footTemplate+
+								'</table>'+
+							'</div>'+
+							'<div class="datepicker-months">'+
+								'<table class="table-condensed">'+
+									DPGlobal.headTemplate+
+									DPGlobal.contTemplate+
+									DPGlobal.footTemplate+
+								'</table>'+
+							'</div>'+
+							'<div class="datepicker-years">'+
+								'<table class="table-condensed">'+
+									DPGlobal.headTemplate+
+									DPGlobal.contTemplate+
+									DPGlobal.footTemplate+
+								'</table>'+
+							'</div>'+
+						'</div>';
+
+	$.fn.datepicker.DPGlobal = DPGlobal;
+
+
+	/* DATEPICKER NO CONFLICT
+	* =================== */
+
+	$.fn.datepicker.noConflict = function(){
+		$.fn.datepicker = old;
+		return this;
+	};
+
+
+	/* DATEPICKER DATA-API
+	* ================== */
+
+	$(document).on(
+		'focus.datepicker.data-api click.datepicker.data-api',
+		'[data-provide="datepicker"]',
+		function(e){
+			var $this = $(this);
+			if ($this.data('datepicker')) return;
+			e.preventDefault();
+			// component click requires us to explicitly show it
+			$this.datepicker('show');
+		}
+	);
+	$(function(){
+		$('[data-provide="datepicker-inline"]').datepicker();
+	});
+
+}( window.jQuery ));

+ 15 - 0
static/cranefly/js/locales/bootstrap-datepicker.ar.js

@@ -0,0 +1,15 @@
+/**
+ * Arabic translation for bootstrap-datepicker
+ * Mohammed Alshehri <alshehri866@gmail.com>
+ */
+;(function($){
+    $.fn.datepicker.dates['ar'] = {
+        days: ["الأحد", "الاثنين", "الثلاثاء", "الأربعاء", "الخميس", "الجمعة", "السبت", "الأحد"],
+        daysShort: ["أحد", "اثنين", "ثلاثاء", "أربعاء", "خميس", "جمعة", "سبت", "أحد"],
+        daysMin: ["ح", "ن", "ث", "ع", "خ", "ج", "س", "ح"],
+        months: ["يناير", "فبراير", "مارس", "أبريل", "مايو", "يونيو", "يوليو", "أغسطس", "سبتمبر", "أكتوبر", "نوفمبر", "ديسمبر"],
+        monthsShort: ["يناير", "فبراير", "مارس", "أبريل", "مايو", "يونيو", "يوليو", "أغسطس", "سبتمبر", "أكتوبر", "نوفمبر", "ديسمبر"],
+        today: "هذا اليوم",
+        rtl: true
+    };
+}(jQuery));

+ 14 - 0
static/cranefly/js/locales/bootstrap-datepicker.bg.js

@@ -0,0 +1,14 @@
+/**
+ * Bulgarian translation for bootstrap-datepicker
+ * Apostol Apostolov <apostol.s.apostolov@gmail.com>
+ */
+;(function($){
+	$.fn.datepicker.dates['bg'] = {
+		days: ["Неделя", "Понеделник", "Вторник", "Сряда", "Четвъртък", "Петък", "Събота", "Неделя"],
+		daysShort: ["Нед", "Пон", "Вто", "Сря", "Чет", "Пет", "Съб", "Нед"],
+		daysMin: ["Н", "П", "В", "С", "Ч", "П", "С", "Н"],
+		months: ["Януари", "Февруари", "Март", "Април", "Май", "Юни", "Юли", "Август", "Септември", "Октомври", "Ноември", "Декември"],
+		monthsShort: ["Ян", "Фев", "Мар", "Апр", "Май", "Юни", "Юли", "Авг", "Сеп", "Окт", "Ное", "Дек"],
+		today: "днес"
+	};
+}(jQuery));

+ 14 - 0
static/cranefly/js/locales/bootstrap-datepicker.ca.js

@@ -0,0 +1,14 @@
+/**
+ * Catalan translation for bootstrap-datepicker
+ * J. Garcia <jogaco.en@gmail.com>
+ */
+;(function($){
+	$.fn.datepicker.dates['ca'] = {
+		days: ["Diumenge", "Dilluns", "Dimarts", "Dimecres", "Dijous", "Divendres", "Dissabte", "Diumenge"],
+		daysShort: ["Diu",  "Dil", "Dmt", "Dmc", "Dij", "Div", "Dis", "Diu"],
+		daysMin: ["dg", "dl", "dt", "dc", "dj", "dv", "ds", "dg"],
+		months: ["Gener", "Febrer", "Març", "Abril", "Maig", "Juny", "Juliol", "Agost", "Setembre", "Octubre", "Novembre", "Desembre"],
+		monthsShort: ["Gen", "Feb", "Mar", "Abr", "Mai", "Jun", "Jul", "Ago", "Set", "Oct", "Nov", "Des"],
+		today: "Avui"
+	};
+}(jQuery));

+ 15 - 0
static/cranefly/js/locales/bootstrap-datepicker.cs.js

@@ -0,0 +1,15 @@
+/**
+ * Czech translation for bootstrap-datepicker
+ * Matěj Koubík <matej@koubik.name>
+ * Fixes by Michal Remiš <michal.remis@gmail.com>
+ */
+;(function($){
+	$.fn.datepicker.dates['cs'] = {
+		days: ["Neděle", "Pondělí", "Úterý", "Středa", "Čtvrtek", "Pátek", "Sobota", "Neděle"],
+		daysShort: ["Ned", "Pon", "Úte", "Stř", "Čtv", "Pát", "Sob", "Ned"],
+		daysMin: ["Ne", "Po", "Út", "St", "Čt", "Pá", "So", "Ne"],
+		months: ["Leden", "Únor", "Březen", "Duben", "Květen", "Červen", "Červenec", "Srpen", "Září", "Říjen", "Listopad", "Prosinec"],
+		monthsShort: ["Led", "Úno", "Bře", "Dub", "Kvě", "Čer", "Čnc", "Srp", "Zář", "Říj", "Lis", "Pro"],
+		today: "Dnes"
+	};
+}(jQuery));

+ 14 - 0
static/cranefly/js/locales/bootstrap-datepicker.cy.js

@@ -0,0 +1,14 @@
+/**
+ * Welsh translation for bootstrap-datepicker
+ * S. Morris <s.morris@bangor.ac.uk>
+ */
+;(function($){
+	$.fn.datepicker.dates['cy'] = {
+		days: ["Sul", "Llun", "Mawrth", "Mercher", "Iau", "Gwener", "Sadwrn", "Sul"],
+		daysShort: ["Sul", "Llu", "Maw", "Mer", "Iau", "Gwe", "Sad", "Sul"],
+		daysMin: ["Su", "Ll", "Ma", "Me", "Ia", "Gwe", "Sa", "Su"],
+		months: ["Ionawr", "Chewfror", "Mawrth", "Ebrill", "Mai", "Mehefin", "Gorfennaf", "Awst", "Medi", "Hydref", "Tachwedd", "Rhagfyr"],
+		monthsShort: ["Ion", "Chw", "Maw", "Ebr", "Mai", "Meh", "Gor", "Aws", "Med", "Hyd", "Tach", "Rha"],
+		today: "Heddiw"
+	};
+}(jQuery));

+ 14 - 0
static/cranefly/js/locales/bootstrap-datepicker.da.js

@@ -0,0 +1,14 @@
+/**
+ * Danish translation for bootstrap-datepicker
+ * Christian Pedersen <http://github.com/chripede>
+ */
+;(function($){
+	$.fn.datepicker.dates['da'] = {
+		days: ["Søndag", "Mandag", "Tirsdag", "Onsdag", "Torsdag", "Fredag", "Lørdag", "Søndag"],
+		daysShort: ["Søn", "Man", "Tir", "Ons", "Tor", "Fre", "Lør", "Søn"],
+		daysMin: ["Sø", "Ma", "Ti", "On", "To", "Fr", "Lø", "Sø"],
+		months: ["Januar", "Februar", "Marts", "April", "Maj", "Juni", "Juli", "August", "September", "Oktober", "November", "December"],
+		monthsShort: ["Jan", "Feb", "Mar", "Apr", "Maj", "Jun", "Jul", "Aug", "Sep", "Okt", "Nov", "Dec"],
+		today: "I Dag"
+	};
+}(jQuery));

+ 17 - 0
static/cranefly/js/locales/bootstrap-datepicker.de.js

@@ -0,0 +1,17 @@
+/**
+ * German translation for bootstrap-datepicker
+ * Sam Zurcher <sam@orelias.ch>
+ */
+;(function($){
+	$.fn.datepicker.dates['de'] = {
+		days: ["Sonntag", "Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag", "Sonntag"],
+		daysShort: ["Son", "Mon", "Die", "Mit", "Don", "Fre", "Sam", "Son"],
+		daysMin: ["So", "Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"],
+		months: ["Januar", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember"],
+		monthsShort: ["Jan", "Feb", "Mär", "Apr", "Mai", "Jun", "Jul", "Aug", "Sep", "Okt", "Nov", "Dez"],
+		today: "Heute",
+		clear: "Löschen",
+		weekStart: 1,
+		format: "dd.mm.yyyy"
+	};
+}(jQuery));

+ 13 - 0
static/cranefly/js/locales/bootstrap-datepicker.el.js

@@ -0,0 +1,13 @@
+/**
+* Greek translation for bootstrap-datepicker
+*/
+;(function($){
+  $.fn.datepicker.dates['el'] = {
+    days: ["Κυριακή", "Δευτέρα", "Τρίτη", "Τετάρτη", "Πέμπτη", "Παρασκευή", "Σάββατο", "Κυριακή"],
+    daysShort: ["Κυρ", "Δευ", "Τρι", "Τετ", "Πεμ", "Παρ", "Σαβ", "Κυρ"],
+    daysMin: ["Κυ", "Δε", "Τρ", "Τε", "Πε", "Πα", "Σα", "Κυ"],
+    months: ["Ιανουάριος", "Φεβρουάριος", "Μάρτιος", "Απρίλιος", "Μάιος", "Ιούνιος", "Ιούλιος", "Αύγουστος", "Σεπτέμβριος", "Οκτώβριος", "Νοέμβριος", "Δεκέμβριος"],
+    monthsShort: ["Ιαν", "Φεβ", "Μαρ", "Απρ", "Μάι", "Ιουν", "Ιουλ", "Αυγ", "Σεπ", "Οκτ", "Νοε", "Δεκ"],
+    today: "Σήμερα"
+  };
+}(jQuery));

+ 14 - 0
static/cranefly/js/locales/bootstrap-datepicker.es.js

@@ -0,0 +1,14 @@
+/**
+ * Spanish translation for bootstrap-datepicker
+ * Bruno Bonamin <bruno.bonamin@gmail.com>
+ */
+;(function($){
+	$.fn.datepicker.dates['es'] = {
+		days: ["Domingo", "Lunes", "Martes", "Miércoles", "Jueves", "Viernes", "Sábado", "Domingo"],
+		daysShort: ["Dom", "Lun", "Mar", "Mié", "Jue", "Vie", "Sáb", "Dom"],
+		daysMin: ["Do", "Lu", "Ma", "Mi", "Ju", "Vi", "Sa", "Do"],
+		months: ["Enero", "Febrero", "Marzo", "Abril", "Mayo", "Junio", "Julio", "Agosto", "Septiembre", "Octubre", "Noviembre", "Diciembre"],
+		monthsShort: ["Ene", "Feb", "Mar", "Abr", "May", "Jun", "Jul", "Ago", "Sep", "Oct", "Nov", "Dic"],
+		today: "Hoy"
+	};
+}(jQuery));

+ 14 - 0
static/cranefly/js/locales/bootstrap-datepicker.et.js

@@ -0,0 +1,14 @@
+/**
+ * Estonian translation for bootstrap-datepicker
+ * Ando Roots <https://github.com/anroots>
+ */
+;(function($){
+	$.fn.datepicker.dates['et'] = {
+		days: ["Pühapäev", "Esmaspäev", "Teisipäev", "Kolmapäev", "Neljapäev", "Reede", "Laupäev", "Pühapäev"],
+		daysShort: ["Püh", "Esm", "Tei", "Kol", "Nel", "Ree", "Lau", "Sun"],
+		daysMin: ["P", "E", "T", "K", "N", "R", "L", "P"],
+		months: ["Jaanuar", "Veebruar", "Märts", "Aprill", "Mai", "Juuni", "Juuli", "August", "September", "Oktoober", "November", "Detsember"],
+		monthsShort: ["Jaan", "Veeb", "Märts", "Apr", "Mai", "Juuni", "Juuli", "Aug", "Sept", "Okt", "Nov", "Dets"],
+		today: "Täna"
+	};
+}(jQuery));

+ 16 - 0
static/cranefly/js/locales/bootstrap-datepicker.fi.js

@@ -0,0 +1,16 @@
+/**
+ * Finnish translation for bootstrap-datepicker
+ * Jaakko Salonen <https://github.com/jsalonen>
+ */
+;(function($){
+	$.fn.datepicker.dates['fi'] = {
+		days: ["sunnuntai", "maanantai", "tiistai", "keskiviikko", "torstai", "perjantai", "lauantai", "sunnuntai"],
+		daysShort: ["sun", "maa", "tii", "kes", "tor", "per", "lau", "sun"],
+		daysMin: ["su", "ma", "ti", "ke", "to", "pe", "la", "su"],
+		months: ["tammikuu", "helmikuu", "maaliskuu", "huhtikuu", "toukokuu", "kesäkuu", "heinäkuu", "elokuu", "syyskuu", "lokakuu", "marraskuu", "joulukuu"],
+		monthsShort: ["tam", "hel", "maa", "huh", "tou", "kes", "hei", "elo", "syy", "lok", "mar", "jou"],
+		today: "tänään",
+		weekStart: 1,
+		format: "d.m.yyyy"
+	};
+}(jQuery));

+ 17 - 0
static/cranefly/js/locales/bootstrap-datepicker.fr.js

@@ -0,0 +1,17 @@
+/**
+ * French translation for bootstrap-datepicker
+ * Nico Mollet <nico.mollet@gmail.com>
+ */
+;(function($){
+	$.fn.datepicker.dates['fr'] = {
+		days: ["Dimanche", "Lundi", "Mardi", "Mercredi", "Jeudi", "Vendredi", "Samedi", "Dimanche"],
+		daysShort: ["Dim", "Lun", "Mar", "Mer", "Jeu", "Ven", "Sam", "Dim"],
+		daysMin: ["D", "L", "Ma", "Me", "J", "V", "S", "D"],
+		months: ["Janvier", "Février", "Mars", "Avril", "Mai", "Juin", "Juillet", "Août", "Septembre", "Octobre", "Novembre", "Décembre"],
+		monthsShort: ["Jan", "Fev", "Mar", "Avr", "Mai", "Jui", "Jul", "Aou", "Sep", "Oct", "Nov", "Dec"],
+		today: "Aujourd'hui",
+		clear: "Effacer",
+		weekStart: 1,
+		format: "dd/mm/yyyy"
+	};
+}(jQuery));

+ 15 - 0
static/cranefly/js/locales/bootstrap-datepicker.he.js

@@ -0,0 +1,15 @@
+/**
+ * Hebrew translation for bootstrap-datepicker
+ * Sagie Maoz <sagie@maoz.info>
+ */
+;(function($){
+  $.fn.datepicker.dates['he'] = {
+      days: ["ראשון", "שני", "שלישי", "רביעי", "חמישי", "שישי", "שבת", "ראשון"],
+      daysShort: ["א", "ב", "ג", "ד", "ה", "ו", "ש", "א"],
+      daysMin: ["א", "ב", "ג", "ד", "ה", "ו", "ש", "א"],
+      months: ["ינואר", "פברואר", "מרץ", "אפריל", "מאי", "יוני", "יולי", "אוגוסט", "ספטמבר", "אוקטובר", "נובמבר", "דצמבר"],
+      monthsShort: ["ינו", "פבר", "מרץ", "אפר", "מאי", "יונ", "יול", "אוג", "ספט", "אוק", "נוב", "דצמ"],
+      today: "היום",
+      rtl: true
+  };
+}(jQuery));

+ 13 - 0
static/cranefly/js/locales/bootstrap-datepicker.hr.js

@@ -0,0 +1,13 @@
+/**
+ * Croatian localisation
+ */
+;(function($){
+	$.fn.datepicker.dates['hr'] = {
+		days: ["Nedjelja", "Ponedjelja", "Utorak", "Srijeda", "Četrtak", "Petak", "Subota", "Nedjelja"],
+		daysShort: ["Ned", "Pon", "Uto", "Srr", "Čet", "Pet", "Sub", "Ned"],
+		daysMin: ["Ne", "Po", "Ut", "Sr", "Če", "Pe", "Su", "Ne"],
+		months: ["Siječanj", "Veljača", "Ožujak", "Travanj", "Svibanj", "Lipanj", "Srpanj", "Kolovoz", "Rujan", "Listopad", "Studeni", "Prosinac"],
+		monthsShort: ["Sije", "Velj", "Ožu", "Tra", "Svi", "Lip", "Jul", "Kol", "Ruj", "Lis", "Stu", "Pro"],
+		today: "Danas"
+	};
+}(jQuery));

+ 16 - 0
static/cranefly/js/locales/bootstrap-datepicker.hu.js

@@ -0,0 +1,16 @@
+/**
+ * Hungarian translation for bootstrap-datepicker
+ * Sotus László <lacisan@gmail.com>
+ */
+;(function($){
+  $.fn.datepicker.dates['hu'] = {
+		days: ["Vasárnap", "Hétfő", "Kedd", "Szerda", "Csütörtök", "Péntek", "Szombat", "Vasárnap"],
+		daysShort: ["Vas", "Hét", "Ked", "Sze", "Csü", "Pén", "Szo", "Vas"],
+		daysMin: ["Va", "Hé", "Ke", "Sz", "Cs", "Pé", "Sz", "Va"],
+		months: ["Január", "Február", "Március", "Április", "Május", "Június", "Július", "Augusztus", "Szeptember", "Október", "November", "December"],
+		monthsShort: ["Jan", "Feb", "Már", "Ápr", "Máj", "Jún", "Júl", "Aug", "Sze", "Okt", "Nov", "Dec"],
+		today: "Ma",
+		weekStart: 1,
+		format: "yyyy.mm.dd"
+	};
+}(jQuery));

+ 15 - 0
static/cranefly/js/locales/bootstrap-datepicker.id.js

@@ -0,0 +1,15 @@
+/**
+ * Bahasa translation for bootstrap-datepicker
+ * Azwar Akbar <azwar.akbar@gmail.com>
+ */
+;(function($){
+	$.fn.datepicker.dates['id'] = {
+		days: ["Minggu", "Senin", "Selasa", "Rabu", "Kamis", "Jumat", "Sabtu", "Minggu"],
+		daysShort: ["Mgu", "Sen", "Sel", "Rab", "Kam", "Jum", "Sab", "Mgu"],
+		daysMin: ["Mg", "Sn", "Sl", "Ra", "Ka", "Ju", "Sa", "Mg"],
+		months: ["Januari", "Februari", "Maret", "April", "Mei", "Juni", "Juli", "Agustus", "September", "Oktober", "November", "Desember"],
+		monthsShort: ["Jan", "Feb", "Mar", "Apr", "Mei", "Jun", "Jul", "Ags", "Sep", "Okt", "Nov", "Des"],
+		today: "Hari Ini",
+		clear: "Kosongkan"
+	};
+}(jQuery));

+ 14 - 0
static/cranefly/js/locales/bootstrap-datepicker.is.js

@@ -0,0 +1,14 @@
+/**
+ * Icelandic translation for bootstrap-datepicker
+ * Hinrik Örn Sigurðsson <hinrik.sig@gmail.com>
+ */
+;(function($){
+	$.fn.datepicker.dates['is'] = {
+		days: ["Sunnudagur", "Mánudagur", "Þriðjudagur", "Miðvikudagur", "Fimmtudagur", "Föstudagur", "Laugardagur", "Sunnudagur"],
+		daysShort: ["Sun", "Mán", "Þri", "Mið", "Fim", "Fös", "Lau", "Sun"],
+		daysMin: ["Su", "Má", "Þr", "Mi", "Fi", "Fö", "La", "Su"],
+		months: ["Janúar", "Febrúar", "Mars", "Apríl", "Maí", "Júní", "Júlí", "Ágúst", "September", "Október", "Nóvember", "Desember"],
+		monthsShort: ["Jan", "Feb", "Mar", "Apr", "Maí", "Jún", "Júl", "Ágú", "Sep", "Okt", "Nóv", "Des"],
+		today: "Í Dag"
+	};
+}(jQuery));

+ 16 - 0
static/cranefly/js/locales/bootstrap-datepicker.it.js

@@ -0,0 +1,16 @@
+/**
+ * Italian translation for bootstrap-datepicker
+ * Enrico Rubboli <rubboli@gmail.com>
+ */
+;(function($){
+	$.fn.datepicker.dates['it'] = {
+		days: ["Domenica", "Lunedì", "Martedì", "Mercoledì", "Giovedì", "Venerdì", "Sabato", "Domenica"],
+		daysShort: ["Dom", "Lun", "Mar", "Mer", "Gio", "Ven", "Sab", "Dom"],
+		daysMin: ["Do", "Lu", "Ma", "Me", "Gi", "Ve", "Sa", "Do"],
+		months: ["Gennaio", "Febbraio", "Marzo", "Aprile", "Maggio", "Giugno", "Luglio", "Agosto", "Settembre", "Ottobre", "Novembre", "Dicembre"],
+		monthsShort: ["Gen", "Feb", "Mar", "Apr", "Mag", "Giu", "Lug", "Ago", "Set", "Ott", "Nov", "Dic"],
+		today: "Oggi",
+		weekStart: 1,
+		format: "dd/mm/yyyy"
+	};
+}(jQuery));

+ 15 - 0
static/cranefly/js/locales/bootstrap-datepicker.ja.js

@@ -0,0 +1,15 @@
+/**
+ * Japanese translation for bootstrap-datepicker
+ * Norio Suzuki <https://github.com/suzuki/>
+ */
+;(function($){
+	$.fn.datepicker.dates['ja'] = {
+		days: ["日曜", "月曜", "火曜", "水曜", "木曜", "金曜", "土曜", "日曜"],
+		daysShort: ["日", "月", "火", "水", "木", "金", "土", "日"],
+		daysMin: ["日", "月", "火", "水", "木", "金", "土", "日"],
+		months: ["1月", "2月", "3月", "4月", "5月", "6月", "7月", "8月", "9月", "10月", "11月", "12月"],
+		monthsShort: ["1月", "2月", "3月", "4月", "5月", "6月", "7月", "8月", "9月", "10月", "11月", "12月"],
+		today: "今日",
+		format: "yyyy/mm/dd"
+	};
+}(jQuery));

+ 15 - 0
static/cranefly/js/locales/bootstrap-datepicker.ka.js

@@ -0,0 +1,15 @@
+/**
+ * Georgian translation for bootstrap-datepicker
+ * Levan Melikishvili <levani0101@yahoo.com>
+ */
+;(function($){
+    $.fn.datepicker.dates['ka'] = {
+        days: ["კვირა", "ორშაბათი", "სამშაბათი", "ოთხშაბათი", "ხუთშაბათი", "პარასკევი", "შაბათი", "კვირა"],
+        daysShort: ["კვი", "ორშ", "სამ", "ოთხ", "ხუთ", "პარ", "შაბ", "კვი"],
+        daysMin: ["კვ", "ორ", "სა", "ოთ", "ხუ", "პა", "შა", "კვ"],
+        months: ["იანვარი", "თებერვალი", "მარტი", "აპრილი", "მაისი", "ივნისი", "ივლისი", "აგვისტო", "სექტემბერი", "ოქტომები", "ნოემბერი", "დეკემბერი"],
+        monthsShort: ["იან", "თებ", "მარ", "აპრ", "მაი", "ივნ", "ივლ", "აგვ", "სექ", "ოქტ", "ნოე", "დეკ"],
+        today: "დღეს",
+        clear: "გასუფთავება"
+    };
+}(jQuery));

+ 13 - 0
static/cranefly/js/locales/bootstrap-datepicker.kr.js

@@ -0,0 +1,13 @@
+/**
+ * Korean translation for bootstrap-datepicker
+ * Gu Youn <http://github.com/guyoun>
+ */
+;(function($){
+	$.fn.datepicker.dates['kr'] = {
+		days: ["일요일", "월요일", "화요일", "수요일", "목요일", "금요일", "토요일", "일요일"],
+		daysShort: ["일", "월", "화", "수", "목", "금", "토", "일"],
+		daysMin: ["일", "월", "화", "수", "목", "금", "토", "일"],
+		months: ["1월", "2월", "3월", "4월", "5월", "6월", "7월", "8월", "9월", "10월", "11월", "12월"],
+		monthsShort: ["1월", "2월", "3월", "4월", "5월", "6월", "7월", "8월", "9월", "10월", "11월", "12월"]
+	};
+}(jQuery));

+ 16 - 0
static/cranefly/js/locales/bootstrap-datepicker.lt.js

@@ -0,0 +1,16 @@
+/**
+ * Lithuanian translation for bootstrap-datepicker
+ * Šarūnas Gliebus <ssharunas@yahoo.co.uk>
+ */
+
+;(function($){
+    $.fn.datepicker.dates['lt'] = {
+        days: ["Sekmadienis", "Pirmadienis", "Antradienis", "Trečiadienis", "Ketvirtadienis", "Penktadienis", "Šeštadienis", "Sekmadienis"],
+        daysShort: ["S", "Pr", "A", "T", "K", "Pn", "Š", "S"],
+        daysMin: ["Sk", "Pr", "An", "Tr", "Ke", "Pn", "Št", "Sk"],
+        months: ["Sausis", "Vasaris", "Kovas", "Balandis", "Gegužė", "Birželis", "Liepa", "Rugpjūtis", "Rugsėjis", "Spalis", "Lapkritis", "Gruodis"],
+        monthsShort: ["Sau", "Vas", "Kov", "Bal", "Geg", "Bir", "Lie", "Rugp", "Rugs", "Spa", "Lap", "Gru"],
+        today: "Šiandien",
+        weekStart: 1
+    };
+}(jQuery));

+ 16 - 0
static/cranefly/js/locales/bootstrap-datepicker.lv.js

@@ -0,0 +1,16 @@
+/**
+ * Latvian translation for bootstrap-datepicker
+ * Artis Avotins <artis@apit.lv>
+ */
+
+;(function($){
+    $.fn.datepicker.dates['lv'] = {
+        days: ["Svētdiena", "Pirmdiena", "Otrdiena", "Trešdiena", "Ceturtdiena", "Piektdiena", "Sestdiena", "Svētdiena"],
+        daysShort: ["Sv", "P", "O", "T", "C", "Pk", "S", "Sv"],
+        daysMin: ["Sv", "Pr", "Ot", "Tr", "Ce", "Pk", "St", "Sv"],
+        months: ["Janvāris", "Februāris", "Marts", "Aprīlis", "Maijs", "Jūnijs", "Jūlijs", "Augusts", "Septembris", "Oktobris", "Novembris", "Decembris"],
+        monthsShort: ["Jan", "Feb", "Mar", "Apr", "Mai", "Jūn", "Jūl", "Aug", "Sep", "Okt", "Nov", "Dec."],
+        today: "Šodien",
+        weekStart: 1
+    };
+}(jQuery));

+ 14 - 0
static/cranefly/js/locales/bootstrap-datepicker.mk.js

@@ -0,0 +1,14 @@
+/**
+ * Macedonian translation for bootstrap-datepicker
+ * Marko Aleksic <psybaron@gmail.com>
+ */
+;(function($){
+    $.fn.datepicker.dates['mk'] = {
+        days: ["Недела", "Понеделник", "Вторник", "Среда", "Четврток", "Петок", "Сабота", "Недела"],
+        daysShort: ["Нед", "Пон", "Вто", "Сре", "Чет", "Пет", "Саб", "Нед"],
+        daysMin: ["Не", "По", "Вт", "Ср", "Че", "Пе", "Са", "Не"],
+        months: ["Јануари", "Февруари", "Март", "Април", "Мај", "Јуни", "Јули", "Август", "Септември", "Октомври", "Ноември", "Декември"],
+        monthsShort: ["Јан", "Фев", "Мар", "Апр", "Мај", "Јун", "Јул", "Авг", "Сеп", "Окт", "Ное", "Дек"],
+        today: "Денес"
+    };
+}(jQuery));

+ 14 - 0
static/cranefly/js/locales/bootstrap-datepicker.ms.js

@@ -0,0 +1,14 @@
+/**
+ * Malay translation for bootstrap-datepicker
+ * Ateman Faiz <noorulfaiz@gmail.com>
+ */
+;(function($){
+	$.fn.datepicker.dates['ms'] = {
+		days: ["Ahad", "Isnin", "Selasa", "Rabu", "Khamis", "Jumaat", "Sabtu", "Ahad"],
+		daysShort: ["Aha", "Isn", "Sel", "Rab", "Kha", "Jum", "Sab", "Aha"],
+		daysMin: ["Ah", "Is", "Se", "Ra", "Kh", "Ju", "Sa", "Ah"],
+		months: ["Januari", "Februari", "Mac", "April", "Mei", "Jun", "Julai", "Ogos", "September", "Oktober", "November", "Disember"],
+		monthsShort: ["Jan", "Feb", "Mar", "Apr", "Mei", "Jun", "Jul", "Ogo", "Sep", "Okt", "Nov", "Dis"],
+		today: "Hari Ini"
+	};
+}(jQuery));

+ 14 - 0
static/cranefly/js/locales/bootstrap-datepicker.nb.js

@@ -0,0 +1,14 @@
+/**
+ * Norwegian (bokmål) translation for bootstrap-datepicker
+ * Fredrik Sundmyhr <http://github.com/fsundmyhr>
+ */
+;(function($){
+	$.fn.datepicker.dates['nb'] = {
+		days: ["Søndag", "Mandag", "Tirsdag", "Onsdag", "Torsdag", "Fredag", "Lørdag", "Søndag"],
+		daysShort: ["Søn", "Man", "Tir", "Ons", "Tor", "Fre", "Lør", "Søn"],
+		daysMin: ["Sø", "Ma", "Ti", "On", "To", "Fr", "Lø", "Sø"],
+		months: ["Januar", "Februar", "Mars", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Desember"],
+		monthsShort: ["Jan", "Feb", "Mar", "Apr", "Mai", "Jun", "Jul", "Aug", "Sep", "Okt", "Nov", "Des"],
+		today: "I Dag"
+	};
+}(jQuery));

+ 16 - 0
static/cranefly/js/locales/bootstrap-datepicker.nl-BE.js

@@ -0,0 +1,16 @@
+/**
+ * Belgium-Dutch translation for bootstrap-datepicker
+ * Julien Poulin <poulin_julien@hotmail.com>
+ */
+; (function ($) {
+  $.fn.datepicker.dates['nl-BE'] = {
+    days: ["Zondag", "Maandag", "Dinsdag", "Woensdag", "Donderdag", "Vrijdag", "Zaterdag", "Zondag"],
+    daysShort: ["Zo", "Ma", "Di", "Wo", "Do", "Vr", "Za", "Zo"],
+    daysMin: ["Zo", "Ma", "Di", "Wo", "Do", "Vr", "Za", "Zo"],
+    months: ["Januari", "Februari", "Maart", "April", "Mei", "Juni", "Juli", "Augustus", "September", "Oktober", "November", "December"],
+    monthsShort: ["Jan", "Feb", "Mrt", "Apr", "Mei", "Jun", "Jul", "Aug", "Sep", "Okt", "Nov", "Dec"],
+    today: "Vandaag",
+    weekStart: 1,
+    format: "dd/mm/yyyy"
+  };
+}(jQuery));

+ 14 - 0
static/cranefly/js/locales/bootstrap-datepicker.nl.js

@@ -0,0 +1,14 @@
+/**
+ * Dutch translation for bootstrap-datepicker
+ * Reinier Goltstein <mrgoltstein@gmail.com>
+ */
+;(function($){
+	$.fn.datepicker.dates['nl'] = {
+		days: ["Zondag", "Maandag", "Dinsdag", "Woensdag", "Donderdag", "Vrijdag", "Zaterdag", "Zondag"],
+		daysShort: ["Zo", "Ma", "Di", "Wo", "Do", "Vr", "Za", "Zo"],
+		daysMin: ["Zo", "Ma", "Di", "Wo", "Do", "Vr", "Za", "Zo"],
+		months: ["Januari", "Februari", "Maart", "April", "Mei", "Juni", "Juli", "Augustus", "September", "Oktober", "November", "December"],
+		monthsShort: ["Jan", "Feb", "Mrt", "Apr", "Mei", "Jun", "Jul", "Aug", "Sep", "Okt", "Nov", "Dec"],
+		today: "Vandaag"
+	};
+}(jQuery));

+ 15 - 0
static/cranefly/js/locales/bootstrap-datepicker.no.js

@@ -0,0 +1,15 @@
+/**
+ *  Norwegian translation for bootstrap-datepicker
+ **/
+;(function($){
+  $.fn.datepicker.dates['no'] = {
+    days: ['Søndag','Mandag','Tirsdag','Onsdag','Torsdag','Fredag','Lørdag'],
+    daysShort: ['Søn','Man','Tir','Ons','Tor','Fre','Lør'],
+    daysMin: ['Sø','Ma','Ti','On','To','Fr','Lø'],
+    months: ['Januar','Februar','Mars','April','Mai','Juni','Juli','August','September','Oktober','November','Desember'],
+    monthsShort: ['Jan','Feb','Mar','Apr','Mai','Jun','Jul','Aug','Sep','Okt','Nov','Des'],
+    today: 'I dag',
+    clear: 'Nullstill',
+    weekStart: 0
+  };
+}(jQuery));

+ 15 - 0
static/cranefly/js/locales/bootstrap-datepicker.pl.js

@@ -0,0 +1,15 @@
+/**
+ * Polish translation for bootstrap-datepicker
+ * Robert <rtpm@gazeta.pl>
+ */
+;(function($){
+        $.fn.datepicker.dates['pl'] = {
+                days: ["Niedziela", "Poniedziałek", "Wtorek", "Środa", "Czwartek", "Piątek", "Sobota", "Niedziela"],
+                daysShort: ["Nie", "Pn", "Wt", "Śr", "Czw", "Pt", "So", "Nie"],
+                daysMin: ["N", "Pn", "Wt", "Śr", "Cz", "Pt", "So", "N"],
+                months: ["Styczeń", "Luty", "Marzec", "Kwiecień", "Maj", "Czerwiec", "Lipiec", "Sierpień", "Wrzesień", "Październik", "Listopad", "Grudzień"],
+                monthsShort: ["Sty", "Lu", "Mar", "Kw", "Maj", "Cze", "Lip", "Sie", "Wrz", "Pa", "Lis", "Gru"],
+                today: "Dzisiaj",
+                weekStart: 1
+        };
+}(jQuery));

+ 15 - 0
static/cranefly/js/locales/bootstrap-datepicker.pt-BR.js

@@ -0,0 +1,15 @@
+/**
+ * Brazilian translation for bootstrap-datepicker
+ * Cauan Cabral <cauan@radig.com.br>
+ */
+;(function($){
+	$.fn.datepicker.dates['pt-BR'] = {
+		days: ["Domingo", "Segunda", "Terça", "Quarta", "Quinta", "Sexta", "Sábado", "Domingo"],
+		daysShort: ["Dom", "Seg", "Ter", "Qua", "Qui", "Sex", "Sáb", "Dom"],
+		daysMin: ["Do", "Se", "Te", "Qu", "Qu", "Se", "Sa", "Do"],
+		months: ["Janeiro", "Fevereiro", "Março", "Abril", "Maio", "Junho", "Julho", "Agosto", "Setembro", "Outubro", "Novembro", "Dezembro"],
+		monthsShort: ["Jan", "Fev", "Mar", "Abr", "Mai", "Jun", "Jul", "Ago", "Set", "Out", "Nov", "Dez"],
+		today: "Hoje",
+		clear: "Limpar"
+	};
+}(jQuery));

+ 16 - 0
static/cranefly/js/locales/bootstrap-datepicker.pt.js

@@ -0,0 +1,16 @@
+/**
+ * Portuguese translation for bootstrap-datepicker
+ * Original code: Cauan Cabral <cauan@radig.com.br>
+ * Tiago Melo <tiago.blackcode@gmail.com>
+ */
+;(function($){
+	$.fn.datepicker.dates['pt'] = {
+		days: ["Domingo", "Segunda", "Terça", "Quarta", "Quinta", "Sexta", "Sábado", "Domingo"],
+		daysShort: ["Dom", "Seg", "Ter", "Qua", "Qui", "Sex", "Sáb", "Dom"],
+		daysMin: ["Do", "Se", "Te", "Qu", "Qu", "Se", "Sa", "Do"],
+		months: ["Janeiro", "Fevereiro", "Março", "Abril", "Maio", "Junho", "Julho", "Agosto", "Setembro", "Outubro", "Novembro", "Dezembro"],
+		monthsShort: ["Jan", "Fev", "Mar", "Abr", "Mai", "Jun", "Jul", "Ago", "Set", "Out", "Nov", "Dez"],
+		today: "Hoje",
+		clear: "Limpar"
+	};
+}(jQuery));

+ 15 - 0
static/cranefly/js/locales/bootstrap-datepicker.ro.js

@@ -0,0 +1,15 @@
+/**
+ * Romanian translation for bootstrap-datepicker
+ * Cristian Vasile <cristi.mie@gmail.com>
+ */
+;(function($){
+	$.fn.datepicker.dates['ro'] = {
+		days: ["Duminică", "Luni", "Marţi", "Miercuri", "Joi", "Vineri", "Sâmbătă", "Duminică"],
+		daysShort: ["Dum", "Lun", "Mar", "Mie", "Joi", "Vin", "Sâm", "Dum"],
+		daysMin: ["Du", "Lu", "Ma", "Mi", "Jo", "Vi", "Sâ", "Du"],
+		months: ["Ianuarie", "Februarie", "Martie", "Aprilie", "Mai", "Iunie", "Iulie", "August", "Septembrie", "Octombrie", "Noiembrie", "Decembrie"],
+		monthsShort: ["Ian", "Feb", "Mar", "Apr", "Mai", "Iun", "Iul", "Aug", "Sep", "Oct", "Nov", "Dec"],
+		today: "Astăzi",
+		weekStart: 1
+	};
+}(jQuery));

+ 14 - 0
static/cranefly/js/locales/bootstrap-datepicker.rs-latin.js

@@ -0,0 +1,14 @@
+/**
+ * Serbian latin translation for bootstrap-datepicker
+ * Bojan Milosavlević <milboj@gmail.com>
+ */
+;(function($){
+	$.fn.datepicker.dates['rs-latin'] = {
+		days: ["Nedelja","Ponedeljak", "Utorak", "Sreda", "Četvrtak", "Petak", "Subota", "Nedelja"],
+		daysShort: ["Ned", "Pon", "Uto", "Sre", "Čet", "Pet", "Sub", "Ned"],
+		daysMin: ["N", "Po", "U", "Sr", "Č", "Pe", "Su", "N"],
+		months: ["Januar", "Februar", "Mart", "April", "Maj", "Jun", "Jul", "Avgust", "Septembar", "Oktobar", "Novembar", "Decembar"],
+		monthsShort: ["Jan", "Feb", "Mar", "Apr", "Maj", "Jun", "Jul", "Avg", "Sep", "Okt", "Nov", "Dec"],
+		today: "Danas"
+	};
+}(jQuery));

+ 14 - 0
static/cranefly/js/locales/bootstrap-datepicker.rs.js

@@ -0,0 +1,14 @@
+/**
+ * Serbian cyrillic translation for bootstrap-datepicker
+ * Bojan Milosavlević <milboj@gmail.com>
+ */
+;(function($){
+	$.fn.datepicker.dates['rs'] = {
+		days: ["Недеља","Понедељак", "Уторак", "Среда", "Четвртак", "Петак", "Субота", "Недеља"],
+		daysShort: ["Нед", "Пон", "Уто", "Сре", "Чет", "Пет", "Суб", "Нед"],
+		daysMin: ["Н", "По", "У", "Ср", "Ч", "Пе", "Су", "Н"],
+		months: ["Јануар", "Фебруар", "Март", "Април", "Мај", "Јун", "Јул", "Август", "Септембар", "Октобар", "Новембар", "Децембар"],
+		monthsShort: ["Јан", "Феб", "Мар", "Апр", "Мај", "Јун", "Јул", "Авг", "Сеп", "Окт", "Нов", "Дец"],
+		today: "Данас"
+	};
+}(jQuery));

+ 15 - 0
static/cranefly/js/locales/bootstrap-datepicker.ru.js

@@ -0,0 +1,15 @@
+/**
+ * Russian translation for bootstrap-datepicker
+ * Victor Taranenko <darwin@snowdale.com>
+ */
+;(function($){
+	$.fn.datepicker.dates['ru'] = {
+		days: ["Воскресенье", "Понедельник", "Вторник", "Среда", "Четверг", "Пятница", "Суббота", "Воскресенье"],
+		daysShort: ["Вск", "Пнд", "Втр", "Срд", "Чтв", "Птн", "Суб", "Вск"],
+		daysMin: ["Вс", "Пн", "Вт", "Ср", "Чт", "Пт", "Сб", "Вс"],
+		months: ["Январь", "Февраль", "Март", "Апрель", "Май", "Июнь", "Июль", "Август", "Сентябрь", "Октябрь", "Ноябрь", "Декабрь"],
+		monthsShort: ["Янв", "Фев", "Мар", "Апр", "Май", "Июн", "Июл", "Авг", "Сен", "Окт", "Ноя", "Дек"],
+		today: "Сегодня",
+		weekStart: 1
+	};
+}(jQuery));

+ 15 - 0
static/cranefly/js/locales/bootstrap-datepicker.sk.js

@@ -0,0 +1,15 @@
+/**
+ * Slovak translation for bootstrap-datepicker
+ * Marek Lichtner <marek@licht.sk>
+ * Fixes by Michal Remiš <michal.remis@gmail.com>
+ */
+;(function($){
+	$.fn.datepicker.dates["sk"] = {
+		days: ["Nedeľa", "Pondelok", "Utorok", "Streda", "Štvrtok", "Piatok", "Sobota", "Nedeľa"],
+		daysShort: ["Ned", "Pon", "Uto", "Str", "Štv", "Pia", "Sob", "Ned"],
+		daysMin: ["Ne", "Po", "Ut", "St", "Št", "Pia", "So", "Ne"],
+		months: ["Január", "Február", "Marec", "Apríl", "Máj", "Jún", "Júl", "August", "September", "Október", "November", "December"],
+		monthsShort: ["Jan", "Feb", "Mar", "Apr", "Máj", "Jún", "Júl", "Aug", "Sep", "Okt", "Nov", "Dec"],
+		today: "Dnes"
+	};
+}(jQuery));

+ 14 - 0
static/cranefly/js/locales/bootstrap-datepicker.sl.js

@@ -0,0 +1,14 @@
+/**
+ * Slovene translation for bootstrap-datepicker
+ * Gregor Rudolf <gregor.rudolf@gmail.com>
+ */
+;(function($){
+	$.fn.datepicker.dates['sl'] = {
+		days: ["Nedelja", "Ponedeljek", "Torek", "Sreda", "Četrtek", "Petek", "Sobota", "Nedelja"],
+		daysShort: ["Ned", "Pon", "Tor", "Sre", "Čet", "Pet", "Sob", "Ned"],
+		daysMin: ["Ne", "Po", "To", "Sr", "Če", "Pe", "So", "Ne"],
+		months: ["Januar", "Februar", "Marec", "April", "Maj", "Junij", "Julij", "Avgust", "September", "Oktober", "November", "December"],
+		monthsShort: ["Jan", "Feb", "Mar", "Apr", "Maj", "Jun", "Jul", "Avg", "Sep", "Okt", "Nov", "Dec"],
+		today: "Danes"
+	};
+}(jQuery));

+ 15 - 0
static/cranefly/js/locales/bootstrap-datepicker.sq.js

@@ -0,0 +1,15 @@
+/**
+ * Albanian translation for bootstrap-datepicker
+ * Tomor Pupovci <http://www.github.com/ttomor>
+ */
+;(function($){
+	$.fn.datepicker.dates['sq'] = {
+		days: ["E Diel", "E Hënë", "E martē", "E mërkurë", "E Enjte", "E Premte", "E Shtunë", "E Diel"],
+		daysShort: ["Die", "Hën", "Mar", "Mër", "Enj", "Pre", "Shtu", "Die"],
+		daysMin: ["Di", "Hë", "Ma", "Më", "En", "Pr", "Sht", "Di"],
+		months: ["Janar", "Shkurt", "Mars", "Prill", "Maj", "Qershor", "Korrik", "Gusht", "Shtator", "Tetor", "Nëntor", "Dhjetor"],
+		monthsShort: ["Jan", "Shk", "Mar", "Pri", "Maj", "Qer", "Korr", "Gu", "Sht", "Tet", "Nën", "Dhjet"],
+		today: "Sot"
+	};
+}(jQuery));
+

+ 16 - 0
static/cranefly/js/locales/bootstrap-datepicker.sv.js

@@ -0,0 +1,16 @@
+/**
+ * Swedish translation for bootstrap-datepicker
+ * Patrik Ragnarsson <patrik@starkast.net>
+ */
+;(function($){
+	$.fn.datepicker.dates['sv'] = {
+		days: ["Söndag", "Måndag", "Tisdag", "Onsdag", "Torsdag", "Fredag", "Lördag", "Söndag"],
+		daysShort: ["Sön", "Mån", "Tis", "Ons", "Tor", "Fre", "Lör", "Sön"],
+		daysMin: ["Sö", "Må", "Ti", "On", "To", "Fr", "Lö", "Sö"],
+		months: ["Januari", "Februari", "Mars", "April", "Maj", "Juni", "Juli", "Augusti", "September", "Oktober", "November", "December"],
+		monthsShort: ["Jan", "Feb", "Mar", "Apr", "Maj", "Jun", "Jul", "Aug", "Sep", "Okt", "Nov", "Dec"],
+		today: "I Dag",
+		format: "yyyy-mm-dd",
+		weekStart: 1
+	};
+}(jQuery));

+ 15 - 0
static/cranefly/js/locales/bootstrap-datepicker.sw.js

@@ -0,0 +1,15 @@
+/**
+ * Swahili translation for bootstrap-datepicker
+ * Edwin Mugendi <https://github.com/edwinmugendi>
+ * Source: http://scriptsource.org/cms/scripts/page.php?item_id=entry_detail&uid=xnfaqyzcku
+ */
+;(function($){
+    $.fn.datepicker.dates['sw'] = {
+        days: ["Jumapili", "Jumatatu", "Jumanne", "Jumatano", "Alhamisi", "Ijumaa", "Jumamosi", "Jumapili"],
+        daysShort: ["J2", "J3", "J4", "J5", "Alh", "Ij", "J1", "J2"],
+        daysMin: ["2", "3", "4", "5", "A", "I", "1", "2"],
+        months: ["Januari", "Februari", "Machi", "Aprili", "Mei", "Juni", "Julai", "Agosti", "Septemba", "Oktoba", "Novemba", "Desemba"],
+        monthsShort: ["Jan", "Feb", "Mac", "Apr", "Mei", "Jun", "Jul", "Ago", "Sep", "Okt", "Nov", "Des"],
+        today: "Leo"
+    };
+}(jQuery));

+ 14 - 0
static/cranefly/js/locales/bootstrap-datepicker.th.js

@@ -0,0 +1,14 @@
+/**
+ * Thai translation for bootstrap-datepicker
+ * Suchau Jiraprapot <seroz24@gmail.com>
+ */
+;(function($){
+	$.fn.datepicker.dates['th'] = {
+		days: ["อาทิตย์", "จันทร์", "อังคาร", "พุธ", "พฤหัส", "ศุกร์", "เสาร์", "อาทิตย์"],
+		daysShort: ["อา", "จ", "อ", "พ", "พฤ", "ศ", "ส", "อา"],
+		daysMin: ["อา", "จ", "อ", "พ", "พฤ", "ศ", "ส", "อา"],
+		months: ["มกราคม", "กุมภาพันธ์", "มีนาคม", "เมษายน", "พฤษภาคม", "มิถุนายน", "กรกฎาคม", "สิงหาคม", "กันยายน", "ตุลาคม", "พฤศจิกายน", "ธันวาคม"],
+		monthsShort: ["ม.ค.", "ก.พ.", "มี.ค.", "เม.ย.", "พ.ค.", "มิ.ย.", "ก.ค.", "ส.ค.", "ก.ย.", "ต.ค.", "พ.ย.", "ธ.ค."],
+		today: "วันนี้"
+	};
+}(jQuery));

+ 16 - 0
static/cranefly/js/locales/bootstrap-datepicker.tr.js

@@ -0,0 +1,16 @@
+/**
+ * Turkish translation for bootstrap-datepicker
+ * Serkan Algur <kaisercrazy_2@hotmail.com>
+ */
+;(function($){
+	$.fn.datepicker.dates['tr'] = {
+		days: ["Pazar", "Pazartesi", "Salı", "Çarşamba", "Perşembe", "Cuma", "Cumartesi", "Pazar"],
+		daysShort: ["Pz", "Pzt", "Sal", "Çrş", "Prş", "Cu", "Cts", "Pz"],
+		daysMin: ["Pz", "Pzt", "Sa", "Çr", "Pr", "Cu", "Ct", "Pz"],
+		months: ["Ocak", "Şubat", "Mart", "Nisan", "Mayıs", "Haziran", "Temmuz", "Ağustos", "Eylül", "Ekim", "Kasım", "Aralık"],
+		monthsShort: ["Oca", "Şub", "Mar", "Nis", "May", "Haz", "Tem", "Ağu", "Eyl", "Eki", "Kas", "Ara"],
+		today: "Bugün",
+		format: "dd.mm.yyyy"
+	};
+}(jQuery));
+

+ 15 - 0
static/cranefly/js/locales/bootstrap-datepicker.ua.js

@@ -0,0 +1,15 @@
+/**
+ * Ukrainian translation for bootstrap-datepicker
+ * Igor Polynets
+ */
+;(function($){
+	$.fn.datepicker.dates['ua'] = {
+		days: ["Неділя", "Понеділок", "Вівторок", "Середа", "Четверг", "П'ятница", "Субота", "Неділя"],
+		daysShort: ["Нед", "Пнд", "Втр", "Срд", "Чтв", "Птн", "Суб", "Нед"],
+		daysMin: ["Нд", "Пн", "Вт", "Ср", "Чт", "Пт", "Сб", "Нд"],
+		months: ["Cічень", "Лютий", "Березень", "Квітень", "Травень", "Червень", "Липень", "Серпень", "Вересень", "Жовтень", "Листопад", "Грудень"],
+		monthsShort: ["Січ", "Лют", "Бер", "Квт", "Трв", "Чер", "Лип", "Сер", "Вер", "Жов", "Лис", "Грд"],
+		today: "Сьогодні",
+		weekStart: 1
+	};
+}(jQuery));

+ 14 - 0
static/cranefly/js/locales/bootstrap-datepicker.uk.js

@@ -0,0 +1,14 @@
+/**
+ * Ukrainian translation for bootstrap-datepicker
+ * Andrey Vityuk <andrey [dot] vityuk [at] gmail.com>
+ */
+;(function($){
+	$.fn.datepicker.dates['uk'] = {
+		days: ["Неділя", "Понеділок", "Вівторок", "Середа", "Четвер", "П'ятниця", "Субота", "Неділя"],
+		daysShort: ["Нед", "Пнд", "Втр", "Срд", "Чтв", "Птн", "Суб", "Нед"],
+		daysMin: ["Нд", "Пн", "Вт", "Ср", "Чт", "Пт", "Сб", "Нд"],
+		months: ["Січень", "Лютий", "Березень", "Квітень", "Травень", "Червень", "Липень", "Серпень", "Вересень", "Жовтень", "Листопад", "Грудень"],
+		monthsShort: ["Січ", "Лют", "Бер", "Кві", "Тра", "Чер", "Лип", "Сер", "Вер", "Жов", "Лис", "Гру"],
+		today: "Сьогодні"
+	};
+}(jQuery));

+ 16 - 0
static/cranefly/js/locales/bootstrap-datepicker.zh-CN.js

@@ -0,0 +1,16 @@
+/**
+ * Simplified Chinese translation for bootstrap-datepicker
+ * Yuan Cheung <advanimal@gmail.com>
+ */
+;(function($){
+	$.fn.datepicker.dates['zh-CN'] = {
+		days: ["星期日", "星期一", "星期二", "星期三", "星期四", "星期五", "星期六", "星期日"],
+		daysShort: ["周日", "周一", "周二", "周三", "周四", "周五", "周六", "周日"],
+		daysMin:  ["日", "一", "二", "三", "四", "五", "六", "日"],
+		months: ["一月", "二月", "三月", "四月", "五月", "六月", "七月", "八月", "九月", "十月", "十一月", "十二月"],
+		monthsShort: ["一月", "二月", "三月", "四月", "五月", "六月", "七月", "八月", "九月", "十月", "十一月", "十二月"],
+		today: "今日",
+		format: "yyyy年mm月dd日",
+		weekStart: 1
+	};
+}(jQuery));

+ 17 - 0
static/cranefly/js/locales/bootstrap-datepicker.zh-TW.js

@@ -0,0 +1,17 @@
+/**
+ * Traditional Chinese translation for bootstrap-datepicker
+ * Rung-Sheng Jang <daniel@i-trend.co.cc>
+ * FrankWu  <frankwu100@gmail.com> Fix more appropriate use of Traditional Chinese habit
+ */
+;(function($){
+	$.fn.datepicker.dates['zh-TW'] = {
+		days: ["星期日", "星期一", "星期二", "星期三", "星期四", "星期五", "星期六", "星期日"],
+		daysShort: ["週日", "週一", "週二", "週三", "週四", "週五", "週六", "週日"],
+		daysMin:  ["日", "一", "二", "三", "四", "五", "六", "日"],
+		months: ["一月", "二月", "三月", "四月", "五月", "六月", "七月", "八月", "九月", "十月", "十一月", "十二月"],
+		monthsShort: ["一月", "二月", "三月", "四月", "五月", "六月", "七月", "八月", "九月", "十月", "十一月", "十二月"],
+		today: "今天",
+		format: "yyyy年mm月dd日",
+		weekStart: 1
+	};
+}(jQuery));

+ 6 - 6
templates/cranefly/layout.html

@@ -11,12 +11,12 @@
       <div class="container">
         <a href="{{ url('index') }}" class="brand">{% if settings.board_header %}{{ settings.board_header }}{% else %}{{ settings.board_name }}{% endif %}</a>
         {% if acl.search.can_search() and not user.is_crawler() %}
-        <form action="{{ url('search') }}" method="post" class="navbar-form pull-left">
+        <form action="{{ url('search_quick') }}" method="post" class="navbar-form pull-left">
           <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
           {% if thread is defined and thread %}
-          <input type="hidden" name="search_thread" value="{{ thread.pk }}">
+          <input type="hidden" name="search_thread" value="{{ request_path }}">
           {% elif search_thread is defined and search_thread %}
-          <input type="hidden" name="search_thread" value="{{ search_thread.pk }}">
+          <input type="hidden" name="search_thread" value="{{ request_path }}">
           {% endif %}
           <div class="navbar-search-form">
             <div id="navbar-search" class="navbar-search-border">
@@ -29,7 +29,7 @@
                   <div class="control">
                     <label>{% trans %}Search in{% endtrans %}:</label>
                     <select name="search_in">
-                      <option value="threads"{% if not search_in is defined or search_in == 'threads' %} selected="selected"{% endif %}>{% trans %}Forums{% endtrans%}</option>
+                      <option value="forums"{% if not search_in is defined or search_in == 'threads' %} selected="selected"{% endif %}>{% trans %}Forums{% endtrans%}</option>
                       {% if settings.enable_private_threads and acl.private_threads.can_participate()%}
                       <option value="private_threads"{% if search_in == 'private_threads' %} selected="selected"{% endif %}>{% trans %}Private Threads{% endtrans %}</option>
                       {% endif %}
@@ -55,7 +55,7 @@
                 </div>
                 <div class="form-actions">
                   <button type="submit" class="btn btn-primary"><i class="icon-search"></i> {% trans %}Search{% endtrans%}</button>
-                  <a href="{{ url('search') }}">{% trans %}Advanced Search{% endtrans %}</a>
+                  <a href="{{ url('search_forums') }}">{% trans %}Advanced Search{% endtrans %}</a>
                 </div>
               </div>
             </div>
@@ -68,7 +68,7 @@
           <li><a href="{{ url('popular_threads') }}" title="{% trans %}Popular Threads{% endtrans %}" class="hot tooltip-bottom"><i class="icon-fire"></i></a></li>
           <li><a href="{{ url('new_threads') }}" title="{% trans %}New Threads{% endtrans %}" class="fresh tooltip-bottom"><i class="icon-leaf"></i></a></li>{% if not user.crawler %}
           {% if acl.search.can_search() and not user.is_crawler() %}
-          <li><a href="{{ url('search') }}" title="{% trans %}Search Forums{% endtrans %}" class="tooltip-bottom"><i class="icon-search"></i></a></li>{% endif %}
+          <li><a href="{{ url('search_forums') }}" title="{% trans %}Search Forums{% endtrans %}" class="tooltip-bottom"><i class="icon-search"></i></a></li>{% endif %}
           {% endif %}
           <li><a href="{{ url('users') }}" title="{% trans %}Browse Users{% endtrans %}" class="tooltip-bottom"><i class="icon-group"></i></a></li>
           {% if settings.tos_url or settings.tos_content %}<li><a href="{% if settings.tos_url %}{{ settings.tos_url }}{% else %}{{ url('tos') }}{% endif %}" title="{% if settings.tos_title %}{{ settings.tos_title }}{% else %}{% trans %}Forum Terms of Service{% endtrans %}{% endif %}" class="tooltip-bottom"><i class="icon-certificate"></i></a></li>{% endif %}

+ 0 - 26
templates/cranefly/macros.html

@@ -53,30 +53,4 @@ itemprop="breadcrumb"
 
 {% macro wrap(item, wrap, extra='') -%}
 <{{ wrap }}{% if extra %} {{ extra|safe }}{% endif %}>{{ item }}</{{ wrap }}>
-{%- endmacro %}
-
-{% macro thread_flags(thread) -%}
-<ul class="unstyled thread-flags">
-  {% if thread.replies_reported and ((forum is defined and acl.threads.can_mod_posts(forum)) or acl.threads.can_mod_posts(thread.forum)) %}
-  <li class="flag-reported"><i class="icon-warning-sign tooltip-top" title="{% trans %}This thread has reported replies{% endtrans %}"></i></li>
-  {% endif %}
-  {% if thread.replies_moderated %}
-  <li class="flag-notreviewed"><i class="icon-question-sign tooltip-top" title="{% trans %}This thread has unreviewed replies{% endtrans %}"></i></li>
-  {% endif %}
-  {% if thread.weight == 2 %}
-  <li class="flag-announcement"><i class="icon-star tooltip-top" title="{% trans %}This thread is an annoucement{% endtrans %}"></i></li>
-  {% endif %}
-  {% if thread.weight == 1 %}
-  <li class="flag-sticky"><i class="icon-star-empty tooltip-top" title="{% trans %}This thread is sticky{% endtrans %}"></i></li>
-  {% endif %}
-  {% if thread.moderated  %}
-  <li class="flag-notreviewed"><i class="icon-eye-close tooltip-top" title="{% trans %}This thread awaits review{% endtrans %}"></i></li>
-  {% endif %}
-  {% if thread.deleted %}
-  <li class="flag-deleted"><i class="icon-trash tooltip-top" title="{% trans %}This thread is deleted{% endtrans %}"></i></li>
-  {% endif %}
-  {% if thread.closed %}
-  <li class="flag-closed"><i class="icon-lock tooltip-top" title="{% trans %}This thread is closed{% endtrans %}"></i></li>
-  {% endif %}
-</ul>
 {%- endmacro %}

+ 0 - 17
templates/cranefly/search/home.html

@@ -1,17 +0,0 @@
-{% extends "cranefly/search/layout.html" %}
-{% import "cranefly/macros.html" as macros with context %}
-
-{% block action %}
-{% if search_result %}
-<div class="search-resume">
-  <p class="lead muted">{% trans search_query=style_query(search_result.search_query) %}Your search results for query "{{ search_query }}" are still available.{% endtrans %}</p>
-  <p class="lead muted">{% trans %}To discard it and start new search, simply perform new search using form above.{% endtrans %}</p>
-</div>
-{% else %}
-<p class="lead">{% trans %}To search forums, enter phrases you want to find in text field above and press search button.{% endtrans %}</p>
-{% endif %}
-{% endblock %}
-
-{% macro style_query(query) -%}
-<a href="{{ url('search_results') }}">{{ query }}</a>
-{%- endmacro %}

+ 15 - 0
templates/cranefly/search/layout.html

@@ -8,6 +8,21 @@
 <div class="page-header header-primary header-search">
   <div class="container">
     <h1>{% trans %}Search Community{% endtrans %}</h1>
+    <ul class="nav nav-tabs header-tabs">
+      <li{% if search_route == 'search_forums' %} class="active"{% endif %}>
+        <a href="{{ url('search_forums') }}">{% trans %}Search Forums{% endtrans %}</a>
+      </li>
+      {% if acl.private_threads.can_participate() %}
+      <li{% if search_route == 'search_private_threads' %} class="active"{% endif %}>
+        <a href="{{ url('search_private_threads') }}">{% trans %}Search Private Threads{% endtrans %}</a>
+      </li>
+      {% endif %}
+      {% if acl.reports.can_handle() %}
+      <li{% if search_route == 'search_reports' %} class="active"{% endif %}>
+        <a href="{{ url('search_reports') }}">{% trans %}Search Reports{% endtrans %}</a>
+      </li>
+      {% endif %}
+    </ul>
   </div>
 </div>
 

+ 10 - 2
templates/cranefly/search/results.html

@@ -1,12 +1,15 @@
 {% extends "cranefly/search/layout.html" %}
 {% import "cranefly/macros.html" as macros with context %}
 
-{% block title %}{{ macros.page_title(title=_("Results"),parent=_('Search Community')) }}{% endblock %}
+{% block title %}{{ macros.page_title(title=_("Search Results"),parent=_('Search Community')) }}{% endblock %}
 
 {% block action %}
   <div class="search-results">
     {% if results %}
-    <h2>{% trans count=results|length, results=results|length|intcomma %}Search has returned one result:{% pluralize %}Search has returned {{ results }} results:{% endtrans %}</h2>
+    <h2>
+      {% trans count=results|length, results=results|length|intcomma, query=query_string(search_query) %}Search for "{{ query }}" has returned one result:{% pluralize %}Search for "{{ query }}" has returned {{ results }} results:{% endtrans %}
+      <a href="{{ url(search_route) }}" class="btn btn-inverse pull-right"><i class="icon-search"></i> {% trans %}New Search{% endtrans %}</a>
+    </h2>
     <div class="results-list">
       {% for result in results %}
       <div class="result">
@@ -32,6 +35,11 @@
   </div>
 {% endblock %}
 
+
+{% macro query_string(query) -%}
+<strong>{{ query }}</strong>
+{%- endmacro %}
+
 {% macro forum(forum) -%}
 <a href="{{ url('forum', forum=forum.pk, slug=forum.slug) }}" class="forum-link">{{ forum.name }}</a>
 {%- endmacro %}

+ 37 - 0
templates/cranefly/search/search_form.html

@@ -0,0 +1,37 @@
+{% extends "cranefly/search/layout.html" %}
+{% import "cranefly/macros.html" as macros with context %}
+
+{% block action %}
+<div class="row">
+  <div class="span8 offset2">
+    {% if search_result and search_result.search_route == search_route %}
+    <div class="search-resume">
+      <p class="lead"><a href="{{ url('search_results') }}">{% trans search_query=style_query(search_result.search_query) %}Your search results for query "{{ search_query }}" are still available.{% endtrans %}</a></p>
+    </div>
+    {% endif %}
+    {% block form %}{% endblock %}
+  </div>
+</div>
+{% endblock %}
+
+{% block javascripts %}
+{{ super() }}
+<script src="{{ STATIC_URL }}cranefly/js/bootstrap-datepicker.js"></script>
+<script type="text/javascript">
+  $(function () {
+    $('.form-datepicker').datepicker({
+        format: 'yyyy-mm-dd',
+        language: '{{ LANGUAGE_CODE|replace('_', '-') }}'
+    });
+  });
+</script>
+{% endblock %}
+
+
+{% macro style_query(query) -%}
+<strong>{{ query }}</strong>
+{%- endmacro %}
+
+{% macro lang_search_thread_titles() -%}
+{% trans %}Search only in threads titles.{% endtrans %}
+{%- endmacro %}

+ 47 - 0
templates/cranefly/search/search_forums.html

@@ -0,0 +1,47 @@
+{% extends "cranefly/search/search_form.html" %}
+{% import "forms.html" as form_theme with context %}
+{% import "cranefly/macros.html" as macros with context %}
+
+{% block title %}{{ macros.page_title(title=_('Search Forums'), parent=_("Search Community")) }}{% endblock %}
+
+{% block form %}
+<div class="form-container">
+
+  <div class="form-header">
+    <h1>{% trans %}Search Forums{% endtrans %}</h1>
+  </div>
+
+  {% if message %}
+  <div class="messages-list">
+    {{ macros.draw_message(message) }}
+  </div>
+  {% endif %}
+
+  <form action="{{ url(search_route) }}" method="post">
+    {{ form_theme.hidden_fields(form) }}
+    <div class="form-fields">
+      <fieldset class="first last">
+        {{ form_theme.row(form.search_query, attrs={'class': 'span8'}) }}
+        {{ form_theme.row(form.search_thread_titles, attrs={'inline': lang_search_thread_titles()}) }}
+        {{ form_theme.row(form.search_thread, attrs={'class': 'span8'}) }}
+        {{ form_theme.row(form.search_author, attrs={'class': 'span8'}) }}
+        <hr>
+        {{ form_theme.row(form.search_forums, attrs={'class': 'span8'}) }}
+        {{ form_theme.row(form.search_forums_childs, attrs={'inline': lang_search_forum_children()}) }}
+        <hr>
+        {{ form_theme.row(form.search_before, attrs={'class': 'span8 form-datepicker'}) }}
+        {{ form_theme.row(form.search_after, attrs={'class': 'span8 form-datepicker'}) }}
+      </fieldset>
+    </div>
+    <div class="form-actions">
+      <button type="submit" class="btn btn-primary">{% trans %}Search{% endtrans %}</button>
+    </div>
+  </form>
+
+</div>
+{% endblock %}
+
+
+{% macro lang_search_forum_children() -%}
+{% trans %}Include Children Forums in Search{% endtrans %}
+{%- endmacro %}

+ 39 - 0
templates/cranefly/search/search_private_threads.html

@@ -0,0 +1,39 @@
+{% extends "cranefly/search/search_form.html" %}
+{% import "forms.html" as form_theme with context %}
+{% import "cranefly/macros.html" as macros with context %}
+
+{% block title %}{{ macros.page_title(title=_('Search Private Threads'), parent=_("Search Community")) }}{% endblock %}
+
+{% block form %}
+<div class="form-container">
+
+  <div class="form-header">
+    <h1>{% trans %}Search Private Threads{% endtrans %}</h1>
+  </div>
+
+  {% if message %}
+  <div class="messages-list">
+    {{ macros.draw_message(message) }}
+  </div>
+  {% endif %}
+
+  <form action="{{ url(search_route) }}" method="post">
+    {{ form_theme.hidden_fields(form) }}
+    <div class="form-fields">
+      <fieldset class="first last">
+        {{ form_theme.row(form.search_query, attrs={'class': 'span8'}) }}
+        {{ form_theme.row(form.search_thread_titles, attrs={'inline': lang_search_thread_titles()}) }}
+        {{ form_theme.row(form.search_thread, attrs={'class': 'span8'}) }}
+        {{ form_theme.row(form.search_author, attrs={'class': 'span8'}) }}
+        <hr>
+        {{ form_theme.row(form.search_before, attrs={'class': 'span8 form-datepicker'}) }}
+        {{ form_theme.row(form.search_after, attrs={'class': 'span8 form-datepicker'}) }}
+      </fieldset>
+    </div>
+    <div class="form-actions">
+      <button type="submit" class="btn btn-primary">{% trans %}Search{% endtrans %}</button>
+    </div>
+  </form>
+
+</div>
+{% endblock %}

+ 41 - 0
templates/cranefly/search/search_reports.html

@@ -0,0 +1,41 @@
+{% extends "cranefly/search/search_form.html" %}
+{% import "forms.html" as form_theme with context %}
+{% import "cranefly/macros.html" as macros with context %}
+
+{% block title %}{{ macros.page_title(title=_('Search Reports'), parent=_("Search Community")) }}{% endblock %}
+
+{% block form %}
+<div class="form-container">
+
+  <div class="form-header">
+    <h1>{% trans %}Search Reports{% endtrans %}</h1>
+  </div>
+
+  {% if message %}
+  <div class="messages-list">
+    {{ macros.draw_message(message) }}
+  </div>
+  {% endif %}
+
+  <form action="{{ url(search_route) }}" method="post">
+    {{ form_theme.hidden_fields(form) }}
+    <div class="form-fields">
+      <fieldset class="first last">
+        {{ form_theme.row(form.search_query, attrs={'class': 'span8'}) }}
+        {{ form_theme.row(form.search_thread_titles, attrs={'inline': lang_search_thread_titles()}) }}
+        {{ form_theme.row(form.search_thread, attrs={'class': 'span8'}) }}
+        {{ form_theme.row(form.search_author, attrs={'class': 'span8'}) }}
+        <hr>
+        {{ form_theme.row(form.search_weight, attrs={'class': 'span8'}) }}
+        <hr>
+        {{ form_theme.row(form.search_before, attrs={'class': 'span8 form-datepicker'}) }}
+        {{ form_theme.row(form.search_after, attrs={'class': 'span8 form-datepicker'}) }}
+      </fieldset>
+    </div>
+    <div class="form-actions">
+      <button type="submit" class="btn btn-primary">{% trans %}Search{% endtrans %}</button>
+    </div>
+  </form>
+
+</div>
+{% endblock %}

+ 6 - 0
templates/forms.html

@@ -99,6 +99,8 @@
 {{ _yesno(_field, context) }}
 {% elif widget == 'Select' %}
 {{ _select(_field, context) }}
+{% elif widget == 'SelectDateWidget' %}
+{{ _select_date(_field, context) }}
 {% elif widget == 'RadioSelect' %}
 {{ _radio_select(_field, context) }}
 {% elif widget == 'CheckboxSelectMultiple' %}
@@ -146,6 +148,10 @@
 </select>
 {%- endmacro %}
 
+{% macro _select_date(_field, context) -%}
+{{ context }}
+{%- endmacro %}
+
 {% macro _radio_select(_field, context) -%}
 <div class="radio-group">
   {% for option in context['optgroups'][0][1] %}