wip: limits and new reseller profile

We add an intermediate role : Reseller. It is somewhere between a super administrator and a domain administrator. A reseller can create objects (a limited or not amount) and then define permissions on those objects:

* Create a domain admins,
* Assign limits to those admins,
* ...

see #105
This commit is contained in:
Antoine Nguyen
2012-01-12 16:49:09 +01:00
parent 84e2a25319
commit 99888a861b
20 changed files with 453 additions and 31 deletions
+40 -1
View File
@@ -2,8 +2,10 @@
from django.http import HttpResponse, HttpResponseRedirect
from django.contrib.auth.models import User, Group
from django.utils.translation import ugettext as _
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from models import *
from modoboa.lib.webutils import _render_error
from modoboa.lib.webutils import _render_error, _render
from modoboa.admin.tables import *
def is_not_localadmin(errortpl="error"):
def dec(f):
@@ -64,3 +66,40 @@ def good_domain(f):
login_url = settings.LOGIN_URL
return HttpResponseRedirect("%s?next=%s" % (login_url, path))
return dec
def render_listing(request, objtype, tplname="admin/listing.html",
**kwargs):
"""Common function to render a listing
All listing pages available into the admin application use the
same layout, rendered by this function.
:param request: a ``Request`` object
:param objtype: the object type's name (lowercase)
:param tplname: the template used to render the HTML
"""
tblclass = "%sTable" % objtype.capitalize()
if not globals().has_key(tblclass):
raise AdminError(_("Unknown object type"))
tblclass = globals()[tblclass]
if request.GET.has_key("domid"):
kwargs["domid"] = request.GET["domid"]
else:
kwargs["domid"] = ""
kwargs["selection"] = objtype
paginator = Paginator(kwargs["objects"],
int(parameters.get_admin("ITEMS_PER_PAGE")))
try:
page = request.GET.get("page", "1")
except ValueError:
page = 1
try:
kwargs["objects"] = paginator.page(page)
except (EmptyPage, PageNotAnInteger):
kwargs["objects"] = paginator.page(paginator.num_pages)
kwargs["last_page"] = paginator.num_pages
kwargs["total"] = paginator.count
kwargs["table"] = tblclass(request, kwargs["objects"].object_list)
return _render(request, tplname, kwargs)
-28
View File
@@ -9,7 +9,6 @@ from django.db.models import Q
from django.contrib.auth.models import User, Group
from django.contrib import messages
from django.db import transaction, IntegrityError
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from lib import *
from modoboa import admin, userprefs
@@ -24,33 +23,6 @@ from modoboa.lib.emailutils import split_mailbox
from modoboa.lib.models import Parameter
import copy
def render_listing(request, objtype, tplname="admin/listing.html",
**kwargs):
tblclass = "%sTable" % objtype.capitalize()
if not globals().has_key(tblclass):
raise AdminError(_("Unknown object type"))
tblclass = globals()[tblclass]
if request.GET.has_key("domid"):
kwargs["domid"] = request.GET["domid"]
else:
kwargs["domid"] = ""
kwargs["selection"] = objtype
paginator = Paginator(kwargs["objects"],
int(parameters.get_admin("ITEMS_PER_PAGE")))
try:
page = request.GET.get("page", "1")
except ValueError:
page = 1
try:
kwargs["objects"] = paginator.page(page)
except (EmptyPage, PageNotAnInteger):
kwargs["objects"] = paginator.page(paginator.num_pages)
kwargs["last_page"] = paginator.num_pages
kwargs["total"] = paginator.count
kwargs["table"] = tblclass(request, kwargs["objects"].object_list)
return _render(request, tplname, kwargs)
@login_required
def domains(request):
if not request.user.has_perm("admin.view_domains"):
View File
@@ -0,0 +1 @@
[{"pk": 2, "model": "auth.group", "fields": {"name": "Resellers", "permissions": [["add_domain", "admin", "domain"], ["change_domain", "admin", "domain"], ["delete_domain", "admin", "domain"], ["view_domains", "admin", "domain"], ["add_mailbox", "admin", "mailbox"], ["change_mailbox", "admin", "mailbox"], ["delete_mailbox", "admin", "mailbox"], ["add_alias", "admin", "alias"], ["change_alias", "admin", "alias"], ["delete_alias", "admin", "alias"], ["view_domain", "admin", "domain"], ["view_mailboxes", "admin", "mailbox"], ["view_aliases", "admin", "alias"], ["view_domaliases", "admin", "domainalias"], ["add_domainalias", "admin", "domainalias"], ["change_domainalias", "admin", "domainalias"], ["delete_domainalias", "admin", "domainalias"]]}}]
+50
View File
@@ -0,0 +1,50 @@
# coding: utf-8
from django import forms
from django.utils.translation import ugettext_noop as _
from django.contrib.auth.models import User, Group
from models import *
class ResellerForm(forms.ModelForm):
class Meta:
model = User
fields = ("username", "first_name", "last_name", "email")
def save(self, commit=True):
user = super(ResellerForm, self).save(commit=False)
if self.cleaned_data.has_key("password1"):
user.set_password(self.cleaned_data["password1"])
if commit:
user.save()
grp = Group.objects.get(name="Resellers")
if not grp in user.groups.all():
user.groups.add(grp)
user.save()
try:
pool = LimitsPool.objects.get(user=user)
except LimitsPool.DoesNotExist:
pool = LimitsPool()
pool.user = user
pool.save()
return user
class ResellerWithPasswordForm(ResellerForm):
password1 = forms.CharField(label=_("Password"), widget=forms.PasswordInput)
password2 = forms.CharField(label=_("Confirmation"),
widget=forms.PasswordInput,
help_text = _("Enter the same password as above, for verification."))
def clean_password2(self):
password1 = self.cleaned_data.get("password1", "")
password2 = self.cleaned_data["password2"]
if password1 != password2:
raise forms.ValidationError(_("The two password fields didn't match."))
return password2
class LimitsPoolForm(forms.ModelForm):
class Meta:
model = LimitsPool
fields = ("maxdomains", "maxdomaliases", "maxmboxes", "maxmbaliases")
+47
View File
@@ -0,0 +1,47 @@
# coding: utf-8
"""
The *limits* extension
----------------------
"""
from django.utils.translation import ugettext_noop as _, ugettext
from django.core.urlresolvers import reverse
from django.conf.urls.defaults import include
from modoboa.lib import events, parameters
from modoboa.lib.webutils import static_url
baseurl = "limits"
def init():
events.register("AdminMenuDisplay", menu)
def destroy():
events.unregister("AdminMenuDisplay", menu)
def infos():
return {
"name" : "Limits",
"version" : "1.0",
"description" : ugettext("Limits for objects creation"),
"url" : baseurl
}
def menu(**kwargs):
import views
if kwargs["target"] != "top_menu":
return []
if not kwargs["user"].is_superuser:
return []
return [
{"name" : "resellers",
"label" : _("Resellers"),
"url" : reverse(views.resellers),
"img" : static_url("pics/resellers.png")}
]
def urls(prefix):
return (r'^%s%s/' % (prefix, baseurl),
include('modoboa.extensions.limits.urls'))
+21
View File
@@ -0,0 +1,21 @@
# coding: utf-8
from django.db import models
from django.contrib.auth.models import User
from django.utils.translation import ugettext as _, ugettext_noop
class LimitsPool(models.Model):
user = models.OneToOneField(User)
maxdomains = models.IntegerField(ugettext_noop("Max domains"),
default=0,
help_text=ugettext_noop("Maximum number of domains that can be created by this user"))
maxdomaliases = models.IntegerField(ugettext_noop("Max domain aliases"),
default=0,
help_text=ugettext_noop("Maximum number of domain aliases that can be created by this user"))
maxmboxes = models.IntegerField(ugettext_noop("Max mailboxes"),
default=0,
help_text=ugettext_noop("Maximum number of mailboxes that can be created by this user"))
maxmbaliases = models.IntegerField(ugettext_noop("Max mailbox aliases"),
default=0,
help_text=ugettext_noop("Maximum number of mailbox aliases that can be created by this user"))
+42
View File
@@ -0,0 +1,42 @@
var Resellers = new Class({
initialize: function() {
this.resellers_table = new HtmlTable($("resellers_table"), {
selectable: true
});
SqueezeBox.assign($("content").getElements('a[class=boxed]'), {
parse: 'rel'
});
$$("a[name=remove]").addEvent("click", this.remove_clickcb.bind(this));
},
remove_clickcb: function(evt) {
evt.stop();
if (this.resellers_table._selectedRows.length == 0) {
return;
}
if (!confirm(gettext("Remove this selection?"))) {
return;
}
var selection = "";
this.resellers_table._selectedRows.each(function(item) {
if (selection != "") {
selection += ",";
}
selection += item.get("id");
});
new Request.JSON.mdb({
url: evt.target.get("href"),
method: "GET",
onSuccess: function(res) {
if (res.status == "ok") {
window.location.reload();
}
}
}).send("selection=" + selection);
}
});
+23
View File
@@ -0,0 +1,23 @@
# coding: utf-8
from django.utils.translation import ugettext as _, ugettext_noop
from modoboa.lib import tables
from templatetags.limits_tags import reseller_actions
class ResellersTable(tables.Table):
tableid = "resellers_table"
idkey = "id"
fname = tables.Column("first_name", label=ugettext_noop("First name"))
lname = tables.Column("last_name", label=ugettext_noop("Last name"))
username = tables.Column("username", label=ugettext_noop("Username"))
created = tables.Column("date_joined", label=ugettext_noop("Created on"))
actions = tables.ActionColumn("actions", label=ugettext_noop("Actions"),
width="50px",
align="center", defvalue=reseller_actions)
cols_order = ["username", "fname", "lname", "created", "actions"]
def __init__(self, request, users):
super(ResellersTable, self).__init__(request)
self.populate(self._rows_from_model(users))
@@ -0,0 +1,16 @@
{% extends "layout_simple.html" %}
{% load i18n %}
{% block html_head %}
<script src="{{ MEDIA_URL }}js/form.js" type="text/javascript"></script>
{% endblock %}
{% block content %}
<div class="title"><h1>{% trans "Limits pool for" %} {{ reseller.username }}</h1></div>
{% if error %}<div class="error">{{ error }}</div>{% endif %}
<form action="" method="POST" id="limits_pool_form" name="limits_pool_form">
{% include "common/genericform.html" %}
<div class="spacer"></div>
<div class="lastrow" align="center">
<input type="submit" name="submit" value="{% trans 'Update' %}" />
</div>
</form>
{% endblock %}
@@ -0,0 +1,16 @@
{% extends "layout_simple.html" %}
{% load i18n %}
{% block html_head %}
<script src="{{ MEDIA_URL }}js/form.js" type="text/javascript"></script>
{% endblock %}
{% block content %}
<div class="title"><h1>{% trans "New reseller" %}</h1></div>
{% if error %}<div class="error">{{ error }}</div>{% endif %}
<form action="" method="POST" id="reseller_form" name="reseller_form">
{% include "common/genericform.html" %}
<div class="spacer"></div>
<div class="lastrow" align="center">
<input type="submit" name="submit" value="{{ submit_label }}" />
</div>
</form>
{% endblock %}
@@ -0,0 +1,43 @@
{% extends "nlayout.html" %}
{% load i18n limits_tags libextras %}
{% block html_head %}
<script src="{{ MEDIA_URL }}limits/js/resellers.js" type="text/javascript"></script>
<script type="text/javascript">
window.addEvent("domready", function() {
new Resellers();
});
</script>
{% endblock %}
{% block middletitle %}{% trans "Available resellers" %}{% endblock %}
{% block menu %}{% resellers_menu user %} {% endblock %}
{% block content %}
{% if messages %}
{% display_messages messages %}
{% endif %}
<div id="resellers" name="resellers">
{{ table|safe }}
</div>
{% endblock %}
{% block footer %}
<div id="navbar">
{% if paginator.count %}
<label>{% trans "Items" %} {{ page.start_index }} {% trans "to" %} {{ page.end_index }} {% trans "of" %} {{ paginator.count }}</label>
{% endif %}
{% if page.has_previous %}
<a href="?page=1">
<img src="{{ MEDIA_URL }}pics/first.png" border="0" title="{% trans "First" %}" />
</a>
<a href="?page={{ page.previous_page_number }}">
<img src="{{ MEDIA_URL }}pics/back.png" border="0" title="{% trans "Previous" %}" />
</a>
{% endif %}
{% if page.has_next %}
<a href="?page={{ page.next_page_number }}">
<img src="{{ MEDIA_URL }}pics/next.png" border="0" title="{% trans "Next" %}" />
</a>
<a href="?page={{ page.last_page }}">
<img src="{{ MEDIA_URL }}pics/last.png" border="0" title="{% trans "Last" %}" />
</a>
{% endif %}
</div>
{% endblock %}
@@ -0,0 +1,44 @@
# coding: utf-8
from django import template
from django.utils.translation import ugettext as _
from django.template.loader import render_to_string
from django.core.urlresolvers import reverse
from modoboa.lib.webutils import static_url, render_actions
from modoboa.extensions import limits
register = template.Library()
@register.simple_tag
def resellers_menu(user):
entries = [{"name" : "new",
"label" : _("New"),
"img" : static_url("pics/add.png"),
"class" : "boxed",
"rel" : "{handler:'iframe',size:{x:350,y:300}}",
"url" : reverse(limits.views.new_reseller)},
{"name" : "remove",
"label" : _("Remove"),
"img" : static_url("pics/remove.png"),
"url" : reverse(limits.views.delete_resellers)
}]
return render_to_string("common/menulist.html",
{"entries" : entries, "user" : user})
@register.simple_tag
def reseller_actions(user, resid):
actions = [
{"name" : "editreseller",
"url" : reverse(limits.views.edit_reseller, args=[resid]),
"img" : static_url("pics/edit.png"),
"title" : _("Edit reseller"),
"class" : "boxed",
"rel" : "{handler:'iframe',size:{x:350,y:300}}"},
{"name" : "editlimits",
"url" : reverse(limits.views.edit_limits_pool, args=[resid]),
"img" : static_url("pics/settings.png"),
"title" : _("Edit limits allocated to this reseller"),
"class" : "boxed",
"rel" : "{handler:'iframe',size:{x:330,y:280}}"},
]
return render_actions(actions)
+10
View File
@@ -0,0 +1,10 @@
from django.conf.urls.defaults import *
urlpatterns = patterns(
'modoboa.extensions.limits.views',
(r'^$', 'resellers'),
(r'reseller/new/$', 'new_reseller'),
(r'reseller/edit/(?P<resid>\d+)/$', 'edit_reseller'),
(r'reseller/delete/$', 'delete_resellers'),
(r'pool/edit/(?P<resid>\d+)/$', 'edit_limits_pool'),
)
+93
View File
@@ -0,0 +1,93 @@
# coding: utf-8
from django.utils.translation import ugettext as _, ungettext
from django.db import IntegrityError
from django.contrib.auth.models import *
from django.contrib import messages
from django.contrib.auth.decorators \
import login_required, user_passes_test
from django.core.urlresolvers import reverse
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from tables import *
from forms import *
from modoboa.lib import parameters
from modoboa.lib.webutils import _render, ajax_response
@login_required
@user_passes_test(lambda u: u.is_superuser)
def resellers(request, tplname="limits/resellers.html"):
users = User.objects.filter(groups__name="Resellers")
paginator = Paginator(users,
int(parameters.get_admin("ITEMS_PER_PAGE", app="admin")))
page = int(request.GET.get("page", "1"))
try:
subset = paginator.page(page)
except (EmptyPage, PageNotAnInteger):
subset = paginator.page(paginator.num_pages)
ctx = dict(table=ResellersTable(request, subset.object_list),
page=subset,
paginator=paginator)
return _render(request, tplname, ctx)
def validate_reseller(request, form, action, tplname="limits/reseller.html"):
error = None
if form.is_valid():
try:
form.save()
except IntegrityError:
error = _("User with this Username already exists")
else:
msg = _("Reseller created") if action == "create" \
else _("Reseller updated")
messages.info(request, msg, fail_silently=True)
return ajax_response(request, url=reverse(resellers))
submit_label = _("Create") if action == "create" else _("Update")
ctx = dict(form=form, error=error, submit_label=submit_label)
return ajax_response(request, status="ko", template=tplname, **ctx)
@login_required
@user_passes_test(lambda u: u.is_superuser)
def new_reseller(request, tplname="limits/reseller.html"):
if request.method == "POST":
form = ResellerWithPasswordForm(request.POST)
return validate_reseller(request, form, "create")
form = ResellerWithPasswordForm()
ctx = dict(form=form, submit_label=_("Create"))
return _render(request, tplname, ctx)
@login_required
@user_passes_test(lambda u: u.is_superuser)
def edit_reseller(request, resid, tplname="limits/reseller.html"):
reseller = User.objects.get(pk=resid)
if request.method == "POST":
if request.POST.get("password1", "") != "" and \
request.POST.get("password2", "") != "":
form = ResellerWithPasswordForm(request.POST, instance=reseller)
else:
form = ResellerForm(request.POST, instance=reseller)
return validate_reseller(request, form, action="update")
form = ResellerWithPasswordForm(instance=reseller)
ctx = dict(form=form, submit_label=_("Update"))
return _render(request, tplname, ctx)
@login_required
@user_passes_test(lambda u: u.is_superuser)
def delete_resellers(request):
selection = map(lambda idx: int(idx), request.GET.get("selection", "").split(","))
if not len(selection):
raise Exception("")
User.objects.filter(id__in=selection).delete()
msg = ungettext("Reselled deleted", "Resellers deleted", len(selection))
messages.info(request, msg, fail_silently=True)
return ajax_response(request)
@login_required
@user_passes_test(lambda u: u.is_superuser)
def edit_limits_pool(request, resid, tplname="limits/pool.html"):
pool = LimitsPool.objects.get(user=resid)
form = LimitsPoolForm(instance=pool)
ctx = dict(form=form, reseller=pool.user)
return _render(request, tplname, ctx)
+1
View File
@@ -15,6 +15,7 @@ div.row label {
float: left;
width: 120px;
padding: 3px 0;
margin-right: 5px;
}
input {
+5 -1
View File
@@ -25,6 +25,10 @@ Request.JSON.mdb = new Class({
Extends: Request.JSON,
options: {
onComplete: ajax_login_redirect
onComplete: ajax_login_redirect,
onFailure: function(xhr) {
$(document.body).setStyle("overflow", "auto");
$(document.body).set("html", xhr.responseText);
}
}
});
Binary file not shown.

After

Width:  |  Height:  |  Size: 734 B

+1 -1
View File
@@ -23,7 +23,7 @@
<a href="?page={{ objects.next_page_number }}">
<img src="{{ MEDIA_URL }}pics/next.png" border="0" title="{% trans "Next" %}" />
</a>
<a href="?page={{ page.last_page }}">
<a href="?page={{ objects.last_page }}">
<img src="{{ MEDIA_URL }}pics/last.png" border="0" title="{% trans "Last" %}" />
</a>
{% endif %}