wip: Radicale extension.

Graphical improvements.
This commit is contained in:
Antoine Nguyen
2014-06-19 12:10:51 +02:00
parent daeeaf952b
commit 15d4e53a5d
20 changed files with 158 additions and 132 deletions
+1 -42
View File
@@ -2,11 +2,10 @@
from functools import wraps
from itertools import chain
from django.db.models import Q
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from django.contrib.contenttypes.models import ContentType
from django.utils.translation import ugettext as _
from modoboa.core.models import User
from modoboa.lib import parameters, events
from modoboa.lib import events
from modoboa.lib.exceptions import PermDeniedException
from modoboa.lib.emailutils import split_mailbox
from modoboa.extensions.admin.models import (
@@ -30,46 +29,6 @@ def needs_mailbox():
return decorator
def get_sort_order(qdict, default, allowed_values=None):
"""Return a sort order from a querydict object
:param QueryDict qdict: the object to analyse
:param string default: the default sort order if no one is found
:param list allowed_values: an optional list of allowed values
:return: a 2uple of strings
"""
sort_order = qdict.get("sort_order", default)
if sort_order.startswith("-"):
sort_dir = "-"
sort_order = sort_order[1:]
else:
sort_dir = ""
if allowed_values is not None and not sort_order in allowed_values:
return (default, "")
return (sort_order, sort_dir)
def get_listing_page(objects, pagenum):
"""Return specific a listing page.
A page contains a limited number of elements (see
ITEMS_PER_PAGE). If the given page number is wrong, the first page
will be always returned.
:param list objects: object list to paginate
:param int pagenum: page number
:return: a ``Page`` object
"""
paginator = Paginator(
objects, int(parameters.get_admin("ITEMS_PER_PAGE", app="core"))
)
try:
page = paginator.page(int(pagenum))
except (EmptyPage, PageNotAnInteger, ValueError):
page = paginator.page(paginator.num_pages)
return page
def get_identities(user, searchquery=None, idtfilter=None, grpfilter=None):
"""Return all the identities owned by a user.
@@ -6,7 +6,7 @@
var Admin = function(options) {
Listing.call(this, options);
};
Admin.prototype = {
defaults: {
deflocation: "list/",
@@ -17,7 +17,6 @@ Admin.prototype = {
Listing.prototype.initialize.call(this, options);
this.options = $.extend({}, this.defaults, this.options);
this.options.defcallback = $.proxy(this.list_cb, this);
this.tag_handlers = {};
this.navobj = new History(this.options);
@@ -50,54 +49,6 @@ Admin.prototype = {
$("#searchform").submit($.proxy(this.do_search, this));
},
register_tag_handler: function(name, handler) {
this.tag_handlers[name] = handler;
if (this.navobj.getparam(name + "filter") !== undefined) {
var text = this.navobj.getparam(name + "filter");
$("#searchform").parent().after(this.make_tag(text, name));
}
},
generic_tag_handler: function(tag, $link) {
if (this.navobj.getparam(tag + "filter") === undefined && $link.hasClass(tag)) {
var text = $link.attr("name");
this.navobj.setparam(tag + "filter", text).update();
$("#searchform").parent().after(this.make_tag(text, tag));
return true;
}
return false;
},
make_tag: function(text, type) {
var $tag = $("<a />", {"name": type, "class" : "btn btn-mini", "html": text});
var $i = $("<i />", {"class" : "icon-remove"}).prependTo($tag);
$tag.click($.proxy(this.remove_tag, this));
return $tag;
},
remove_tag: function(e) {
var $tag = $(e.target);
if ($tag.is("i")) {
$tag = $tag.parent();
}
e.preventDefault();
this.navobj.delparam($tag.attr("name") + "filter").update();
$tag.remove();
},
filter_by_tag: function(e) {
var $link = $(e.target);
e.preventDefault();
for (var name in this.tag_handlers) {
if (this.tag_handlers[name].apply(this, [name, $link])) {
break;
}
}
},
do_search: function(e) {
e.preventDefault();
var squery = $("#searchquery").val();
+1 -1
View File
@@ -16,7 +16,7 @@ from modoboa.lib.exceptions import (
PermDeniedException
)
from modoboa.lib.templatetags.lib_tags import pagination_bar
from modoboa.extensions.admin.lib import get_sort_order, get_listing_page
from modoboa.lib.listing import get_sort_order, get_listing_page
from modoboa.extensions.admin.models import Domain, Mailbox
from modoboa.extensions.admin.forms import (
DomainForm
+3 -2
View File
@@ -16,9 +16,10 @@ from modoboa.lib.webutils import (
from modoboa.lib.templatetags.lib_tags import pagination_bar
from modoboa.core.models import User
from modoboa.extensions.admin.models import Mailbox, Domain
from modoboa.extensions.admin.lib import (
get_sort_order, get_listing_page, get_identities
from modoboa.lib.listing import (
get_sort_order, get_listing_page
)
from modoboa.extensions.admin.lib import get_identities
from modoboa.extensions.admin.forms import (
AccountForm
)
+32 -8
View File
@@ -6,7 +6,7 @@ from django.core.urlresolvers import reverse
from django.utils.translation import ugettext as _, ugettext_lazy
from modoboa.lib.webutils import render_to_json_response
from modoboa.lib.formutils import WizardForm, TabForms, DynamicForm
from modoboa.extensions.admin.models import Domain
from modoboa.extensions.admin.models import Domain, Mailbox
from modoboa.extensions.radicale.models import UserCalendar, SharedCalendar
@@ -16,7 +16,24 @@ class UserCalendarForm(forms.ModelForm):
"""
class Meta:
model = UserCalendar
fields = ('name', )
fields = ('name', 'mailbox', )
widgets = {
'mailbox': forms.widgets.Select(
attrs={"class": "selectpicker", "data-live-search": "true"}
)
}
def __init__(self, user, *args, **kwargs):
"""Custom constructor.
We need the current user to filter the mailbox list.
"""
super(UserCalendarForm, self).__init__(*args, **kwargs)
if user.group == 'SimpleUsers':
del self.fields['mailbox']
else:
self.fields['mailbox'].queryset = \
Mailbox.objects.get_for_admin(user)
class SharedCalendarForm(forms.ModelForm):
@@ -25,6 +42,11 @@ class SharedCalendarForm(forms.ModelForm):
"""
class Meta:
model = SharedCalendar
widgets = {
"domain": forms.widgets.Select(
attrs={"class": "selectpicker", "data-live-search": "true"}
)
}
def __init__(self, user, *args, **kwargs):
"""Custom constructor.
@@ -127,9 +149,9 @@ class UserCalendarWizard(WizardForm):
"""
def __init__(self, request):
super(UserCalendarWizard, self).__init__(request)
self.add_step(UserCalendarForm, _("General"))
self.add_step(UserCalendarForm, _("General"), new_args=[request.user])
self.add_step(
RightsForm, _("Rights"), formtpl="radicale/rightsform.html"
RightsForm, _("Rights"), formtpl="radicale/rightsform.html",
)
def extra_context(self, context):
@@ -141,7 +163,8 @@ class UserCalendarWizard(WizardForm):
def done(self):
calendar = self.first_step.form.save(commit=False)
calendar.mailbox = self.request.user.mailbox_set.all()[0]
if self.request.user.group == 'SimpleUsers':
calendar.mailbox = self.request.user.mailbox_set.all()[0]
calendar.save()
self.steps[1].form.calendar = calendar
self.steps[1].form.save()
@@ -151,13 +174,14 @@ class UserCalendarWizard(WizardForm):
class UserCalendarEditionForm(TabForms):
"""User calendar edition form.
"""
def __init__(self, *args, **kwargs):
def __init__(self, request, *args, **kwargs):
self.forms = []
self.forms.append({
"id": "general",
"title": _("General"),
"cls": UserCalendarForm,
"mandatory": True
"mandatory": True,
"new_args": [request.user]
})
self.forms.append({
"id": "rights",
@@ -166,7 +190,7 @@ class UserCalendarEditionForm(TabForms):
"formtpl": "radicale/rightsform.html",
"mandatory": True
})
super(UserCalendarEditionForm, self).__init__(*args, **kwargs)
super(UserCalendarEditionForm, self).__init__(request, *args, **kwargs)
def extra_context(self, context):
calendar = self.instances["general"]
+7 -6
View File
@@ -27,8 +27,8 @@ class Calendar(models.Model):
raise NotImplementedError
@property
def type(self):
"""Return calendar type"""
def tags(self):
"""Return calendar tags"""
raise NotImplementedError
@property
@@ -72,8 +72,8 @@ class UserCalendar(Calendar):
)
@property
def type(self):
return _("user")
def tags(self):
return [{"name": "user", "label": _("User"), "type": "cal"}]
@property
def owner(self):
@@ -115,8 +115,9 @@ class SharedCalendar(Calendar):
)
@property
def type(self):
return _("shared")
def tags(self):
return [{"name": "shared", "label": _("Shared"), "type": "cal"}]
@property
def owner(self):
@@ -1,3 +1,7 @@
#searchform {
margin-bottom: 10px;
}
#original_row {
margin-left: 100px;
}
@@ -18,6 +18,7 @@ Radicale.prototype = {
this.options = $.extend({}, this.defaults, this.options);
this.options.defcallback = $.proxy(this.list_cb, this);
this.navobj = new History(this.options);
this.register_tag_handler("cal", this.generic_tag_handler);
$(document).on(
"click", "a[name=delcalendar]", $.proxy(this.del_calendar, this)
);
@@ -3,8 +3,8 @@
<thead>
<tr>
<th id="name" class="sortable" data-sort_order="name" width="">{% trans "Name" %}</th>
{% if with_owner %}<th id="owner" class="sortable" data-sort_order="mailbox">{% trans "Owner" %}</th>{% endif %}
<th id="type">{% trans "Type" %}</th>
{% if with_owner %}<th id="owner" class="sortable" data-sort_order="owner">{% trans "Owner" %}</th>{% endif %}
<th id="tags">{% trans "Tags" %}</th>
<th id="url">{% trans "Address" %}</th>
<th id="actions" width="">{% trans "Actions" %}</th>
</tr>
@@ -14,9 +14,7 @@
<tr id="{{ cal.id }}" class="" name="{{ forloop.counter }}">
<td name="name">{% calendar_modify_link cal %}</td>
{% if with_owner %}<td name="owner">{{ cal.owner }}</td>{% endif %}
<td name="type">
<span class="label label-info"><a href="#" class="filter">{{ cal.type }}</a></span>
</td>
<td name="tags">{% render_tags cal.tags %}</td>
<td name="url">{{ cal.url }}</td>
<td name="actions">{% calendar_actions cal %}</td>
</tr>
@@ -1,5 +1,6 @@
{% extends "twocols.html" %}{% load i18n radicale_tags %}
{% block html_head %}
<link rel="stylesheet" type="text/css" href="{{ STATIC_URL }}css/jquery.sortable.css" />
<link rel="stylesheet" type="text/css" href="{{ STATIC_URL }}radicale/css/radicale.css" />
<script type="text/javascript" src="{{ STATIC_URL }}js/jquery.sortable.js"></script>
<script type="text/javascript" src="{{ STATIC_URL }}js/history.js"></script>
@@ -20,7 +21,6 @@ $(document).ready(function() {
<ul class="nav nav-list">
<li class="nav-header">{% trans "Filter" %}</li>
<li>{% include "admin/searchform.html" %}</li>
<li id="taglist"></li>
<li class="nav-header">{% trans "Actions" %}</li>
{% radicale_left_menu user %}
</ul>
@@ -1,4 +0,0 @@
{% extends "common/generic_modal_form.html" %}{% load crispy_forms_tags %}
{% block formbody %}
{{ form|crispy }}
{% endblock %}
+14 -4
View File
@@ -10,10 +10,12 @@ from django.contrib.auth.decorators import (
)
from django.utils.translation import ugettext as _
from modoboa.lib.listing import get_sort_order
from modoboa.lib.webutils import (
_render_to_string, render_to_json_response
)
from modoboa.lib.exceptions import NotFound, PermDeniedException
from modoboa.extensions.radicale.forms import (
UserCalendarWizard, SharedCalendarForm, UserCalendarEditionForm
)
@@ -33,16 +35,24 @@ def calendars(request, tplname="radicale/calendar_list.html"):
The content depends on current user's role.
"""
sort_order, sort_dir = get_sort_order(request.GET, "name")
calfilter = request.GET.get("calfilter", None)
if request.user.group == "SimpleUsers":
mbox = request.user.mailbox_set.all()[0]
cals = UserCalendar.objects.filter(mailbox=mbox).select_related().all()
with_owner = False
else:
cals = chain(
UserCalendar.objects.get_for_admin(request.user),
SharedCalendar.objects.get_for_admin(request.user)
)
ucals = []
if calfilter is None or calfilter == "user":
ucals = UserCalendar.objects.get_for_admin(request.user)
scals = []
if calfilter is None or calfilter == "shared":
scals = SharedCalendar.objects.get_for_admin(request.user)
cals = chain(ucals, scals)
with_owner = True
cals = sorted(
cals, key=lambda c: getattr(c, sort_order), reverse=sort_dir == '-'
)
return render_to_json_response({
"table": _render_to_string(request, tplname, {
"calendars": cals, "with_owner": with_owner
+13 -1
View File
@@ -22,7 +22,19 @@ def get_sort_order(qdict, default, allowed_values=None):
def get_listing_page(objects, pagenum):
paginator = Paginator(objects, int(parameters.get_admin("ITEMS_PER_PAGE", app="core")))
"""Return specific a listing page.
A page contains a limited number of elements (see
ITEMS_PER_PAGE). If the given page number is wrong, the first page
will be always returned.
:param list objects: object list to paginate
:param int pagenum: page number
:return: a ``Page`` object
"""
paginator = Paginator(
objects, int(parameters.get_admin("ITEMS_PER_PAGE", app="core"))
)
try:
page = paginator.page(int(pagenum))
except (EmptyPage, PageNotAnInteger, ValueError):
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+8 -5
View File
@@ -23,11 +23,11 @@ function get_static_url(value) {
function modalbox(e, css, defhref, defcb, defclosecb) {
e.preventDefault();
var $this = $(this);
var href = (defhref != undefined) ? defhref : $this.attr('href');
var modalcb = (defcb != undefined) ? defcb : $this.attr('modalcb');
var closecb = (defclosecb != undefined) ? defclosecb : $this.attr("closecb");
var href = (defhref !== undefined) ? defhref : $this.attr('href');
var modalcb = (defcb !== undefined) ? defcb : $this.attr('modalcb');
var closecb = (defclosecb !== undefined) ? defclosecb : $this.attr("closecb");
if (href.indexOf('#') == 0) {
if (href.indexOf('#') === 0) {
$(href).modal('open');
return;
}
@@ -41,6 +41,9 @@ function modalbox(e, css, defhref, defcb, defclosecb) {
$div.modal({show: false});
$div.one('shown', function() {
$(".selectpicker").selectpicker({
container: 'body'
});
$(".help").popover({
container: "#modalbox",
trigger: "hover"
@@ -323,4 +326,4 @@ $(document).ready(function() {
$(document).on('click', 'a[data-toggle="ajaxmodal-autowidth"]', modalbox_autowidth);
$(document).on('click', '.activator', activate_widget);
$(".help").popover({trigger: 'hover'}).click(function(e) {e.preventDefault();});
});
});
+50 -1
View File
@@ -13,6 +13,7 @@ Listing.prototype = {
initialize: function(options) {
this.options = $.extend({}, this.listing_defaults, options);
this.tag_handlers = {};
$(document).on("click", this.options.pbar_container + " a",
$.proxy(this.load_page, this));
},
@@ -63,5 +64,53 @@ Listing.prototype = {
sort_order = "-" + sort_order;
}
this.navobj.setparam("sort_order", sort_order).update();
},
register_tag_handler: function(name, handler) {
this.tag_handlers[name] = handler;
if (this.navobj.getparam(name + "filter") !== undefined) {
var text = this.navobj.getparam(name + "filter");
$("#searchform").parent().after(this.make_tag(text, name));
}
},
generic_tag_handler: function(tag, $link) {
if (this.navobj.getparam(tag + "filter") === undefined && $link.hasClass(tag)) {
var text = $link.attr("name");
this.navobj.setparam(tag + "filter", text).update();
$("#searchform").parent().after(this.make_tag(text, tag));
return true;
}
return false;
},
make_tag: function(text, type) {
var $tag = $("<a />", {"name": type, "class" : "btn btn-mini", "html": text});
var $i = $("<i />", {"class" : "icon-remove"}).prependTo($tag);
$tag.click($.proxy(this.remove_tag, this));
return $tag;
},
remove_tag: function(e) {
var $tag = $(e.target);
if ($tag.is("i")) {
$tag = $tag.parent();
}
e.preventDefault();
this.navobj.delparam($tag.attr("name") + "filter").update();
$tag.remove();
},
filter_by_tag: function(e) {
var $link = $(e.target);
e.preventDefault();
for (var name in this.tag_handlers) {
if (this.tag_handlers[name].apply(this, [name, $link])) {
break;
}
}
}
};
};
+1 -1
View File
@@ -3,7 +3,7 @@
<a class="close" data-dismiss="modal">x</a>
<h3>{{ title }}</h3>
</div>
<div class="modal-body">
<div class="modal-body" style="overflow: visible;">
{% block modalbody %}{% endblock %}
</div>
<div class="modal-footer">
+1 -1
View File
@@ -3,7 +3,7 @@
<a class="close" data-dismiss="modal">x</a>
<h3>{{ title }} / <small>{{ wizard.first_step.title }}</small></h3>
</div>
<div class="modal-body">
<div class="modal-body" style="overflow: visible;">
<form class="form-horizontal" action="{{ action }}" enctype="{{ enctype }}"
method="POST" id="{{ formid }}" name="{{ formid }}">{% csrf_token %}
<div id="wizard" class="carousel">
+2
View File
@@ -8,10 +8,12 @@
<link rel="stylesheet" type="text/css" href="{{ STATIC_URL }}css/bootstrap.css" />
<link rel="stylesheet" type="text/css" href="{{ STATIC_URL }}css/bootstrap-responsive.css" />
<link rel="stylesheet" type="text/css" href="{{ STATIC_URL }}css/bootstrap-datetimepicker.css" />
<link rel="stylesheet" type="text/css" href="{{ STATIC_URL }}css/bootstrap-select.min.css" />
<link rel="stylesheet" type="text/css" href="{{ STATIC_URL }}css/custom.css" />
<script src="{{ STATIC_URL }}js/jquery.js" type="text/javascript"></script>
<script src="{{ STATIC_URL }}js/bootstrap.js" type="text/javascript"></script>
<script src="{{ STATIC_URL }}js/bootstrap-datetimepicker.js" type="text/javascript"></script>
<script src="{{ STATIC_URL }}js/bootstrap-select.min.js" type="text/javascript"></script>
{% if LANGUAGE_CODE != "en" %}<script src="{{ STATIC_URL }}js/locales/bootstrap-datetimepicker.{{ LANGUAGE_CODE }}.js" type="text/javascript"></script>{% endif %}
<script src="{{ STATIC_URL }}js/global.js" type="text/javascript"></script>
<script src="{{ STATIC_URL }}js/csrf.js" type="text/javascript"></script>