From a295aa1ab66e0e0d8521f7d226d06b05522ad1c6 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Mon, 19 Mar 2012 15:13:01 +0100 Subject: [PATCH] wip: Global user interface improvment * Use of jQuery in place of Mootools * Most of the custom mootools plugins have been ported to jQuery * Use of bootstrap from Twitter in place of the old CSS * Most of the admin templates have been reworked * The sievefilters extension has been ported * The postfix autoreply extension is almost done * more... see #204 --- admin/forms.py | 604 +++++---- admin/lib.py | 102 +- admin/migrations/0004_add_permissions.py | 2 +- ...8_auto__add_unique_alias_domain_address.py | 121 ++ admin/models.py | 163 ++- admin/permissions.py | 133 -- admin/tables.py | 153 +-- admin/templatetags/admin_extras.py | 293 ++--- admin/urls.py | 37 +- admin/views.py | 738 ++++------- auth/forms.py | 12 +- extensions/limits/__init__.py | 43 +- extensions/limits/controls.py | 108 +- extensions/limits/forms.py | 43 +- extensions/limits/permissions.py | 53 +- extensions/postfix_autoreply/__init__.py | 56 +- extensions/postfix_autoreply/forms.py | 11 +- .../postfix_autoreply/autoreply.html | 55 +- extensions/postfix_autoreply/views.py | 23 +- extensions/sievefilters/__init__.py | 4 +- extensions/sievefilters/forms.py | 51 +- extensions/sievefilters/lib.py | 6 +- extensions/sievefilters/static/css/style.css | 52 +- .../static/js/jquery.dynfields.js | 328 +++++ extensions/sievefilters/static/js/lib.js | 227 ++-- .../templates/sievefilters/filter.html | 89 +- .../templates/sievefilters/filters.html | 28 - .../templates/sievefilters/guieditor.html | 33 +- .../templates/sievefilters/index.html | 86 +- .../templates/sievefilters/newfiltersset.html | 41 - .../templates/sievefilters/rawfilter.html | 5 - .../templatetags/sfilters_tags.py | 72 +- extensions/sievefilters/views.py | 49 +- extensions/stats/views.py | 2 +- lib/events.py | 15 +- lib/formutils.py | 165 +++ lib/permissions.py | 118 ++ lib/tables.py | 15 +- lib/templatetags/libextras.py | 103 +- lib/webutils.py | 6 +- static/css/Autocompleter.css | 50 - static/css/SqueezeBox.css | 141 --- static/css/bg_e.png | Bin 990 -> 0 bytes static/css/bg_n.png | Bin 986 -> 0 bytes static/css/bg_ne.png | Bin 1367 -> 0 bytes static/css/bg_nw.png | Bin 1246 -> 0 bytes static/css/bg_s.png | Bin 985 -> 0 bytes static/css/bg_se.png | Bin 1260 -> 0 bytes static/css/bg_sw.png | Bin 1223 -> 0 bytes static/css/bg_w.png | Bin 981 -> 0 bytes static/css/bootstrap-responsive.css | 4 + static/css/bootstrap.css | 632 ++++++++++ static/css/box.css | 207 ---- static/css/calendar.css | 91 -- static/css/closebox.png | Bin 1910 -> 0 bytes static/css/corner-tl.png | Bin 495 -> 0 bytes static/css/custom.css | 64 + static/css/default.css | 10 +- static/css/login.css | 94 +- static/img/glyphicons-halflings-white.png | Bin 0 -> 4352 bytes static/img/glyphicons-halflings.png | Bin 0 -> 4352 bytes static/js/Autocompleter.Local.js | 31 - static/js/Autocompleter.Request.js | 109 -- static/js/Autocompleter.js | 442 ------- static/js/Observer.js | 69 -- static/js/ProgressBar.js | 19 - static/js/SqueezeBox.js | 443 ------- static/js/admin.js | 67 + static/js/autocompleter.js | 166 +++ static/js/bootstrap.js | 1 + static/js/calendar.js | 1104 ----------------- static/js/common.js | 74 +- static/js/confirmation.js | 91 -- static/js/creation_wizard.js | 79 ++ static/js/domains.js | 67 +- static/js/dynamic_input.js | 106 ++ static/js/dynamic_textinput.js | 94 -- static/js/form.js | 126 -- static/js/{anchor_nav.js => history.js} | 111 +- static/js/infobox.js | 123 -- static/js/jquery.js | 4 + static/js/mootools-more.js | 314 ----- static/js/mootools.js | 485 -------- static/js/notify.js | 102 ++ static/js/permissions.js | 67 - static/js/preferences.js | 15 - static/js/settings.js | 15 - static/js/utils.js | 129 +- templates/admin/add_domain_admin.html | 59 - templates/admin/add_permission.html | 25 - templates/admin/adminform.html | 22 - templates/admin/create_domain_admin.html | 9 - templates/admin/dlistform.html | 7 + templates/admin/domain_admin_promotion.html | 10 - templates/admin/domainform.html | 8 + templates/admin/domains.html | 16 +- templates/admin/domains_header.html | 23 - templates/admin/extensions.html | 8 +- templates/admin/header.html | 4 + templates/admin/identities.html | 34 + templates/admin/import.html | 65 - templates/admin/import_done.html | 2 +- templates/admin/importform.html | 12 + templates/admin/listing.html | 37 +- templates/admin/mailform.html | 3 + templates/admin/mbaliasform.html | 37 - templates/admin/newaccount.html | 33 + templates/admin/parameters.html | 11 +- templates/admin/permissions.html | 22 - templates/admin/permsform.html | 2 + templates/admin/settings_header.html | 33 +- templates/admin/view_domain_admins.html | 8 - templates/common/generic_field.html | 6 + templates/common/generic_fields_group.html | 8 + templates/common/generic_modal_form.html | 20 + templates/common/genericform.html | 12 - templates/common/menu.html | 2 +- templates/common/menulist.html | 24 +- templates/common/pagination_bar.html | 22 + templates/common/parameters.html | 21 +- templates/common/tabforms.html | 31 + templates/common/table_body.html | 2 +- templates/layout_simple.html | 23 - templates/nlayout.html | 86 +- templates/registration/login.html | 51 +- templates/twocols.html | 9 + templates/userprefs/chpassword.html | 38 +- templates/userprefs/preferences.html | 39 +- userprefs/templatetags/uprefs_extras.py | 96 +- userprefs/views.py | 9 +- 130 files changed, 4255 insertions(+), 6725 deletions(-) create mode 100644 admin/migrations/0018_auto__add_unique_alias_domain_address.py delete mode 100644 admin/permissions.py create mode 100644 extensions/sievefilters/static/js/jquery.dynfields.js delete mode 100644 extensions/sievefilters/templates/sievefilters/filters.html delete mode 100644 extensions/sievefilters/templates/sievefilters/newfiltersset.html create mode 100644 lib/formutils.py create mode 100644 lib/permissions.py delete mode 100644 static/css/Autocompleter.css delete mode 100644 static/css/SqueezeBox.css delete mode 100644 static/css/bg_e.png delete mode 100644 static/css/bg_n.png delete mode 100644 static/css/bg_ne.png delete mode 100644 static/css/bg_nw.png delete mode 100644 static/css/bg_s.png delete mode 100644 static/css/bg_se.png delete mode 100644 static/css/bg_sw.png delete mode 100644 static/css/bg_w.png create mode 100644 static/css/bootstrap-responsive.css create mode 100644 static/css/bootstrap.css delete mode 100644 static/css/box.css delete mode 100644 static/css/calendar.css delete mode 100644 static/css/closebox.png delete mode 100644 static/css/corner-tl.png create mode 100644 static/css/custom.css create mode 100644 static/img/glyphicons-halflings-white.png create mode 100644 static/img/glyphicons-halflings.png delete mode 100644 static/js/Autocompleter.Local.js delete mode 100644 static/js/Autocompleter.Request.js delete mode 100644 static/js/Autocompleter.js delete mode 100644 static/js/Observer.js delete mode 100644 static/js/ProgressBar.js delete mode 100644 static/js/SqueezeBox.js create mode 100644 static/js/admin.js create mode 100644 static/js/autocompleter.js create mode 100644 static/js/bootstrap.js delete mode 100644 static/js/calendar.js delete mode 100644 static/js/confirmation.js create mode 100644 static/js/creation_wizard.js create mode 100644 static/js/dynamic_input.js delete mode 100644 static/js/dynamic_textinput.js delete mode 100644 static/js/form.js rename static/js/{anchor_nav.js => history.js} (62%) delete mode 100644 static/js/infobox.js create mode 100644 static/js/jquery.js delete mode 100644 static/js/mootools-more.js delete mode 100644 static/js/mootools.js create mode 100644 static/js/notify.js delete mode 100644 static/js/permissions.js delete mode 100644 static/js/preferences.js delete mode 100644 static/js/settings.js delete mode 100644 templates/admin/add_domain_admin.html delete mode 100644 templates/admin/add_permission.html delete mode 100644 templates/admin/adminform.html delete mode 100644 templates/admin/create_domain_admin.html create mode 100644 templates/admin/dlistform.html delete mode 100644 templates/admin/domain_admin_promotion.html create mode 100644 templates/admin/domainform.html delete mode 100644 templates/admin/domains_header.html create mode 100644 templates/admin/header.html create mode 100644 templates/admin/identities.html delete mode 100644 templates/admin/import.html create mode 100644 templates/admin/importform.html create mode 100644 templates/admin/mailform.html delete mode 100644 templates/admin/mbaliasform.html create mode 100644 templates/admin/newaccount.html delete mode 100644 templates/admin/permissions.html create mode 100644 templates/admin/permsform.html delete mode 100644 templates/admin/view_domain_admins.html create mode 100644 templates/common/generic_field.html create mode 100644 templates/common/generic_fields_group.html create mode 100644 templates/common/generic_modal_form.html delete mode 100644 templates/common/genericform.html create mode 100644 templates/common/pagination_bar.html create mode 100644 templates/common/tabforms.html delete mode 100644 templates/layout_simple.html create mode 100644 templates/twocols.html diff --git a/admin/forms.py b/admin/forms.py index f0f1bc7c8..5d9a5cb71 100644 --- a/admin/forms.py +++ b/admin/forms.py @@ -1,15 +1,25 @@ # coding: utf-8 from django import forms +from django.contrib.auth.models import Group from modoboa.admin.models import * from django.utils.translation import ugettext as _, ugettext_noop +from django.http import QueryDict from modoboa.admin.templatetags.admin_extras import gender -from modoboa.lib import tables from modoboa.lib.emailutils import split_mailbox +from modoboa.lib.permissions import get_account_roles +from modoboa.lib.formutils import * +from modoboa.lib.permissions import * + +class DomainForm(forms.ModelForm, DynamicForm): + aliases = DomainNameField( + label=ugettext_noop("Alias(es)"), + required=False, + help_text=ugettext_noop("Alias(es) of this domain") + ) -class DomainForm(forms.ModelForm): class Meta: model = Domain - fields = ("name", "quota", "enabled") + fields = ("name", "quota", "aliases", "enabled") def __init__(self, *args, **kwargs): self.oldname = None @@ -17,240 +27,235 @@ class DomainForm(forms.ModelForm): self.oldname = kwargs["instance"].name super(DomainForm, self).__init__(*args, **kwargs) - def save(self, force_insert=False, force_update=False, commit=True): + if len(args) and isinstance(args[0], QueryDict): + self._load_from_qdict(args[0], "aliases", DomainNameField) + elif kwargs.has_key("instance"): + d = kwargs["instance"] + for pos, dalias in enumerate(d.domainalias_set.all()): + name = "aliases_%d" % (pos + 1) + self._create_field(forms.CharField, name, dalias.name, 3) + + def clean(self): + super(DomainForm, self).clean() + if len(self._errors): + raise forms.ValidationError(self._errors) + + cleaned_data = self.cleaned_data + name = cleaned_data["name"] + + try: + DomainAlias.objects.get(name=name) + except DomainAlias.DoesNotExist: + pass + else: + self._errors["name"] = self.error_class([_("An alias with this name already exists")]) + del cleaned_data["name"] + + errors = [] + for k in cleaned_data.keys(): + if not k.startswith("aliases"): + continue + if cleaned_data[k] == "": + del cleaned_data[k] + continue + try: + Domain.objects.get(name=cleaned_data[k]) + except Domain.DoesNotExist: + pass + else: + self._errors[k] = self.error_class([_("A domain with this name already exists")]) + del cleaned_data[k] + + return cleaned_data + + def save(self, user, commit=True): d = super(DomainForm, self).save(commit=False) if commit: if self.oldname is not None and d.name != self.oldname: if not d.rename_dir(self.oldname): raise AdminError(_("Failed to rename domain, check permissions")) d.save() + for k, v in self.cleaned_data.iteritems(): + if not k.startswith("aliases"): + continue + if v in ["", None]: + continue + try: + d.domainalias_set.get(name=v) + except DomainAlias.DoesNotExist: + pass + else: + continue + events.raiseEvent("CanCreate", user, "domain_aliases") + al = DomainAlias(name=v, target=d, enabled=d.enabled) + al.save() + grant_access_to_object(user, al, is_owner=True) + events.raiseEvent("DomainAliasCreated", user, al) + + for dalias in d.domainalias_set.all(): + if not len(filter(lambda name: self.cleaned_data[name] == dalias.name, + self.cleaned_data.keys())): + events.raiseEvent("DomainAliasDeleted", dalias) + ungrant_access_to_object(dalias) + dalias.delete() return d -class ProxyForm(forms.ModelForm): - def __init__(self, user, *args, **kwargs): - super(ProxyForm, self).__init__(*args, **kwargs) - - if not user.is_superuser: - self._domains = user.get_domains() - else: - self._domains = None - -class DomainAliasForm(ProxyForm): - class Meta: - model = DomainAlias - fields = ('name', 'target', 'enabled') - - def __init__(self, *args, **kwargs): - super(DomainAliasForm, self).__init__(*args, **kwargs) - if self._domains is not None: - #self.fields["target"].queryset = Domain.objects.filter(pk=self._domain.id) - self.fields["target"].queryset = self._domains - -class MailboxForm(ProxyForm): - name = forms.CharField( - label=ugettext_noop("Name"), max_length=100, - help_text=ugettext_noop("First name and last name of mailbox owner") +class DlistForm(forms.ModelForm, DynamicForm): + email = forms.EmailField( + label=ugettext_noop("Email address"), + help_text=ugettext_noop("") + ) + recipients = forms.EmailField( + label=ugettext_noop("Recipients"), required=False, + help_text=ugettext_noop("Mailbox(es) this alias will point to") ) - quota = forms.IntegerField(label=ugettext_noop("Quota"), required=False, - help_text=ugettext_noop("Mailbox quota in MB (default to domain quota if blank)")) - enabled = forms.BooleanField(label=gender("Enabled", "f"), required=False, - initial=True, - help_text=ugettext_noop("Check to activate this mailbox")) - - class Meta: - model = Mailbox - fields = ('domain', 'address') - - def __init__(self, *args, **kwargs): - super(MailboxForm, self).__init__(*args, **kwargs) - for f in ['name', 'address', 'quota']: - self.fields[f].widget.attrs['size'] = 14 - if self._domains is not None: - self.fields["domain"].queryset = self._domains - #self.fields["domain"].queryset = Domain.objects.filter(pk=self._domain.id) - #self.fields["domain"].initial = self._domain - - if kwargs.has_key("instance"): - mb = kwargs["instance"] - self.fields['name'].initial = unicode(mb.user) - self.fields['quota'].initial = mb.quota - self.fields['enabled'].initial = mb.user.is_active - - def clean_address(self): - if self.cleaned_data["address"].find('@') != -1: - return self.cleaned_data["address"].rsplit("@", 1)[0] - return self.cleaned_data["address"] - - def commit_save(self, mb, **kwargs): - mb.save(name=self.cleaned_data["name"], - enabled=self.cleaned_data["enabled"], - quota=self.cleaned_data["quota"], - **kwargs) - - def save(self, force_insert=False, force_update=False, commit=True): - m = super(MailboxForm, self).save(commit=False) - if commit: - self.commit_save(m) - return m - -class MailboxWithPasswordForm(MailboxForm): - password1 = forms.CharField(label=ugettext_noop("Password"), - widget=forms.PasswordInput(render_value=True), - help_text=ugettext_noop("Password used to log in")) - password2 = forms.CharField(label=ugettext_noop("Confirmation"), - widget=forms.PasswordInput(render_value=True), - help_text=ugettext_noop("Password confirmation")) - - def __init__(self, *args, **kwargs): - super(MailboxWithPasswordForm, self).__init__(*args, **kwargs) - for f in ['password1', 'password2']: - self.fields[f].widget.attrs['size'] = 14 - self.fields.keyOrder = ['name', 'domain', 'address', 'quota', - 'password1', 'password2', 'enabled'] - - def clean_password2(self): - if self.cleaned_data["password1"] != self.cleaned_data["password2"]: - raise forms.ValidationError(_("Passwords mismatch")) - return self.cleaned_data["password2"] - - def commit_save(self, mb): - super(MailboxWithPasswordForm, self).commit_save( - mb, password=self.cleaned_data["password1"] - ) - -class AliasForm(ProxyForm): - targets = forms.CharField(label=ugettext_noop("Target(s)"), required=False, - help_text=ugettext_noop("Mailbox(es) this alias will point to")) - - def __init__(self, *args, **kwargs): - super(AliasForm, self).__init__(*args, **kwargs) - self.fields['address'].widget.attrs['size'] = 14 - - # This part is a little bit tricky. As 'domain' is not a - # member of Alias, we need to fill it manually. if - # self._domain exists, it means a DomainAdmin is going to - # create a new alias, we know how to fill the fields. - # if self._domain is not None: - # self.fields["domain"].queryset = Domain.objects.filter(pk=self._domain.id) - - if self._domains is not None: - self.fields["domain"].queryset = self._domains - - # if 'instance' exists, it means we are modifying an existing - # record, we need to select the right value for 'domain' and - # to fill the first "targets" input (because it is created by the - # Form object, not by us). - #domain = None - # if self._domain: - # domain = self._domain - if kwargs.has_key("instance"): - # if self._domain is None: - # domain = kwargs["instance"].domain - self.fields["targets"].initial = kwargs["instance"].first_target() - - # if domain is not None: - # self.fields["domain"].initial = domain - - self.fields.keyOrder = ['address', 'domain', 'enabled', 'targets'] class Meta: model = Alias - fields = ('address', 'domain', 'enabled') + fields = ("enabled",) - def clean_address(self): - if self.cleaned_data["address"].find('@') != -1: - return self.cleaned_data["address"].rsplit("@", 1)[0] - return self.cleaned_data["address"] + def __init__(self, *args, **kwargs): + super(DlistForm, self).__init__(*args, **kwargs) + self.fields.keyOrder = ['email', 'recipients', 'enabled'] - def set_targets(self, user, values): - """Targets dispatching + if len(args) and isinstance(args[0], QueryDict): + self._load_from_qdict(args[0], "recipients", forms.EmailField) + elif kwargs.has_key("instance"): + dlist = kwargs["instance"] + self.fields["email"].initial = dlist.full_address + cpt = 1 + for mb in dlist.mboxes.all(): + name = "recipients_%d" % (cpt) + self._create_field(forms.EmailField, name, mb.full_address, 2) + cpt += 1 + for addr in dlist.extmboxes.split(','): + if addr == "": + continue + name = "recipients_%d" % (cpt) + self._create_field(forms.EmailField, name, addr, 2) + cpt += 1 - We make a difference between 'local' targets (the ones hosted - by Modoboa) and 'external targets. + def clean_email(self): + localpart, domname = split_mailbox(self.cleaned_data["email"]) + try: + Domain.objects.get(name=domname) + except Domain.DoesNotExist: + raise forms.ValidationError(_("Domain does not exist")) + return self.cleaned_data["email"] + + def set_recipients(self, user): + """Recipients dispatching + + We make a difference between 'local' recipients (the ones hosted + by Modoboa) and 'external' recipients. """ - self.ext_targets = [] - self.int_targets = [] + self.ext_rcpts = [] + self.int_rcpts = [] + total = 0 - for addr in values: - if addr == "": + for k, v in self.cleaned_data.iteritems(): + if not k.startswith("recipients"): continue - local_part, domname = split_mailbox(addr) + if v == "": + continue + local_part, domname = split_mailbox(v) if domname is None: - raise AdminError("%s %s" % (_("Invalid mailbox"), addr)) + raise AdminError("%s %s" % (_("Invalid mailbox"), v)) try: domain = Domain.objects.get(name=domname) except Domain.DoesNotExist: domain = None if domain: if not user.is_owner(domain): - raise PermDeniedException(addr) + raise PermDeniedException(v) try: mb = Mailbox.objects.get(domain=domain, address=local_part) except Mailbox.DoesNotExist: - raise AdminError(_("Mailbox %s does not exist" % addr)) - self.int_targets += [mb] + raise AdminError(_("Mailbox %s does not exist" % v)) + self.int_rcpts += [mb] + total += 1 continue - self.ext_targets += [addr] + self.ext_rcpts += [v] + total += 1 - def save(self, force_insert=False, force_update=False, commit=True): - a = super(AliasForm, self).save(commit=False) + if total == 0: + raise AdminError(_("No recipient defined")) + + if total < 2: + raise AdminError(_("A distribution list must contain at least two recipients")) + + def save(self, commit=True): + dlist = super(DlistForm, self).save(commit=False) + localpart, domname = split_mailbox(self.cleaned_data["email"]) + dlist.address = localpart + dlist.domain = Domain.objects.get(name=domname) if commit: - a.save(self.int_targets, self.ext_targets) + dlist.save(self.int_rcpts, self.ext_rcpts) self.save_m2m() - return a + return dlist -class SuperAdminForm(forms.Form): - def __init__(self, user, *args, **kwargs): - super(SuperAdminForm, self).__init__(*args, **kwargs) +class ImportDataForm(forms.Form): + sourcefile = forms.FileField(label=_("Select a file")) + sepcar = forms.CharField(label=_("Separator"), max_length=1, required=False) - self.fields["user"] = \ - forms.ModelChoiceField([], label=_("User"), required=True, - empty_label=_("Select a user"), - help_text=_("Select a user in the list")) - self.fields["user"].queryset = User.objects.exclude(pk__in=[1, user.id]) + def __init__(self, *args, **kwargs): + super(ImportDataForm, self).__init__(*args, **kwargs) + self.fields["sepcar"].widget.attrs = {"class" : "span1"} -class DomainAdminForm(forms.Form): - createfrom = forms.ChoiceField( - choices=[("new", ugettext_noop("Create a new account")), - ("select", ugettext_noop("Promote an existing account"))], - widget=forms.RadioSelect) + def clean_sepcar(self): + if self.cleaned_data["sepcar"] == "": + return ";" + return str(self.cleaned_data["sepcar"]) - domain = forms.ModelChoiceField( - queryset=Domain.objects.all(), - label=_("Domain"), - required=True, - empty_label=ugettext_noop("Select a domain"), - help_text=ugettext_noop("Select a domain in the list") - ) - - user = forms.ChoiceField( - label=ugettext_noop("User"), required=True, - choices=[("", ugettext_noop("Empty"))], - help_text=ugettext_noop("Select a user in the list")) - -class UserForm(forms.ModelForm): - createmb = forms.BooleanField( - label=ugettext_noop("Create mailbox"), required=False, - help_text=ugettext_noop("Create a mailbox for this user using the given address") +class AccountFormGeneral(forms.ModelForm): + role = forms.ChoiceField( + choices=[('', ugettext_noop("Choose"))] ) class Meta: model = User - fields = ("username", "first_name", "last_name", "email") + fields = ("username", "first_name", "last_name", "role", "is_active") - def save(self, commit=True, group=None): - user = super(UserForm, self).save(commit=False) - if self.cleaned_data.has_key("password1"): - user.set_password(self.cleaned_data["password1"]) + def __init__(self, user, *args, **kwargs): + super(AccountFormGeneral, self).__init__(*args, **kwargs) + + self.user = user + if user.belongs_to_group("DomainAdmins"): + del self.fields["role"] + return + + self.fields["role"].choices = \ + [('', ugettext_noop("Choose"))] + get_account_roles(user) + if kwargs.has_key("instance"): + u = kwargs["instance"] + if u.is_superuser: + role = "SuperAdmins" + else: + try: + role = u.groups.all()[0].name + except IndexError: + pass + self.fields["role"].initial = role + + def save(self, commit=True): + account = super(AccountFormGeneral, self).save(commit=False) if commit: - user.save() - if group: - user.groups.add(Group.objects.get(name=group)) - user.save() - return user + account.save() + if self.cleaned_data.has_key("role"): + role = self.cleaned_data["role"] + else: + role = "SimpleUsers" + if role == "SuperAdmins": + account.is_superuser = True + else: + account.is_superuser = False + account.groups.clear() + account.groups.add(Group.objects.get(name=role)) + account.save() + return account -class UserWithPasswordForm(UserForm): +class AccountFormGeneralPwd(AccountFormGeneral): password1 = forms.CharField(label=_("Password"), widget=forms.PasswordInput) password2 = forms.CharField( label=ugettext_noop("Confirmation"), @@ -258,6 +263,11 @@ class UserWithPasswordForm(UserForm): help_text=ugettext_noop("Enter the same password as above, for verification.") ) + class Meta: + model = User + fields = ("username", "first_name", "last_name", + "password1", "password2", "role", "is_active") + def clean_password2(self): password1 = self.cleaned_data.get("password1", "") password2 = self.cleaned_data["password2"] @@ -265,38 +275,194 @@ class UserWithPasswordForm(UserForm): raise forms.ValidationError(_("The two password fields didn't match.")) return password2 -class DomainAdminPromotionForm(forms.Form): - name = forms.CharField( - label=ugettext_noop("Name"), - help_text=ugettext_noop("The username/email address/name of the account to promote") + def save(self, commit=True, **kwargs): + user = super(AccountFormGeneralPwd, self).save(**kwargs) + if self.cleaned_data.has_key("password1"): + user.set_password(self.cleaned_data["password1"]) + if commit: + user.save() + return user + +class AccountFormMail(forms.Form, DynamicForm): + # FIXME + # * ajout du quota + # * Renommage de boite ? + + email = forms.EmailField(label=_("E-mail"), required=False) + aliases = forms.EmailField( + label=ugettext_noop("Alias(es)"), + required=False, + help_text=ugettext_noop("Alias(es) of this mailbox") ) - def clean_name(self): + def __init__(self, *args, **kwargs): + if kwargs.has_key("instance"): + self.mb = kwargs["instance"] + del kwargs["instance"] + super(AccountFormMail, self).__init__(*args, **kwargs) + if not hasattr(self, "mb") or self.mb is None: + return + self.fields["email"].required = True + for pos, alias in enumerate(self.mb.alias_set.all()): + if len(alias.get_recipients()) >= 2: + continue + name = "aliases_%d" % (pos + 1) + self._create_field(forms.EmailField, name, alias.full_address) + self.fields["email"].initial = self.mb.full_address + if len(args) and isinstance(args[0], QueryDict): + self._load_from_qdict(args[0], "aliases", forms.EmailField) + + def save(self, user, account): + if self.cleaned_data["email"] == "": + return None + + locpart, domname = split_mailbox(self.cleaned_data["email"]) try: - u = User.objects.get(username=self.cleaned_data["name"]) - except User.DoesNotExist: - raise forms.ValidationError(_("Unknown user")) - return self.cleaned_data["name"] + domain = Domain.objects.get(name=domname) + except Domain.DoesNotExist: + raise AdminError(_("Domain does not exist")) -class AssignDomainsForm(forms.Form): - domains = forms.ModelMultipleChoiceField( - queryset=None, - label=ugettext_noop("Domains"), - required=True, - help_text=ugettext_noop("Select one or more domains in the list") + if not user.can_access(domain): + raise PermDeniedException(_("You don't have access to this domain")) + + if not hasattr(self, "mb") or self.mb is None: + try: + self.mb = Mailbox.objects.get(address=locpart, domain=domain) + except Mailbox.DoesNotExist: + events.raiseEvent("CanCreate", user, "mailboxes") + self.mb = Mailbox() + self.mb.save_from_user(locpart, domain, account) + grant_access_to_object(user, self.mb, is_owner=True) + events.raiseEvent("CreateMailbox", user, self.mb) + else: + if self.cleaned_data["email"] != self.mb.full_address: + if self.mb.rename_dir(domname, locpart): + self.mb.domain = domain + self.mb.address = locpart + self.mb.save() + else: + raise AdminError(_("Failed to rename mailbox, check permissions")) + + account.email = self.cleaned_data["email"] + account.save() + + for name, value in self.cleaned_data.iteritems(): + if not name.startswith("aliases"): + continue + if value == "": + continue + local_part, domname = split_mailbox(value) + try: + self.mb.alias_set.get(address=local_part, domain__name=domname) + except Alias.DoesNotExist: + pass + else: + continue + events.raiseEvent("CanCreate", user, "mailbox_aliases") + al = Alias(address=local_part, enabled=account.is_active) + al.domain = Domain.objects.get(name=domname) + al.save([self.mb], []) + grant_access_to_object(user, al, is_owner=True) + events.raiseEvent("MailboxAliasCreated", user, al) + + for alias in self.mb.alias_set.all(): + if len(alias.get_recipients()) >= 2: + continue + if not len(filter(lambda name: self.cleaned_data[name] == alias.full_address, + self.cleaned_data.keys())): + events.raiseEvent("MailboxAliasDeleted", alias) + ungrant_access_to_object(alias) + alias.delete() + + return self.mb + +class AccountPermissionsForm(forms.Form, DynamicForm): + domains = DomainNameField( + label=ugettext_noop("Domain(s)"), + required=False, + help_text=ugettext_noop("Domain(s) that user administrates") ) - def __init__(self, user, domadmin, *args, **kwargs): - super(AssignDomainsForm, self).__init__(*args, **kwargs) - self.fields["domains"].queryset = user.get_domains() - self.fields["domains"].initial = domadmin.get_domains() + def __init__(self, *args, **kwargs): + if kwargs.has_key("instance"): + self.account = kwargs["instance"] + del kwargs["instance"] + + super(AccountPermissionsForm, self).__init__(*args, **kwargs) -class ImportDataForm(forms.Form): - sourcefile = forms.FileField(label=_("Select a file")) - sepcar = forms.CharField(label=_("Separator"), max_length=1, required=False) + if not hasattr(self, "account") or self.account is None: + return + for pos, domain in enumerate(self.account.get_domains()): + name = "domains_%d" % (pos + 1) + self._create_field(DomainNameField, name, domain.name) + if len(args) and isinstance(args[0], QueryDict): + self._load_from_qdict(args[0], "domains", DomainNameField) - def clean_sepcar(self): - if self.cleaned_data["sepcar"] == "": - return ";" - return self.cleaned_data["sepcar"] + def save(self): + current_domains = map(lambda dom: dom.name, self.account.get_domains()) + for name, value in self.cleaned_data.iteritems(): + if not name.startswith("domains"): + continue + if value in ["", None]: + continue + if not value in current_domains: + domain = Domain.objects.get(name=value) + grant_access_to_object(self.account, domain) + + for domain in self.account.get_domains(): + if not len(filter(lambda name: self.cleaned_data[name] == domain.name, + self.cleaned_data.keys())): + ungrant_access_to_object(domain, self.account) + +class AccountForm(TabForms): + + def __init__(self, user, *args, **kwargs): + self.user = user + self.forms = [ + dict(id="general", title=_("General"), cls=AccountFormGeneralPwd, + new_args=[user], mandatory=True), + dict(id="mail", title=_("Mail"), formtpl="admin/mailform.html", + cls=AccountFormMail), + dict(id="perms", title=_("Permissions"), formtpl="admin/permsform.html", + cls=AccountPermissionsForm) + ] + cbargs = [user] + if kwargs.has_key("instances"): + cbargs += [kwargs["instances"]["general"]] + self.forms += events.raiseQueryEvent("ExtraAccountForm", *cbargs) + + super(AccountForm, self).__init__(*args, **kwargs) + + def check_perms(self, account): + if account.is_superuser: + return False + return account.has_perm("admin.view_domain") + + def _before_is_valid(self, form): + if form["id"] == "general": + return True + + if hasattr(self, "check_%s" % form["id"]): + if not getattr(self, "check_%s" % form["id"])(self.account): + return False + return True + + if False in events.raiseQueryEvent("CheckExtraAccountForm", self.account, form): + return False + return True + + def save_general_form(self): + self.account = self.forms[0]["instance"].save() + + def save(self): + """Custom save method + + As forms interact with each other, it is simpler to make + custom code to save them. + """ + self.forms[1]["instance"].save(self.user, self.account) + if len(self.forms) <= 2: + return + for f in self.forms[2:]: + f["instance"].save() diff --git a/admin/lib.py b/admin/lib.py index 1ad7d6bda..e1e9a2a57 100644 --- a/admin/lib.py +++ b/admin/lib.py @@ -1,10 +1,8 @@ # coding: utf-8 -from functools import wraps -from django.contrib.contenttypes.models import ContentType 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, _render +from modoboa.lib.webutils import _render +from modoboa.lib import parameters from modoboa.admin.tables import * from exceptions import * @@ -36,98 +34,10 @@ def render_listing(request, objtype, tplname="admin/listing.html", except ValueError: page = 1 try: - kwargs["objects"] = paginator.page(page) + kwargs["page"] = 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) + kwargs["page"] = paginator.page(paginator.num_pages) + kwargs["table"] = tblclass(request, kwargs["page"].object_list) + kwargs["selection"] = objtype return _render(request, tplname, kwargs) - -def grant_access_to_object(user, obj, is_owner=False): - """Grant access to an object for a given user - - There are two different cases where we want to grant access to an - object for a specific user: - - * He is the owner (he's just created the object) - * He is going to administrate the object (but he is not the owner) - - :param user: a ``User`` object - :param obj: an admin. object (Domain, Mailbox, ...) - :param is_owner: the user is the unique object's owner - """ - ct = get_content_type(obj) - try: - entry = user.objectaccess_set.get(content_type=ct, object_id=obj.id) - entry.is_owner = is_owner - entry.save() - except ObjectAccess.DoesNotExist: - pass - else: - return - - try: - objaccess = ObjectAccess(user=user, content_type=ct, - object_id=obj.id, is_owner=is_owner) - objaccess.save() - except IntegrityError, e: - raise AdminError(_("Failed to grant access (%s)" % str(e))) - -def ungrant_access_to_object(obj, user=None): - """Ungrant access to an object for a specific user - - If no user is provided, all entries referencing this object are - deleted from the database. - - :param obj: an object inheriting from ``models.Model`` - :param user: a ``User`` object - """ - ct = get_content_type(obj) - if user: - try: - ObjectAccess.objects.get(user=user, content_type=ct, object_id=obj.id).delete() - except ObjectAccess.DoesNotExist: - pass - else: - ObjectAccess.objects.filter(content_type=ct, object_id=obj.id).delete() - -def get_object_owner(obj): - """Return the unique owner of this object - - :param obj: an object inheriting from ``model.Model`` - :return: a ``User`` object - """ - ct = get_content_type(obj) - try: - entry = ObjectAccess.objects.get(content_type=ct, object_id=obj.id, is_owner=True) - except ObjectAccess.DoesNotExist: - return None - return entry.user - -def check_domain_access(f): - """Decorator to check if the current user can access a domain - - This decorator only applies to url that contain a 'domid' or - 'mbid' parameter. (for a direct access) - - Raise a ``PermDeniedError`` if the current user can't view the - requested object. - """ - @wraps(f) - def dec(request, **kwargs): - if not request.user.is_superuser: - domain = None - domid = request.GET.get("domid", None) - if domid: - domain = Domain.objects.get(pk=domid) - else: - mbid = request.GET.get("mbid", None) - if mbid: - domain = Mailbox.objects.get(pk=mbid).domain - if domain and not request.user.can_access(domain): - raise PermDeniedError(_("You can't access this domain")) - - return f(request, **kwargs) - return dec diff --git a/admin/migrations/0004_add_permissions.py b/admin/migrations/0004_add_permissions.py index f1c0ec735..8b3416ce7 100644 --- a/admin/migrations/0004_add_permissions.py +++ b/admin/migrations/0004_add_permissions.py @@ -93,4 +93,4 @@ class Migration(DataMigration): } } - complete_apps = ['admin'] + complete_apps = ['admin', 'contenttypes'] diff --git a/admin/migrations/0018_auto__add_unique_alias_domain_address.py b/admin/migrations/0018_auto__add_unique_alias_domain_address.py new file mode 100644 index 000000000..41af5c4b1 --- /dev/null +++ b/admin/migrations/0018_auto__add_unique_alias_domain_address.py @@ -0,0 +1,121 @@ +# encoding: utf-8 +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + +class Migration(SchemaMigration): + + def forwards(self, orm): + + # Adding unique constraint on 'Alias', fields ['domain', 'address'] + db.create_unique('admin_alias', ['domain_id', 'address']) + + + def backwards(self, orm): + + # Removing unique constraint on 'Alias', fields ['domain', 'address'] + db.delete_unique('admin_alias', ['domain_id', 'address']) + + + models = { + 'admin.alias': { + 'Meta': {'unique_together': "(('address', 'domain'),)", 'object_name': 'Alias'}, + 'address': ('django.db.models.fields.CharField', [], {'max_length': '254'}), + 'dates': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['admin.ObjectDates']"}), + 'domain': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['admin.Domain']"}), + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'extmboxes': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'mboxes': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['admin.Mailbox']", 'symmetrical': 'False'}) + }, + 'admin.domain': { + 'Meta': {'object_name': 'Domain'}, + 'dates': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['admin.ObjectDates']"}), + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100'}), + 'quota': ('django.db.models.fields.IntegerField', [], {}) + }, + 'admin.domainalias': { + 'Meta': {'object_name': 'DomainAlias'}, + 'dates': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['admin.ObjectDates']"}), + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100'}), + 'target': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['admin.Domain']"}) + }, + 'admin.extension': { + 'Meta': {'object_name': 'Extension'}, + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '150'}) + }, + 'admin.mailbox': { + 'Meta': {'object_name': 'Mailbox'}, + 'address': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'dates': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['admin.ObjectDates']"}), + 'domain': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['admin.Domain']"}), + 'gid': ('django.db.models.fields.IntegerField', [], {}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'path': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'quota': ('django.db.models.fields.IntegerField', [], {}), + 'uid': ('django.db.models.fields.IntegerField', [], {}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + }, + 'admin.objectaccess': { + 'Meta': {'unique_together': "(('user', 'content_type', 'object_id'),)", 'object_name': 'ObjectAccess'}, + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_owner': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + }, + 'admin.objectdates': { + 'Meta': {'object_name': 'ObjectDates'}, + 'creation': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'last_modification': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}) + }, + 'admin.user': { + 'Meta': {'object_name': 'User', 'db_table': "'auth_user'", '_ormbases': ['auth.User'], 'proxy': 'True'} + }, + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + } + } + + complete_apps = ['admin'] diff --git a/admin/models.py b/admin/models.py index b53a2228b..1e771ac3b 100644 --- a/admin/models.py +++ b/admin/models.py @@ -24,15 +24,6 @@ try: except ImportError: ldap_available = False -def get_content_type(obj): - """Simple function that use the right method to retrieve a content type - - :param obj: a django model - :return: a ``ContentType`` object - """ - return obj.get_content_type() if hasattr(obj, "get_content_type") \ - else ContentType.objects.get_for_model(obj) - class User(DUser): """Proxy for the ``User`` model. @@ -50,6 +41,37 @@ class User(DUser): def __unicode__(self): return u"%s %s" % (self.first_name, self.last_name) + def delete(self, fromuser, keep_mb_dir, *args, **kwargs): + """Custom delete method + """ + from modoboa.lib.permissions import \ + get_object_owner, grant_access_to_object, ungrant_access_to_object + + if not fromuser.can_access(self): + raise PermDeniedException(_("You don't have access to this account")) + if fromuser == self: + raise AdminError(_("You can't delete your own account")) + if self.has_mailbox: + mb = self.mailbox_set.all()[0] + if not fromuser.can_access(mb.domain): + raise PermDeniedException(_("You don't have access to this domain")) + events.raiseEvent("DeleteMailbox", mb) + ungrant_access_to_object(mb) + mb.delete(keepdir=keep_mb_dir) + + owner = get_object_owner(self) + for ooentry in self.objectaccess_set.all(): + if ooentry.is_owner: + if ooentry.content_object is not None: + grant_access_to_object(owner, ooentry.content_object, True) + else: + print "ORPHELIN!!!" + ooentry.delete() + + events.raiseEvent("AccountDeleted", self) + ungrant_access_to_object(self) + super(User, self).delete(*args, **kwargs) + @staticmethod def get_content_type(): """An uggly hack to retrieve the appropriate content_type! @@ -130,6 +152,27 @@ class User(DUser): return "%s %s" % (self.first_name, self.last_name) return self.username + @property + def identity(self): + if self.email != "": + return self.email + return self.username + + @property + def name_or_rcpt(self): + if self.first_name != "": + return "%s %s" % (self.first_name, self.last_name) + return "----" + + @property + def group(self): + if self.is_superuser: + return "SuperAdmin" + try: + return self.groups.all()[0].name + except IndexError: + return "?" + def belongs_to_group(self, name): """Simple shortcut to check if this user is a member of a specific group. @@ -216,6 +259,8 @@ class User(DUser): :param obj: an object inheriting from ``models.Model`` :return: a boolean """ + from modoboa.lib.permissions import get_content_type + ct = get_content_type(obj) try: ooentry = self.objectaccess_set.get(content_type=ct, object_id=obj.id) @@ -233,6 +278,8 @@ class User(DUser): :param obj: a admin object :return: a boolean """ + from modoboa.lib.permissions import get_content_type + if self.is_superuser: return True @@ -360,8 +407,8 @@ class Domain(DatesAware): # Not very optimized but currently, this is the simple way # I've found to delete all related objects (mailboxes, users # and aliases)! - for mb in self.mailbox_set.all(): - mb.delete(keepdir=keepdir) + # for mb in self.mailbox_set.all(): + # mb.delete(keepdir=keepdir) super(Domain, self).delete(*args, **kwargs) if not keepdir: self.delete_dir() @@ -379,14 +426,19 @@ class DomainAlias(DatesAware): help_text=ugettext_noop("The alias name")) target = models.ForeignKey(Domain, verbose_name=ugettext_noop('target'), help_text=ugettext_noop("The domain this alias points to")) - enabled = models.BooleanField(ugettext_noop('enabled'), - help_text=ugettext_noop("Check to activate this alias")) + enabled = models.BooleanField( + ugettext_noop('enabled'), + help_text=ugettext_noop("Check to activate this alias") + ) class Meta: permissions = ( ("view_domaliases", "View domain aliases"), ) + def __unicode__(self): + return self.name + class Mailbox(DatesAware): address = models.CharField( ugettext_noop('address'), max_length=100, @@ -456,13 +508,14 @@ class Mailbox(DatesAware): % (abspath, self.mdirroot, dir, sdir)) return True - def rename_dir(self, domain, newaddress): + def rename_dir(self, newdomain, newaddress): if parameters.get_admin("CREATE_DIRECTORIES") == "yes": if self.domain.name == domain and newaddress == self.address: return True - path = "%s/%s" % (parameters.get_admin("STORAGE_PATH"), domain) + oldpath = "%s/%s" % (parameters.get_admin("STORAGE_PATH"), self.domain.name) + newpath = "%s/%s" % (parameters.get_admin("STORAGE_PATH"), newdomain) code = exec_as_vuser("mv %s/%s %s/%s" \ - % (path, self.address, path, newaddress)) + % (oldpath, self.address, newpath, newaddress)) if code: self.path = "%s/" % newaddress return code @@ -498,8 +551,8 @@ class Mailbox(DatesAware): user.set_password(kwargs["password"]) try: user.save() - except IntegrityError: - raise AdminError(_("Mailbox with this address already exists")) + except IntegrityError, e: + raise AdminError(_("Account '%s' already exists" % user.username)) self.user = user if len(user.groups.all()) == 0: @@ -538,7 +591,6 @@ class Mailbox(DatesAware): :param domain: the associated Domain object :param user: the associated User object """ - self.name = "%s %s" % (user.first_name, user.last_name) self.address = localpart self.domain = domain self.user = user @@ -556,7 +608,7 @@ class Mailbox(DatesAware): self.gid = v_gid else: self.gid = pwd.getpwnam(parameters.get_admin("VIRTUAL_GID")).pw_gid - self.quota = self.domain.quota + self.quota = self.domain.quota super(Mailbox, self).save() def create_from_csv(self, user, line): @@ -591,18 +643,21 @@ class Mailbox(DatesAware): alias.mboxes.remove(self) if len(alias.mboxes.all()) == 0: alias.delete() - self.user.delete() super(Mailbox, self).delete(*args, **kwargs) if not keepdir: self.delete_dir() class Alias(DatesAware): - address = models.CharField(ugettext_noop('address'), max_length=254, - help_text=ugettext_noop("The alias address (without the domain part). For a 'catch-all' address, just enter an * character.")) + address = models.CharField( + ugettext_noop('address'), max_length=254, + help_text=ugettext_noop("The alias address (without the domain part). For a 'catch-all' address, just enter an * character.") + ) domain = models.ForeignKey(Domain) - mboxes = models.ManyToManyField(Mailbox, verbose_name=ugettext_noop('mailboxes'), - help_text=ugettext_noop("The mailboxes this alias points to")) + mboxes = models.ManyToManyField( + Mailbox, verbose_name=ugettext_noop('mailboxes'), + help_text=ugettext_noop("The mailboxes this alias points to") + ) extmboxes = models.TextField(blank=True) enabled = models.BooleanField(ugettext_noop('enabled'), help_text=ugettext_noop("Check to activate this alias")) @@ -611,64 +666,43 @@ class Alias(DatesAware): permissions = ( ("view_aliases", "View aliases"), ) + unique_together = (("address", "domain"),) @property def full_address(self): return "%s@%s" % (self.address, self.domain.name) @property - def targets(self): - ret = "
".join(map(lambda m: str(m), self.mboxes.all())) - ret += "
" + self.repr_extmboxes() - return ret + def identity(self): + return self.full_address - def save(self, int_targets, ext_targets, *args, **kwargs): - if len(ext_targets): - self.extmboxes = ",".join(ext_targets) + @property + def name_or_rcpt(self): + return "%s, ..." % self.get_recipients()[0] + + def save(self, int_rcpts, ext_rcpts, *args, **kwargs): + if len(ext_rcpts): + self.extmboxes = ",".join(ext_rcpts) else: self.extmboxes = "" super(Alias, self).save(*args, **kwargs) curmboxes = self.mboxes.all() - for t in int_targets: + for t in int_rcpts: if not t in curmboxes: self.mboxes.add(t) for t in curmboxes: - if not t in int_targets: + if not t in int_rcpts: self.mboxes.remove(t) - def first_target(self): - """Return the first target of this alias - - It is taken from internal addresses, or external addresses. - """ - if len(self.mboxes.all()): - return self.mboxes.all()[0] - if len(self.extmboxes): - return self.extmboxes.split(",")[0] - return None - - def get_targets(self): - """Return a list containing targeted mailboxes + def get_recipients(self): + """Return a the recipients list Internal and external addresses are mixed into a single list. """ - result = [] - if len(self.mboxes.all()): - for mb in self.mboxes.all()[1:]: - result += [mb.full_address] - if len(self.extmboxes): - result += self.extmboxes.split(",") - else: - result = self.extmboxes.split(",")[1:] + result = map(lambda mb: mb.full_address, self.mboxes.all()) + result += self.extmboxes.split(',') return result - def repr_extmboxes(self): - """Return external targets as a string - - A ready to print representation of external targets. - """ - return "
".join(self.extmboxes.split(",")) - def ui_disabled(self, user): if user.is_superuser: return False @@ -699,11 +733,12 @@ class Extension(models.Model): module.destroy() def on(self): - self.load() - self.init() self.enabled = True self.save() + self.load() + self.init() + extdir = "%s/extensions/%s" % (settings.MODOBOA_DIR, self.name) scriptdir = "%s/%s" % (extdir, "scripts") if os.path.exists(scriptdir): diff --git a/admin/permissions.py b/admin/permissions.py deleted file mode 100644 index 31b50317d..000000000 --- a/admin/permissions.py +++ /dev/null @@ -1,133 +0,0 @@ -# coding: utf-8 - -import datetime -from django.utils.translation import ugettext as _, ugettext_noop -from django.contrib.auth.models import Group -from modoboa.lib.webutils import _render, getctx, _render_to_string -from modoboa.lib import events -from forms import * -from models import User -from tables import SuperAdminsTable, DomainAdminsTable -from lib import grant_access_to_object, ungrant_access_to_object - -class Permissions(object): - submit_label = ugettext_noop("Add") - formtpl = 'admin/add_permission.html' - - @staticmethod - def is_authorized(user): - return user.is_superuser - - def get_add_form(self, request): - raise NotImplementedError - - def add(self, request): - raise NotImplementedError - - def delete(self, request, selection): - raise NotImplementedError - - def get(self, request): - raise NotImplementedError - - def _render_form(self, request, form, instring=False): - ctx = dict(form=form, title=self.title, submit_label=self.submit_label, - role=self.role) - if instring: - return _render_to_string(request, self.formtpl, ctx) - return _render(request, self.formtpl, ctx) - -class SuperAdminsPerms(Permissions): - title = ugettext_noop("Add super administrator") - role = "super_admins" - - def get_add_form(self, request): - form = SuperAdminForm(request.user) - return self._render_form(request, form) - - def add(self, request): - form = SuperAdminForm(request.user, request.POST) - if form.is_valid(): - user = User.objects.get(pk=request.POST["user"]) - user.is_superuser = True - user.date_joined = datetime.datetime.now() - events.raiseEvent("SuperAdminPromotion", user) - user.groups.clear() - user.save() - return True, None - - content = self._render_form(request, form, True) - return False, getctx("ko", content=content) - - def delete(self, selection): - for s in selection: - user = User.objects.get(pk=int(s)) - if user.username == "admin": - continue - user.is_superuser = False - user.save() - - def get(self, request): - admins = User.objects.filter(is_superuser=True) - return SuperAdminsTable(request, admins).render() - - -class DomainAdminsPerms(Permissions): - title = ugettext_noop("Add domain administrator") - role = "domain_admins" - formtpl = 'admin/add_domain_admin.html' - - @staticmethod - def is_authorized(user): - return user.has_perm("admin.view_domains") - - def get_add_form(self, request): - form = DomainAdminForm() - return self._render_form(request, form) - - def delete(self, selection): - dagrp = Group.objects.get(name="DomainAdmins") - sugrp = Group.objects.get(name="SimpleUsers") - for uid in selection: - u = User.objects.get(pk=uid) - u.groups.remove(dagrp) - u.save() - events.raiseEvent("DomainAdminDeleted", u) - if not len(u.mailbox_set.all()): - ungrant_access_to_object(u) - u.delete() - else: - u.groups.add(sugrp) - - def get(self, request): - domainadmins = User.objects.filter(groups__name="DomainAdmins") - domainadmins = filter(lambda da: request.user.is_owner(da), domainadmins) - return DomainAdminsTable(request, domainadmins).render() - -def get_perms_class(user, role): - """Retrieves the class associated to a given role - - The provided user must have the required permission to retrieve - this class. - - If "role" is one of those provided by the admin component, its - class is directly returned. Otherwise, a "PermsGetClass" event is - raised to check if one of the active plugins provide the desired - class. - - :param user: a User object - :param role: the role to look for - """ - parts = role.split("_") - cname = "" - for p in parts: - cname += p.capitalize() - cname += "Perms" - if globals().has_key(cname): - if globals()[cname].is_authorized(user): - return globals()[cname] - return None - res = events.raiseQueryEvent("PermsGetClass", role) - if not len(res): - return None - return res[0] diff --git a/admin/tables.py b/admin/tables.py index 4e6f7f182..b8c9cc304 100644 --- a/admin/tables.py +++ b/admin/tables.py @@ -26,7 +26,7 @@ class DomainsTable(tables.Table): cols_order = ["name", "creation", "modified", "domaliases", "mboxes", "mbaliases", - "quota", "enabled", "actions"] + "quota", "actions"] def __init__(self, request, doms): super(DomainsTable, self).__init__(request) @@ -38,91 +38,10 @@ class DomainsTable(tables.Table): def parse_enabled(self, value): return _("yes") if value else _("no") -class DomaliasesTable(tables.Table): - tableid = "objects_table" - idkey = "id" - - name = tables.Column("name", label=ugettext_noop("Name")) - target = tables.Column("target", label=ugettext_noop("Targeted domain")) - creation = tables.Column("creation", label=ugettext_noop("Created"), width="170px") - modified = tables.Column("last_modification", label=ugettext_noop("Last modified"), - width="170px") - enabled = tables.Column("enabled", label=gender("Enabled", "m"), width="50px", - align="center") - actions = tables.ActionColumn("actions", label=ugettext_noop("Actions"), - width="50px", align="center", - defvalue=domalias_actions) - - cols_order = ["name", "target", "creation", - "modified", "enabled", "actions"] - - def __init__(self, request, domaliases): - super(DomaliasesTable, self).__init__(request) - self.populate(self._rows_from_model(domaliases)) - - def parse_enabled(self, value): - return _("yes") if value else _("no") - -class MailboxesTable(tables.Table): - tableid = "objects_table" - idkey = "id" - - address = tables.Column("full_address", label=ugettext_noop("Address")) - name = tables.Column("user", label=ugettext_noop("Name")) - creation = tables.Column("creation", label=ugettext_noop("Created"), - width="160px") - modified = tables.Column("last_modification", label=ugettext_noop("Last modified"), - width="160px") - aliases = tables.Column("alias_count", label=ugettext_noop("Aliases"), width="70px", - align="center") - quota = tables.Column("quota", label=ugettext_noop("Quota"), - width="50px", align="center") - enabled = tables.Column("enabled", label=gender("Enabled", "f"), width="50px", - align="center") - actions = tables.ActionColumn("actions", label=ugettext_noop("Actions"), - width="70px", align="center", defvalue=mailbox_actions) - - cols_order = ["address", "name", "creation", "modified", "aliases", - "quota", "enabled", "actions"] - - def __init__(self, request, mailboxes): - super(MailboxesTable, self).__init__(request) - self.populate(self._rows_from_model(mailboxes)) - - def parse_quota(self, value): - return "%s %s" % (value, _("MB")) - - def parse_enabled(self, value): - return _("yes") if value else _("no") - - def parse_name(self, user): - return "%s %s" % (user.first_name, user.last_name) - -class MbaliasesTable(tables.Table): - tableid = "objects_table" - idkey = "id" - - address = tables.Column("full_address", label=ugettext_noop("Address")) - targets = tables.Column("targets", label=ugettext_noop("Target(s)"), safe=True) - creation = tables.Column("creation", label=ugettext_noop("Created"), width="170px") - modified = tables.Column("last_modification", label=ugettext_noop("Last modified"), - width="170px") - enabled = tables.Column("enabled", label=gender("Enabled", "m"), width="50px", - align="center") - actions = tables.ActionColumn("actions", label=ugettext_noop("Actions"), width="50px", - align="center", defvalue=mbalias_actions) - - cols_order = ["address", "targets", "creation", "modified", "enabled", "actions"] - - def __init__(self, request, aliases): - super(MbaliasesTable, self).__init__(request) - self.populate(self._rows_from_model(aliases)) - - def rowoptions(self, request, alias): - return "{ui_disabled : '%s'}" % alias.ui_disabled(request.user) - - def parse_enabled(self, value): - return _("yes") if value else _("no") + def row_class(self, request, domain): + if not domain.enabled: + return "muted" + return "" class ExtensionsTable(tables.Table): idkey = "id" @@ -133,43 +52,55 @@ class ExtensionsTable(tables.Table): cols_order = ["selection", "name", "version", "descr"] -class SuperAdminsTable(tables.Table): +class AccountsTable(tables.Table): idkey = "id" - selection = tables.SelectionColumn("selection", width="4%") - username = tables.Column("username", label=ugettext_noop("User name")) - first_name = tables.Column("first_name", label=ugettext_noop("First name")) - last_name = tables.Column("last_name", label=ugettext_noop("Last name")) - date_joined = tables.Column("date_joined", label=ugettext_noop("Defined")) - enabled = tables.Column("enabled", label=gender("Enabled", "m"), width="10%") - - cols_order = ["selection", "username", "first_name", "last_name", - "date_joined", "enabled"] - - def __init__(self, request, users): - super(SuperAdminsTable, self).__init__(request) - self.populate(self._rows_from_model(users)) - - def parse_enabled(self, value): - return _("yes") if value else _("no") - -class DomainAdminsTable(tables.Table): - idkey = "id" - selection = tables.SelectionColumn("selection", width="4%") username = tables.Column("username", label=ugettext_noop("User name")) first_name = tables.Column("first_name", label=ugettext_noop("First name")) last_name = tables.Column("last_name", label=ugettext_noop("Last name")) date_joined = tables.Column("date_joined", label=ugettext_noop("Defined")) enabled = tables.Column("is_active", label=gender("Enabled", "m"), width="10%") + role = tables.Column("group", label=ugettext_noop("Role")) actions = tables.ActionColumn("actions", label=ugettext_noop("Actions"), width="70px", align="center", - defvalue=domain_admin_actions) + defvalue=account_actions) - cols_order = ["selection", "username", "first_name", - "last_name", "date_joined", "enabled", "actions"] + cols_order = ["username", "first_name", "last_name", "date_joined", + "role", "actions"] def __init__(self, request, users): - super(DomainAdminsTable, self).__init__(request) + super(AccountsTable, self).__init__(request) self.populate(self._rows_from_model(users)) def parse_is_active(self, value): return _("yes") if value else _("no") + + def parse_group(self, value): + if value == "SimpleUsers": + return "U" + return value[:1] + + def row_class(self, request, obj): + if not obj.is_active: + return "muted" + return "" + +class IdentitiesTable(tables.Table): + idkey = "id" + identity = tables.Column("identity", label=ugettext_noop("Email/Username")) + name_or_rcpt = tables.Column("name_or_rcpt", label=ugettext_noop("Fullname/Recipient")) + actions = tables.ActionColumn("actions", label=ugettext_noop("Actions"), + width="70px", align="center", + defvalue=identity_actions) + + cols_order = ["identity", "name_or_rcpt", "actions"] + + def __init__(self, request, identities): + super(IdentitiesTable, self).__init__(request) + self.populate(self._rows_from_model(identities, True)) + + def row_class(self, request, obj): + if obj.__class__.__name__ == "User" and not obj.is_active: + return "muted" + if not obj.enabled: + return "muted" + return "" diff --git a/admin/templatetags/admin_extras.py b/admin/templatetags/admin_extras.py index e71e665cf..c13daa964 100644 --- a/admin/templatetags/admin_extras.py +++ b/admin/templatetags/admin_extras.py @@ -13,15 +13,7 @@ genders = { } @register.simple_tag -def admin_menu(user): - # entries = [ - # {"name" : "admin", - # "img" : static_url('pics/admin.png'), - # "label" : _("Admin"), - # "class" : "topdropdown", - # "menu" : [] - # } - # ] +def admin_menu(selection, user): entries = [] if user.has_perm("admin.view_domains"): entries += [ @@ -30,21 +22,14 @@ def admin_menu(user): "label" : _("Domains"), "img" : static_url("pics/domains.png")} ] - elif user.has_perm("admin.view_mailboxes"): - entries += [ - {"name" : "mailboxes", - "url" : reverse(admin.views.domains), - "label" : _("Mailboxes"), - "img" : static_url("pics/mailbox.png")} - ] entries += \ events.raiseQueryEvent("AdminMenuDisplay", "top_menu", user) - if user.has_perm("auth.view_permissions"): + if user.has_perm("auth.add_user") or user.has_perm("admin.add_mailbox"): entries += [ - {"name" : "permissions", - "url" : reverse(admin.views.permissions), - "label" : _("Accounts"), - "img" : static_url("pics/permissions.png")} + {"name" : "identities", + "url" : reverse(admin.views.identities), + "label" : _("Identities")}, + ] if user.is_superuser: entries += [ @@ -57,193 +42,127 @@ def admin_menu(user): if not len(entries): return "" return render_to_string("common/menulist.html", - {"entries" : entries, "user" : user}) + {"entries" : entries, "selection" : selection, "user" : user}) @register.simple_tag def settings_menu(selection, user): entries = [ - {"name" : "parameters", - "url" : reverse(admin.views.viewparameters), - "img" : static_url("pics/domains.png"), - "label" : _("Parameters")}, {"name" : "extensions", "url" : reverse(admin.views.viewextensions), "label" : _("Extensions"), "img" : static_url("pics/extensions.png")}, + {"name" : "parameters", + "url" : reverse(admin.views.viewparameters), + "img" : static_url("pics/domains.png"), + "label" : _("Parameters")}, ] - return render_to_string('common/menu.html', - {"selection" : selection, "entries" : entries, - "user" : user}) - -def dommenu_entry(name, label, img, listurl, newurl): - result = {"name" : name, - "label" : label, - "img" : img, - "url" : listurl, - "class" : "menubardropdown", - } - return result + return render_to_string('common/menu.html', { + "entries" : entries, + "css" : "nav nav-list", + "selection" : selection, + "user" : user + }) @register.simple_tag -def domains_menu(selection, user, domid): - args = domid != "" and ("?domid=%s" % domid) or "" - entries = [] - if selection == "stats": - disabled = True - else: - disabled = False - - entries += [ - {"name" : "actions", - "label" : _("Actions"), - "img" : static_url("pics/tools.png"), - "class" : "menubardropdown", - "menu" : [ - {"name" : "new", - "label" : _("New"), - "img" : static_url("pics/add.png"), - "disabled" : disabled}, - {"name" : "remove", - "label" : _("Remove"), - "img" : static_url("pics/remove.png"), - "disabled" : disabled - }, - {"name" : "import", - "label" : _("Import data"), - "img" : static_url("pics/import.png"), - "class" : "boxed", - "rel" : "{handler:'iframe',size:{x:370,y:250}}", - "url" : reverse(admin.views.importdata)} - ] - }, - ] - - entries += [{"separator" : True}] - if user.has_perm("admin.view_domains"): - entries += dommenu_entry("domains", _("Domains"), - static_url("pics/domains.png"), - reverse(admin.views.domains), - reverse(admin.views.newdomain)), - entries += [ - dommenu_entry("domaliases", _("Domain aliases"), static_url("pics/alias.png"), - reverse(admin.views.domaliases) + args, - reverse(admin.views.newdomalias) + args), - dommenu_entry("mailboxes", _("Mailboxes"), static_url("pics/mailbox.png"), - reverse(admin.views.mailboxes) + args, - reverse(admin.views.newmailbox) + args), - dommenu_entry("mbaliases", _("Mailbox aliases"), static_url("pics/alias.png"), - reverse(admin.views.mbaliases) + args, - reverse(admin.views.newmbalias) + args), - ] - entries += events.raiseQueryEvent("AdminMenuDisplay", "admin_menu_box", user) - return render_to_string('common/menu.html', - {"entries" : entries, - "selection" : selection, - "user" : user}) - -@register.simple_tag -def permissions_menu(user): +def domains_menu(selection, user): entries = [ - {"name" : "addperm", - "label" : _("Add permission"), - "img" : static_url("pics/add.png"), - "url" : reverse(admin.views.add_permission)}, - {"name" : "delperms", - "label" : _("Remove permissions"), - "img" : static_url("pics/remove.png"), - "url" : reverse(admin.views.delete_permissions)}, + {"name" : "newdomain", + "label" : _("New domain"), + "img" : "icon-plus", + "modal" : True, + "modalcb" : "domainform_cb", + "url" : reverse("modoboa.admin.views.newdomain")}, + {"name" : "import", + "label" : _("Import"), + "img" : "icon-folder-open", + "url" : reverse(admin.views.import_domains), + "modal" : True, + "modalcb" : "importform_cb"} ] - return render_to_string('common/menu.html', - {"entries" : entries, "user" : user}) + return render_to_string('common/menu.html', { + "entries" : entries, + "css" : "nav nav-list", + "selection" : selection, + "user" : user + }) + +@register.simple_tag +def identities_menu(user): + entries = [ + {"name" : "newaccount", + "label" : _("New account"), + "img" : "icon-plus", + "modal" : True, + "modalcb" : "newaccount_cb", + "url" : reverse(admin.views.newaccount)}, + {"name" : "newdlist", + "label" : _("New distribution list"), + "img" : "icon-plus", + "modal" : True, + "modalcb" : "dlistform_cb", + "url" : reverse(admin.views.newdlist)}, + {"name" : "import", + "label" : _("Import"), + "img" : "icon-folder-open", + "url" : reverse(admin.views.import_identities), + "modal" : True, + "modalcb" : "importform_cb"} + ] + + return render_to_string('common/menu.html', { + "entries" : entries, + "css" : "nav nav-list", + "user" : user + }) @register.simple_tag def domain_actions(user, domid): actions = [ {"name" : "editdomain", "url" : reverse(admin.views.editdomain, args=[domid]), - "img" : static_url("pics/edit.png"), + "img" : "icon-edit", "title" : _("Edit domain"), - "class" : "boxed", - "rel" : "{handler:'iframe',size:{x:310,y:190}}"}, - {"name" : "domaliases", - "url" : reverse(admin.views.domaliases) + "?domid=%s" % domid, - "img" : static_url("pics/alias.png"), - "title" : _("View aliases of this domain")} + "modal" : True, + "modalcb" : "domainform_cb"}, + {"name" : "deldomain", + "url" : reverse(admin.views.deldomain) + "?selection=%s" % domid, + "img" : "icon-remove"}, ] - if user.has_perm('admin.view_domains'): - actions += [ - {"name" : "viewadmins", - "url" : reverse(admin.views.view_domain_admins, args=[domid]), - "img" : static_url("pics/administrators.png"), - "title" : _("View administrators of this domain"), - "class" : "boxed", - "rel" : "{handler:'iframe',size:{x:310,y:190}}"}, - ] + return render_actions(actions) @register.simple_tag -def domalias_actions(user, aid): +def account_actions(user, accountid): actions = [ - {"name" : "editalias", - "url" : reverse(admin.views.editdomalias, args=[aid]), - "img" : static_url("pics/edit.png"), - "title" : _("Edit alias"), - "class" : "boxed", - "rel" : "{handler:'iframe',size:{x:310,y:190}}"} + {"name" : "editaccount", + "url" : reverse(admin.views.editaccount, args=[accountid]), + "img" : "icon-edit", + "modal" : True, + "modalcb" : "editaccount_cb", + "title" : _("Edit account")}, + {"name" : "delaccount", + "url" : reverse(admin.views.delaccount) + "?selection=%s" % accountid, + "img" : "icon-remove", + "title" : _("Delete this account")}, ] return render_actions(actions) @register.simple_tag -def mailbox_actions(user, mboxid): +def identity_actions(user, iid): + name, objid = iid.split(':') + if name == "User": + return account_actions(user, objid) actions = [ - {"name" : "editmailbox", - "url" : reverse(admin.views.editmailbox, args=[mboxid]), - "img" : static_url("pics/edit.png"), - "title" : _("Edit mailbox"), - "class" : "boxed", - "rel" : "{handler:'iframe',size:{x:310,y:320}}"}, - {"name" : "aliases", - "url" : reverse(admin.views.mbaliases) + "?mbid=%d" % mboxid, - "img" : static_url("pics/alias.png"), - "title" : _("View this mailbox aliases")}, - ] - return render_actions(actions) - -@register.simple_tag -def domain_admin_actions(user, daid): - from modoboa.admin.models import User - - actions = [ - {"name" : "editdomainadmin", - "url" : reverse(admin.views.edit_domain_admin, args=[daid]), - "img" : static_url("pics/edit.png"), - "title" : _("Edit domain admin"), - "class" : "boxed", - "rel" : "{handler:'iframe',size:{x:350,y:320}}"}, - {"name" : "assigndomains", - "url" : reverse(admin.views.assign_domains_to_admin, args=[daid]), - "img" : static_url("pics/domains.png"), - "title" : _("Assign domain(s) to this administrator"), - "class" : "boxed", - "rel" : "{handler:'iframe',size:{x:350,y:320}}"}, - ] - actions += events.raiseQueryEvent("DomainAdminActions", User.objects.get(pk=daid)) - return render_actions(actions) - -@register.simple_tag -def mbalias_actions(user, aliasid): - from modoboa.admin.models import Alias - - alias = Alias.objects.get(pk=aliasid) - if alias.ui_disabled(user): - return "--" - actions = [ - {"name" : "editmbalias", - "url" : reverse(admin.views.editmbalias, args=[aliasid]), - "img" : static_url("pics/edit.png"), - "title" : _("Edit mailbox alias"), - "class" : "boxed", - "rel" : "{handler:'iframe',size:{x:320,y:300}}"} + {"name" : "editdlist", + "url" : reverse(admin.views.editdlist, args=[objid]), + "img" : "icon-edit", + "modal" : True, + "modalcb" : "dlistform_cb", + "title" : _("Edit distribution list")}, + {"name" : "deldlist", + "url" : reverse(admin.views.deldlist) + "?selection=%s" % objid, + "img" : "icon-remove", + "title" : _("Delete this distribution list")}, ] return render_actions(actions) @@ -255,9 +174,11 @@ def loadadminextmenu(user): @register.simple_tag def param(app, definition): - result = """
- """ % (definition.has_key("label") \ - and _(definition["label"]) or definition["name"]) + result = """
+ +
""" \ + % (definition.has_key("label") and _(definition["label"]) + or definition["name"]) name = "%s.%s" % (app, definition["name"]) value = definition.has_key("value") \ and definition["value"] or definition["deflt"] @@ -290,10 +211,14 @@ def param(app, definition): if definition.has_key("help"): result += """ - - - """ % (_(definition["help"]), _("Help"), static_url("pics/info.png")) +

%s

+""" % definition["help"] + # result += """ + # + # + # """ % (_(definition["help"]), _("Help"), static_url("pics/info.png")) result += """ +
""" return result diff --git a/admin/urls.py b/admin/urls.py index a47f8a591..5586301a2 100644 --- a/admin/urls.py +++ b/admin/urls.py @@ -4,40 +4,31 @@ urlpatterns = patterns( 'modoboa.admin.views', url(r'^$', 'domains', name="index"), url(r'^domains/$', 'domains', name="domains"), + (r'^domains/list/$', 'domains_list'), (r'^domains/new/', 'newdomain'), (r'^domains/(?P\d+)/edit/$', 'editdomain'), (r'^domains/delete/$', 'deldomain'), - (r'^domains/import/$', 'importdata'), + (r'^domains/import/$', 'import_domains'), - (r'^domaliases/$', "domaliases"), - (r'^domaliases/new/', 'newdomalias'), - (r'^domaliases/edit/(?P\d+)/', 'editdomalias'), - (r'^domaliases/delete/$', 'deldomalias'), - - (r'^mailboxes/$', 'mailboxes'), - (r'^mailboxes/new/$', 'newmailbox'), - (r'^mailboxes/edit/(?P\d+)/$', 'editmailbox'), - (r'^mailboxes/delete/$', 'delmailbox'), (r'^mailboxes/search/$', 'mailboxes_search'), - (r'^mbaliases/$', 'mbaliases'), - (r'^mbaliases/new/$', 'newmbalias'), - (r'^mbaliases/edit/(?P\d+)/$', 'editmbalias'), - (r'^mbaliases/delete/$', 'delmbalias'), - (r'^settings/$', 'viewparameters'), (r'^settings/parameters/$', 'viewparameters'), (r'^settings/parameters/save/$', 'saveparameters'), (r'^settings/extensions/$', 'viewextensions'), (r'^settings/extensions/save/$', 'saveextensions'), - (r'^permissions/$', 'permissions'), - (r'^permissions/add/$', 'add_permission'), - (r'^permissions/domainadmin/new/$', 'create_domain_admin'), - (r'^permissions/domainadmin/edit/(?P\d+)/$', 'edit_domain_admin'), - (r'^permissions/domainadmin/promote/$', 'domain_admin_promotion'), - (r'^permissions/domainadmin/assign/(?P\d+)/$', 'assign_domains_to_admin'), - (r'^permissions/domainadmins/(?P\d+)/$', 'view_domain_admins'), (r'^permissions/searchaccount/$', 'search_account'), - (r'^permissions/delete/$', 'delete_permissions'), + + (r'^identities/$', 'identities'), + (r'^identities/import/$', 'import_identities'), + + (r'^accounts/list/$', 'accounts_list'), + (r'^accounts/new/$', 'newaccount'), + (r'^accounts/edit/(?P\d+)/$', 'editaccount'), + (r'^accounts/delete/$', 'delaccount'), + + (r'^distriblists/new/$', 'newdlist'), + (r'^distriblists/edit/(?P\d+)/$', 'editdlist'), + (r'^distriblists/delete/$', 'deldlist'), ) diff --git a/admin/views.py b/admin/views.py index 925d0e681..942e22059 100644 --- a/admin/views.py +++ b/admin/views.py @@ -1,61 +1,61 @@ -# -*- coding: utf-8 -*- +# coding: utf-8 from django.http import HttpResponse, HttpResponseRedirect -from django.utils import simplejson from django.utils.translation import ugettext as _, ungettext from django.core.urlresolvers import reverse from django.contrib.auth.decorators \ import login_required, permission_required, user_passes_test from django.db.models import Q -from django.contrib.auth.models import Group from django.contrib import messages -from django.db import transaction, IntegrityError, transaction +from django.db import transaction, IntegrityError -from lib import * +from lib import render_listing +from forms import * from modoboa import admin, userprefs -from models import * -from modoboa.admin.permissions import * +from models import * from modoboa.admin.tables import * from modoboa.lib import events, parameters from modoboa.lib.exceptions import * from modoboa.lib.webutils \ - import _render, ajax_response, ajax_simple_response, getctx + import _render, ajax_response, ajax_simple_response from modoboa.lib.emailutils import split_mailbox from modoboa.lib.models import Parameter -import copy +from modoboa.lib.permissions import * @login_required def domains(request): if not request.user.has_perm("admin.view_domains"): if request.user.has_perm("admin.view_mailboxes"): - return HttpResponseRedirect(reverse(mailboxes)) + return HttpResponseRedirect(reverse(identities)) return HttpResponseRedirect(reverse(userprefs.views.preferences)) domains = request.user.get_domains() for dom in domains: dom.mbalias_counter = len(Alias.objects.filter(domain=dom.id)) - deloptions = {"keepdir" : _("Do not delete domain directory")} return render_listing(request, "domains", "admin/domains.html", - title=_("Available domains"), - objects=domains, - rel="310 190", - deloptions=deloptions) + objects=domains) -def _validate_domain(request, form, successmsg, commonctx, - callback=None, tplname="admin/adminform.html"): - """Domain validation +@login_required +@permission_required("admin.view_domains") +def domains_list(request): + doms = map(lambda dom: dom.name, request.user.get_domains()) + return ajax_simple_response(doms) + +def _validate_domain(request, form, successmsg, commonctx, tplname, + callback=None): + """Domain form validation Common function shared between creation and modification actions. """ error = None if form.is_valid(): try: - domain = form.save() + domain = form.save(request.user) except AdminError, e: error = str(e) except IntegrityError: - error = _("Domain with this name already defined") + error = _("An alias with this name already exists") else: if callback is not None: callback(request.user, domain) @@ -69,7 +69,7 @@ def _validate_domain(request, form, successmsg, commonctx, @login_required @permission_required("admin.add_domain") @transaction.commit_on_success -def newdomain(request, tplname="admin/adminform.html"): +def newdomain(request): events.raiseEvent("CanCreate", request.user, "domains") def newdomain_cb(user, domain): grant_access_to_object(user, domain, is_owner=True) @@ -82,15 +82,15 @@ def newdomain(request, tplname="admin/adminform.html"): if request.method == "POST": form = DomainForm(request.POST) return _validate_domain(request, form, _("Domain created"), commonctx, - newdomain_cb) - + "admin/domainform.html", + callback=newdomain_cb) form = DomainForm() commonctx["form"] = form - return _render(request, tplname, commonctx) + return _render(request, "common/generic_modal_form.html", commonctx) @login_required @permission_required("admin.change_domain") -def editdomain(request, dom_id, tplname="admin/adminform.html"): +def editdomain(request, dom_id, tplname="admin/domainform.html"): domain = Domain.objects.get(pk=dom_id) if not request.user.is_owner(domain): raise PermDeniedException(_("You can't edit this domain")) @@ -101,7 +101,7 @@ def editdomain(request, dom_id, tplname="admin/adminform.html"): "formid" : "domform"} if request.method == "POST": form = DomainForm(request.POST, instance=domain) - return _validate_domain(request, form, _("Domain modified"), commonctx) + return _validate_domain(request, form, _("Domain modified"), commonctx, tplname) form = DomainForm(instance=domain) commonctx["form"] = form @@ -120,11 +120,13 @@ def deldomain(request): mb = None for dom in Domain.objects.filter(id__in=selection): - if not request.user.is_owner(dom): + if not request.user.can_access(dom): raise PermDeniedException(_("You can't delete this domain")) if mb and mb.domain == dom: raise AdminError(_("You can't delete your own domain")) + events.raiseEvent("DomainAliasDeleted", dom.domainalias_set.all()) + events.raiseEvent("DeleteMailbox", dom.mailbox_set.all()) events.raiseEvent("DeleteDomain", dom) ungrant_access_to_object(dom) dom.delete(keepdir=keepdir) @@ -133,126 +135,6 @@ def deldomain(request): messages.info(request, msg, fail_silently=True) return ajax_response(request) -@login_required -@permission_required("admin.view_domaliases") -@check_domain_access -def domaliases(request): - if request.GET.has_key("domid"): - domain = Domain.objects.get(pk=request.GET["domid"]) - domaliases = DomainAlias.objects.filter(target=request.GET["domid"]) - else: - domain = None - domaliases = request.user.get_domaliases() - return render_listing(request, "domaliases", - title=_("Domain aliases"), - rel="310 190", - objects=domaliases) - -@login_required -@permission_required("admin.add_domainalias") -@transaction.commit_on_success -def newdomalias(request, tplname="admin/adminform.html"): - events.raiseEvent("CanCreate", request.user, "domain_aliases") - commonctx = {"title" : _("New domain alias"), - "submit_label" : _("Create"), - "action" : reverse(newdomalias), - "formid" : "domaliasform"} - if request.method == "POST": - form = DomainAliasForm(request.user, request.POST) - error = None - if form.is_valid(): - domalias = form.save(commit=False) - if Domain.objects.filter(name=domalias.name): - error = _("A domain with this name already exists") - else: - try: - domalias.save() - except IntegrityError: - error = _("Alias with this name already exists") - else: - grant_access_to_object(request.user, domalias, is_owner=True) - events.raiseEvent("DomainAliasCreated", request.user, domalias) - messages.info(request, _("Domain alias created"), fail_silently=True) - return ajax_response(request, url=reverse(admin.views.domaliases)) - - commonctx["form"] = form - commonctx["error"] = error - return ajax_response(request, status="ko", template=tplname, **commonctx) - - form = DomainAliasForm(request.user) - if request.GET.has_key("domid"): - form.fields["target"].initial = request.GET["domid"] - commonctx["form"] = form - return _render(request, tplname, commonctx) - -@login_required -@permission_required("admin.change_domainalias") -def editdomalias(request, alias_id, tplname="admin/adminform.html"): - domalias = DomainAlias.objects.get(pk=alias_id) - if not request.user.is_owner(domalias.target): - raise PermDeniedException(_("You do not have access to this domain")) - - commonctx = {"title" : _("Domain alias editing"), - "submit_label" : _("Update"), - "action" : reverse(editdomalias, args=[alias_id]), - "formid" : "domaliasform"} - if request.method == "POST": - form = DomainAliasForm(request.user, request.POST, instance=domalias) - error = None - if form.is_valid(): - ndomalias = form.save(commit=False) - if Domain.objects.filter(name=ndomalias.name): - error = _("A domain with this name already exists") - else: - try: - ndomalias.save() - except IntegrityError: - error = _("Alias with this name already exists") - else: - messages.info(request, _("Domain alias updated"), fail_silently=True) - return ajax_response(request, url=reverse(admin.views.domaliases)) - commonctx["form"] = form - commonctx["error"] = error - return ajax_response(request, status="ko", template=tplname, **commonctx) - - form = DomainAliasForm(request.user, instance=domalias) - commonctx["form"] = form - return _render(request, tplname, commonctx) - -@login_required -@permission_required("admin.delete_domainalias") -@transaction.commit_on_success -def deldomalias(request): - selection = request.GET["selection"].split(",") - for daid in selection: - domalias = DomainAlias.objects.get(pk=daid) - if not request.user.is_owner(domalias.target): - raise PermDeniedException(_("You don't have acces to this domain")) - events.raiseEvent("DomainAliasDeleted", domalias) - ungrant_access_to_object(domalias) - domalias.delete() - msg = ungettext("Domain alias deleted", "Domain aliases deleted", len(selection)) - messages.info(request, msg, fail_silently=True) - return ajax_response(request) - -@login_required -@permission_required("admin.view_mailboxes") -@check_domain_access -def mailboxes(request): - title = _("Available mailboxes") - if request.GET.has_key("domid"): - domain = Domain.objects.get(pk=request.GET["domid"]) - mboxes = Mailbox.objects.filter(domain=domain.id) - title += " (%s)" % domain.name - else: - mboxes = request.user.get_mailboxes() - domain = None - deloptions = {"keepdir" : _("Do not delete mailbox directory")} - return render_listing(request, "mailboxes", - title=title, - rel="310 320", - objects=mboxes, domain=domain, deloptions=deloptions) - @login_required @permission_required("admin.view_mailboxes") @check_domain_access @@ -271,135 +153,14 @@ def mailboxes_search(request): result = map(lambda mb: mb.full_address, mboxes) return ajax_simple_response(result) -@login_required -@permission_required("admin.add_mailbox") -@transaction.commit_on_success -def newmailbox(request, tplname="admin/adminform.html"): - events.raiseEvent("CanCreate", request.user, "mailboxes") - commonctx = {"title" : _("New mailbox"), - "submit_label" : _("Create"), - "action" : reverse(newmailbox), - "formid" : "mboxform"} - if request.method == "POST": - form = MailboxWithPasswordForm(request.user, request.POST) - error = None - if form.is_valid(): - if not request.user.is_superuser: - if not request.user.can_access(form.cleaned_data["domain"]): - raise PermDeniedException(_("You do not have access to this domain")) - - try: - mb = form.save() - except AdminError as e: - error = str(e) - else: - grant_access_to_object(request.user, mb, is_owner=True) - grant_access_to_object(request.user, mb.user, is_owner=True) - events.raiseEvent("CreateMailbox", request.user, mb) - messages.info(request, _("Mailbox created"), - fail_silently=True) - return ajax_response(request, url=reverse(admin.views.mailboxes) + "?domid=%d" % mb.domain.id) - - commonctx["form"] = form - commonctx["error"] = error - return ajax_response(request, status="ko", template=tplname, **commonctx) - - form = MailboxWithPasswordForm(request.user) - commonctx["form"] = form - return _render(request, tplname, commonctx) - -@login_required -@permission_required("admin.change_mailbox") -def editmailbox(request, mbox_id=None, tplname="admin/adminform.html"): - mb = Mailbox.objects.get(pk=mbox_id) - if not request.user.can_access(mb.domain): - raise PermDeniedException(_("You do not have access to this domain")) - - commonctx = {"title" : _("Mailbox editing"), - "submit_label" : _("Create"), - "action" : reverse(editmailbox, args=[mbox_id]), - "formid" : "mboxform"} - if request.method == "POST": - if request.POST.get("password1", "") != "" or \ - request.POST.get("password2", "") != "": - formclass = MailboxWithPasswordForm - else: - formclass = MailboxForm - oldmb = copy.deepcopy(mb) - form = formclass(request.user, request.POST, instance=mb) - error = None - if form.is_valid(): - try: - mb = form.save(commit=False) - except AdminError, inst: - error = str(inst) - else: - if oldmb.rename_dir(mb.domain.name, mb.address): - form.commit_save(mb) - events.raiseEvent("ModifyMailbox", mb, oldmb) - messages.info(request, _("Mailbox modified"), - fail_silently=True) - return ajax_response(request, url=reverse(admin.views.mailboxes) + "?domid=%d" % mb.domain.id) - error = _("Failed to rename mailbox, check permissions") - - commonctx["form"] = form - commonctx["error"] = error - return ajax_response(request, status="ko", template=tplname, **commonctx) - - form = MailboxWithPasswordForm(request.user, instance=mb) - commonctx["form"] = form - return _render(request, tplname, commonctx) - -@login_required -@permission_required("admin.delete_mailbox") -@transaction.commit_on_success -def delmailbox(request): - selection = request.GET["selection"].split(",") - error = None - keepdir = True if request.GET.get("keepdir", "false") == "true" else False - usermb = request.user.mailbox_set.all()[0] \ - if len(request.user.mailbox_set.all()) else None - - for mb in Mailbox.objects.filter(id__in=selection): - if not request.user.can_access(mb.domain): - raise PermDeniedException(_("You don't have access to this domain")) - if usermb and usermb == mb: - raise AdminError(_("You can't delete your own mailbox")) - events.raiseEvent("DeleteMailbox", mb) - ungrant_access_to_object(mb) - ungrant_access_to_object(mb.user) - mb.delete(keepdir=keepdir) - - msg = ungettext("Mailbox deleted", "Mailboxes deleted", len(selection)) - messages.info(request, msg, fail_silently=True) - return ajax_response(request) - -@login_required -@permission_required("admin.view_aliases") -@check_domain_access -def mbaliases(request): - if request.GET.has_key("domid"): - aliases = Alias.objects.filter(domain=request.GET["domid"]) - elif request.GET.has_key("mbid"): - aliases = Alias.objects.filter(mboxes__id=request.GET["mbid"]).distinct() - else: - aliases = request.user.get_mbaliases() - - return render_listing(request, "mbaliases", - title=_("Mailbox aliases"), - rel="320 300", - objects=aliases) - -def _validate_mbalias(request, form, successmsg, tplname, commonctx, callback=None): - """Mailbox alias validation +def _validate_dlist(request, form, successmsg, tplname, commonctx, callback=None): + """Distribution list validation Common function shared between creation and modification actions. """ error = None if form.is_valid(): - if not request.POST.has_key("targets"): - raise AdminError(_("No target defined")) - form.set_targets(request.user, request.POST.getlist("targets")) + form.set_recipients(request.user) try: alias = form.save() except IntegrityError: @@ -408,8 +169,8 @@ def _validate_mbalias(request, form, successmsg, tplname, commonctx, callback=No if callback: callback(request.user, alias) - messages.info(request, successmsg, fail_silently=True) - return ajax_response(request, url=reverse(admin.views.mbaliases)) + messages.info(request, successmsg) + return ajax_response(request, url=reverse(admin.views.identities)) if request.POST.has_key("targets"): targets = request.POST.getlist("targets") @@ -422,218 +183,184 @@ def _validate_mbalias(request, form, successmsg, tplname, commonctx, callback=No @login_required @permission_required("admin.add_alias") @transaction.commit_on_success -def newmbalias(request, tplname="admin/mbaliasform.html"): +def newdlist(request, tplname="common/generic_modal_form.html"): events.raiseEvent("CanCreate", request.user, "mailbox_aliases") - commonctx = {"title" : _("New mailbox alias"), + commonctx = {"title" : _("New distribution list"), "submit_label" : _("Create"), - "action" : reverse(newmbalias), - "formid" : "mbaliasform"} + "action" : reverse(newdlist), + "formid" : "dlistform"} + if request.method == "POST": - def callback(user, mbalias): - grant_access_to_object(user, mbalias, is_owner=True) - events.raiseEvent("MailboxAliasCreated", user, mbalias) + def callback(user, dlist): + grant_access_to_object(user, dlist, is_owner=True) + events.raiseEvent("MailboxAliasCreated", user, dlist) - form = AliasForm(request.user, request.POST) - return _validate_mbalias(request, form, _("Mailbox alias created"), - tplname, commonctx, callback) + form = DlistForm(request.POST) + return _validate_dlist(request, form, _("Distribution list created"), + tplname, commonctx, callback) - form = AliasForm(request.user) + form = DlistForm() commonctx["form"] = form - commonctx["noerrors"] = True - return _render(request, tplname, commonctx) + return _render(request, "common/generic_modal_form.html", commonctx) @login_required @permission_required("admin.change_alias") -def editmbalias(request, alias_id, tplname="admin/mbaliasform.html"): - alias = Alias.objects.get(pk=alias_id) - if not request.user.is_owner(alias.domain): +def editdlist(request, dlist_id, tplname="admin/dlistform.html"): + dlist = Alias.objects.get(pk=dlist_id) + if not request.user.is_owner(dlist.domain): raise PermDeniedException(_("You do not have access to this domain")) - commonctx = {"title" : _("Mailbox alias editing"), + commonctx = {"title" : dlist.full_address, "submit_label" : _("Update"), - "action" : reverse(editmbalias, args=[alias.id]), - "formid" : "mbaliasform"} + "action" : reverse(editdlist, args=[dlist.id]), + "formid" : "dlistform"} if request.method == "POST": - form = AliasForm(request.user, request.POST, instance=alias) - return _validate_mbalias(request, form, _("Mailbox alias modified"), - tplname, commonctx) + form = DlistForm(request.POST, instance=dlist) + return _validate_dlist(request, form, _("Distribution list modified"), + tplname, commonctx) - form = AliasForm(request.user, instance=alias) + form = DlistForm(instance=dlist) commonctx["form"] = form - commonctx["targets"] = alias.get_targets() return _render(request, tplname, commonctx) @login_required @permission_required("admin.delete_alias") @transaction.commit_on_success -def delmbalias(request): +def deldlist(request): selection = request.GET["selection"].split(",") - for mbaid in selection: - mbalias = Alias.objects.get(pk=mbaid) - if not request.user.is_owner(mbalias.domain): + for dlist_id in selection: + dlist = Alias.objects.get(pk=dlist_id) + if not request.user.is_owner(dlist.domain): raise PermDeniedException(_("You do not have access to this domain")) - events.raiseEvent("MailboxAliasDeleted", mbalias) - ungrant_access_to_object(mbalias) - mbalias.delete() + events.raiseEvent("MailboxAliasDeleted", dlist) + ungrant_access_to_object(dlist) + dlist.delete() - msg = ungettext("Alias deleted", "Aliases deleted", len(selection)) - messages.info(request, msg, fail_silently=True) + msg = ungettext("Distribution list deleted", + "Distribution lists deleted", len(selection)) + messages.info(request, msg) return ajax_response(request) @login_required -@permission_required("auth.view_permissions") -def permissions(request, tplname='admin/permissions.html'): - permtables = [] - if request.user.is_superuser: - permtables += [ - {"id" : "super_admins", - "title" : _("Super administrators"), - "rel" : "300 140", - "content" : SuperAdminsPerms().get(request)} - ] - - permtables += events.raiseQueryEvent("PermsGetTables", request) - - permtables += [ - {"id" : "domain_admins", - "title" : _("Domain administrators"), - "rel" : "350 100", - "content" : DomainAdminsPerms().get(request)} - ] - - return _render(request, tplname, { - "permtables" : permtables - }) +@user_passes_test(lambda u: u.has_perm("auth.add_user") or u.has_perm("admin.add_mailbox")) +def identities(request, tplname='admin/identities.html'): + accounts_list = [] + for account in User.objects.all(): + if request.user.can_access(account) or \ + account.has_mailbox and request.user.can_access(account.mailbox_set.all()[0].domain): + accounts_list += [account] + for mbalias in Alias.objects.all(): + if len(mbalias.get_recipients()) > 2: + accounts_list += [mbalias] + return render_listing(request, "identities", tplname, objects=accounts_list) @login_required -@permission_required("auth.view_permissions") -def add_permission(request): - role = request.method == "GET" and request.GET["role"] or request.POST["role"] - pclass = get_perms_class(request.user, role) - if pclass is None: - messages.error(request, _("Permission denied!"), - fail_silently=True) - else: - events.raiseEvent("CanCreate", request.user, role) - pobj = pclass() - if request.method == "GET": - return pobj.get_add_form(request) - status, data = pobj.add(request) - if not status: - return HttpResponse(simplejson.dumps(data), mimetype="application/json") - try: - msg = pobj.add_success_msg - except AttributeError: - msg = _("Permission added") - messages.info(request, msg, fail_silently=True) - return ajax_response(request, url=reverse(admin.views.permissions)) +def accounts_list(request): + accs = User.objects.filter(is_superuser=False).exclude(groups__name='SimpleUsers') + res = map(lambda a: a.username, accs.all()) + return ajax_simple_response(res) @login_required -@permission_required("auth.view_permissions") -def create_domain_admin(request, tplname="admin/create_domain_admin.html"): - events.raiseEvent("CanCreate", request.user, "domain_admins") - if request.method == "POST": - form = UserWithPasswordForm(request.POST) - if form.is_valid(): - user = form.save(group="DomainAdmins") - grant_access_to_object(request.user, user, is_owner=True) - events.raiseEvent("DomainAdminCreated", user) - messages.info(request, _("Domain admin created"), fail_silently=True) - return ajax_response(request, url=reverse(admin.views.permissions)) - - return ajax_response(request, status="ko", template=tplname, form=form) - - form = UserWithPasswordForm() - return ajax_response(request, template=tplname, form=form) - -@login_required -@permission_required("auth.view_permissions") -def edit_domain_admin(request, da_id, tplname="admin/adminform.html"): - ctx = dict(title=_("Edit domain admin"), - action=reverse(edit_domain_admin, args=[da_id]), - formid="eda_form", - submit_label=_("Update")) - u = User.objects.get(pk=da_id, groups__name="DomainAdmins") - if request.method == "POST": - if request.POST.get("password1", "") != "" and \ - request.POST.get("password2", "") != "": - form = UserWithPasswordForm(request.POST, instance=u) - else: - form = UserForm(request.POST, instance=u) - if form.is_valid(): - form.save() - if form.cleaned_data["createmb"]: - pass - messages.info(request, _("Domain admin updated"), fail_silently=True) - return ajax_response(request, - url=reverse("modoboa.admin.views.permissions")) - ctx.update(form=form) - return ajax_response(request, status="ko", template=tplname, **ctx) - - form = UserWithPasswordForm(instance=u) - ctx.update(form=form) - return _render(request, tplname, ctx) - -@login_required -@permission_required("auth.view_permissions") -def domain_admin_promotion(request, tplname="admin/domain_admin_promotion.html"): - if request.method == "POST": - form = DomainAdminPromotionForm(request.POST) - if form.is_valid(): - u = User.objects.get(username=form.cleaned_data["name"]) - u.groups.add(Group.objects.get(name="DomainAdmins")) - u.save() - if not request.user.can_access(u): - # The only case where we need to grant access to the - # object is when ``u`` has been create by a super - # admin - grant_access_to_object(request.user, u, is_owner=True) - events.raiseEvent("DomainAdminCreated", u) - messages.info(request, _("Domain admin added"), fail_silently=True) - return ajax_response(request, url=reverse(admin.views.permissions)) - - return ajax_response(request, status="ko", template=tplname, form=form) - - form = DomainAdminPromotionForm() - return ajax_response(request, template=tplname, form=form) - -@login_required -@permission_required("auth.view_permissions") @transaction.commit_on_success -def assign_domains_to_admin(request, da_id, tplname="admin/adminform.html"): - da = User.objects.get(pk=da_id) - ctx = dict(title=_("Assign domains to %s" % da.username), - action=reverse(assign_domains_to_admin, args=[da_id]), - formid="assignform", - submit_label=_("Assign")) +def newaccount(request, tplname='admin/newaccount.html'): + from modoboa.lib.formutils import CreationWizard + + def create_account(steps): + """Account creation callback + + Called when all the creation steps have been validated. + + :param steps: the steps data + """ + genform = steps[0]["form"] + genform.is_valid() + account = genform.save() + grant_access_to_object(request.user, account, is_owner=True) + events.raiseEvent("AccountCreated", account) + + mailform = steps[1]["form"] + mailform.save(request.user, account) + + ctx = dict( + title=_("New account"), + action=reverse(newaccount), + formid="newaccount_form", + submit_label=_("Create") + ) + cwizard = CreationWizard(create_account) + cwizard.add_step(AccountFormGeneralPwd, + buttons=[dict(classes="btn-inverse next", label=_("Next"))], + new_args=[request.user]) + cwizard.add_step(AccountFormMail, + buttons=[dict(classes="btn-primary submit", label=_("Create")), + dict(classes="btn-inverse prev", label=_("Previous"))]) + if request.method == "POST": - form = AssignDomainsForm(request.user, da, request.POST) - if form.is_valid(): - current_domains = da.get_domains() - for domain in form.cleaned_data["domains"]: - if not domain in current_domains: - grant_access_to_object(da, domain) - else: - current_domains.remove(domain) - for domain in current_domains: - ungrant_access_to_object(domain, da) - msg = ungettext("Domain assigned", "Domains assigned", - len(form.cleaned_data["domains"])) - messages.info(request, msg) - return ajax_response(request, url=reverse(admin.views.permissions)) - ctx.update(form=form) + retcode, data = cwizard.validate_step(request) + if retcode == -1: + raise AdminError(data) + if retcode == 1: + return ajax_simple_response(dict(status="ok")) + if retcode == 2: + messages.info(request, _("Account created")) + return ajax_simple_response(dict(status="ok")) + ctx.update(steps=cwizard.steps) return ajax_response(request, status="ko", template=tplname, **ctx) - form = AssignDomainsForm(request.user, da) - ctx.update(form=form) + cwizard.create_forms() + ctx.update(steps=cwizard.steps) return _render(request, tplname, ctx) @login_required -@permission_required("auth.view_permissions") -def view_domain_admins(request, dom_id, tplname="admin/view_domain_admins.html"): - domain = Domain.objects.get(pk=dom_id) - return _render(request, tplname, { - "domain" : domain - }) +@transaction.commit_on_success +def editaccount(request, accountid, tplname="common/tabforms.html"): + account = User.objects.get(pk=accountid) + mb = account.mailbox_set.all()[0] if account.has_mailbox else None + instances = dict(general=account, mail=mb, perms=account) + events.raiseEvent("FillAccountInstances", request.user, account, instances) + + ctx = dict( + title=account.username, + action=reverse(editaccount, args=[accountid]), + submit_label=_("Update") + ) + + if request.method == "POST": + classes = {} + if request.POST.get("password1", "") == "" \ + and request.POST.get("password2", "") == "": + classes["general"] = AccountFormGeneral + form = AccountForm(request.user, request.POST, + instances=instances, classes=classes) + account.oldgroup = account.group + if form.is_valid(mandatory_only=True): + form.save_general_form() + if form.is_valid(optional_only=True): + events.raiseEvent("AccountModified", account, form.account) + form.save() + messages.info(request, _("Account updated")) + return ajax_simple_response(dict(status="ok")) + transaction.rollback() + + ctx["tabs"] = form + return ajax_response(request, status="ko", template=tplname, **ctx) + + ctx["tabs"] = AccountForm(request.user, instances=instances) + return _render(request, tplname, ctx) + +@login_required +@user_passes_test(lambda u: u.has_perm("auth.delete_user") or \ + u.has_perm("admin.delete_mailbox")) +@transaction.commit_on_success +def delaccount(request): + selection = request.GET["selection"].split(",") + keepdir = True if request.GET.get("keepdir", "false") == "true" else False + + for account in User.objects.filter(id__in=selection): + account.delete(request.user, keepdir) + + msg = ungettext("Account deleted", "Accounts deleted", len(selection)) + messages.info(request, msg, fail_silently=True) + return ajax_response(request) @login_required @permission_required("auth.view_permissions") @@ -654,19 +381,6 @@ def search_account(request): result = map(lambda u: u.username, users.all()) return ajax_simple_response(result) -@login_required -def delete_permissions(request): - pclass = get_perms_class(request.user, request.GET["role"]) - if pclass is None: - ctx = getctx("ko", message=_("Permission denied!")) - else: - selection = request.GET["selection"].split(",") - pobj = pclass() - pobj.delete(selection) - ctx = getctx("ok", content=pobj.get(request), - message=_("Permissions removed")) - return HttpResponse(simplejson.dumps(ctx), mimetype="application/json") - @login_required @user_passes_test(lambda u: u.is_superuser) def viewparameters(request, tplname='admin/parameters.html'): @@ -688,6 +402,7 @@ def viewparameters(request, tplname='admin/parameters.html'): gparams += [tmp] return _render(request, tplname, { + "selection" : "parameters", "gparams" : gparams }) @@ -699,7 +414,7 @@ def saveparameters(request): continue app, name = pname.split('.') parameters.save_admin(name, v, app=app) - return ajax_response(request) + return ajax_simple_response(dict(status="ok", message=_("Parameters saved"))) @login_required @user_passes_test(lambda u: u.is_superuser) @@ -720,6 +435,7 @@ def viewextensions(request, tplname='admin/extensions.html'): tbl = ExtensionsTable(request, exts) return _render(request, tplname, { + "selection" : "extensions", "extensions" : tbl }) @@ -741,40 +457,92 @@ def saveextensions(request): for ext in actived_exts: if not ext in found: ext.off() - messages.info(request, _("Modifications applied."), fail_silently=True) - return HttpResponseRedirect(reverse(admin.views.viewextensions)) -@login_required -@permission_required("admin.add_mailbox") -@transaction.commit_on_success -def importdata(request, tplname="admin/import.html"): - if request.method == "POST": - error = None - form = ImportDataForm(request.POST, request.FILES) - if form.is_valid(): - import csv + return ajax_simple_response(dict( + status="ok", message=_("Modifications applied."), reload=True + )) + +def import_domain(user, row): + pass + +def import_account(user, row): + """Specific code for accounts import""" + mb = Mailbox() + mb.create_from_csv(user, row) + events.raiseEvent("AccountCreated", user, mb.user) + events.raiseEvent("CreateMailbox", user, mb) + grant_access_to_object(user, mb.user) + grant_access_to_object(user, mb) + +def importdata(request, typ): + """Generic import function + + As the process of importing data from a CSV file is the same + whatever the type, we do a maximum of the work here. + + :param request: a ``Request`` instance + :param typ: a string indicating the object type being imported + :return: a ``Response`` instance + """ + error = None + form = ImportDataForm(request.POST, request.FILES) + if form.is_valid(): + import csv + + try: reader = csv.reader(request.FILES['sourcefile'], delimiter=form.cleaned_data['sepcar']) + except Exception, e: + error = str(e) + + if error is None: + fct = globals()["import_%s" % typ] try: cpt = 0 for row in reader: - mb = Mailbox() - mb.create_from_csv(request.user, row) - events.raiseEvent("CreateMailbox", request.user, mb) + fct(request.user, row) cpt += 1 - messages.info(request, _("%d mailboxes imported successfully" % cpt)) + messages.info(request, _("%d %s imported successfully" % (cpt, typ))) return _render(request, "admin/import_done.html", { "status" : "ok", "msg" : "" }) except ModoboaException, e: error = str(e) - return _render(request, "admin/import_done.html", { - "status" : "ko", "msg" : error - }) + return _render(request, "admin/import_done.html", { + "status" : "ko", "msg" : error + }) - form = ImportDataForm() - ctx = dict(title=_("Import data"), action="", formid="importform", - enctype="multipart/form-data", form=form, submit_label=_("Import")) - return _render(request, tplname, ctx) +@login_required +@transaction.commit_on_success +def import_identities(request): + if request.method == "POST": + return importdata(request, "account") + ctx = dict( + title=_("Import identities"), + submit_label=_("Import"), + action=reverse(import_identities), + formid="importform", + enctype="multipart/form-data", + target="import_target", + form=ImportDataForm() + ) + return _render(request, "admin/importform.html", ctx) + +@login_required +@transaction.commit_on_success +def import_domains(request): + if request.method == "POST": + return importdata(request, "domain") + + ctx = dict( + title=_("Import domains"), + submit_label=_("Import"), + action=reverse(import_domains), + formid="importform", + enctype="multipart/form-data", + target="import_target", + form=ImportDataForm() + ) + return _render(request, "admin/importform.html", ctx) diff --git a/auth/forms.py b/auth/forms.py index 909d7d3a6..ddb148482 100644 --- a/auth/forms.py +++ b/auth/forms.py @@ -2,6 +2,12 @@ from django import forms from django.utils.translation import ugettext as _ class LoginForm(forms.Form): - username = forms.CharField(label=_("Username")) - password = forms.CharField(label=_("Password"), widget=forms.PasswordInput, - help_text=_("Password used to authenticate")) + username = forms.CharField( + label=_("Username"), + widget=forms.TextInput() + ) + password = forms.CharField( + label=_("Password"), + widget=forms.PasswordInput(), + help_text=_("Password used to authenticate") + ) diff --git a/extensions/limits/__init__.py b/extensions/limits/__init__.py index ffea693a2..76ee8ac81 100644 --- a/extensions/limits/__init__.py +++ b/extensions/limits/__init__.py @@ -9,10 +9,10 @@ from django.db import IntegrityError from django.contrib.contenttypes.models import ContentType from django.utils.translation import ugettext_noop as _, ugettext from django.core.urlresolvers import reverse -from modoboa.lib import events, parameters +from modoboa.lib import events from modoboa.lib.webutils import static_url -from modoboa.admin.models import User from models import * +from forms import ResourcePoolForm import permissions import controls import views @@ -20,6 +20,8 @@ import views baseurl = "limits" def init(): + from modoboa.admin.models import User + ct = ContentType.objects.get(app_label="admin", model="domain") dagrp = Group.objects.get(name="DomainAdmins") for pname in ["view_domains", "add_domain", "change_domain", "delete_domain"]: @@ -59,9 +61,9 @@ def infos(): "url" : baseurl } -@events.observe('AdminFooterDisplay') +#@events.observe('AdminFooterDisplay') def display_pool_usage(user, objtype): - if user.id == 1: + if user.is_superuser: return [] if objtype == "domaliases": objtype = "domain_aliases" @@ -89,3 +91,36 @@ def get_da_actions(user): "class" : "boxed", "rel" : "{handler:'iframe',size:{x:330,y:280}}"}, ] + +@events.observe("ExtraAccountForm") +def extra_account_form(user, account=None): + if not user.is_superuser and not user.belongs_to_group("Resellers"): + return [] + if account: + if not account.belongs_to_group("Resellers") and \ + not account.belongs_to_group("DomainAdmins"): + return [] + + return [ + dict( + id="resources", title=_("Resources"), cls=ResourcePoolForm + ) + ] + +@events.observe("CheckExtraAccountForm") +def check_form_access(account, form): + if form["id"] != "resources": + return [True] + if not account.belongs_to_group("Resellers") and \ + not account.belongs_to_group("DomainAdmins"): + return [False] + return [True] + +@events.observe("FillAccountInstances") +def fill_account_instances(user, account, instances): + if not user.is_superuser and not user.belongs_to_group("Resellers"): + return + if not account.belongs_to_group("Resellers") and \ + not account.belongs_to_group("DomainAdmins"): + return + instances["resources"] = account diff --git a/extensions/limits/controls.py b/extensions/limits/controls.py index 12ef82b78..17d0346c4 100644 --- a/extensions/limits/controls.py +++ b/extensions/limits/controls.py @@ -4,13 +4,10 @@ ---------------------------------------------------------------------------- """ -from modoboa.admin.models import Mailbox -from modoboa.admin.lib import \ - get_object_owner, grant_access_to_object, \ - ungrant_access_to_object from modoboa.lib import events from lib import * from models import * +from modoboa.lib.permissions import get_object_owner def check_limit(user, lname): """Check if a user has reached a defined limit @@ -40,6 +37,22 @@ def dec_limit(user, lname): except LimitsPool.DoesNotExist: pass +def move_pool_resource(owner, user): + try: + pool = user.limitspool + except LimitsPool.DoesNotExist: + return + if not owner.is_superuser: + for lname in reseller_limits_tpl: + l = user.limitspool.get_limit(lname) + if l.maxvalue < 0: + continue + ol = owner.limitspool.get_limit(lname) + ol.curvalue += l.curvalue + ol.maxvalue += l.maxvalue + ol.save() + + user.limitspool.delete() @events.observe('CreateDomain') def inc_nb_domains(user, domain): @@ -61,18 +74,29 @@ def inc_nb_domaliases(user, domalias): inc_limit(user, 'domain_aliases_limit') @events.observe('DomainAliasDeleted') -def dec_nb_domaliases(domainalias): - owner = get_object_owner(domainalias) - dec_limit(owner, 'domain_aliases_limit') +def dec_nb_domaliases(domainaliases): + from modoboa.admin.models import DomainAlias + + if isinstance(domainaliases, DomainAlias): + domainaliases = [domainaliases] + for domainalias in domainaliases: + owner = get_object_owner(domainalias) + dec_limit(owner, 'domain_aliases_limit') @events.observe('CreateMailbox') def inc_nb_mailboxes(user, mailbox): inc_limit(user, 'mailboxes_limit') @events.observe('DeleteMailbox') -def dec_nb_mailboxes(mailbox): - owner = get_object_owner(mailbox) - dec_limit(owner, 'mailboxes_limit') +def dec_nb_mailboxes(mailboxes): + from modoboa.admin.models import Mailbox + + if isinstance(mailboxes, Mailbox): + mailboxes = [mailboxes] + for mailbox in mailboxes: + owner = get_object_owner(mailbox) + dec_limit(owner, 'mailboxes_limit') + # FIXME: Gestion des alias ici @events.observe('MailboxAliasCreated') def inc_nb_mbaliases(user, mailboxalias): @@ -87,30 +111,48 @@ def dec_nb_mbaliases(mailboxalias): def can_create_new_object(user, objtype): check_limit(user, '%s_limit' % objtype) -@events.observe("DomainAdminCreated") +@events.observe("AccountCreated") def create_pool(user): owner = get_object_owner(user) - inc_limit(owner, 'domain_admins_limit') - p = LimitsPool(user=user) - p.save() - p.create_limits() + if not owner.is_superuser and \ + not owner.belongs_to_group("Resellers"): + return -@events.observe("DomainAdminDeleted", "SuperAdminPromotion") -def move_pool_resource(user): - owner = get_object_owner(user) - if not owner.is_superuser: + if user.belongs_to_group("DomainAdmins"): + check_limit(owner, 'domain_admins_limit') + inc_limit(owner, 'domain_admins_limit') + + if user.belongs_to_group("DomainAdmins") or \ + user.belongs_to_group("Resellers"): + p = LimitsPool(user=user) + p.save() + p.create_limits() + +@events.observe("AccountModified") +def on_account_modified(old, new): + owner = get_object_owner(old) + if owner.group not in ["SuperAdmins", "Resellers"]: + return + + if not new.group in ["DomainAdmins", "Resellers"]: + move_pool_resource(owner, new) + + if old.oldgroup == "DomainAdmins": + if new.group != "DomainAdmins": + dec_limit(owner, 'domain_admins_limit') + return + + if new.group == "DomainAdmins": + check_limit(owner, 'domain_admins_limit') + inc_limit(owner, 'domain_admins_limit') + +@events.observe("AccountDeleted") +def on_account_deleted(account): + owner = get_object_owner(account) + if not owner.group in ["SuperAdmins", "Resellers"]: + return + + move_pool_resource(owner, account) + + if account.group == "DomainAdmins": dec_limit(owner, 'domain_admins_limit') - - for ooentry in user.objectaccess_set.all(): - if ooentry.is_owner: - grant_access_to_object(owner, ooentry.content_object, True) - ooentry.delete() - - for lname in reseller_limits_tpl: - l = user.limitspool.get_limit(lname) - ol = owner.limitspool.get_limit(lname) - ol.curvalue += l.curvalue - ol.maxvalue += l.maxvalue - ol.save() - - user.limitspool.delete() diff --git a/extensions/limits/forms.py b/extensions/limits/forms.py index 55814b710..0324271a3 100644 --- a/extensions/limits/forms.py +++ b/extensions/limits/forms.py @@ -3,33 +3,10 @@ from django import forms from django.utils.translation import ugettext_noop as _ from django.contrib.auth.models import User, Group -from modoboa.admin.forms import UserForm, UserWithPasswordForm -from modoboa.admin.lib import get_object_owner from models import * from lib import * -class ResellerForm(UserForm): - def __init__(self, *args, **kwargs): - super(ResellerForm, self).__init__(*args, **kwargs) - del self.fields["createmb"] - - def save(self, commit=True, group=None): - user = super(ResellerForm, self).save(commit, group) - if commit: - try: - pool = user.limitspool - except LimitsPool.DoesNotExist: - pool = LimitsPool() - pool.user = user - pool.save() - pool.create_limits() - - return user - -class ResellerWithPasswordForm(ResellerForm, UserWithPasswordForm): - pass - -class ResellerPoolForm(forms.Form): +class ResourcePoolForm(forms.Form): domain_admins_limit = forms.IntegerField( ugettext_noop("Max domain admins"), help_text=ugettext_noop("Maximum number of domain administrators that can be created by this user") @@ -43,6 +20,16 @@ class ResellerPoolForm(forms.Form): mailbox_aliases_limit = forms.IntegerField(ugettext_noop("Max mailbox aliases"), help_text=ugettext_noop("Maximum number of mailbox aliases that can be created by this user")) + def __init__(self, *args, **kwargs): + if kwargs.has_key("instance"): + self.account = kwargs["instance"] + del kwargs["instance"] + super(ResourcePoolForm, self).__init__(*args, **kwargs) + if hasattr(self, "account"): + if not self.account.belongs_to_group("Resellers"): + del self.fields["domain_admins_limit"] + self.load_from_user(self.account) + def check_limit_value(self, lname): if self.cleaned_data[lname] < -1: raise forms.ValidationError(_("Invalid limit")) @@ -84,12 +71,14 @@ class ResellerPoolForm(forms.Form): ol.maxvalue -= newvalue ol.save() - def save_new_limits(self, pool): - owner = get_object_owner(pool.user) + def save(self): + from modoboa.lib.permissions import get_object_owner + + owner = get_object_owner(self.account) for lname in reseller_limits_tpl: if not self.cleaned_data.has_key(lname): continue - l = pool.limit_set.get(name=lname) + l = self.account.limitspool.limit_set.get(name=lname) if not owner.is_superuser: self.allocate_from_pool(l, owner.limitspool) l.maxvalue = self.cleaned_data[lname] diff --git a/extensions/limits/permissions.py b/extensions/limits/permissions.py index fd4bf1f91..2f5b316b8 100644 --- a/extensions/limits/permissions.py +++ b/extensions/limits/permissions.py @@ -1,53 +1,10 @@ # coding: utf-8 from django.utils.translation import ugettext as _, ugettext_noop -from modoboa.admin.permissions import Permissions -from modoboa.admin.lib import grant_access_to_object, ungrant_access_to_object -from modoboa.admin.models import User, ObjectAccess from modoboa.lib import events -from modoboa.lib.webutils import _render, _render_to_string -from tables import * -from forms import * -class ResellersPerms(Permissions): - title = ugettext_noop("New reseller") - role = "resellers" - add_success_msg = ugettext_noop("Reseller added") - - def get_add_form(self, request): - form = ResellerWithPasswordForm() - return self._render_form(request, form) - - def add(self, request): - form = ResellerWithPasswordForm(request.POST) - error = None - if form.is_valid(): - r = form.save(group="Resellers") - grant_access_to_object(request.user, r, True) - return True, None - content = self._render_form(request, form, True) - return False, dict(status="ko", content=content) - - def delete(self, selection): - for u in User.objects.filter(id__in=selection): - ungrant_access_to_object(u) - u.delete() - - def get(self, request): - resellers = User.objects.filter(groups__name="Resellers") - return ResellersTable(request, resellers).render() - -@events.observe('PermsGetTables') -def get_resellers_table(request): - if not request.user.is_superuser: - return [] - return [{"id" : "resellers", - "title" : _("Resellers"), - "rel" : "350 300", - "content" : ResellersPerms().get(request)}] - -@events.observe("PermsGetClass") -def get_perms_class(role): - if role != "resellers": - return [] - return [ResellersPerms] +@events.observe("GetExtraRoles") +def get_extra_roles(user): + if user.is_superuser: + return [("Resellers", _("Reseller")),] + return [] diff --git a/extensions/postfix_autoreply/__init__.py b/extensions/postfix_autoreply/__init__.py index 6d99c025b..e38c9c8ca 100644 --- a/extensions/postfix_autoreply/__init__.py +++ b/extensions/postfix_autoreply/__init__.py @@ -21,6 +21,23 @@ def infos(): "url" : "postfix_autoreply" } +def init(): + from modoboa.admin.models import Domain + + for dom in Domain.objects.all(): + try: + trans = Transport.objects.get(domain="autoreply.%s" % dom.name) + except Transport.DoesNotExist: + onCreateDomain(None, dom) + else: + continue + + for mb in dom.mailbox_set.all(): + try: + alias = Alias.objects.get(full_address=mb.full_address) + except Alias.DoesNotExist: + onCreateMailbox(None, mb) + def load(): parameters.register_admin( "AUTOREPLIES_TIMEOUT", @@ -48,10 +65,9 @@ def menu(target, user): return [ {"name" : "autoreply", "url" : reverse(views.autoreply), - "img" : static_url("pics/auto-reply.png"), - "class" : "boxed", - "rel" : "{handler:'iframe',size:{x:440,y:400}}", - "label" : ugettext("Auto-reply message")} + "label" : ugettext("Auto-reply message"), + "modal": True, + "modalcb" : "arform_cb"} ] @events.observe("CreateDomain") @@ -75,12 +91,18 @@ def onCreateMailbox(user, mailbox): alias.save() @events.observe("DeleteMailbox") -def onDeleteMailbox(mailbox): - try: - alias = Alias.objects.get(full_address=mailbox.full_address) - alias.delete() - except Alias.DoesNotExist: - pass +def onDeleteMailbox(mailboxes): + from modoboa.admin.models import Mailbox + + if isinstance(mailboxes, Mailbox): + mailboxes = [mailboxes] + for mailbox in mailboxes: + try: + alias = Alias.objects.get(full_address=mailbox.full_address) + except Alias.DoesNotExist: + pass + else: + alias.delete() @events.observe("ModifyMailbox") def onModifyMailbox(mailbox, oldmailbox): @@ -91,3 +113,17 @@ def onModifyMailbox(mailbox, oldmailbox): alias.autoreply_address = \ "%s@autoreply.%s" % (mailbox.full_address, mailbox.domain.name) alias.save() + +@events.observe("GetStaticContent") +def get_static_content(target): + if target != "userprefs": + return [] + return """ +""" diff --git a/extensions/postfix_autoreply/forms.py b/extensions/postfix_autoreply/forms.py index 0c46c5e5c..5ce746e2a 100644 --- a/extensions/postfix_autoreply/forms.py +++ b/extensions/postfix_autoreply/forms.py @@ -4,18 +4,15 @@ from django.utils.translation import ugettext as _ import datetime class ARmessageForm(forms.ModelForm): - untildate = forms.DateField(label=_('Until'), required=False, - help_text=_("Activate your auto reply until this date")) + untildate = forms.DateField( + label=_('Until'), required=False, + help_text=_("Activate your auto reply until this date") + ) class Meta: model = ARmessage fields = ('subject', 'content', 'enabled') - def __init__(self, *args, **kwargs): - super(ARmessageForm, self).__init__(*args, **kwargs) - self.fields['subject'].widget.attrs['size'] = 40 - self.fields['content'].widget.attrs['cols'] = 50 - def clean_untildate(self): if self.cleaned_data["untildate"] is not None: if self.cleaned_data["untildate"] < datetime.date.today(): diff --git a/extensions/postfix_autoreply/templates/postfix_autoreply/autoreply.html b/extensions/postfix_autoreply/templates/postfix_autoreply/autoreply.html index c7a9a6fe2..9a909fe31 100644 --- a/extensions/postfix_autoreply/templates/postfix_autoreply/autoreply.html +++ b/extensions/postfix_autoreply/templates/postfix_autoreply/autoreply.html @@ -1,42 +1,15 @@ -{% extends "layout_simple.html" %} {% load i18n %} -{% block html_head %} - - - - - -{% endblock %} - -{% block content %} -

{% trans "Auto-reply message" %}

- -
- {% include "common/genericform.html" %} -
- -
- -
-
-{% endblock %} + + + diff --git a/extensions/postfix_autoreply/views.py b/extensions/postfix_autoreply/views.py index 907a4ba40..ff924e1ac 100644 --- a/extensions/postfix_autoreply/views.py +++ b/extensions/postfix_autoreply/views.py @@ -1,5 +1,4 @@ -# -*- coding: utf-8 -*- - +# coding: utf-8 from django.http import HttpResponse from django.utils import simplejson from django.contrib.auth.decorators \ @@ -14,7 +13,13 @@ from models import * @login_required @needs_mailbox() -def autoreply(request, tplname="postfix_autoreply/autoreply.html"): +def autoreply(request, tplname="common/generic_modal_form.html"): + ctx = dict( + title=_("Auto-reply message"), + action=reverse(autoreply), + formid="arform", + submit_label=_("Update") + ) mb = Mailbox.objects.get(user=request.user.id) try: arm = ARmessage.objects.get(mbox=mb.id) @@ -33,15 +38,13 @@ def autoreply(request, tplname="postfix_autoreply/autoreply.html"): arm.untildate = form.cleaned_data["untildate"] arm.mbox = mb arm.save() - messages.info(request, _("Auto reply message updated successfully."), - fail_silently=True) + messages.info(request, _("Auto reply message updated successfully.")) return ajax_response(request, url=reverse(userprefs.views.index)) - return ajax_response(request, status="ko", template=tplname, - form=form, error=error) + ctx.update(form=form, error=error) + return ajax_response(request, status="ko", template=tplname, **ctx) form = ARmessageForm(instance=arm) if arm is not None: form.fields['untildate'].initial = arm.untildate - return _render(request, tplname, { - "form" : form - }) + ctx.update(form=form) + return _render(request, tplname, ctx) diff --git a/extensions/sievefilters/__init__.py b/extensions/sievefilters/__init__.py index ed6043711..865877bed 100644 --- a/extensions/sievefilters/__init__.py +++ b/extensions/sievefilters/__init__.py @@ -1,4 +1,4 @@ -# -*- coding: utf-8 -*- +# coding: utf-8 from django.utils.translation import ugettext_noop as _, ugettext from django.core.urlresolvers import reverse from modoboa.lib import events, parameters @@ -55,7 +55,7 @@ def menu(target, user): {"name" : "sievefilters", "label" : ugettext("Message filters"), "url" : reverse(views.index), - "img" : static_url("pics/filters.png")} + "img" : "icon-check"} ] @events.observe("Userlogout") diff --git a/extensions/sievefilters/forms.py b/extensions/sievefilters/forms.py index c07ac1a36..549c9934d 100644 --- a/extensions/sievefilters/forms.py +++ b/extensions/sievefilters/forms.py @@ -1,6 +1,10 @@ from django import forms from django.http import QueryDict from django.utils.translation import ugettext as _, ugettext_noop +from django.forms.widgets import RadioFieldRenderer, RadioSelect, RadioInput +from django.utils.safestring import mark_safe +from django.utils.html import conditional_escape +from django.utils.encoding import force_unicode from modoboa.admin.templatetags.admin_extras import gender class FiltersSetForm(forms.Form): @@ -9,17 +13,52 @@ class FiltersSetForm(forms.Form): initial=False, help_text=ugettext_noop("Check to activate this filters set")) +class CustomRadioInput(RadioInput): + def __unicode__(self): + return self.render() + + def render(self, name=None, value=None, attrs=None, choices=()): + name = name or self.name + value = value or self.value + attrs = attrs or self.attrs + if 'id' in self.attrs: + label_for = ' for="%s_%s"' % (self.attrs['id'], self.index) + else: + label_for = '' + choice_label = conditional_escape(force_unicode(self.choice_label)) + return mark_safe( + u'%s %s' \ + % (label_for, self.tag(), choice_label) + ) + +class CustomRadioFieldRenderer(RadioFieldRenderer): + def __iter__(self): + for i, choice in enumerate(self.choices): + yield CustomRadioInput(self.name, self.value, self.attrs.copy(), choice, i) + + def __getitem__(self, idx): + choice = self.choices[idx] + return CustomRadioInput(self.name, self.value, self.attrs.copy(), choice, idx) + + def render(self): + return mark_safe(u'\n'.join([force_unicode(w) for w in self])) + +class CustomRadioSelect(RadioSelect): + renderer = CustomRadioFieldRenderer + class FilterForm(forms.Form): def __init__(self, conditions, actions, request, *args, **kwargs): super(FilterForm, self).__init__(*args, **kwargs) self.fields["name"] = forms.CharField(label=_("Name")) - self.fields["match_type"] = forms.ChoiceField(choices=[("allof", _("All of the following")), - ("anyof", _("Any of the following")), - ("all", _("All messages"))], - initial="anyof", - widget=forms.RadioSelect) - + self.fields["match_type"] = forms.ChoiceField( + choices=[("allof", _("All of the following")), + ("anyof", _("Any of the following")), + ("all", _("All messages"))], + initial="anyof", + widget=CustomRadioSelect(attrs={"class" : "radio inline"}) + ) + self.header_operators = [("contains", _("contains"), "string"), ("notcontains", _("does not contain"), "string"), ("is", _("is"), "string"), diff --git a/extensions/sievefilters/lib.py b/extensions/sievefilters/lib.py index 41f321017..4d58addd5 100644 --- a/extensions/sievefilters/lib.py +++ b/extensions/sievefilters/lib.py @@ -42,6 +42,8 @@ class SieveClient(object): self.msc = None def refresh(self, user, password): + import ssl + if self.msc is not None: try: self.msc.capability() @@ -51,7 +53,7 @@ class SieveClient(object): return try: ret, msg = self.login(user, password) - except Error, e: + except (Error, ssl.SSLError), e: raise ConnectionError(e) if not ret: raise ConnectionError(msg) @@ -74,6 +76,8 @@ class SieveClient(object): return fs def pushscript(self, name, content, active=False): + if type(content) is unicode: + content = str(content) if not self.msc.havespace(name, len(content)): error = "%s (%s)" % (_("Not enough space on server"), self.msc.errmsg) raise SieveClientError(error) diff --git a/extensions/sievefilters/static/css/style.css b/extensions/sievefilters/static/css/style.css index cf487b3f2..123766e87 100644 --- a/extensions/sievefilters/static/css/style.css +++ b/extensions/sievefilters/static/css/style.css @@ -1,43 +1,6 @@ -#fsets_list { - margin-left: 5px; - float: left; - display: inline; -} - -#fset_menu { - position: absolute; - top: 0px; - left: 10px; -} - -#set_content { - position: absolute; - left: 10px; - right: 10px; - top: 0px; - bottom: 10px; - padding: 0; -} - -#set_content h1 { - padding: 2px 5px; - margin: 0; - font-size: 1.2em; - background: #CEDBEC; - -moz-border-radius: 5px; - -webkit-border-radius: 5px; -} - -#fsets_bar { - margin-left: 10px; - width: 250px; -} - #set_content textarea { - margin: 0; width: 100%; - height: 100%; - padding: 2px; + height: 500px; } #set_content label { @@ -54,14 +17,6 @@ bottom: 0px; } -#feditor { - position: absolute; - left: 190px; - right: 10px; - top: 0px; - bottom: 0px; -} - #id_content { width: 95% !important; height: 270px; @@ -143,11 +98,6 @@ div.item input, div.item select { margin: 0 2px; } -#condlist label { - margin: 0 !important; - padding: 0; -} - ul.errors { list-style: none; display: inline; diff --git a/extensions/sievefilters/static/js/jquery.dynfields.js b/extensions/sievefilters/static/js/jquery.dynfields.js new file mode 100644 index 000000000..eb3c61264 --- /dev/null +++ b/extensions/sievefilters/static/js/jquery.dynfields.js @@ -0,0 +1,328 @@ +(function($) { + var DynamicCondition = function(element, options) { + this.$element = $(element); + this.options = $.extend({}, $.fn.dyncondition.defaults, options); + this.fields_cnt = 1; + this.nextid = 1; + + var $addlink = $('', { + href: '#', + id: this.$element.attr("id") + '_rmbtn', + html: '' + }); + $("select[name*=cond_target_]").change($.proxy(this.target_change, this)); + $addlink.click($.proxy(this.addfields, this)); + this.$element.append($addlink); + + for (var cpt = 1; true; cpt++) { + var $div = $("#condition_" + cpt); + + if (!$div.length) { + break; + } + $div.append(this.newrmlink()); + this.nextid++; + this.fields_cnt++; + } + }; + + DynamicCondition.prototype = { + constructor: DynamicCondition, + + loadoperators: function($container, tpl) { + $container.empty(); + for (var ocpt = 0; ocpt < tpl.operators.length; ocpt++) { + var $opt = $("', { + href: '#', + id: 'condition_' + this.nextid + '_rmbtn', + html: '' + }).click($.proxy(this.removefields, this)); + }, + + addfields: function(e) { + e.preventDefault(); + var $div = $("
", { + id: "condition_" + this.nextid, 'class': "item" + }); + var $condtarget = $("", { + id: "id_cond_operator_" + this.nextid, + name: "cond_operator_" + this.nextid + }); + var $condvalue = $("", { + id: "id_cond_value_" + this.nextid, + name: "cond_value_" + this.nextid, + type: "text" + }); + var $rmlink = this.newrmlink(); + + for (var cpt = 0; cpt < this.options.templates.length; cpt++) { + var tpl = this.options.templates[cpt]; + var $opt = $("', { + href: '#', + id: this.$element.attr("id") + '_rmbtn', + html: '' + }); + $("select[name*=action_name_]").change($.proxy(this.name_change, this)); + $addlink.click($.proxy(this.addfields, this)); + this.$element.append($addlink); + + for (var cpt = 1; true; cpt++) { + var $div = $("#action_" + cpt); + + if (!$div.length) { + break; + } + $div.append(this.newrmlink()); + this.nextid++; + this.fields_cnt++; + } + + $("#id_action_arg_0_0").find("option").each(function() { + var $this = $(this), + cpt = 1, + start = 0, + value = $this.attr("value"); + + while (true) { + var id = value.indexOf(".", start); + if (id == -1) { + break; + } + $this.css("margin-left", (cpt * 20) + "px"); + cpt++; + start = id + 1; + } + }); + + this.$fdoptions = $("#id_action_arg_0_0").find("option"); + }; + + DynamicAction.prototype = { + constructor: DynamicAction, + + loadargs: function(id, tpl) { + var res = []; + + for (var acpt = 0; acpt < tpl.args.length; acpt++) { + var argname = "action_arg_" + id + "_" + acpt; + var $child = null; + + if (tpl.args[acpt].type == "list") { + $child = $("", { + type: "text", + id: "id_" + argname, + name: argname + }); + } + res.push($child); + } + return res; + }, + + deletePrevArgs: function(id) { + for (var cpt = 0; true; cpt++) { + var $arg = $("#id_action_arg_" + id + "_" + cpt); + if (!$arg.length) { + break; + } + $arg.remove(); + } + }, + + name_change: function(e) { + var $target = $(e.target); + var parts = $target.attr("name").split("_"); + var $pos = $("#action_" + parts[2] + "_rmbtn"); + + e.preventDefault(); + this.deletePrevArgs(parts[2]); + for (var cpt = 0; cpt < this.options.templates.length; cpt++) { + var tpl = this.options.templates[cpt]; + if (tpl.name != $target.attr("value")) { + continue; + } + $.each(this.loadargs(parts[2], tpl), function(idx, arg) { + $pos.before(arg); + }); + } + }, + + newrmlink: function() { + return $('', { + href: '#', + id: 'action_' + this.nextid + '_rmbtn', + html: '' + }).click($.proxy(this.removefields, this)); + }, + + addfields: function(e) { + e.preventDefault(); + var $div = $("
", {id: "action_" + this.nextid, "class": "item"}); + var $actioname = $("{% endif %} - : - {{ form.name }}{% display_errors form.name.errors %} -

{% trans "Conditions" %}

+ {% render_field form.name %} +
{% trans "Conditions" %}
- - {{ form.match_type }}{% display_errors form.match_type.errors %} -
+ {% render_field form.match_type %}
-
{% for ec in conds_nb %} {% display_condition form ec %} {% endfor %}
-

{% trans "Actions" %}

+
{% trans "Actions" %}
{% for ea in actions_nb %} {% display_action form ea %} {% endfor %} -
- - +
{% endblock %} diff --git a/extensions/sievefilters/templates/sievefilters/filters.html b/extensions/sievefilters/templates/sievefilters/filters.html deleted file mode 100644 index 42f561aaa..000000000 --- a/extensions/sievefilters/templates/sievefilters/filters.html +++ /dev/null @@ -1,28 +0,0 @@ -{% load i18n sfilters_tags %} -

{% trans "Filters" %}

-

- {% if fs.filters %} - - - - - - - {% for filter in fs.filters %} - - - - - - {% endfor %} -
{% trans "Actions" %}{% trans "Name" %}{% trans "Active" %}
{% filter_actions fs.name filter forloop.counter forloop.last %}{{ filter.name }} - - {{ filter.enabled|yesno }} - -
- {% else %} -

{% trans "No filter defined yet" %}
- {% endif %} -

diff --git a/extensions/sievefilters/templates/sievefilters/guieditor.html b/extensions/sievefilters/templates/sievefilters/guieditor.html index 8923141c0..8e3150185 100644 --- a/extensions/sievefilters/templates/sievefilters/guieditor.html +++ b/extensions/sievefilters/templates/sievefilters/guieditor.html @@ -1,8 +1,25 @@ -{% load sfilters_tags i18n %} -
-

{% trans "Actions" %}

- {% fset_menu "gui" fs.name %} -
-
- {% include "sievefilters/filters.html" %} -
+{% load i18n sfilters_tags %} +{% if fs.filters %} + + + + + + + {% for filter in fs.filters %} + + + + + + {% endfor %} +
{% trans "Actions" %}{% trans "Name" %}{% trans "Active" %}
{% filter_actions fs.name filter forloop.counter forloop.last %}{{ filter.name }} + + {{ filter.enabled|yesno }} + +
+{% else %} +
{% trans "No filter defined yet" %}
+{% endif %} diff --git a/extensions/sievefilters/templates/sievefilters/index.html b/extensions/sievefilters/templates/sievefilters/index.html index 3a85e91b6..03dcc4eb5 100644 --- a/extensions/sievefilters/templates/sievefilters/index.html +++ b/extensions/sievefilters/templates/sievefilters/index.html @@ -1,41 +1,77 @@ -{% extends "nlayout.html" %} -{% load i18n libextras sfilters_tags %} +{% extends "twocols.html" %} +{% load i18n sfilters_tags %} {% block html_head %} - - + + + + {% endblock %} -{% block middletitle %}{% trans "Message filters" %}{% endblock %} -{% block menu %} -
- - +{% block leftcol %} + -
    {% sfilters_menu user %}
{% endblock %} -{% block content %} -{% if messages %} - {% display_messages messages %} -{% endif %} +{% block apparea %}
{% if not active_script and not scripts %} -
{% trans "No filters set defined yet. You must create one in order to define filters." %}
+
{% trans "No filters set defined yet. You must create one in order to define filters." %}
{% endif %}
{% endblock %} diff --git a/extensions/sievefilters/templates/sievefilters/newfiltersset.html b/extensions/sievefilters/templates/sievefilters/newfiltersset.html deleted file mode 100644 index 134c60d77..000000000 --- a/extensions/sievefilters/templates/sievefilters/newfiltersset.html +++ /dev/null @@ -1,41 +0,0 @@ -{% extends "layout_simple.html" %} -{% load i18n %} -{% block html_head %} - - - -{% endblock %} -{% block content %} -

{{ title }}

-{% if error %}
{{ error }}
{% endif %} -
- {% include "common/genericform.html" %} -
- -
-
-{% endblock %} diff --git a/extensions/sievefilters/templates/sievefilters/rawfilter.html b/extensions/sievefilters/templates/sievefilters/rawfilter.html index 895609303..20fbf55d3 100644 --- a/extensions/sievefilters/templates/sievefilters/rawfilter.html +++ b/extensions/sievefilters/templates/sievefilters/rawfilter.html @@ -1,8 +1,3 @@ -{% load i18n sfilters_tags %} -
-

{% trans "Actions" %}

- {% fset_menu "raw" name %} -
diff --git a/extensions/sievefilters/templatetags/sfilters_tags.py b/extensions/sievefilters/templatetags/sfilters_tags.py index bfba5f0cb..9e5b38081 100644 --- a/extensions/sievefilters/templatetags/sfilters_tags.py +++ b/extensions/sievefilters/templatetags/sfilters_tags.py @@ -2,7 +2,6 @@ from django import template from django.template.loader import render_to_string from django.utils.translation import ugettext as _ from django.core.urlresolvers import reverse -from modoboa.extensions.sievefilters import views from modoboa.lib.webutils import render_actions register = template.Library() @@ -13,11 +12,11 @@ from modoboa.lib.webutils import static_url def sfilters_menu(user): entries = [ {"name" : "newfilterset", - "img" : static_url("pics/add.png"), + "img" : "icon-plus", "label" : _("New filters set"), - "url" : reverse(views.new_filters_set), - "class" : "boxed", - "rel" : "{handler:'iframe',size:{x:300,y:170}}" + "url" : reverse("modoboa.extensions.sievefilters.views.new_filters_set"), + "modal" : True, + "modalcb" : "filtersetform_cb" } ] @@ -30,65 +29,74 @@ def fset_menu(mode, setname): if mode == "gui": entries += [ {"name" : "newfilter", - "img" : static_url("pics/add.png"), + "img" : "icon-plus", "label" : _("New filter"), - "url" : reverse(views.newfilter, args=[setname]), - "class" : "boxed", - "rel" : "{handler:'iframe',size:{x:700,y:400}}"}, - + "url" : reverse("modoboa.extensions.sievefilters.views.newfilter", + args=[setname]), + "modal" : True, + "autowidth" : True, + "modalcb" : "filterform_cb"} ] if mode == "raw": entries += [ {"name" : "savefs", - "img" : static_url("pics/save.png"), + "img" : "icon-download-alt", "label" : _("Save filters set"), - "url" : reverse(views.savefs, args=[setname])}, + "url" : reverse("modoboa.extensions.sievefilters.views.savefs", + args=[setname])}, ] entries += [ {"name" : "activatefs", - "img" : static_url("pics/active.png"), + "img" : "icon-ok", "label" : _("Activate filters set"), - "url" : reverse(views.activate_filters_set, args=[setname])}, + "url" : reverse("modoboa.extensions.sievefilters.views.activate_filters_set", + args=[setname])}, {"name" : "removefs", - "img" : static_url("pics/remove.png"), + "img" : "icon-remove", "label" : _("Remove filters set"), - "url" : reverse(views.remove_filters_set, args=[setname])}, + "url" : reverse("modoboa.extensions.sievefilters.views.remove_filters_set", + args=[setname])}, {"name" : "downloadfs", - "img" : static_url("pics/download.png"), + "img" : "icon-download", "label" : _("Download"), - "url" : reverse(views.download_filters_set, args=[setname])} + "url" : reverse("modoboa.extensions.sievefilters.views.download_filters_set", + args=[setname])} ] - return render_to_string('common/menu.html', + return render_to_string('common/menulist.html', {"entries" : entries}) @register.simple_tag def filter_actions(setname, f, position, islast): actions = [ {"name" : "editfilter", - "url" : reverse(views.editfilter, args=[setname, f["name"]]), - "img" : static_url("pics/edit.png"), + "url" : reverse("modoboa.extensions.sievefilters.views.editfilter", + args=[setname, f["name"]]), + "img" : "icon-edit", "title" : _("Edit filter"), - "class" : "boxed", - "rel" : "{handler:'iframe',size:{x:650,y:400}}"}, + "modal" : True, + "modalcb" : "filterform_cb"}, {"name" : "removefilter", - "url" : reverse(views.removefilter, args=[setname, f["name"]]), - "img" : static_url("pics/remove.png"), + "url" : reverse("modoboa.extensions.sievefilters.views.removefilter", + args=[setname, f["name"]]), + "img" : "icon-remove", "title" : _("Remove this filter")} ] if position != 1: actions += [ {"name" : "movefilter_up", - "url" : reverse(views.move_filter_up, args=[setname, f["name"]]), - "img" : static_url("pics/moveup.png"), + "url" : reverse("modoboa.extensions.sievefilters.views.move_filter_up", + args=[setname, f["name"]]), + "img" : "icon-arrow-up", "title" : _("Move this filter up")} ] if not islast: actions += [ {"name" : "movefilter_down", - "url" : reverse(views.move_filter_down, args=[setname, f["name"]]), - "img" : static_url("pics/movedown.png"), + "url" : reverse("modoboa.extensions.sievefilters.views.move_filter_down", + args=[setname, f["name"]]), + "img" : "icon-arrow-down", "title" : _("Move this filter down")}, ] return render_actions(actions) @@ -114,11 +122,12 @@ def display_condition(form, cnt): operator = form["cond_operator_%d" % cnt] value = form["cond_value_%d" % cnt] t = template.Template(""" -
+
{{ tfield }}{{ofield}}{{ vfield }}{{ verrors }}
""") return t.render(template.Context({ + "idx" : cnt, "tfield" : target, "ofield" : operator, "vfield" : value, @@ -140,11 +149,12 @@ def display_action(form, cnt): except KeyError: break t = template.Template(""" -
+
{{ afield }}{% for v in values %}{{ v }}{% endfor %}{{ verrors }}
""") return t.render(template.Context({ + "idx" : cnt, "afield" : action, "values" : values, "verrors" : display_errors(verrors) })) diff --git a/extensions/sievefilters/views.py b/extensions/sievefilters/views.py index 70c2dba2a..1693c3702 100644 --- a/extensions/sievefilters/views.py +++ b/extensions/sievefilters/views.py @@ -1,6 +1,7 @@ # coding: utf-8 from sievelib.managesieve import Error from django.http import HttpResponse +from django.template.loader import render_to_string from django.contrib.auth.decorators import login_required from django.utils.translation import ugettext as _ from django.core.urlresolvers import reverse @@ -12,6 +13,7 @@ from modoboa.lib.connections import ConnectionError from modoboa.auth.lib import get_password from lib import * from forms import * +from templatetags.sfilters_tags import fset_menu @login_required @needs_mailbox() @@ -26,6 +28,7 @@ def index(request, tplname="sievefilters/index.html"): active_script, scripts = sc.listscripts() except Error, e: return _render_error(request, user_context={"error" : e}) + if active_script is None: active_script = "" default_script = "%s/" % scripts[0] if len(scripts) else "" @@ -73,10 +76,18 @@ def getfs(request, name): return ajax_response(request, "ko", respmsg=error) if editormode == "raw": - return ajax_response(request, template="sievefilters/rawfilter.html", - name=name, scriptcontent=content) - return ajax_response(request, template="sievefilters/guieditor.html", - fs=content) + htmlcontent = render_to_string("sievefilters/rawfilter.html", dict( + name=name, scriptcontent=content + )) + else: + htmlcontent = render_to_string("sievefilters/guieditor.html", dict( + fs=content + )) + + menu = '' % \ + (_("Actions"), fset_menu(editormode, name)) + resp = dict(status="ok", menu=menu, content=htmlcontent) + return ajax_simple_response(resp) def build_filter_ctx(ctx, form): ctx["form"] = form @@ -111,8 +122,11 @@ def submitfilter(request, setname, okmsg, tplname, tplctx, update=False, sc=None @login_required @needs_mailbox() def newfilter(request, setname, tplname="sievefilters/filter.html"): - ctx = {"title" : _("New filter"), - "actionurl" : reverse(newfilter, args=[setname])} + ctx = dict( + title=_("New filter"), + action=reverse(newfilter, args=[setname]), + submit_label=_("Create") + ) if request.method == "POST": return submitfilter(request, setname, _("Filter created"), tplname, ctx) @@ -124,9 +138,12 @@ def newfilter(request, setname, tplname="sievefilters/filter.html"): @login_required @needs_mailbox() -def editfilter(request, setname, fname, tplname="sievefilters/filter.html"): - ctx = {"title" : _("Edit filter"), - "actionurl" : reverse(editfilter, args=[setname, fname])} +def editfilter(request, setname, fname, tplname="sievefilters/filter2.html"): + ctx = dict( + title=_("Edit filter"), + action=reverse(editfilter, args=[setname, fname]), + submit_label=_("Update") + ) sc = SieveClient(user=request.user.username, password=request.session["password"]) if request.method == "POST": @@ -169,10 +186,11 @@ def savefs(request, name): @login_required @needs_mailbox() -def new_filters_set(request, tplname="sievefilters/newfiltersset.html"): +def new_filters_set(request, tplname="common/generic_modal_form.html"): ctx = {"title" : _("Create a new filters set"), - "fname" : "newfiltersset", + "formid" : "newfiltersset", "submit_label" : _("Create"), + "action" : reverse(new_filters_set), "withmenu" : False, "withunseen" : False} if request.method == "POST": @@ -206,8 +224,11 @@ def remove_filters_set(request, name): try: sc.deletescript(name) except SieveClientError, e: - return ajax_response(request, "ko", respmsg=str(e)) - return ajax_response(request, respmsg=_("Filters set deleted")) + return ajax_simple_response(dict(status="ko", respmsg=str(e))) + acs, scripts = sc.listscripts() + return ajax_simple_response(dict( + status="ok", respmsg=_("Filters set deleted"), newfs=acs + )) @login_required @needs_mailbox() @@ -272,7 +293,7 @@ def move_filter(request, setname, fname, direction): sc.pushscript(setname, str(fset)) except (SieveClientError), e: return ajax_response(request, "ko", respmsg=str(e)) - return ajax_response(request, template="sievefilters/filters.html", fs=fset) + return ajax_response(request, template="sievefilters/guieditor.html", fs=fset) @login_required @needs_mailbox() diff --git a/extensions/stats/views.py b/extensions/stats/views.py index 47acb78e1..67c31f2e2 100644 --- a/extensions/stats/views.py +++ b/extensions/stats/views.py @@ -5,7 +5,7 @@ from django.core.urlresolvers import reverse from django.utils.translation import ugettext as _, ugettext_noop from django.utils import simplejson from modoboa.lib.webutils import _render, _render_to_string, getctx -from modoboa.admin.lib import check_domain_access +from modoboa.lib.permissions import check_domain_access from modoboa.admin.models import Domain, Mailbox from django.contrib.auth.decorators \ import login_required, user_passes_test, permission_required diff --git a/lib/events.py b/lib/events.py index 82adfb485..bf0c975f1 100644 --- a/lib/events.py +++ b/lib/events.py @@ -13,8 +13,6 @@ import inspect import re events = [ - "SuperAdminPromotion", - "CanCreate", "CreateDomain", @@ -30,17 +28,22 @@ events = [ "MailboxAliasCreated", "MailboxAliasDeleted", - "DomainAdminCreated", + "AccountCreated", + "AccountModified", + "AccountDeleted", "DomainAdminActions", - "DomainAdminDeleted", "PasswordChange", "UserMenuDisplay", "AdminMenuDisplay", + "GetStaticContent", + + "ExtraAccountForm", + "CheckExtraAccountForm", + "FillAccountInstances", - "PermsGetTables", - "PermsGetClass", + "GetExtraRoles", "ExtEnabled", "ExtDisabled", diff --git a/lib/formutils.py b/lib/formutils.py new file mode 100644 index 000000000..a193a6f4f --- /dev/null +++ b/lib/formutils.py @@ -0,0 +1,165 @@ +# coding: utf-8 +import re +from django.utils.translation import ugettext as _ +from django import forms +from django.forms.widgets import MultiWidget, TextInput +from django.forms.fields import MultiValueField +from django.core.exceptions import ValidationError + +class CreationWizard(object): + def __init__(self, done_cb): + self.steps = [] + self.done_cb = done_cb + + def add_step(self, cls, buttons, new_args=None): + self.steps += [dict(cls=cls, buttons=buttons, new_args=new_args)] + + def create_forms(self, data=None): + for step in self.steps: + args = [] + if step.has_key("new_args") and step["new_args"]: + args += step["new_args"] + if data: + args.append(data) + step["form"] = step["cls"](*args) + + def validate_step(self, request): + stepid = request.POST.get("stepid", None) + if stepid is None: + return -1, _("Invalid request") + + stepid = stepid.replace("step", "") + stepid = int(stepid) - 1 + if stepid < 0 or stepid >= len(self.steps): + return -1, _("Invalid request") + self.create_forms(request.POST) + if self.steps[stepid]["form"].is_valid(): + if stepid == len(self.steps) - 1: + self.done_cb(self.steps) + return 2, None + return 1, None + return 0, stepid + +class DynamicForm(object): + + def _create_field(self, typ, name, value=None, pos=None): + self.fields[name] = typ(label="", required=False) + if value is not None: + self.fields[name].initial = value + if pos: + self.fields.keyOrder.remove(name) + self.fields.keyOrder.insert(pos, name) + + def _load_from_qdict(self, qdict, pattern, typ): + expr = re.compile(r'%s_\d+' % pattern) + values = [] + for k, v in qdict.iteritems(): + if k == pattern or expr.match(k): + values.append((k, v)) + + ndata = self.data.copy() + values.reverse() + for v in values: + if self.fields.has_key(v[0]): + continue + self._create_field(typ, v[0]) + ndata[v[0]] = v[1] + self.data = ndata + +class TabForms(object): + """ + Simple forms container + + This class tries to encapsulate multiple forms that will be + displayed using tabs. It is different from a classical formset + because it can contain different forms. + """ + def __init__(self, data=None, instances=None, classes=None): + to_remove = [] + for fd in self.forms: + args = [] + kwargs = {} + if fd.has_key("new_args"): + args += fd["new_args"] + if data: + args.append(data) + if instances: + if hasattr(self, "check_%s" % fd["id"]): + if not getattr(self, "check_%s" % fd["id"])(instances[fd["id"]]): + to_remove += [fd] + continue + kwargs["instance"] = instances[fd["id"]] + if classes and classes.has_key(fd["id"]): + fd["instance"] = classes[fd["id"]](*args, **kwargs) + else: + fd["instance"] = fd["cls"](*args, **kwargs) + map(lambda fd: self.forms.remove(fd), to_remove) + self.active_id = self.forms[0]["id"] + + def _before_is_valid(self, form): + return True + + def is_valid(self, mandatory_only=False, optional_only=False): + to_remove = [] + for f in self.forms: + if mandatory_only and \ + (not f.has_key("mandatory") or not f["mandatory"]): + continue + elif optional_only and \ + (f.has_key("mandatory") and f["mandatory"]): + continue + if not self._before_is_valid(f): + to_remove.append(f) + continue + if not f["instance"].is_valid(): + self.active_id = f["id"] + return False + map(lambda fd: self.forms.remove(fd), to_remove) + return True + + def save(self, *args, **kwargs): + raise RuntimeError + + def remove_tab(self, tabid): + for f in self.forms: + if f["id"] == tabid: + self.forms.remove(f) + break + + def __iter__(self): + return self.forward() + + def forward(self): + for form in self.forms: + yield form + +# +# Custom fields from here +# + +def is_valid_host(host): + """IDN compatible domain validator + """ + host = host.encode('idna').lower() + if not hasattr(is_valid_host, '_re'): + import re + is_valid_host._re = re.compile(r'^([0-9a-z][-\w]*[0-9a-z]\.)+[a-z0-9\-]{2,15}$') + return bool(is_valid_host._re.match(host)) + +def validate_domain_name(value): + if not is_valid_host(value): + raise ValidationError(_('Enter a valid domain name'), 'invalid') + +class DomainNameField(forms.CharField): + """ + A subclass of CharField that only accepts a valid domain name. + """ + default_error_messages = { + 'invalid' : _('Enter a valid domain name') + } + + default_validators = [validate_domain_name] + + def clean(self, value): + value = self.to_python(value).strip() + return super(DomainNameField, self).clean(value) diff --git a/lib/permissions.py b/lib/permissions.py new file mode 100644 index 000000000..85114c25d --- /dev/null +++ b/lib/permissions.py @@ -0,0 +1,118 @@ +# coding: utf-8 +from functools import wraps +from django.contrib.contenttypes.models import ContentType +from django.utils.translation import ugettext as _ +from modoboa.admin.models import ObjectAccess +import events + +def get_content_type(obj): + """Simple function that use the right method to retrieve a content type + + :param obj: a django model + :return: a ``ContentType`` object + """ + return obj.get_content_type() if hasattr(obj, "get_content_type") \ + else ContentType.objects.get_for_model(obj) + +def get_account_roles(user): + """Return the list of supported account roles + + This list can be extended by extensions which listen to the + ``GetExtraRoles`` event. + + :return: list of strings + """ + std_roles = [("SimpleUsers", _("Simple user"))] + if user.is_superuser: + std_roles += [("SuperAdmins", _("Super administrator"))] + if user.has_perm("admin.add_domain"): + std_roles += [("DomainAdmins", _("Domain administrator"))] + std_roles += events.raiseQueryEvent("GetExtraRoles", user) + return sorted(std_roles, key=lambda role: role[1]) + +def grant_access_to_object(user, obj, is_owner=False): + """Grant access to an object for a given user + + There are two different cases where we want to grant access to an + object for a specific user: + + * He is the owner (he's just created the object) + * He is going to administrate the object (but he is not the owner) + + :param user: a ``User`` object + :param obj: an admin. object (Domain, Mailbox, ...) + :param is_owner: the user is the unique object's owner + """ + ct = get_content_type(obj) + try: + entry = user.objectaccess_set.get(content_type=ct, object_id=obj.id) + entry.is_owner = is_owner + entry.save() + except ObjectAccess.DoesNotExist: + pass + else: + return + + try: + objaccess = ObjectAccess(user=user, content_type=ct, + object_id=obj.id, is_owner=is_owner) + objaccess.save() + except IntegrityError, e: + raise AdminError(_("Failed to grant access (%s)" % str(e))) + +def ungrant_access_to_object(obj, user=None): + """Ungrant access to an object for a specific user + + If no user is provided, all entries referencing this object are + deleted from the database. + + :param obj: an object inheriting from ``models.Model`` + :param user: a ``User`` object + """ + ct = get_content_type(obj) + if user: + try: + ObjectAccess.objects.get(user=user, content_type=ct, object_id=obj.id).delete() + except ObjectAccess.DoesNotExist: + pass + else: + ObjectAccess.objects.filter(content_type=ct, object_id=obj.id).delete() + +def get_object_owner(obj): + """Return the unique owner of this object + + :param obj: an object inheriting from ``model.Model`` + :return: a ``User`` object + """ + ct = get_content_type(obj) + try: + entry = ObjectAccess.objects.get(content_type=ct, object_id=obj.id, is_owner=True) + except ObjectAccess.DoesNotExist: + return None + return entry.user + +def check_domain_access(f): + """Decorator to check if the current user can access a domain + + This decorator only applies to url that contain a 'domid' or + 'mbid' parameter. (for a direct access) + + Raise a ``PermDeniedError`` if the current user can't view the + requested object. + """ + @wraps(f) + def dec(request, **kwargs): + if not request.user.is_superuser: + domain = None + domid = request.GET.get("domid", None) + if domid: + domain = Domain.objects.get(pk=domid) + else: + mbid = request.GET.get("mbid", None) + if mbid: + domain = Mailbox.objects.get(pk=mbid).domain + if domain and not request.user.can_access(domain): + raise PermDeniedError(_("You can't access this domain")) + + return f(request, **kwargs) + return dec diff --git a/lib/tables.py b/lib/tables.py index a40a942f3..7b8e295ca 100644 --- a/lib/tables.py +++ b/lib/tables.py @@ -155,13 +155,15 @@ class Table(object): self.rows += [nrow] trcpt += 1 - def _rows_from_model(self, objects): + def _rows_from_model(self, objects, include_type_in_id=False): rows = [] for obj in objects: nrow = {} try: idkey = getattr(self, "idkey") nrow[idkey] = getattr(obj, idkey) + if include_type_in_id: + nrow[idkey] = "%s:%s" % (obj.__class__.__name__, nrow[idkey]) except AttributeError: pass for c in self.columns: @@ -169,10 +171,11 @@ class Table(object): nrow[c.name] = getattr(obj, c.name) except AttributeError: pass - try: - nrow["options"] = getattr(self, "rowoptions")(self.request, obj) - except AttributeError: - pass + for meth in ["options", "class"]: + try: + nrow[meth] = getattr(self, "row_%s" % meth)(self.request, obj) + except AttributeError: + pass rows += [nrow] return rows @@ -188,7 +191,7 @@ class Table(object): def render(self): if len(self.rows): t = Template(""" - +
{% include "common/table_head.html" %} {% include "common/table_body.html" %}
diff --git a/lib/templatetags/libextras.py b/lib/templatetags/libextras.py index ba083a0cf..b6336bb05 100644 --- a/lib/templatetags/libextras.py +++ b/lib/templatetags/libextras.py @@ -1,4 +1,4 @@ -# -*- coding: utf-8 -*- +# coding: utf-8 import os from django.conf import settings from django import template @@ -44,15 +44,20 @@ def display_messages(msgs): for m in msgs: level = m.tags text += unicode(m) + "\\\n" - + + if level == "info": + level = "success" + timeout = "2000" + else: + timeout = "undefined" + return """ -""" % (level, text) +""" % (level, text, timeout) @register.simple_tag def load_optionalmenu(user): @@ -61,41 +66,53 @@ def load_optionalmenu(user): {"entries" : menu, "user" : user}) @register.simple_tag -def user_menu(user): - entries = [ - {"name" : "user", - "img" : static_url('pics/administrators.png'), - "label" : user.fullname, - "class" : "topdropdown", - "menu" : [ - {"name" : "changepwd", - "url" : reverse("modoboa.userprefs.views.changepassword"), - "img" : static_url("pics/edit.png"), - "label" : _("Change password"), - "class" : "boxed", - "rel" : "{handler:'iframe',size:{x:360,y:200},closeBtn:true}"}, - {"name" : "preferences", - "img" : static_url("pics/user.png"), - "label" : _("Preferences"), - "url" : reverse("modoboa.userprefs.views.preferences")}, - {"name" : "logout", - "url" : reverse("modoboa.auth.views.dologout"), - "label" : _("Logout"), - "img" : static_url("pics/logout.png")} - ] - } - ] - if user.belongs_to_group('SimpleUsers'): - entries[0]["menu"] += [{ - "name" : "setforwards", - "url" : reverse("modoboa.userprefs.views.setforward"), - "img" : static_url("pics/alias.png"), - "label" : _("Forward"), - "class" : "boxed", - "rel" : "{handler:'iframe',size:{x:360,y:350},closeBtn:true}" - }] +def render_form(form, tpl=None): + if tpl is not None: + return render_to_string(tpl, dict(form=form)) - entries[0]["menu"] += events.raiseQueryEvent("UserMenuDisplay", "options_menu", user) + ret = "" + for field in form: + ret += "%s\n" % render_field(field) + return ret - return render_to_string("common/menulist.html", - {"entries" : entries, "user" : user}) +@register.simple_tag +def render_field(field): + return render_to_string("common/generic_field.html", dict( + field=field + )) + +@register.simple_tag +def render_fields_group(form, pattern): + from django.forms import forms + + first = forms.BoundField(form, form.fields[pattern], pattern) + label = first.label + group = [first] + cpt = 1 + haserror = len(first.errors) != 0 + while True: + fname = "%s_%d" % (pattern, cpt) + if not form.fields.has_key(fname): + break + bfield = forms.BoundField(form, form.fields[fname], fname) + if len(bfield.errors): + haserror = True + group += [bfield] + cpt += 1 + + return render_to_string("common/generic_fields_group.html", dict( + label=label, group=group, haserror=haserror + )) + +@register.simple_tag +def pagination_bar(page): + return render_to_string("common/pagination_bar.html", dict( + page=page + )) + +@register.simple_tag +def alert(msg, typ): + return """
+× +%s +
""" % (typ, msg) diff --git a/lib/webutils.py b/lib/webutils.py index ef99251eb..8c310f6ce 100644 --- a/lib/webutils.py +++ b/lib/webutils.py @@ -37,9 +37,11 @@ def _render_error(request, errortpl="error", user_context={}): def render_actions(actions): t = template.Template(""" {% for a in actions %} - - + {% endfor %} """) return t.render(template.Context({ diff --git a/static/css/Autocompleter.css b/static/css/Autocompleter.css deleted file mode 100644 index 99a3c727e..000000000 --- a/static/css/Autocompleter.css +++ /dev/null @@ -1,50 +0,0 @@ -ul.autocompleter-choices -{ - position: absolute; - margin: 0; - padding: 0; - list-style: none; - border: 1px solid #7c7c7c; - border-left-color: #c3c3c3; - border-right-color: #c3c3c3; - border-bottom-color: #ddd; - background-color: #fff; - text-align: left; - font-family: Verdana, Geneva, Arial, Helvetica, sans-serif; - z-index: 50; - background-color: #fff; -} - -ul.autocompleter-choices li -{ - position: relative; - margin: -2px 0 0 0; - padding: 0.2em 1.5em 0.2em 1em; - display: block; - float: none !important; - cursor: pointer; - font-weight: normal; - white-space: nowrap; - font-size: 1em; - line-height: 1.5em; -} - -ul.autocompleter-choices li.autocompleter-selected -{ - background-color: #444; - color: #fff; -} - -ul.autocompleter-choices span.autocompleter-queried -{ - display: inline; - float: none; - font-weight: bold; - margin: 0; - padding: 0; -} - -ul.autocompleter-choices li.autocompleter-selected span.autocompleter-queried -{ - color: #9FCFFF; -} \ No newline at end of file diff --git a/static/css/SqueezeBox.css b/static/css/SqueezeBox.css deleted file mode 100644 index df77cf907..000000000 --- a/static/css/SqueezeBox.css +++ /dev/null @@ -1,141 +0,0 @@ -/** - * SqueezeBox - Expandable Lightbox - * - * Allows to open various content as modal, - * centered and animated box. - * - * @version 1.1 rc4 - * - * @license MIT-style license - * @author Harald Kirschner - * @copyright Author - */ - -#sbox-overlay { - position: absolute; - background-color: #000; - left: 0px; - top: 0px; - zoom: 1; -} - -#sbox-window { - position: absolute; - background-color: #fff; - text-align: left; - overflow: visible; - padding: 10px; - /* invalid values, but looks smoother! */ - -moz-border-radius: 3px; - -webkit-border-radius: 3px; -} - -#sbox-btn-close { - position: absolute; - width: 30px; - height: 30px; - right: -15px; - top: -15px; - background: url(closebox.png) no-repeat center; - border: none; -} - -.sbox-window-ie6 #sbox-btn-close { - background-image: url(closebox.gif); -} - -.sbox-loading #sbox-content { - background-image: url(spinner.gif); - background-repeat: no-repeat; - background-position: center; -} - -#sbox-content { - clear: both; - overflow: auto; - background-color: #fff; - height: 100%; - width: 100%; -} - -.sbox-content-image#sbox-content { - overflow: visible; -} - -#sbox-image { - display: block; -} - -.sbox-content-image img { - display: block; - width: 100%; - height: 100%; -} - -.sbox-content-iframe#sbox-content { - overflow: visible; -} - -/* Hides scrollbars */ -.body-overlayed { - overflow: hidden; -} -/* Hides flash (Firefox problem) and selects (IE) */ -.body-overlayed embed, .body-overlayed object, .body-overlayed select { - visibility: hidden; -} -#sbox-window embed, #sbox-window object, #sbox-window select { - visibility: visible; -} - -/* Shadows */ -.sbox-bg { - position: absolute; - width: 33px; - height: 40px; -} - -.sbox-bg-n { - left: 0; - top: -40px; - width: 100%; - background: url(bg_n.png) repeat-x; -} -.sbox-bg-ne { - right: -33px; - top: -40px; - background: url(bg_ne.png) no-repeat; -} -.sbox-bg-e { - right: -33px; - top: 0; - height: 100%; - background: url(bg_e.png) repeat-y; -} -.sbox-bg-se { - right: -33px; - bottom: -40px; - background: url(bg_se.png) no-repeat; -} -.sbox-bg-s { - left: 0; - bottom: -40px; - width: 100%; - background: url(bg_s.png) repeat-x; -} -.sbox-bg-sw { - left: -33px; - bottom: -40px; - background: url(bg_sw.png) no-repeat; -} -.sbox-bg-w { - left: -33px; - top: 0; - height: 100%; - background: url(bg_w.png) repeat-y; -} -.sbox-bg-nw { - left: -33px; - top: -40px; - background: url(bg_nw.png) no-repeat; -} \ No newline at end of file diff --git a/static/css/bg_e.png b/static/css/bg_e.png deleted file mode 100644 index 17811a0ff99eb0730e4eab5ea55b3c2c6adad61c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 990 zcmV<410np0P)U8P*7-ZbZ>KLZ*U+lnSp_Ufq@}0xwybFAi#%#fq@|}KQEO56)-X|e7nZL z$iTqBa9P*U#mSX{G{Bl%P*lRez;J+pfx##xwK$o9f#C}S14DXwNkIt%17i#W1A|CX zc0maP17iUL1A|C*NRTrF17iyV0~1e4YDEbH0|SF|enDkXW_m`6f}y3QrGjHhep0GJ zaAk2xYHqQDXI^rCQ9*uDVo7QW0|Nup4h9AW240u^5(W3f%sd4n162kpgNVo|1qcff zJ_s=cNG>fZg9jx8g8+j9g8_pBLjXe}Lp{R+hNBE`7{wV~7)u#fFy3PlV+vxLz;uCG zm^qSpA@ds+OO_6nTdaDlt*rOhEZL^9ePa)2-_4=K(Z%tFGm-NGmm}8}ZcXk5JW@PU zd4+f<@d@)yL(o<5icqT158+-B6_LH7;i6x}CW#w~Uy-Pgl#@Irl`kzV zeL|*8R$ca%T%Wv){2zs_iiJvgN^h0dsuZZ2sQy$tsNSU!s;Q*;LF<6_B%M@UD?LHI zSNcZ`78uqV#TeU~$eS{ozBIdFzSClfs*^S+dw;4dus<{M;#|MXC)T}S9v!D zcV!QCPhBq)ZyO(X-(bH4|NMaZz==UigLj2o41F2S6d@OB6%`R(5i>J(Puzn9wnW{e zu;hl6HK{k#IWjCVGqdJqU(99Cv(K+6*i`tgSi2;vbXD1#3jNBGs$DgVwO(~o>mN4i zHPtkqZIx>)Y(Ls5-Br|mx>vQYvH$Kwn@O`L|D75??eGkZnfg$5<;Xeg_o%+-I&+-3%01W^SH2RkDT>t<81ZP1_K>z@;j(q!3lK=n! zAY({UO#lFTB>(_`g8%^e{{R4h=>PzAFaQARU;qF*m;eA5Z<1fdMgRZ+FiAu~RCwA{ z#jy>*Fc1SV#!Gqsy4P)U8P*7-ZbZ>KLZ*U+lnSp_Ufq@}0xwybFAi#%#fq@|}KQEO56)-X|e7nZL z$iTqBa9P*U#mSX{G{Bl%P*lRez;J+pfx##xwK$o9f#C}S14DXwNkIt%17i#W1A|CX zc0maP17iUL1A|C*NRTrF17iyV0~1e4YDEbH0|SF|enDkXW_m`6f}y3QrGjHhep0GJ zaAk2xYHqQDXI^rCQ9*uDVo7QW0|Nup4h9AW240u^5(W3f%sd4n162kpgNVo|1qcff zJ_s=cNG>fZg9jx8g8+j9g8_pBLjXe}Lp{R+hNBE`7{wV~7)u#fFy3PlV+vxLz;uCG zm^qSpA@ds+OO_6nTdaDlt*rOhEZL^9ePa)2-_4=K(Z%tFGm-NGmm}8}ZcXk5JW@PU zd4+f<@d@)yL(o<5icqT158+-B6_LH7;i6x}CW#w~Uy-Pgl#@Irl`kzV zeL|*8R$ca%T%Wv){2zs_iiJvgN^h0dsuZZ2sQy$tsNSU!s;Q*;LF<6_B%M@UD?LHI zSNcZ`78uqV#TeU~$eS{ozBIdFzSClfs*^S+dw;4dus<{M;#|MXC)T}S9v!D zcV!QCPhBq)ZyO(X-(bH4|NMaZz==UigLj2o41F2S6d@OB6%`R(5i>J(Puzn9wnW{e zu;hl6HK{k#IWjCVGqdJqU(99Cv(K+6*i`tgSi2;vbXD1#3jNBGs$DgVwO(~o>mN4i zHPtkqZIx>)Y(Ls5-Br|mx>vQYvH$Kwn@O`L|D75??eGkZnfg$5<;Xeg_o%+-I&+-3%01W^SH2RkDT>t<81ZP1_K>z@;j(q!3lK=n! zAY({UO#lFTB>(_`g8%^e{{R4h=>PzAFaQARU;qF*m;eA5Z<1fdMgRZ+EJ;K`RCwBA zU|?ioV1OY;7ztr8A#oX*7?@BQa1xFfnHZQ+MVK*22;(nc03Htkhoq}o8~^|S07*qo IM6N<$g2URD1ONa4 diff --git a/static/css/bg_ne.png b/static/css/bg_ne.png deleted file mode 100644 index 92f8d5e0e6e425d606a6666f26580e138149e2b6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1367 zcmV-d1*rOoP)U8P*7-ZbZ>KLZ*U+lnSp_Ufq@}0xwybFAi#%#fq@|}KQEO56)-X|e7nZL z$iTqBa9P*U#mSX{G{Bl%P*lRez;J+pfx##xwK$o9f#C}S14DXwNkIt%17i#W1A|CX zc0maP17iUL1A|C*NRTrF17iyV0~1e4YDEbH0|SF|enDkXW_m`6f}y3QrGjHhep0GJ zaAk2xYHqQDXI^rCQ9*uDVo7QW0|Nup4h9AW240u^5(W3f%sd4n162kpgNVo|1qcff zJ_s=cNG>fZg9jx8g8+j9g8_pBLjXe}Lp{R+hNBE`7{wV~7)u#fFy3PlV+vxLz;uCG zm^qSpA@ds+OO_6nTdaDlt*rOhEZL^9ePa)2-_4=K(Z%tFGm-NGmm}8}ZcXk5JW@PU zd4+f<@d@)yL(o<5icqT158+-B6_LH7;i6x}CW#w~Uy-Pgl#@Irl`kzV zeL|*8R$ca%T%Wv){2zs_iiJvgN^h0dsuZZ2sQy$tsNSU!s;Q*;LF<6_B%M@UD?LHI zSNcZ`78uqV#TeU~$eS{ozBIdFzSClfs*^S+dw;4dus<{M;#|MXC)T}S9v!D zcV!QCPhBq)ZyO(X-(bH4|NMaZz==UigLj2o41F2S6d@OB6%`R(5i>J(Puzn9wnW{e zu;hl6HK{k#IWjCVGqdJqU(99Cv(K+6*i`tgSi2;vbXD1#3jNBGs$DgVwO(~o>mN4i zHPtkqZIx>)Y(Ls5-Br|mx>vQYvH$Kwn@O`L|D75??eGkZnfg$5<;Xeg_o%+-I&+-3%01W^SH2RkDT>t<81ZP1_K>z@;j(q!3lK=n! zAY({UO#lFTB>(_`g8%^e{{R4h=>PzAFaQARU;qF*m;eA5Z<1fdMgRZ-sYygZRCwB~ z)7MtRFc3x2W8(DQdm*HdLJEWh|Nmt?SaKvwvK?UbzSq4oHhBDbJmdWX@i6A4cnR~A z3i0BJ`}-x65icEN9UmXxfF448<)vec$&w{&Fqv2;GC6YOB2aP~ZSv#=lXEf&M1ewt z(MzR3iWDPEG>aWkrbNlgJb;ubQ>G-4f+dR!38z9e!pJNp;vj0&s8X?JNr}{{Q=@7y zX%I0DnjsU1#1Ji7G--rcY=|p|m^K~SsS#_AE?qjbX^BEQl2itOK7A*WoCAOXL;CdS z(lHRTOpqf&j2JUyU?3G`BkmjkOqg1bnuh2(3}Vha0g+{#RTeyJNU+#OxDk5}jac#$ zgWPk(R9UgskUl-FjJ!;c!>j^q*a~FEL?FRN;&Uj(jy+q}ELkvP!bq$&C=<>{^8&=vz^c=xmWEFLxz=bPs?0IDqZp4~J z4$H_K_q&wb#Enm`oH!Uc)VruxF^Dg|xp5JQyv&%P_{j7sRzjvYDl1jDA-@j&Ur}!I ZGXUJ4D$Lp#gM0u0002ovPDHLkV1ff~NDKe~ diff --git a/static/css/bg_nw.png b/static/css/bg_nw.png deleted file mode 100644 index bc7ed49278569223642917b11bf5f295a3d2faf4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1246 zcmV<41R?v0P)U8P*7-ZbZ>KLZ*U+lnSp_Ufq@}0xwybFAi#%#fq@|}KQEO56)-X|e7nZL z$iTqBa9P*U#mSX{G{Bl%P*lRez;J+pfx##xwK$o9f#C}S14DXwNkIt%17i#W1A|CX zc0maP17iUL1A|C*NRTrF17iyV0~1e4YDEbH0|SF|enDkXW_m`6f}y3QrGjHhep0GJ zaAk2xYHqQDXI^rCQ9*uDVo7QW0|Nup4h9AW240u^5(W3f%sd4n162kpgNVo|1qcff zJ_s=cNG>fZg9jx8g8+j9g8_pBLjXe}Lp{R+hNBE`7{wV~7)u#fFy3PlV+vxLz;uCG zm^qSpA@ds+OO_6nTdaDlt*rOhEZL^9ePa)2-_4=K(Z%tFGm-NGmm}8}ZcXk5JW@PU zd4+f<@d@)yL(o<5icqT158+-B6_LH7;i6x}CW#w~Uy-Pgl#@Irl`kzV zeL|*8R$ca%T%Wv){2zs_iiJvgN^h0dsuZZ2sQy$tsNSU!s;Q*;LF<6_B%M@UD?LHI zSNcZ`78uqV#TeU~$eS{ozBIdFzSClfs*^S+dw;4dus<{M;#|MXC)T}S9v!D zcV!QCPhBq)ZyO(X-(bH4|NMaZz==UigLj2o41F2S6d@OB6%`R(5i>J(Puzn9wnW{e zu;hl6HK{k#IWjCVGqdJqU(99Cv(K+6*i`tgSi2;vbXD1#3jNBGs$DgVwO(~o>mN4i zHPtkqZIx>)Y(Ls5-Br|mx>vQYvH$Kwn@O`L|D75??eGkZnfg$5<;Xeg_o%+-I&+-3%01W^SH2RkDT>t<81ZP1_K>z@;j(q!3lK=n! zAY({UO#lFTB>(_`g8%^e{{R4h=>PzAFaQARU;qF*m;eA5Z<1fdMgRZ-FiAu~RCwB? zmD>`7APhvaf&c&H+6PO?Wdk_YX?3(Y*=&M{y!`;rL$)_g1J4n7ML$$ndhtLVUT8!8 zXKcgW8OH!n2noQWaH0Ym@nFz?=2L8p^#FfwqXH>w&JW)a z90BHcfK;x&bRq(L{`-u+2xb5q{`>8R{m6j(iWUON)8YYYApb(@q2ho6Q9)M0=0FHQ zql(pnw@NZVnVle^44@nVqG4htU_ut7g)oWJ3lI1quytWpJ^=$ETFeE=fLYW4sP719 zDX{>nL$eoHc12RSGCQjUj^M(;v{gs4k=%=`Z}%*_I@KUZAs`Ek_d*RM6atDtI&^I( zcSGoSp+Zy}U!~;g00009a7bBm000XU z000XU0RWnu7ytkR7->U8P*7-ZbZ>KLZ*U+lnSp_Ufq@}0xwybFAi#%#fq@|}KQEO56)-X|e7nZL z$iTqBa9P*U#mSX{G{Bl%P*lRez;J+pfx##xwK$o9f#C}S14DXwNkIt%17i#W1A|CX zc0maP17iUL1A|C*NRTrF17iyV0~1e4YDEbH0|SF|enDkXW_m`6f}y3QrGjHhep0GJ zaAk2xYHqQDXI^rCQ9*uDVo7QW0|Nup4h9AW240u^5(W3f%sd4n162kpgNVo|1qcff zJ_s=cNG>fZg9jx8g8+j9g8_pBLjXe}Lp{R+hNBE`7{wV~7)u#fFy3PlV+vxLz;uCG zm^qSpA@ds+OO_6nTdaDlt*rOhEZL^9ePa)2-_4=K(Z%tFGm-NGmm}8}ZcXk5JW@PU zd4+f<@d@)yL(o<5icqT158+-B6_LH7;i6x}CW#w~Uy-Pgl#@Irl`kzV zeL|*8R$ca%T%Wv){2zs_iiJvgN^h0dsuZZ2sQy$tsNSU!s;Q*;LF<6_B%M@UD?LHI zSNcZ`78uqV#TeU~$eS{ozBIdFzSClfs*^S+dw;4dus<{M;#|MXC)T}S9v!D zcV!QCPhBq)ZyO(X-(bH4|NMaZz==UigLj2o41F2S6d@OB6%`R(5i>J(Puzn9wnW{e zu;hl6HK{k#IWjCVGqdJqU(99Cv(K+6*i`tgSi2;vbXD1#3jNBGs$DgVwO(~o>mN4i zHPtkqZIx>)Y(Ls5-Br|mx>vQYvH$Kwn@O`L|D75??eGkZnfg$5<;Xeg_o%+-I&+-3%01W^SH2RkDT>t<81ZP1_K>z@;j(q!3lK=n! zAY({UO#lFTB>(_`g8%^e{{R4h=>PzAFaQARU;qF*m;eA5Z<1fdMgRZ+D@jB_RCwBA zU@&H4U}j?Y%f#@PiQykA`47hakHr0l#QhIr|HpU8P*7-ZbZ>KLZ*U+lnSp_Ufq@}0xwybFAi#%#fq@|}KQEO56)-X|e7nZL z$iTqBa9P*U#mSX{G{Bl%P*lRez;J+pfx##xwK$o9f#C}S14DXwNkIt%17i#W1A|CX zc0maP17iUL1A|C*NRTrF17iyV0~1e4YDEbH0|SF|enDkXW_m`6f}y3QrGjHhep0GJ zaAk2xYHqQDXI^rCQ9*uDVo7QW0|Nup4h9AW240u^5(W3f%sd4n162kpgNVo|1qcff zJ_s=cNG>fZg9jx8g8+j9g8_pBLjXe}Lp{R+hNBE`7{wV~7)u#fFy3PlV+vxLz;uCG zm^qSpA@ds+OO_6nTdaDlt*rOhEZL^9ePa)2-_4=K(Z%tFGm-NGmm}8}ZcXk5JW@PU zd4+f<@d@)yL(o<5icqT158+-B6_LH7;i6x}CW#w~Uy-Pgl#@Irl`kzV zeL|*8R$ca%T%Wv){2zs_iiJvgN^h0dsuZZ2sQy$tsNSU!s;Q*;LF<6_B%M@UD?LHI zSNcZ`78uqV#TeU~$eS{ozBIdFzSClfs*^S+dw;4dus<{M;#|MXC)T}S9v!D zcV!QCPhBq)ZyO(X-(bH4|NMaZz==UigLj2o41F2S6d@OB6%`R(5i>J(Puzn9wnW{e zu;hl6HK{k#IWjCVGqdJqU(99Cv(K+6*i`tgSi2;vbXD1#3jNBGs$DgVwO(~o>mN4i zHPtkqZIx>)Y(Ls5-Br|mx>vQYvH$Kwn@O`L|D75??eGkZnfg$5<;Xeg_o%+-I&+-3%01W^SH2RkDT>t<81ZP1_K>z@;j(q!3lK=n! zAY({UO#lFTB>(_`g8%^e{{R4h=>PzAFaQARU;qF*m;eA5Z<1fdMgRZ-K1oDDRCwB) zm1~lOAPj^*)cfrH@3zqML&YQn#$73EtPwi>2w3=GqCf&CIf(tosEq` zp_r$80R0!Zm)Yx}QhLp5YPVReKLwcW5)hZRK2_#Uc^ovjmRlG)9EM3E8 z<93Pwi>`P=^&ljhEaRYu1^)tYeGw0q8Ne4Iv67M)Cy8Xl^Ja!e6@ao(-!D%S;l-Y0es=!7c6hr}zBkYMQ?&OF{|nXmoX-H$ W>9(`PKkg*}0000U8P*7-ZbZ>KLZ*U+lnSp_Ufq@}0xwybFAi#%#fq@|}KQEO56)-X|e7nZL z$iTqBa9P*U#mSX{G{Bl%P*lRez;J+pfx##xwK$o9f#C}S14DXwNkIt%17i#W1A|CX zc0maP17iUL1A|C*NRTrF17iyV0~1e4YDEbH0|SF|enDkXW_m`6f}y3QrGjHhep0GJ zaAk2xYHqQDXI^rCQ9*uDVo7QW0|Nup4h9AW240u^5(W3f%sd4n162kpgNVo|1qcff zJ_s=cNG>fZg9jx8g8+j9g8_pBLjXe}Lp{R+hNBE`7{wV~7)u#fFy3PlV+vxLz;uCG zm^qSpA@ds+OO_6nTdaDlt*rOhEZL^9ePa)2-_4=K(Z%tFGm-NGmm}8}ZcXk5JW@PU zd4+f<@d@)yL(o<5icqT158+-B6_LH7;i6x}CW#w~Uy-Pgl#@Irl`kzV zeL|*8R$ca%T%Wv){2zs_iiJvgN^h0dsuZZ2sQy$tsNSU!s;Q*;LF<6_B%M@UD?LHI zSNcZ`78uqV#TeU~$eS{ozBIdFzSClfs*^S+dw;4dus<{M;#|MXC)T}S9v!D zcV!QCPhBq)ZyO(X-(bH4|NMaZz==UigLj2o41F2S6d@OB6%`R(5i>J(Puzn9wnW{e zu;hl6HK{k#IWjCVGqdJqU(99Cv(K+6*i`tgSi2;vbXD1#3jNBGs$DgVwO(~o>mN4i zHPtkqZIx>)Y(Ls5-Br|mx>vQYvH$Kwn@O`L|D75??eGkZnfg$5<;Xeg_o%+-I&+-3%01W^SH2RkDT>t<81ZP1_K>z@;j(q!3lK=n! zAY({UO#lFTB>(_`g8%^e{{R4h=>PzAFaQARU;qF*m;eA5Z<1fdMgRZ-8A(JzRCwBq zl-m-7APhqjy8r*lb{}-Kx1j9kfXZo-1qkA79DfT7!~$X0$Uqnj3xg3kXW0oh!MFV5ym>qm(;)ZX&oG zSxqRmqatc-@pC3a!3czvI(_~}BqD>(y9-D{=vqBWVMOVutzZ>msy9Q53XJSI+zC+|`-j-Qv*7zJQzpFBwDgI=k5g1R9Xk%1??WTH8wi4$a;sfq61dXWTbHG8% zjOeRk>UlQu4Kn59lnAASwP)j^#js1dmMdm%5opKWFfy@ul=~sEcf>1(z$b`aTrxs@ l*Te0Hczw%v_a9LI0RR*MBZzn-LRSC)002ovPDHLkV1lNk3T6NR diff --git a/static/css/bg_w.png b/static/css/bg_w.png deleted file mode 100644 index 71c89d65d02bec67592979c838287f316fa89f78..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 981 zcmV;`11kK9P)U8P*7-ZbZ>KLZ*U+lnSp_Ufq@}0xwybFAi#%#fq@|}KQEO56)-X|e7nZL z$iTqBa9P*U#mSX{G{Bl%P*lRez;J+pfx##xwK$o9f#C}S14DXwNkIt%17i#W1A|CX zc0maP17iUL1A|C*NRTrF17iyV0~1e4YDEbH0|SF|enDkXW_m`6f}y3QrGjHhep0GJ zaAk2xYHqQDXI^rCQ9*uDVo7QW0|Nup4h9AW240u^5(W3f%sd4n162kpgNVo|1qcff zJ_s=cNG>fZg9jx8g8+j9g8_pBLjXe}Lp{R+hNBE`7{wV~7)u#fFy3PlV+vxLz;uCG zm^qSpA@ds+OO_6nTdaDlt*rOhEZL^9ePa)2-_4=K(Z%tFGm-NGmm}8}ZcXk5JW@PU zd4+f<@d@)yL(o<5icqT158+-B6_LH7;i6x}CW#w~Uy-Pgl#@Irl`kzV zeL|*8R$ca%T%Wv){2zs_iiJvgN^h0dsuZZ2sQy$tsNSU!s;Q*;LF<6_B%M@UD?LHI zSNcZ`78uqV#TeU~$eS{ozBIdFzSClfs*^S+dw;4dus<{M;#|MXC)T}S9v!D zcV!QCPhBq)ZyO(X-(bH4|NMaZz==UigLj2o41F2S6d@OB6%`R(5i>J(Puzn9wnW{e zu;hl6HK{k#IWjCVGqdJqU(99Cv(K+6*i`tgSi2;vbXD1#3jNBGs$DgVwO(~o>mN4i zHPtkqZIx>)Y(Ls5-Br|mx>vQYvH$Kwn@O`L|D75??eGkZnfg$5<;Xeg_o%+-I&+-3%01W^SH2RkDT>t<81ZP1_K>z@;j(q!3lK=n! zAY({UO#lFTB>(_`g8%^e{{R4h=>PzAFaQARU;qF*m;eA5Z<1fdMgRZ+CrLy>RCwBA zWME`~07f*(#K43^GBYqSFhOZ%B*?<>9{>OV|NjF32J!(qOJ9xK00000NkvXXu0mjf D5LcGN diff --git a/static/css/bootstrap-responsive.css b/static/css/bootstrap-responsive.css new file mode 100644 index 000000000..2d1695537 --- /dev/null +++ b/static/css/bootstrap-responsive.css @@ -0,0 +1,4 @@ +.clearfix{*zoom:1;}.clearfix:before,.clearfix:after{display:table;content:"";} +.clearfix:after{clear:both;} +.hidden{display:none;visibility:hidden;} +@media (max-width:480px){.nav-collapse{-webkit-transform:translate3d(0, 0, 0);} .page-header h1 small{display:block;line-height:18px;} input[class*="span"],select[class*="span"],textarea[class*="span"],.uneditable-input{display:block;width:100%;min-height:28px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;box-sizing:border-box;} .input-prepend input[class*="span"],.input-append input[class*="span"]{width:auto;} input[type="checkbox"],input[type="radio"]{border:1px solid #ccc;} .form-horizontal .control-group>label{float:none;width:auto;padding-top:0;text-align:left;} .form-horizontal .controls{margin-left:0;} .form-horizontal .control-list{padding-top:0;} .form-horizontal .form-actions{padding-left:10px;padding-right:10px;} .modal{position:absolute;top:10px;left:10px;right:10px;width:auto;margin:0;}.modal.fade.in{top:auto;} .modal-header .close{padding:10px;margin:-10px;} .carousel-caption{position:static;}}@media (max-width:767px){.container{width:auto;padding:0 20px;} .row-fluid{width:100%;} .row{margin-left:0;} .row>[class*="span"],.row-fluid>[class*="span"]{float:none;display:block;width:auto;margin:0;}}@media (min-width:768px) and (max-width:979px){.row{margin-left:-20px;*zoom:1;}.row:before,.row:after{display:table;content:"";} .row:after{clear:both;} [class*="span"]{float:left;margin-left:20px;} .span1{width:42px;} .span2{width:104px;} .span3{width:166px;} .span4{width:228px;} .span5{width:290px;} .span6{width:352px;} .span7{width:414px;} .span8{width:476px;} .span9{width:538px;} .span10{width:600px;} .span11{width:662px;} .span12,.container{width:724px;} .offset1{margin-left:82px;} .offset2{margin-left:144px;} .offset3{margin-left:206px;} .offset4{margin-left:268px;} .offset5{margin-left:330px;} .offset6{margin-left:392px;} .offset7{margin-left:454px;} .offset8{margin-left:516px;} .offset9{margin-left:578px;} .offset10{margin-left:640px;} .offset11{margin-left:702px;} .row-fluid{width:100%;*zoom:1;}.row-fluid:before,.row-fluid:after{display:table;content:"";} .row-fluid:after{clear:both;} .row-fluid>[class*="span"]{float:left;margin-left:2.762430939%;} .row-fluid>[class*="span"]:first-child{margin-left:0;} .row-fluid>.span1{width:5.801104972%;} .row-fluid>.span2{width:14.364640883%;} .row-fluid>.span3{width:22.928176794%;} .row-fluid>.span4{width:31.491712705%;} .row-fluid>.span5{width:40.055248616%;} .row-fluid>.span6{width:48.618784527%;} .row-fluid>.span7{width:57.182320438000005%;} .row-fluid>.span8{width:65.74585634900001%;} .row-fluid>.span9{width:74.30939226%;} .row-fluid>.span10{width:82.87292817100001%;} .row-fluid>.span11{width:91.436464082%;} .row-fluid>.span12{width:99.999999993%;} input.span1,textarea.span1,.uneditable-input.span1{width:32px;} input.span2,textarea.span2,.uneditable-input.span2{width:94px;} input.span3,textarea.span3,.uneditable-input.span3{width:156px;} input.span4,textarea.span4,.uneditable-input.span4{width:218px;} input.span5,textarea.span5,.uneditable-input.span5{width:280px;} input.span6,textarea.span6,.uneditable-input.span6{width:342px;} input.span7,textarea.span7,.uneditable-input.span7{width:404px;} input.span8,textarea.span8,.uneditable-input.span8{width:466px;} input.span9,textarea.span9,.uneditable-input.span9{width:528px;} input.span10,textarea.span10,.uneditable-input.span10{width:590px;} input.span11,textarea.span11,.uneditable-input.span11{width:652px;} input.span12,textarea.span12,.uneditable-input.span12{width:714px;}}@media (max-width:979px){body{padding-top:0;} .navbar-fixed-top{position:static;margin-bottom:18px;} .navbar-fixed-top .navbar-inner{padding:5px;} .navbar .container{width:auto;padding:0;} .navbar .brand{padding-left:10px;padding-right:10px;margin:0 0 0 -5px;} .navbar .nav-collapse{clear:left;} .navbar .nav{float:none;margin:0 0 9px;} .navbar .nav>li{float:none;} .navbar .nav>li>a{margin-bottom:2px;} .navbar .nav>.divider-vertical{display:none;} .navbar .nav .nav-header{color:#999999;text-shadow:none;} .navbar .nav>li>a,.navbar .dropdown-menu a{padding:6px 15px;font-weight:bold;color:#999999;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;} .navbar .dropdown-menu li+li a{margin-bottom:2px;} .navbar .nav>li>a:hover,.navbar .dropdown-menu a:hover{background-color:#222222;} .navbar .dropdown-menu{position:static;top:auto;left:auto;float:none;display:block;max-width:none;margin:0 15px;padding:0;background-color:transparent;border:none;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;} .navbar .dropdown-menu:before,.navbar .dropdown-menu:after{display:none;} .navbar .dropdown-menu .divider{display:none;} .navbar-form,.navbar-search{float:none;padding:9px 15px;margin:9px 0;border-top:1px solid #222222;border-bottom:1px solid #222222;-webkit-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.1),0 1px 0 rgba(255, 255, 255, 0.1);-moz-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.1),0 1px 0 rgba(255, 255, 255, 0.1);box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.1),0 1px 0 rgba(255, 255, 255, 0.1);} .navbar .nav.pull-right{float:none;margin-left:0;} .navbar-static .navbar-inner{padding-left:10px;padding-right:10px;} .btn-navbar{display:block;} .nav-collapse{overflow:hidden;height:0;}}@media (min-width:980px){.nav-collapse.collapse{height:auto !important;}}@media (min-width:1200px){.row{margin-left:-30px;*zoom:1;}.row:before,.row:after{display:table;content:"";} .row:after{clear:both;} [class*="span"]{float:left;margin-left:30px;} .span1{width:70px;} .span2{width:170px;} .span3{width:270px;} .span4{width:370px;} .span5{width:470px;} .span6{width:570px;} .span7{width:670px;} .span8{width:770px;} .span9{width:870px;} .span10{width:970px;} .span11{width:1070px;} .span12,.container{width:1170px;} .offset1{margin-left:130px;} .offset2{margin-left:230px;} .offset3{margin-left:330px;} .offset4{margin-left:430px;} .offset5{margin-left:530px;} .offset6{margin-left:630px;} .offset7{margin-left:730px;} .offset8{margin-left:830px;} .offset9{margin-left:930px;} .offset10{margin-left:1030px;} .offset11{margin-left:1130px;} .row-fluid{width:100%;*zoom:1;}.row-fluid:before,.row-fluid:after{display:table;content:"";} .row-fluid:after{clear:both;} .row-fluid>[class*="span"]{float:left;margin-left:2.564102564%;} .row-fluid>[class*="span"]:first-child{margin-left:0;} .row-fluid>.span1{width:5.982905983%;} .row-fluid>.span2{width:14.529914530000001%;} .row-fluid>.span3{width:23.076923077%;} .row-fluid>.span4{width:31.623931624%;} .row-fluid>.span5{width:40.170940171000005%;} .row-fluid>.span6{width:48.717948718%;} .row-fluid>.span7{width:57.264957265%;} .row-fluid>.span8{width:65.81196581200001%;} .row-fluid>.span9{width:74.358974359%;} .row-fluid>.span10{width:82.905982906%;} .row-fluid>.span11{width:91.45299145300001%;} .row-fluid>.span12{width:100%;} input.span1,textarea.span1,.uneditable-input.span1{width:60px;} input.span2,textarea.span2,.uneditable-input.span2{width:160px;} input.span3,textarea.span3,.uneditable-input.span3{width:260px;} input.span4,textarea.span4,.uneditable-input.span4{width:360px;} input.span5,textarea.span5,.uneditable-input.span5{width:460px;} input.span6,textarea.span6,.uneditable-input.span6{width:560px;} input.span7,textarea.span7,.uneditable-input.span7{width:660px;} input.span8,textarea.span8,.uneditable-input.span8{width:760px;} input.span9,textarea.span9,.uneditable-input.span9{width:860px;} input.span10,textarea.span10,.uneditable-input.span10{width:960px;} input.span11,textarea.span11,.uneditable-input.span11{width:1060px;} input.span12,textarea.span12,.uneditable-input.span12{width:1160px;} .thumbnails{margin-left:-30px;} .thumbnails>li{margin-left:30px;}} diff --git a/static/css/bootstrap.css b/static/css/bootstrap.css new file mode 100644 index 000000000..30dcae0e9 --- /dev/null +++ b/static/css/bootstrap.css @@ -0,0 +1,632 @@ +article,aside,details,figcaption,figure,footer,header,hgroup,nav,section{display:block;} +audio,canvas,video{display:inline-block;*display:inline;*zoom:1;} +audio:not([controls]){display:none;} +html{font-size:100%;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;} +a:focus{outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px;} +a:hover,a:active{outline:0;} +sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline;} +sup{top:-0.5em;} +sub{bottom:-0.25em;} +img{max-width:100%;height:auto;border:0;-ms-interpolation-mode:bicubic;} +button,input,select,textarea{margin:0;font-size:100%;vertical-align:middle;} +button,input{*overflow:visible;line-height:normal;} +button::-moz-focus-inner,input::-moz-focus-inner{padding:0;border:0;} +button,input[type="button"],input[type="reset"],input[type="submit"]{cursor:pointer;-webkit-appearance:button;} +input[type="search"]{-webkit-appearance:textfield;-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;} +input[type="search"]::-webkit-search-decoration,input[type="search"]::-webkit-search-cancel-button{-webkit-appearance:none;} +textarea{overflow:auto;vertical-align:top;} +.clearfix{*zoom:1;}.clearfix:before,.clearfix:after{display:table;content:"";} +.clearfix:after{clear:both;} +body{margin:0;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:13px;line-height:18px;color:#333333;background-color:#ffffff;} +a{color:#0088cc;text-decoration:none;} +a:hover{color:#005580;text-decoration:underline;} +.row{margin-left:-20px;*zoom:1;}.row:before,.row:after{display:table;content:"";} +.row:after{clear:both;} +[class*="span"]{float:left;margin-left:20px;} +.span1{width:60px;} +.span2{width:140px;} +.span3{width:220px;} +.span4{width:300px;} +.span5{width:380px;} +.span6{width:460px;} +.span7{width:540px;} +.span8{width:620px;} +.span9{width:700px;} +.span10{width:780px;} +.span11{width:860px;} +.span12,.container{width:940px;} +.offset1{margin-left:100px;} +.offset2{margin-left:180px;} +.offset3{margin-left:260px;} +.offset4{margin-left:340px;} +.offset5{margin-left:420px;} +.offset6{margin-left:500px;} +.offset7{margin-left:580px;} +.offset8{margin-left:660px;} +.offset9{margin-left:740px;} +.offset10{margin-left:820px;} +.offset11{margin-left:900px;} +.row-fluid{width:100%;*zoom:1;}.row-fluid:before,.row-fluid:after{display:table;content:"";} +.row-fluid:after{clear:both;} +.row-fluid>[class*="span"]{float:left;margin-left:2.127659574%;} +.row-fluid>[class*="span"]:first-child{margin-left:0;} +.row-fluid>.span1{width:6.382978723%;} +.row-fluid>.span2{width:14.89361702%;} +.row-fluid>.span3{width:23.404255317%;} +.row-fluid>.span4{width:31.914893614%;} +.row-fluid>.span5{width:40.425531911%;} +.row-fluid>.span6{width:48.93617020799999%;} +.row-fluid>.span7{width:57.446808505%;} +.row-fluid>.span8{width:65.95744680199999%;} +.row-fluid>.span9{width:74.468085099%;} +.row-fluid>.span10{width:82.97872339599999%;} +.row-fluid>.span11{width:91.489361693%;} +.row-fluid>.span12{width:99.99999998999999%;} +.container{width:940px;margin-left:auto;margin-right:auto;*zoom:1;}.container:before,.container:after{display:table;content:"";} +.container:after{clear:both;} +.container-fluid{padding-left:20px;padding-right:20px;*zoom:1;}.container-fluid:before,.container-fluid:after{display:table;content:"";} +.container-fluid:after{clear:both;} +p{margin:0 0 9px;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:13px;line-height:18px;}p small{font-size:11px;color:#999999;} +.lead{margin-bottom:18px;font-size:20px;font-weight:200;line-height:27px;} +h1,h2,h3,h4,h5,h6{margin:0;font-weight:bold;color:#333333;text-rendering:optimizelegibility;}h1 small,h2 small,h3 small,h4 small,h5 small,h6 small{font-weight:normal;color:#999999;} +h1{font-size:30px;line-height:36px;}h1 small{font-size:18px;} +h2{font-size:24px;line-height:36px;}h2 small{font-size:18px;} +h3{line-height:27px;font-size:18px;}h3 small{font-size:14px;} +h4,h5,h6{line-height:18px;} +h4{font-size:14px;}h4 small{font-size:12px;} +h5{font-size:12px;} +h6{font-size:11px;color:#999999;text-transform:uppercase;} +.page-header{padding-bottom:17px;margin:18px 0;border-bottom:1px solid #eeeeee;} +.page-header h1{line-height:1;} +ul,ol{padding:0;margin:0 0 9px 25px;} +ul ul,ul ol,ol ol,ol ul{margin-bottom:0;} +ul{list-style:disc;} +ol{list-style:decimal;} +li{line-height:18px;} +ul.unstyled,ol.unstyled{margin-left:0;list-style:none;} +dl{margin-bottom:18px;} +dt,dd{line-height:18px;} +dt{font-weight:bold;} +dd{margin-left:9px;} +hr{margin:18px 0;border:0;border-top:1px solid #eeeeee;border-bottom:1px solid #ffffff;} +strong{font-weight:bold;} +em{font-style:italic;} +.muted{color:#999999;} +abbr{font-size:90%;text-transform:uppercase;border-bottom:1px dotted #ddd;cursor:help;} +blockquote{padding:0 0 0 15px;margin:0 0 18px;border-left:5px solid #eeeeee;}blockquote p{margin-bottom:0;font-size:16px;font-weight:300;line-height:22.5px;} +blockquote small{display:block;line-height:18px;color:#999999;}blockquote small:before{content:'\2014 \00A0';} +blockquote.pull-right{float:right;padding-left:0;padding-right:15px;border-left:0;border-right:5px solid #eeeeee;}blockquote.pull-right p,blockquote.pull-right small{text-align:right;} +q:before,q:after,blockquote:before,blockquote:after{content:"";} +address{display:block;margin-bottom:18px;line-height:18px;font-style:normal;} +small{font-size:100%;} +cite{font-style:normal;} +code,pre{padding:0 3px 2px;font-family:Menlo,Monaco,"Courier New",monospace;font-size:12px;color:#333333;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;} +code{padding:3px 4px;color:#d14;background-color:#f7f7f9;border:1px solid #e1e1e8;} +pre{display:block;padding:8.5px;margin:0 0 9px;font-size:12px;line-height:18px;background-color:#f5f5f5;border:1px solid #ccc;border:1px solid rgba(0, 0, 0, 0.15);-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;white-space:pre;white-space:pre-wrap;word-break:break-all;word-wrap:break-word;}pre.prettyprint{margin-bottom:18px;} +pre code{padding:0;color:inherit;background-color:transparent;border:0;} +.pre-scrollable{max-height:340px;overflow-y:scroll;} +form{margin:0 0 18px;} +fieldset{padding:0;margin:0;border:0;} +legend{display:block;width:100%;padding:0;margin-bottom:27px;font-size:19.5px;line-height:36px;color:#333333;border:0;border-bottom:1px solid #eee;}legend small{font-size:13.5px;color:#999999;} +label,input,button,select,textarea{font-size:13px;font-weight:normal;line-height:18px;} +input,button,select,textarea{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;} +label{display:block;margin-bottom:5px;color:#333333;} +input,textarea,select,.uneditable-input{display:inline-block;width:210px;height:18px;padding:4px;margin-bottom:9px;font-size:13px;line-height:18px;color:#555555;border:1px solid #ccc;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;} +.uneditable-textarea{width:auto;height:auto;} +label input,label textarea,label select{display:block;} +input[type="image"],input[type="checkbox"],input[type="radio"]{width:auto;height:auto;padding:0;margin:3px 0;*margin-top:0;line-height:normal;cursor:pointer;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;border:0 \9;} +input[type="image"]{border:0;} +input[type="file"]{width:auto;padding:initial;line-height:initial;border:initial;background-color:#ffffff;background-color:initial;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;} +input[type="button"],input[type="reset"],input[type="submit"]{width:auto;height:auto;} +select,input[type="file"]{height:28px;*margin-top:4px;line-height:28px;} +input[type="file"]{line-height:18px \9;} +select{width:220px;background-color:#ffffff;} +select[multiple],select[size]{height:auto;} +input[type="image"]{-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;} +textarea{height:auto;} +input[type="hidden"]{display:none;} +.radio,.checkbox{padding-left:18px;} +.radio input[type="radio"],.checkbox input[type="checkbox"]{float:left;margin-left:-18px;} +.controls>.radio:first-child,.controls>.checkbox:first-child{padding-top:5px;} +.radio.inline,.checkbox.inline{display:inline-block;padding-top:5px;margin-bottom:0;vertical-align:middle;} +.radio.inline+.radio.inline,.checkbox.inline+.checkbox.inline{margin-left:10px;} +input,textarea{-webkit-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075);-moz-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075);box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075);-webkit-transition:border linear 0.2s,box-shadow linear 0.2s;-moz-transition:border linear 0.2s,box-shadow linear 0.2s;-ms-transition:border linear 0.2s,box-shadow linear 0.2s;-o-transition:border linear 0.2s,box-shadow linear 0.2s;transition:border linear 0.2s,box-shadow linear 0.2s;} +input:focus,textarea:focus{border-color:rgba(82, 168, 236, 0.8);-webkit-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075),0 0 8px rgba(82, 168, 236, 0.6);-moz-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075),0 0 8px rgba(82, 168, 236, 0.6);box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075),0 0 8px rgba(82, 168, 236, 0.6);outline:0;outline:thin dotted \9;} +input[type="file"]:focus,input[type="radio"]:focus,input[type="checkbox"]:focus,select:focus{-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px;} +.input-mini{width:60px;} +.input-small{width:90px;} +.input-medium{width:150px;} +.input-large{width:210px;} +.input-xlarge{width:270px;} +.input-xxlarge{width:530px;} +input[class*="span"],select[class*="span"],textarea[class*="span"],.uneditable-input{float:none;margin-left:0;} +input.span1,textarea.span1,.uneditable-input.span1{width:50px;} +input.span2,textarea.span2,.uneditable-input.span2{width:130px;} +input.span3,textarea.span3,.uneditable-input.span3{width:210px;} +input.span4,textarea.span4,.uneditable-input.span4{width:290px;} +input.span5,textarea.span5,.uneditable-input.span5{width:370px;} +input.span6,textarea.span6,.uneditable-input.span6{width:450px;} +input.span7,textarea.span7,.uneditable-input.span7{width:530px;} +input.span8,textarea.span8,.uneditable-input.span8{width:610px;} +input.span9,textarea.span9,.uneditable-input.span9{width:690px;} +input.span10,textarea.span10,.uneditable-input.span10{width:770px;} +input.span11,textarea.span11,.uneditable-input.span11{width:850px;} +input.span12,textarea.span12,.uneditable-input.span12{width:930px;} +input[disabled],select[disabled],textarea[disabled],input[readonly],select[readonly],textarea[readonly]{background-color:#f5f5f5;border-color:#ddd;cursor:not-allowed;} +.control-group.warning>label,.control-group.warning .help-block,.control-group.warning .help-inline{color:#c09853;} +.control-group.warning input,.control-group.warning select,.control-group.warning textarea{color:#c09853;border-color:#c09853;}.control-group.warning input:focus,.control-group.warning select:focus,.control-group.warning textarea:focus{border-color:#a47e3c;-webkit-box-shadow:0 0 6px #dbc59e;-moz-box-shadow:0 0 6px #dbc59e;box-shadow:0 0 6px #dbc59e;} +.control-group.warning .input-prepend .add-on,.control-group.warning .input-append .add-on{color:#c09853;background-color:#fcf8e3;border-color:#c09853;} +.control-group.error>label,.control-group.error .help-block,.control-group.error .help-inline{color:#b94a48;} +.control-group.error input,.control-group.error select,.control-group.error textarea{color:#b94a48;border-color:#b94a48;}.control-group.error input:focus,.control-group.error select:focus,.control-group.error textarea:focus{border-color:#953b39;-webkit-box-shadow:0 0 6px #d59392;-moz-box-shadow:0 0 6px #d59392;box-shadow:0 0 6px #d59392;} +.control-group.error .input-prepend .add-on,.control-group.error .input-append .add-on{color:#b94a48;background-color:#f2dede;border-color:#b94a48;} +.control-group.success>label,.control-group.success .help-block,.control-group.success .help-inline{color:#468847;} +.control-group.success input,.control-group.success select,.control-group.success textarea{color:#468847;border-color:#468847;}.control-group.success input:focus,.control-group.success select:focus,.control-group.success textarea:focus{border-color:#356635;-webkit-box-shadow:0 0 6px #7aba7b;-moz-box-shadow:0 0 6px #7aba7b;box-shadow:0 0 6px #7aba7b;} +.control-group.success .input-prepend .add-on,.control-group.success .input-append .add-on{color:#468847;background-color:#dff0d8;border-color:#468847;} +input:focus:required:invalid,textarea:focus:required:invalid,select:focus:required:invalid{color:#b94a48;border-color:#ee5f5b;}input:focus:required:invalid:focus,textarea:focus:required:invalid:focus,select:focus:required:invalid:focus{border-color:#e9322d;-webkit-box-shadow:0 0 6px #f8b9b7;-moz-box-shadow:0 0 6px #f8b9b7;box-shadow:0 0 6px #f8b9b7;} +.form-actions{padding:17px 20px 18px;margin-top:18px;margin-bottom:18px;background-color:#f5f5f5;border-top:1px solid #ddd;} +.uneditable-input{display:block;background-color:#ffffff;border-color:#eee;-webkit-box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.025);-moz-box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.025);box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.025);cursor:not-allowed;} +:-moz-placeholder{color:#999999;} +::-webkit-input-placeholder{color:#999999;} +.help-block{display:block;margin-top:5px;margin-bottom:0;color:#999999;} +.help-inline{display:inline-block;*display:inline;*zoom:1;margin-bottom:9px;vertical-align:middle;padding-left:5px;} +.input-prepend,.input-append{margin-bottom:5px;*zoom:1;}.input-prepend:before,.input-append:before,.input-prepend:after,.input-append:after{display:table;content:"";} +.input-prepend:after,.input-append:after{clear:both;} +.input-prepend input,.input-append input,.input-prepend .uneditable-input,.input-append .uneditable-input{-webkit-border-radius:0 3px 3px 0;-moz-border-radius:0 3px 3px 0;border-radius:0 3px 3px 0;}.input-prepend input:focus,.input-append input:focus,.input-prepend .uneditable-input:focus,.input-append .uneditable-input:focus{position:relative;z-index:2;} +.input-prepend .uneditable-input,.input-append .uneditable-input{border-left-color:#ccc;} +.input-prepend .add-on,.input-append .add-on{float:left;display:block;width:auto;min-width:16px;height:18px;margin-right:-1px;padding:4px 5px;font-weight:normal;line-height:18px;color:#999999;text-align:center;text-shadow:0 1px 0 #ffffff;background-color:#f5f5f5;border:1px solid #ccc;-webkit-border-radius:3px 0 0 3px;-moz-border-radius:3px 0 0 3px;border-radius:3px 0 0 3px;} +.input-prepend .active,.input-append .active{background-color:#a9dba9;border-color:#46a546;} +.input-prepend .add-on{*margin-top:1px;} +.input-append input,.input-append .uneditable-input{float:left;-webkit-border-radius:3px 0 0 3px;-moz-border-radius:3px 0 0 3px;border-radius:3px 0 0 3px;} +.input-append .uneditable-input{border-left-color:#eee;border-right-color:#ccc;} +.input-append .add-on{margin-right:0;margin-left:-1px;-webkit-border-radius:0 3px 3px 0;-moz-border-radius:0 3px 3px 0;border-radius:0 3px 3px 0;} +.input-append input:first-child{*margin-left:-160px;}.input-append input:first-child+.add-on{*margin-left:-21px;} +.search-query{padding-left:14px;padding-right:14px;margin-bottom:0;-webkit-border-radius:14px;-moz-border-radius:14px;border-radius:14px;} +.form-search input,.form-inline input,.form-horizontal input,.form-search textarea,.form-inline textarea,.form-horizontal textarea,.form-search select,.form-inline select,.form-horizontal select,.form-search .help-inline,.form-inline .help-inline,.form-horizontal .help-inline,.form-search .uneditable-input,.form-inline .uneditable-input,.form-horizontal .uneditable-input{display:inline-block;margin-bottom:0;} +.form-search .hide,.form-inline .hide,.form-horizontal .hide{display:none;} +.form-search label,.form-inline label,.form-search .input-append,.form-inline .input-append,.form-search .input-prepend,.form-inline .input-prepend{display:inline-block;} +.form-search .input-append .add-on,.form-inline .input-prepend .add-on,.form-search .input-append .add-on,.form-inline .input-prepend .add-on{vertical-align:middle;} +.form-search .radio,.form-inline .radio,.form-search .checkbox,.form-inline .checkbox{margin-bottom:0;vertical-align:middle;} +.control-group{margin-bottom:9px;} +legend+.control-group{margin-top:18px;-webkit-margin-top-collapse:separate;} +.form-horizontal .control-group{margin-bottom:18px;*zoom:1;}.form-horizontal .control-group:before,.form-horizontal .control-group:after{display:table;content:"";} +.form-horizontal .control-group:after{clear:both;} +.form-horizontal .control-label{float:left;width:140px;padding-top:5px;text-align:right;} +.form-horizontal .controls{margin-left:160px;} +.form-horizontal .form-actions{padding-left:160px;} +table{max-width:100%;border-collapse:collapse;border-spacing:0;} +.table{width:100%;margin-bottom:18px;}.table th,.table td{padding:8px;line-height:18px;text-align:left;vertical-align:top;border-top:1px solid #ddd;} +.table th{font-weight:bold;} +.table thead th{vertical-align:bottom;} +.table thead:first-child tr th,.table thead:first-child tr td{border-top:0;} +.table tbody+tbody{border-top:2px solid #ddd;} +.table-condensed th,.table-condensed td{padding:4px 5px;} +.table-bordered{border:1px solid #ddd;border-collapse:separate;*border-collapse:collapsed;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;}.table-bordered th+th,.table-bordered td+td,.table-bordered th+td,.table-bordered td+th{border-left:1px solid #ddd;} +.table-bordered thead:first-child tr:first-child th,.table-bordered tbody:first-child tr:first-child th,.table-bordered tbody:first-child tr:first-child td{border-top:0;} +.table-bordered thead:first-child tr:first-child th:first-child,.table-bordered tbody:first-child tr:first-child td:first-child{-webkit-border-radius:4px 0 0 0;-moz-border-radius:4px 0 0 0;border-radius:4px 0 0 0;} +.table-bordered thead:first-child tr:first-child th:last-child,.table-bordered tbody:first-child tr:first-child td:last-child{-webkit-border-radius:0 4px 0 0;-moz-border-radius:0 4px 0 0;border-radius:0 4px 0 0;} +.table-bordered thead:last-child tr:last-child th:first-child,.table-bordered tbody:last-child tr:last-child td:first-child{-webkit-border-radius:0 0 0 4px;-moz-border-radius:0 0 0 4px;border-radius:0 0 0 4px;} +.table-bordered thead:last-child tr:last-child th:last-child,.table-bordered tbody:last-child tr:last-child td:last-child{-webkit-border-radius:0 0 4px 0;-moz-border-radius:0 0 4px 0;border-radius:0 0 4px 0;} +.table-striped tbody tr:nth-child(odd) td,.table-striped tbody tr:nth-child(odd) th{background-color:#f9f9f9;} +.table tbody tr:hover td,.table tbody tr:hover th{background-color:#f5f5f5;} +table .span1{float:none;width:44px;margin-left:0;} +table .span2{float:none;width:124px;margin-left:0;} +table .span3{float:none;width:204px;margin-left:0;} +table .span4{float:none;width:284px;margin-left:0;} +table .span5{float:none;width:364px;margin-left:0;} +table .span6{float:none;width:444px;margin-left:0;} +table .span7{float:none;width:524px;margin-left:0;} +table .span8{float:none;width:604px;margin-left:0;} +table .span9{float:none;width:684px;margin-left:0;} +table .span10{float:none;width:764px;margin-left:0;} +table .span11{float:none;width:844px;margin-left:0;} +table .span12{float:none;width:924px;margin-left:0;} +[class^="icon-"],[class*=" icon-"]{display:inline-block;width:14px;height:14px;line-height:14px;vertical-align:text-top;background-image:url("../img/glyphicons-halflings.png");background-position:14px 14px;background-repeat:no-repeat;*margin-right:.3em;}[class^="icon-"]:last-child,[class*=" icon-"]:last-child{*margin-left:0;} +.icon-white{background-image:url("../img/glyphicons-halflings-white.png");} +.icon-glass{background-position:0 0;} +.icon-music{background-position:-24px 0;} +.icon-search{background-position:-48px 0;} +.icon-envelope{background-position:-72px 0;} +.icon-heart{background-position:-96px 0;} +.icon-star{background-position:-120px 0;} +.icon-star-empty{background-position:-144px 0;} +.icon-user{background-position:-168px 0;} +.icon-film{background-position:-192px 0;} +.icon-th-large{background-position:-216px 0;} +.icon-th{background-position:-240px 0;} +.icon-th-list{background-position:-264px 0;} +.icon-ok{background-position:-288px 0;} +.icon-remove{background-position:-312px 0;} +.icon-zoom-in{background-position:-336px 0;} +.icon-zoom-out{background-position:-360px 0;} +.icon-off{background-position:-384px 0;} +.icon-signal{background-position:-408px 0;} +.icon-cog{background-position:-432px 0;} +.icon-trash{background-position:-456px 0;} +.icon-home{background-position:0 -24px;} +.icon-file{background-position:-24px -24px;} +.icon-time{background-position:-48px -24px;} +.icon-road{background-position:-72px -24px;} +.icon-download-alt{background-position:-96px -24px;} +.icon-download{background-position:-120px -24px;} +.icon-upload{background-position:-144px -24px;} +.icon-inbox{background-position:-168px -24px;} +.icon-play-circle{background-position:-192px -24px;} +.icon-repeat{background-position:-216px -24px;} +.icon-refresh{background-position:-240px -24px;} +.icon-list-alt{background-position:-264px -24px;} +.icon-lock{background-position:-287px -24px;} +.icon-flag{background-position:-312px -24px;} +.icon-headphones{background-position:-336px -24px;} +.icon-volume-off{background-position:-360px -24px;} +.icon-volume-down{background-position:-384px -24px;} +.icon-volume-up{background-position:-408px -24px;} +.icon-qrcode{background-position:-432px -24px;} +.icon-barcode{background-position:-456px -24px;} +.icon-tag{background-position:0 -48px;} +.icon-tags{background-position:-25px -48px;} +.icon-book{background-position:-48px -48px;} +.icon-bookmark{background-position:-72px -48px;} +.icon-print{background-position:-96px -48px;} +.icon-camera{background-position:-120px -48px;} +.icon-font{background-position:-144px -48px;} +.icon-bold{background-position:-167px -48px;} +.icon-italic{background-position:-192px -48px;} +.icon-text-height{background-position:-216px -48px;} +.icon-text-width{background-position:-240px -48px;} +.icon-align-left{background-position:-264px -48px;} +.icon-align-center{background-position:-288px -48px;} +.icon-align-right{background-position:-312px -48px;} +.icon-align-justify{background-position:-336px -48px;} +.icon-list{background-position:-360px -48px;} +.icon-indent-left{background-position:-384px -48px;} +.icon-indent-right{background-position:-408px -48px;} +.icon-facetime-video{background-position:-432px -48px;} +.icon-picture{background-position:-456px -48px;} +.icon-pencil{background-position:0 -72px;} +.icon-map-marker{background-position:-24px -72px;} +.icon-adjust{background-position:-48px -72px;} +.icon-tint{background-position:-72px -72px;} +.icon-edit{background-position:-96px -72px;} +.icon-share{background-position:-120px -72px;} +.icon-check{background-position:-144px -72px;} +.icon-move{background-position:-168px -72px;} +.icon-step-backward{background-position:-192px -72px;} +.icon-fast-backward{background-position:-216px -72px;} +.icon-backward{background-position:-240px -72px;} +.icon-play{background-position:-264px -72px;} +.icon-pause{background-position:-288px -72px;} +.icon-stop{background-position:-312px -72px;} +.icon-forward{background-position:-336px -72px;} +.icon-fast-forward{background-position:-360px -72px;} +.icon-step-forward{background-position:-384px -72px;} +.icon-eject{background-position:-408px -72px;} +.icon-chevron-left{background-position:-432px -72px;} +.icon-chevron-right{background-position:-456px -72px;} +.icon-plus-sign{background-position:0 -96px;} +.icon-minus-sign{background-position:-24px -96px;} +.icon-remove-sign{background-position:-48px -96px;} +.icon-ok-sign{background-position:-72px -96px;} +.icon-question-sign{background-position:-96px -96px;} +.icon-info-sign{background-position:-120px -96px;} +.icon-screenshot{background-position:-144px -96px;} +.icon-remove-circle{background-position:-168px -96px;} +.icon-ok-circle{background-position:-192px -96px;} +.icon-ban-circle{background-position:-216px -96px;} +.icon-arrow-left{background-position:-240px -96px;} +.icon-arrow-right{background-position:-264px -96px;} +.icon-arrow-up{background-position:-289px -96px;} +.icon-arrow-down{background-position:-312px -96px;} +.icon-share-alt{background-position:-336px -96px;} +.icon-resize-full{background-position:-360px -96px;} +.icon-resize-small{background-position:-384px -96px;} +.icon-plus{background-position:-408px -96px;} +.icon-minus{background-position:-433px -96px;} +.icon-asterisk{background-position:-456px -96px;} +.icon-exclamation-sign{background-position:0 -120px;} +.icon-gift{background-position:-24px -120px;} +.icon-leaf{background-position:-48px -120px;} +.icon-fire{background-position:-72px -120px;} +.icon-eye-open{background-position:-96px -120px;} +.icon-eye-close{background-position:-120px -120px;} +.icon-warning-sign{background-position:-144px -120px;} +.icon-plane{background-position:-168px -120px;} +.icon-calendar{background-position:-192px -120px;} +.icon-random{background-position:-216px -120px;} +.icon-comment{background-position:-240px -120px;} +.icon-magnet{background-position:-264px -120px;} +.icon-chevron-up{background-position:-288px -120px;} +.icon-chevron-down{background-position:-313px -119px;} +.icon-retweet{background-position:-336px -120px;} +.icon-shopping-cart{background-position:-360px -120px;} +.icon-folder-close{background-position:-384px -120px;} +.icon-folder-open{background-position:-408px -120px;} +.icon-resize-vertical{background-position:-432px -119px;} +.icon-resize-horizontal{background-position:-456px -118px;} +.dropdown{position:relative;} +.dropdown-toggle{*margin-bottom:-3px;} +.dropdown-toggle:active,.open .dropdown-toggle{outline:0;} +.caret{display:inline-block;width:0;height:0;text-indent:-99999px;*text-indent:0;vertical-align:top;border-left:4px solid transparent;border-right:4px solid transparent;border-top:4px solid #000000;opacity:0.3;filter:alpha(opacity=30);content:"\2193";} +.dropdown .caret{margin-top:8px;margin-left:2px;} +.dropdown:hover .caret,.open.dropdown .caret{opacity:1;filter:alpha(opacity=100);} +.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;float:left;display:none;min-width:160px;_width:160px;padding:4px 0;margin:0;list-style:none;background-color:#ffffff;border-color:#ccc;border-color:rgba(0, 0, 0, 0.2);border-style:solid;border-width:1px;-webkit-border-radius:0 0 5px 5px;-moz-border-radius:0 0 5px 5px;border-radius:0 0 5px 5px;-webkit-box-shadow:0 5px 10px rgba(0, 0, 0, 0.2);-moz-box-shadow:0 5px 10px rgba(0, 0, 0, 0.2);box-shadow:0 5px 10px rgba(0, 0, 0, 0.2);-webkit-background-clip:padding-box;-moz-background-clip:padding;background-clip:padding-box;*border-right-width:2px;*border-bottom-width:2px;}.dropdown-menu.bottom-up{top:auto;bottom:100%;margin-bottom:2px;} +.dropdown-menu .divider{height:1px;margin:5px 1px;overflow:hidden;background-color:#e5e5e5;border-bottom:1px solid #ffffff;*width:100%;*margin:-5px 0 5px;} +.dropdown-menu a{display:block;padding:3px 15px;clear:both;font-weight:normal;line-height:18px;color:#555555;white-space:nowrap;} +.dropdown-menu li>a:hover,.dropdown-menu .active>a,.dropdown-menu .active>a:hover{color:#ffffff;text-decoration:none;background-color:#0088cc;} +.dropdown.open{*z-index:1000;}.dropdown.open .dropdown-toggle{color:#ffffff;background:#ccc;background:rgba(0, 0, 0, 0.3);} +.dropdown.open .dropdown-menu{display:block;} +.typeahead{margin-top:2px;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;} +.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #eee;border:1px solid rgba(0, 0, 0, 0.05);-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.05);-moz-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.05);box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.05);}.well blockquote{border-color:#ddd;border-color:rgba(0, 0, 0, 0.15);} +.fade{-webkit-transition:opacity 0.15s linear;-moz-transition:opacity 0.15s linear;-ms-transition:opacity 0.15s linear;-o-transition:opacity 0.15s linear;transition:opacity 0.15s linear;opacity:0;}.fade.in{opacity:1;} +.collapse{-webkit-transition:height 0.35s ease;-moz-transition:height 0.35s ease;-ms-transition:height 0.35s ease;-o-transition:height 0.35s ease;transition:height 0.35s ease;position:relative;overflow:hidden;height:0;}.collapse.in{height:auto;} +.close{float:right;font-size:20px;font-weight:bold;line-height:18px;color:#000000;text-shadow:0 1px 0 #ffffff;opacity:0.2;filter:alpha(opacity=20);}.close:hover{color:#000000;text-decoration:none;opacity:0.4;filter:alpha(opacity=40);cursor:pointer;} +.btn{display:inline-block;padding:4px 10px 4px;margin-bottom:0;font-size:13px;line-height:18px;color:#333333;text-align:center;text-shadow:0 1px 1px rgba(255, 255, 255, 0.75);vertical-align:middle;background-color:#f5f5f5;background-image:-moz-linear-gradient(top, #ffffff, #e6e6e6);background-image:-ms-linear-gradient(top, #ffffff, #e6e6e6);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), to(#e6e6e6));background-image:-webkit-linear-gradient(top, #ffffff, #e6e6e6);background-image:-o-linear-gradient(top, #ffffff, #e6e6e6);background-image:linear-gradient(top, #ffffff, #e6e6e6);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#e6e6e6', GradientType=0);border-color:#e6e6e6 #e6e6e6 #bfbfbf;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);border:1px solid #ccc;border-bottom-color:#bbb;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.2),0 1px 2px rgba(0, 0, 0, 0.05);-moz-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.2),0 1px 2px rgba(0, 0, 0, 0.05);box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.2),0 1px 2px rgba(0, 0, 0, 0.05);cursor:pointer;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);*margin-left:.3em;}.btn:hover,.btn:active,.btn.active,.btn.disabled,.btn[disabled]{background-color:#e6e6e6;} +.btn:active,.btn.active{background-color:#cccccc \9;} +.btn:first-child{*margin-left:0;} +.btn:hover{color:#333333;text-decoration:none;background-color:#e6e6e6;background-position:0 -15px;-webkit-transition:background-position 0.1s linear;-moz-transition:background-position 0.1s linear;-ms-transition:background-position 0.1s linear;-o-transition:background-position 0.1s linear;transition:background-position 0.1s linear;} +.btn:focus{outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px;} +.btn.active,.btn:active{background-image:none;-webkit-box-shadow:inset 0 2px 4px rgba(0, 0, 0, 0.15),0 1px 2px rgba(0, 0, 0, 0.05);-moz-box-shadow:inset 0 2px 4px rgba(0, 0, 0, 0.15),0 1px 2px rgba(0, 0, 0, 0.05);box-shadow:inset 0 2px 4px rgba(0, 0, 0, 0.15),0 1px 2px rgba(0, 0, 0, 0.05);background-color:#e6e6e6;background-color:#d9d9d9 \9;outline:0;} +.btn.disabled,.btn[disabled]{cursor:default;background-image:none;background-color:#e6e6e6;opacity:0.65;filter:alpha(opacity=65);-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;} +.btn-large{padding:9px 14px;font-size:15px;line-height:normal;-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px;} +.btn-large [class^="icon-"]{margin-top:1px;} +.btn-small{padding:5px 9px;font-size:11px;line-height:16px;} +.btn-small [class^="icon-"]{margin-top:-1px;} +.btn-mini{padding:2px 6px;font-size:11px;line-height:14px;} +.btn-primary,.btn-primary:hover,.btn-warning,.btn-warning:hover,.btn-danger,.btn-danger:hover,.btn-success,.btn-success:hover,.btn-info,.btn-info:hover,.btn-inverse,.btn-inverse:hover{text-shadow:0 -1px 0 rgba(0, 0, 0, 0.25);color:#ffffff;} +.btn-primary.active,.btn-warning.active,.btn-danger.active,.btn-success.active,.btn-info.active,.btn-dark.active{color:rgba(255, 255, 255, 0.75);} +.btn-primary{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);}.btn-primary:hover,.btn-primary:active,.btn-primary.active,.btn-primary.disabled,.btn-primary[disabled]{background-color:#0044cc;} +.btn-primary:active,.btn-primary.active{background-color:#003399 \9;} +.btn-warning{background-color:#faa732;background-image:-moz-linear-gradient(top, #fbb450, #f89406);background-image:-ms-linear-gradient(top, #fbb450, #f89406);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#fbb450), to(#f89406));background-image:-webkit-linear-gradient(top, #fbb450, #f89406);background-image:-o-linear-gradient(top, #fbb450, #f89406);background-image:linear-gradient(top, #fbb450, #f89406);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fbb450', endColorstr='#f89406', GradientType=0);border-color:#f89406 #f89406 #ad6704;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);}.btn-warning:hover,.btn-warning:active,.btn-warning.active,.btn-warning.disabled,.btn-warning[disabled]{background-color:#f89406;} +.btn-warning:active,.btn-warning.active{background-color:#c67605 \9;} +.btn-danger{background-color:#da4f49;background-image:-moz-linear-gradient(top, #ee5f5b, #bd362f);background-image:-ms-linear-gradient(top, #ee5f5b, #bd362f);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#ee5f5b), to(#bd362f));background-image:-webkit-linear-gradient(top, #ee5f5b, #bd362f);background-image:-o-linear-gradient(top, #ee5f5b, #bd362f);background-image:linear-gradient(top, #ee5f5b, #bd362f);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ee5f5b', endColorstr='#bd362f', GradientType=0);border-color:#bd362f #bd362f #802420;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);}.btn-danger:hover,.btn-danger:active,.btn-danger.active,.btn-danger.disabled,.btn-danger[disabled]{background-color:#bd362f;} +.btn-danger:active,.btn-danger.active{background-color:#942a25 \9;} +.btn-success{background-color:#5bb75b;background-image:-moz-linear-gradient(top, #62c462, #51a351);background-image:-ms-linear-gradient(top, #62c462, #51a351);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#62c462), to(#51a351));background-image:-webkit-linear-gradient(top, #62c462, #51a351);background-image:-o-linear-gradient(top, #62c462, #51a351);background-image:linear-gradient(top, #62c462, #51a351);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#62c462', endColorstr='#51a351', GradientType=0);border-color:#51a351 #51a351 #387038;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);}.btn-success:hover,.btn-success:active,.btn-success.active,.btn-success.disabled,.btn-success[disabled]{background-color:#51a351;} +.btn-success:active,.btn-success.active{background-color:#408140 \9;} +.btn-info{background-color:#49afcd;background-image:-moz-linear-gradient(top, #5bc0de, #2f96b4);background-image:-ms-linear-gradient(top, #5bc0de, #2f96b4);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#5bc0de), to(#2f96b4));background-image:-webkit-linear-gradient(top, #5bc0de, #2f96b4);background-image:-o-linear-gradient(top, #5bc0de, #2f96b4);background-image:linear-gradient(top, #5bc0de, #2f96b4);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#5bc0de', endColorstr='#2f96b4', GradientType=0);border-color:#2f96b4 #2f96b4 #1f6377;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);}.btn-info:hover,.btn-info:active,.btn-info.active,.btn-info.disabled,.btn-info[disabled]{background-color:#2f96b4;} +.btn-info:active,.btn-info.active{background-color:#24748c \9;} +.btn-inverse{background-color:#393939;background-image:-moz-linear-gradient(top, #454545, #262626);background-image:-ms-linear-gradient(top, #454545, #262626);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#454545), to(#262626));background-image:-webkit-linear-gradient(top, #454545, #262626);background-image:-o-linear-gradient(top, #454545, #262626);background-image:linear-gradient(top, #454545, #262626);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#454545', endColorstr='#262626', GradientType=0);border-color:#262626 #262626 #000000;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);}.btn-inverse:hover,.btn-inverse:active,.btn-inverse.active,.btn-inverse.disabled,.btn-inverse[disabled]{background-color:#262626;} +.btn-inverse:active,.btn-inverse.active{background-color:#0c0c0c \9;} +button.btn,input[type="submit"].btn{*padding-top:2px;*padding-bottom:2px;}button.btn::-moz-focus-inner,input[type="submit"].btn::-moz-focus-inner{padding:0;border:0;} +button.btn.large,input[type="submit"].btn.large{*padding-top:7px;*padding-bottom:7px;} +button.btn.small,input[type="submit"].btn.small{*padding-top:3px;*padding-bottom:3px;} +.btn-group{position:relative;*zoom:1;*margin-left:.3em;}.btn-group:before,.btn-group:after{display:table;content:"";} +.btn-group:after{clear:both;} +.btn-group:first-child{*margin-left:0;} +.btn-group+.btn-group{margin-left:5px;} +.btn-toolbar{margin-top:9px;margin-bottom:9px;}.btn-toolbar .btn-group{display:inline-block;*display:inline;*zoom:1;} +.btn-group .btn{position:relative;float:left;margin-left:-1px;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;} +.btn-group .btn:first-child{margin-left:0;-webkit-border-top-left-radius:4px;-moz-border-radius-topleft:4px;border-top-left-radius:4px;-webkit-border-bottom-left-radius:4px;-moz-border-radius-bottomleft:4px;border-bottom-left-radius:4px;} +.btn-group .btn:last-child,.btn-group .dropdown-toggle{-webkit-border-top-right-radius:4px;-moz-border-radius-topright:4px;border-top-right-radius:4px;-webkit-border-bottom-right-radius:4px;-moz-border-radius-bottomright:4px;border-bottom-right-radius:4px;} +.btn-group .btn.large:first-child{margin-left:0;-webkit-border-top-left-radius:6px;-moz-border-radius-topleft:6px;border-top-left-radius:6px;-webkit-border-bottom-left-radius:6px;-moz-border-radius-bottomleft:6px;border-bottom-left-radius:6px;} +.btn-group .btn.large:last-child,.btn-group .large.dropdown-toggle{-webkit-border-top-right-radius:6px;-moz-border-radius-topright:6px;border-top-right-radius:6px;-webkit-border-bottom-right-radius:6px;-moz-border-radius-bottomright:6px;border-bottom-right-radius:6px;} +.btn-group .btn:hover,.btn-group .btn:focus,.btn-group .btn:active,.btn-group .btn.active{z-index:2;} +.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0;} +.btn-group .dropdown-toggle{padding-left:8px;padding-right:8px;-webkit-box-shadow:inset 1px 0 0 rgba(255, 255, 255, 0.125),inset 0 1px 0 rgba(255, 255, 255, 0.2),0 1px 2px rgba(0, 0, 0, 0.05);-moz-box-shadow:inset 1px 0 0 rgba(255, 255, 255, 0.125),inset 0 1px 0 rgba(255, 255, 255, 0.2),0 1px 2px rgba(0, 0, 0, 0.05);box-shadow:inset 1px 0 0 rgba(255, 255, 255, 0.125),inset 0 1px 0 rgba(255, 255, 255, 0.2),0 1px 2px rgba(0, 0, 0, 0.05);*padding-top:5px;*padding-bottom:5px;} +.btn-group.open{*z-index:1000;}.btn-group.open .dropdown-menu{display:block;margin-top:1px;-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px;} +.btn-group.open .dropdown-toggle{background-image:none;-webkit-box-shadow:inset 0 1px 6px rgba(0, 0, 0, 0.15),0 1px 2px rgba(0, 0, 0, 0.05);-moz-box-shadow:inset 0 1px 6px rgba(0, 0, 0, 0.15),0 1px 2px rgba(0, 0, 0, 0.05);box-shadow:inset 0 1px 6px rgba(0, 0, 0, 0.15),0 1px 2px rgba(0, 0, 0, 0.05);} +.btn .caret{margin-top:7px;margin-left:0;} +.btn:hover .caret,.open.btn-group .caret{opacity:1;filter:alpha(opacity=100);} +.btn-primary .caret,.btn-danger .caret,.btn-info .caret,.btn-success .caret,.btn-inverse .caret{border-top-color:#ffffff;opacity:0.75;filter:alpha(opacity=75);} +.btn-small .caret{margin-top:4px;} +.alert{padding:8px 35px 8px 14px;margin-bottom:18px;text-shadow:0 1px 0 rgba(255, 255, 255, 0.5);background-color:#fcf8e3;border:1px solid #fbeed5;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;} +.alert,.alert-heading{color:#c09853;} +.alert .close{position:relative;top:-2px;right:-21px;line-height:18px;} +.alert-success{background-color:#dff0d8;border-color:#d6e9c6;} +.alert-success,.alert-success .alert-heading{color:#468847;} +.alert-danger,.alert-error{background-color:#f2dede;border-color:#eed3d7;} +.alert-danger,.alert-error,.alert-danger .alert-heading,.alert-error .alert-heading{color:#b94a48;} +.alert-info{background-color:#d9edf7;border-color:#bce8f1;} +.alert-info,.alert-info .alert-heading{color:#3a87ad;} +.alert-block{padding-top:14px;padding-bottom:14px;} +.alert-block>p,.alert-block>ul{margin-bottom:0;} +.alert-block p+p{margin-top:5px;} +.nav{margin-left:0;margin-bottom:18px;list-style:none;} +.nav>li>a{display:block;} +.nav>li>a:hover{text-decoration:none;background-color:#eeeeee;} +.nav .nav-header{display:block;padding:3px 15px;font-size:11px;font-weight:bold;line-height:18px;color:#999999;text-shadow:0 1px 0 rgba(255, 255, 255, 0.5);text-transform:uppercase;} +.nav li+.nav-header{margin-top:9px;} +.nav-list{padding-left:14px;padding-right:14px;margin-bottom:0;} +.nav-list>li>a,.nav-list .nav-header{margin-left:-15px;margin-right:-15px;text-shadow:0 1px 0 rgba(255, 255, 255, 0.5);} +.nav-list>li>a{padding:3px 15px;} +.nav-list .active>a,.nav-list .active>a:hover{color:#ffffff;text-shadow:0 -1px 0 rgba(0, 0, 0, 0.2);background-color:#0088cc;} +.nav-list [class^="icon-"]{margin-right:2px;} +.nav-tabs,.nav-pills{*zoom:1;}.nav-tabs:before,.nav-pills:before,.nav-tabs:after,.nav-pills:after{display:table;content:"";} +.nav-tabs:after,.nav-pills:after{clear:both;} +.nav-tabs>li,.nav-pills>li{float:left;} +.nav-tabs>li>a,.nav-pills>li>a{padding-right:12px;padding-left:12px;margin-right:2px;line-height:14px;} +.nav-tabs{border-bottom:1px solid #ddd;} +.nav-tabs>li{margin-bottom:-1px;} +.nav-tabs>li>a{padding-top:9px;padding-bottom:9px;border:1px solid transparent;-webkit-border-radius:4px 4px 0 0;-moz-border-radius:4px 4px 0 0;border-radius:4px 4px 0 0;}.nav-tabs>li>a:hover{border-color:#eeeeee #eeeeee #dddddd;} +.nav-tabs>.active>a,.nav-tabs>.active>a:hover{color:#555555;background-color:#ffffff;border:1px solid #ddd;border-bottom-color:transparent;cursor:default;} +.nav-pills>li>a{padding-top:8px;padding-bottom:8px;margin-top:2px;margin-bottom:2px;-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px;} +.nav-pills .active>a,.nav-pills .active>a:hover{color:#ffffff;background-color:#0088cc;} +.nav-stacked>li{float:none;} +.nav-stacked>li>a{margin-right:0;} +.nav-tabs.nav-stacked{border-bottom:0;} +.nav-tabs.nav-stacked>li>a{border:1px solid #ddd;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;} +.nav-tabs.nav-stacked>li:first-child>a{-webkit-border-radius:4px 4px 0 0;-moz-border-radius:4px 4px 0 0;border-radius:4px 4px 0 0;} +.nav-tabs.nav-stacked>li:last-child>a{-webkit-border-radius:0 0 4px 4px;-moz-border-radius:0 0 4px 4px;border-radius:0 0 4px 4px;} +.nav-tabs.nav-stacked>li>a:hover{border-color:#ddd;z-index:2;} +.nav-pills.nav-stacked>li>a{margin-bottom:3px;} +.nav-pills.nav-stacked>li:last-child>a{margin-bottom:1px;} +.nav-tabs .dropdown-menu,.nav-pills .dropdown-menu{margin-top:1px;border-width:1px;} +.nav-pills .dropdown-menu{-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;} +.nav-tabs .dropdown-toggle .caret,.nav-pills .dropdown-toggle .caret{border-top-color:#0088cc;margin-top:6px;} +.nav-tabs .dropdown-toggle:hover .caret,.nav-pills .dropdown-toggle:hover .caret{border-top-color:#005580;} +.nav-tabs .active .dropdown-toggle .caret,.nav-pills .active .dropdown-toggle .caret{border-top-color:#333333;} +.nav>.dropdown.active>a:hover{color:#000000;cursor:pointer;} +.nav-tabs .open .dropdown-toggle,.nav-pills .open .dropdown-toggle,.nav>.open.active>a:hover{color:#ffffff;background-color:#999999;border-color:#999999;} +.nav .open .caret,.nav .open.active .caret,.nav .open a:hover .caret{border-top-color:#ffffff;opacity:1;filter:alpha(opacity=100);} +.tabs-stacked .open>a:hover{border-color:#999999;} +.tabbable{*zoom:1;}.tabbable:before,.tabbable:after{display:table;content:"";} +.tabbable:after{clear:both;} +.tab-content{overflow:hidden;} +.tabs-below .nav-tabs,.tabs-right .nav-tabs,.tabs-left .nav-tabs{border-bottom:0;} +.tab-content>.tab-pane,.pill-content>.pill-pane{display:none;} +.tab-content>.active,.pill-content>.active{display:block;} +.tabs-below .nav-tabs{border-top:1px solid #ddd;} +.tabs-below .nav-tabs>li{margin-top:-1px;margin-bottom:0;} +.tabs-below .nav-tabs>li>a{-webkit-border-radius:0 0 4px 4px;-moz-border-radius:0 0 4px 4px;border-radius:0 0 4px 4px;}.tabs-below .nav-tabs>li>a:hover{border-bottom-color:transparent;border-top-color:#ddd;} +.tabs-below .nav-tabs .active>a,.tabs-below .nav-tabs .active>a:hover{border-color:transparent #ddd #ddd #ddd;} +.tabs-left .nav-tabs>li,.tabs-right .nav-tabs>li{float:none;} +.tabs-left .nav-tabs>li>a,.tabs-right .nav-tabs>li>a{min-width:74px;margin-right:0;margin-bottom:3px;} +.tabs-left .nav-tabs{float:left;margin-right:19px;border-right:1px solid #ddd;} +.tabs-left .nav-tabs>li>a{margin-right:-1px;-webkit-border-radius:4px 0 0 4px;-moz-border-radius:4px 0 0 4px;border-radius:4px 0 0 4px;} +.tabs-left .nav-tabs>li>a:hover{border-color:#eeeeee #dddddd #eeeeee #eeeeee;} +.tabs-left .nav-tabs .active>a,.tabs-left .nav-tabs .active>a:hover{border-color:#ddd transparent #ddd #ddd;*border-right-color:#ffffff;} +.tabs-right .nav-tabs{float:right;margin-left:19px;border-left:1px solid #ddd;} +.tabs-right .nav-tabs>li>a{margin-left:-1px;-webkit-border-radius:0 4px 4px 0;-moz-border-radius:0 4px 4px 0;border-radius:0 4px 4px 0;} +.tabs-right .nav-tabs>li>a:hover{border-color:#eeeeee #eeeeee #eeeeee #dddddd;} +.tabs-right .nav-tabs .active>a,.tabs-right .nav-tabs .active>a:hover{border-color:#ddd #ddd #ddd transparent;*border-left-color:#ffffff;} +.navbar{overflow:visible;margin-bottom:18px;} +.navbar-inner{padding-left:20px;padding-right:20px;background-color:#2c2c2c;background-image:-moz-linear-gradient(top, #333333, #222222);background-image:-ms-linear-gradient(top, #333333, #222222);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#333333), to(#222222));background-image:-webkit-linear-gradient(top, #333333, #222222);background-image:-o-linear-gradient(top, #333333, #222222);background-image:linear-gradient(top, #333333, #222222);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#333333', endColorstr='#222222', GradientType=0);-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:0 1px 3px rgba(0, 0, 0, 0.25),inset 0 -1px 0 rgba(0, 0, 0, 0.1);-moz-box-shadow:0 1px 3px rgba(0, 0, 0, 0.25),inset 0 -1px 0 rgba(0, 0, 0, 0.1);box-shadow:0 1px 3px rgba(0, 0, 0, 0.25),inset 0 -1px 0 rgba(0, 0, 0, 0.1);} +.btn-navbar{display:none;float:right;padding:7px 10px;margin-left:5px;margin-right:5px;background-color:#2c2c2c;background-image:-moz-linear-gradient(top, #333333, #222222);background-image:-ms-linear-gradient(top, #333333, #222222);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#333333), to(#222222));background-image:-webkit-linear-gradient(top, #333333, #222222);background-image:-o-linear-gradient(top, #333333, #222222);background-image:linear-gradient(top, #333333, #222222);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#333333', endColorstr='#222222', GradientType=0);border-color:#222222 #222222 #000000;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-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.1),0 1px 0 rgba(255, 255, 255, 0.075);-moz-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.1),0 1px 0 rgba(255, 255, 255, 0.075);box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.1),0 1px 0 rgba(255, 255, 255, 0.075);}.btn-navbar:hover,.btn-navbar:active,.btn-navbar.active,.btn-navbar.disabled,.btn-navbar[disabled]{background-color:#222222;} +.btn-navbar:active,.btn-navbar.active{background-color:#080808 \9;} +.btn-navbar .icon-bar{display:block;width:18px;height:2px;background-color:#f5f5f5;-webkit-border-radius:1px;-moz-border-radius:1px;border-radius:1px;-webkit-box-shadow:0 1px 0 rgba(0, 0, 0, 0.25);-moz-box-shadow:0 1px 0 rgba(0, 0, 0, 0.25);box-shadow:0 1px 0 rgba(0, 0, 0, 0.25);} +.btn-navbar .icon-bar+.icon-bar{margin-top:3px;} +.nav-collapse.collapse{height:auto;} +.navbar .brand:hover{text-decoration:none;} +.navbar .brand{float:left;display:block;padding:8px 20px 12px;margin-left:-20px;font-size:20px;font-weight:200;line-height:1;color:#ffffff;} +.navbar .navbar-text{margin-bottom:0;line-height:40px;color:#999999;}.navbar .navbar-text a:hover{color:#ffffff;background-color:transparent;} +.navbar .btn,.navbar .btn-group{margin-top:5px;} +.navbar .btn-group .btn{margin-top:0;} +.navbar-form{margin-bottom:0;*zoom:1;}.navbar-form:before,.navbar-form:after{display:table;content:"";} +.navbar-form:after{clear:both;} +.navbar-form input,.navbar-form select{display:inline-block;margin-top:5px;margin-bottom:0;} +.navbar-form .radio,.navbar-form .checkbox{margin-top:5px;} +.navbar-form input[type="image"],.navbar-form input[type="checkbox"],.navbar-form input[type="radio"]{margin-top:3px;} +.navbar-form .input-append,.navbar-form .input-prepend{margin-top:6px;white-space:nowrap;}.navbar-form .input-append input,.navbar-form .input-prepend input{margin-top:0;} +.navbar-search{position:relative;float:left;margin-top:6px;margin-bottom:0;}.navbar-search .search-query{padding:4px 9px;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:13px;font-weight:normal;line-height:1;color:#ffffff;color:rgba(255, 255, 255, 0.75);background:#666;background:rgba(255, 255, 255, 0.3);border:1px solid #111;-webkit-box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.1),0 1px 0px rgba(255, 255, 255, 0.15);-moz-box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.1),0 1px 0px rgba(255, 255, 255, 0.15);box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.1),0 1px 0px rgba(255, 255, 255, 0.15);-webkit-transition:none;-moz-transition:none;-ms-transition:none;-o-transition:none;transition:none;}.navbar-search .search-query :-moz-placeholder{color:#eeeeee;} +.navbar-search .search-query::-webkit-input-placeholder{color:#eeeeee;} +.navbar-search .search-query:hover{color:#ffffff;background-color:#999999;background-color:rgba(255, 255, 255, 0.5);} +.navbar-search .search-query:focus,.navbar-search .search-query.focused{padding:5px 10px;color:#333333;text-shadow:0 1px 0 #ffffff;background-color:#ffffff;border:0;-webkit-box-shadow:0 0 3px rgba(0, 0, 0, 0.15);-moz-box-shadow:0 0 3px rgba(0, 0, 0, 0.15);box-shadow:0 0 3px rgba(0, 0, 0, 0.15);outline:0;} +.navbar-fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030;} +.navbar-fixed-top .navbar-inner{padding-left:0;padding-right:0;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;} +.navbar .nav{position:relative;left:0;display:block;float:left;margin:0 10px 0 0;} +.navbar .nav.pull-right{float:right;} +.navbar .nav>li{display:block;float:left;} +.navbar .nav>li>a{float:none;padding:10px 10px 11px;line-height:19px;color:#999999;text-decoration:none;text-shadow:0 -1px 0 rgba(0, 0, 0, 0.25);} +.navbar .nav>li>a:hover{background-color:transparent;color:#ffffff;text-decoration:none;} +.navbar .nav .active>a,.navbar .nav .active>a:hover{color:#ffffff;text-decoration:none;background-color:#222222;} +.navbar .divider-vertical{height:40px;width:1px;margin:0 9px;overflow:hidden;background-color:#222222;border-right:1px solid #333333;} +.navbar .nav.pull-right{margin-left:10px;margin-right:0;} +.navbar .dropdown-menu{margin-top:1px;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;}.navbar .dropdown-menu:before{content:'';display:inline-block;border-left:7px solid transparent;border-right:7px solid transparent;border-bottom:7px solid #ccc;border-bottom-color:rgba(0, 0, 0, 0.2);position:absolute;top:-7px;left:9px;} +.navbar .dropdown-menu:after{content:'';display:inline-block;border-left:6px solid transparent;border-right:6px solid transparent;border-bottom:6px solid #ffffff;position:absolute;top:-6px;left:10px;} +.navbar .nav .dropdown-toggle .caret,.navbar .nav .open.dropdown .caret{border-top-color:#ffffff;} +.navbar .nav .active .caret{opacity:1;filter:alpha(opacity=100);} +.navbar .nav .open>.dropdown-toggle,.navbar .nav .active>.dropdown-toggle,.navbar .nav .open.active>.dropdown-toggle{background-color:transparent;} +.navbar .nav .active>.dropdown-toggle:hover{color:#ffffff;} +.navbar .nav.pull-right .dropdown-menu{left:auto;right:0;}.navbar .nav.pull-right .dropdown-menu:before{left:auto;right:12px;} +.navbar .nav.pull-right .dropdown-menu:after{left:auto;right:13px;} +.breadcrumb{padding:7px 14px;margin:0 0 18px;background-color:#fbfbfb;background-image:-moz-linear-gradient(top, #ffffff, #f5f5f5);background-image:-ms-linear-gradient(top, #ffffff, #f5f5f5);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), to(#f5f5f5));background-image:-webkit-linear-gradient(top, #ffffff, #f5f5f5);background-image:-o-linear-gradient(top, #ffffff, #f5f5f5);background-image:linear-gradient(top, #ffffff, #f5f5f5);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#f5f5f5', GradientType=0);border:1px solid #ddd;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;-webkit-box-shadow:inset 0 1px 0 #ffffff;-moz-box-shadow:inset 0 1px 0 #ffffff;box-shadow:inset 0 1px 0 #ffffff;}.breadcrumb li{display:inline-block;text-shadow:0 1px 0 #ffffff;} +.breadcrumb .divider{padding:0 5px;color:#999999;} +.breadcrumb .active a{color:#333333;} +.pagination{height:36px;margin:18px 0;} +.pagination ul{display:inline-block;*display:inline;*zoom:1;margin-left:0;margin-bottom:0;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;-webkit-box-shadow:0 1px 2px rgba(0, 0, 0, 0.05);-moz-box-shadow:0 1px 2px rgba(0, 0, 0, 0.05);box-shadow:0 1px 2px rgba(0, 0, 0, 0.05);} +.pagination li{display:inline;} +.pagination a{float:left;padding:0 14px;line-height:34px;text-decoration:none;border:1px solid #ddd;border-left-width:0;} +.pagination a:hover,.pagination .active a{background-color:#f5f5f5;} +.pagination .active a{color:#999999;cursor:default;} +.pagination .disabled a,.pagination .disabled a:hover{color:#999999;background-color:transparent;cursor:default;} +.pagination li:first-child a{border-left-width:1px;-webkit-border-radius:3px 0 0 3px;-moz-border-radius:3px 0 0 3px;border-radius:3px 0 0 3px;} +.pagination li:last-child a{-webkit-border-radius:0 3px 3px 0;-moz-border-radius:0 3px 3px 0;border-radius:0 3px 3px 0;} +.pagination-centered{text-align:center;} +.pagination-right{text-align:right;} +.pager{margin-left:0;margin-bottom:18px;list-style:none;text-align:center;*zoom:1;}.pager:before,.pager:after{display:table;content:"";} +.pager:after{clear:both;} +.pager li{display:inline;} +.pager a{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;-webkit-border-radius:15px;-moz-border-radius:15px;border-radius:15px;} +.pager a:hover{text-decoration:none;background-color:#f5f5f5;} +.pager .next a{float:right;} +.pager .previous a{float:left;} +.modal-open .dropdown-menu{z-index:2050;} +.modal-open .dropdown.open{*z-index:2050;} +.modal-open .popover{z-index:2060;} +.modal-open .tooltip{z-index:2070;} +.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000000;}.modal-backdrop.fade{opacity:0;} +.modal-backdrop,.modal-backdrop.fade.in{opacity:0.8;filter:alpha(opacity=80);} +.modal{position:fixed;top:50%;left:50%;z-index:1050;max-height:500px;overflow:auto;width:560px;margin:-250px 0 0 -280px;background-color:#ffffff;border:1px solid #999;border:1px solid rgba(0, 0, 0, 0.3);*border:1px solid #999;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;-webkit-box-shadow:0 3px 7px rgba(0, 0, 0, 0.3);-moz-box-shadow:0 3px 7px rgba(0, 0, 0, 0.3);box-shadow:0 3px 7px rgba(0, 0, 0, 0.3);-webkit-background-clip:padding-box;-moz-background-clip:padding-box;background-clip:padding-box;}.modal.fade{-webkit-transition:opacity .3s linear, top .3s ease-out;-moz-transition:opacity .3s linear, top .3s ease-out;-ms-transition:opacity .3s linear, top .3s ease-out;-o-transition:opacity .3s linear, top .3s ease-out;transition:opacity .3s linear, top .3s ease-out;top:-25%;} +.modal.fade.in{top:50%;} +.modal-header{padding:9px 15px;border-bottom:1px solid #eee;}.modal-header .close{margin-top:2px;} +.modal-body{padding:15px;} +.modal-body .modal-form{margin-bottom:0;} +.modal-footer{padding:14px 15px 15px;margin-bottom:0;background-color:#f5f5f5;border-top:1px solid #ddd;-webkit-border-radius:0 0 6px 6px;-moz-border-radius:0 0 6px 6px;border-radius:0 0 6px 6px;-webkit-box-shadow:inset 0 1px 0 #ffffff;-moz-box-shadow:inset 0 1px 0 #ffffff;box-shadow:inset 0 1px 0 #ffffff;*zoom:1;}.modal-footer:before,.modal-footer:after{display:table;content:"";} +.modal-footer:after{clear:both;} +.modal-footer .btn{float:right;margin-left:5px;margin-bottom:0;} +.tooltip{position:absolute;z-index:1020;display:block;visibility:visible;padding:5px;font-size:11px;opacity:0;filter:alpha(opacity=0);}.tooltip.in{opacity:0.8;filter:alpha(opacity=80);} +.tooltip.top{margin-top:-2px;} +.tooltip.right{margin-left:2px;} +.tooltip.bottom{margin-top:2px;} +.tooltip.left{margin-left:-2px;} +.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-left:5px solid transparent;border-right:5px solid transparent;border-top:5px solid #000000;} +.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-top:5px solid transparent;border-bottom:5px solid transparent;border-left:5px solid #000000;} +.tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-left:5px solid transparent;border-right:5px solid transparent;border-bottom:5px solid #000000;} +.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-top:5px solid transparent;border-bottom:5px solid transparent;border-right:5px solid #000000;} +.tooltip-inner{max-width:200px;padding:3px 8px;color:#ffffff;text-align:center;text-decoration:none;background-color:#000000;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;} +.tooltip-arrow{position:absolute;width:0;height:0;} +.popover{position:absolute;top:0;left:0;z-index:1010;display:none;padding:5px;}.popover.top{margin-top:-5px;} +.popover.right{margin-left:5px;} +.popover.bottom{margin-top:5px;} +.popover.left{margin-left:-5px;} +.popover.top .arrow{bottom:0;left:50%;margin-left:-5px;border-left:5px solid transparent;border-right:5px solid transparent;border-top:5px solid #000000;} +.popover.right .arrow{top:50%;left:0;margin-top:-5px;border-top:5px solid transparent;border-bottom:5px solid transparent;border-right:5px solid #000000;} +.popover.bottom .arrow{top:0;left:50%;margin-left:-5px;border-left:5px solid transparent;border-right:5px solid transparent;border-bottom:5px solid #000000;} +.popover.left .arrow{top:50%;right:0;margin-top:-5px;border-top:5px solid transparent;border-bottom:5px solid transparent;border-left:5px solid #000000;} +.popover .arrow{position:absolute;width:0;height:0;} +.popover-inner{padding:3px;width:280px;overflow:hidden;background:#000000;background:rgba(0, 0, 0, 0.8);-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;-webkit-box-shadow:0 3px 7px rgba(0, 0, 0, 0.3);-moz-box-shadow:0 3px 7px rgba(0, 0, 0, 0.3);box-shadow:0 3px 7px rgba(0, 0, 0, 0.3);} +.popover-title{padding:9px 15px;line-height:1;background-color:#f5f5f5;border-bottom:1px solid #eee;-webkit-border-radius:3px 3px 0 0;-moz-border-radius:3px 3px 0 0;border-radius:3px 3px 0 0;} +.popover-content{padding:14px;background-color:#ffffff;-webkit-border-radius:0 0 3px 3px;-moz-border-radius:0 0 3px 3px;border-radius:0 0 3px 3px;-webkit-background-clip:padding-box;-moz-background-clip:padding-box;background-clip:padding-box;}.popover-content p,.popover-content ul,.popover-content ol{margin-bottom:0;} +.thumbnails{margin-left:-20px;list-style:none;*zoom:1;}.thumbnails:before,.thumbnails:after{display:table;content:"";} +.thumbnails:after{clear:both;} +.thumbnails>li{float:left;margin:0 0 18px 20px;} +.thumbnail{display:block;padding:4px;line-height:1;border:1px solid #ddd;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:0 1px 1px rgba(0, 0, 0, 0.075);-moz-box-shadow:0 1px 1px rgba(0, 0, 0, 0.075);box-shadow:0 1px 1px rgba(0, 0, 0, 0.075);} +a.thumbnail:hover{border-color:#0088cc;-webkit-box-shadow:0 1px 4px rgba(0, 105, 214, 0.25);-moz-box-shadow:0 1px 4px rgba(0, 105, 214, 0.25);box-shadow:0 1px 4px rgba(0, 105, 214, 0.25);} +.thumbnail>img{display:block;max-width:100%;margin-left:auto;margin-right:auto;} +.thumbnail .caption{padding:9px;} +.label{padding:2px 4px 3px;font-size:11.049999999999999px;font-weight:bold;color:#ffffff;text-shadow:0 -1px 0 rgba(0, 0, 0, 0.25);background-color:#999999;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;} +.label:hover{color:#ffffff;text-decoration:none;} +.label-important{background-color:#b94a48;} +.label-important:hover{background-color:#953b39;} +.label-warning{background-color:#f89406;} +.label-warning:hover{background-color:#c67605;} +.label-success{background-color:#468847;} +.label-success:hover{background-color:#356635;} +.label-info{background-color:#3a87ad;} +.label-info:hover{background-color:#2d6987;} +@-webkit-keyframes progress-bar-stripes{from{background-position:0 0;} to{background-position:40px 0;}}@-moz-keyframes progress-bar-stripes{from{background-position:0 0;} to{background-position:40px 0;}}@keyframes progress-bar-stripes{from{background-position:0 0;} to{background-position:40px 0;}}.progress{overflow:hidden;height:18px;margin-bottom:18px;background-color:#f7f7f7;background-image:-moz-linear-gradient(top, #f5f5f5, #f9f9f9);background-image:-ms-linear-gradient(top, #f5f5f5, #f9f9f9);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#f5f5f5), to(#f9f9f9));background-image:-webkit-linear-gradient(top, #f5f5f5, #f9f9f9);background-image:-o-linear-gradient(top, #f5f5f5, #f9f9f9);background-image:linear-gradient(top, #f5f5f5, #f9f9f9);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#f5f5f5', endColorstr='#f9f9f9', GradientType=0);-webkit-box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.1);-moz-box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.1);box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.1);-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;} +.progress .bar{width:0%;height:18px;color:#ffffff;font-size:12px;text-align:center;text-shadow:0 -1px 0 rgba(0, 0, 0, 0.25);background-color:#0e90d2;background-image:-moz-linear-gradient(top, #149bdf, #0480be);background-image:-ms-linear-gradient(top, #149bdf, #0480be);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#149bdf), to(#0480be));background-image:-webkit-linear-gradient(top, #149bdf, #0480be);background-image:-o-linear-gradient(top, #149bdf, #0480be);background-image:linear-gradient(top, #149bdf, #0480be);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#149bdf', endColorstr='#0480be', GradientType=0);-webkit-box-shadow:inset 0 -1px 0 rgba(0, 0, 0, 0.15);-moz-box-shadow:inset 0 -1px 0 rgba(0, 0, 0, 0.15);box-shadow:inset 0 -1px 0 rgba(0, 0, 0, 0.15);-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;-webkit-transition:width 0.6s ease;-moz-transition:width 0.6s ease;-ms-transition:width 0.6s ease;-o-transition:width 0.6s ease;transition:width 0.6s ease;} +.progress-striped .bar{background-color:#62c462;background-image:-webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent));background-image:-webkit-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-moz-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-ms-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);-webkit-background-size:40px 40px;-moz-background-size:40px 40px;-o-background-size:40px 40px;background-size:40px 40px;} +.progress.active .bar{-webkit-animation:progress-bar-stripes 2s linear infinite;-moz-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite;} +.progress-danger .bar{background-color:#dd514c;background-image:-moz-linear-gradient(top, #ee5f5b, #c43c35);background-image:-ms-linear-gradient(top, #ee5f5b, #c43c35);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#ee5f5b), to(#c43c35));background-image:-webkit-linear-gradient(top, #ee5f5b, #c43c35);background-image:-o-linear-gradient(top, #ee5f5b, #c43c35);background-image:linear-gradient(top, #ee5f5b, #c43c35);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ee5f5b', endColorstr='#c43c35', GradientType=0);} +.progress-danger.progress-striped .bar{background-color:#ee5f5b;background-image:-webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent));background-image:-webkit-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-moz-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-ms-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);} +.progress-success .bar{background-color:#5eb95e;background-image:-moz-linear-gradient(top, #62c462, #57a957);background-image:-ms-linear-gradient(top, #62c462, #57a957);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#62c462), to(#57a957));background-image:-webkit-linear-gradient(top, #62c462, #57a957);background-image:-o-linear-gradient(top, #62c462, #57a957);background-image:linear-gradient(top, #62c462, #57a957);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#62c462', endColorstr='#57a957', GradientType=0);} +.progress-success.progress-striped .bar{background-color:#62c462;background-image:-webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent));background-image:-webkit-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-moz-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-ms-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);} +.progress-info .bar{background-color:#4bb1cf;background-image:-moz-linear-gradient(top, #5bc0de, #339bb9);background-image:-ms-linear-gradient(top, #5bc0de, #339bb9);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#5bc0de), to(#339bb9));background-image:-webkit-linear-gradient(top, #5bc0de, #339bb9);background-image:-o-linear-gradient(top, #5bc0de, #339bb9);background-image:linear-gradient(top, #5bc0de, #339bb9);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#5bc0de', endColorstr='#339bb9', GradientType=0);} +.progress-info.progress-striped .bar{background-color:#5bc0de;background-image:-webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent));background-image:-webkit-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-moz-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-ms-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);} +.accordion{margin-bottom:18px;} +.accordion-group{margin-bottom:2px;border:1px solid #e5e5e5;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;} +.accordion-heading{border-bottom:0;} +.accordion-heading .accordion-toggle{display:block;padding:8px 15px;} +.accordion-inner{padding:9px 15px;border-top:1px solid #e5e5e5;} +.carousel{position:relative;margin-bottom:18px;line-height:1;} +.carousel-inner{overflow:hidden;width:100%;position:relative;} +.carousel .item{display:none;position:relative;-webkit-transition:0.6s ease-in-out left;-moz-transition:0.6s ease-in-out left;-ms-transition:0.6s ease-in-out left;-o-transition:0.6s ease-in-out left;transition:0.6s ease-in-out left;} +.carousel .item>img{display:block;line-height:1;} +.carousel .active,.carousel .next,.carousel .prev{display:block;} +.carousel .active{left:0;} +.carousel .next,.carousel .prev{position:absolute;top:0;width:100%;} +.carousel .next{left:100%;} +.carousel .prev{left:-100%;} +.carousel .next.left,.carousel .prev.right{left:0;} +.carousel .active.left{left:-100%;} +.carousel .active.right{left:100%;} +.carousel-control{position:absolute;top:40%;left:15px;width:40px;height:40px;margin-top:-20px;font-size:60px;font-weight:100;line-height:30px;color:#ffffff;text-align:center;background:#222222;border:3px solid #ffffff;-webkit-border-radius:23px;-moz-border-radius:23px;border-radius:23px;opacity:0.5;filter:alpha(opacity=50);}.carousel-control.right{left:auto;right:15px;} +.carousel-control:hover{color:#ffffff;text-decoration:none;opacity:0.9;filter:alpha(opacity=90);} +.carousel-caption{position:absolute;left:0;right:0;bottom:0;padding:10px 15px 5px;background:#333333;background:rgba(0, 0, 0, 0.75);} +.carousel-caption h4,.carousel-caption p{color:#ffffff;} +.hero-unit{padding:60px;margin-bottom:30px;background-color:#f5f5f5;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;}.hero-unit h1{margin-bottom:0;font-size:60px;line-height:1;letter-spacing:-1px;} +.hero-unit p{font-size:18px;font-weight:200;line-height:27px;} +.pull-right{float:right;} +.pull-left{float:left;} +.hide{display:none;} +.show{display:block;} +.invisible{visibility:hidden;} diff --git a/static/css/box.css b/static/css/box.css deleted file mode 100644 index 26d0a0a75..000000000 --- a/static/css/box.css +++ /dev/null @@ -1,207 +0,0 @@ -body { - font-family: arial, helvetica, verdana, tahoma, sans-serif; - font-size: 0.9em; - margin: 0 auto; - padding: 0; -} - -div.row { - clear: both; - padding-top: 10px; - padding-left: 20px; -} - -div.row label { - float: left; - width: 120px; - padding: 3px 0; - margin-right: 5px; -} - -input { - border: 1px solid #cccccc; - color: inherit; - background-color: #eee; - padding: 2px; -} - -input[type=text], input[type=password] { - width: 140px; -} - -/*input:hover { - color: inherit; - background-color: #ffffcc; -}*/ - -input:focus { - color: inherit; - background-color: #ffffcc; -} - -textarea { - border: 1px solid #cccccc; - background-color: #eee; - width: 300px; - font-family: inherit; - font-size: inherit; -} - -select { - width: 145px; - background-color: #eee; - border: 1px solid #cccccc; -} - -td.odd { - background: #F7F9Fb; -} - -td.even { - -} - -.title { - float: left; - width: 100%; - margin-bottom: 5px; -} - -.title img { - float: left; - margin: 1px 3px;; -} - -.title h1 { - float: left; - margin: 5px; - font-size: medium; - font-weight: bold; -} - -.lastrow { - margin-top: 20px; -} - -.errorlist ul { - float: left; - list-style: none; -} - -.errorlist li { - margin-left: 10px; - float: left; - display: inline; - color: red; -} - -.error { - margin-left: 20px; - color: red; - font-size: small; - font-weight: bold; -} - -.center { - text-align: center; -} - -#id_username { - width: 120px; -} - -#id_password { - width: 120px; -} - -/* Mootools tooltips */ -.tip { - display: none; - position: absolute; - color: #fff; - z-index: 2; - border: 1px solid #cccccc; -} - -.tip-title { - font-weight: bold; - font-size: 11px; - margin: 0; - color: #9FD4FF; - padding: 8px 8px 4px; - background: #eee; -} - -.tip-text { - font-size: 0.9em; - padding: 4px 8px 8px; - background: #eee; - color: black; -} - -.spacer { - clear: both; -} - -ul { - list-style: none; - padding: 0; -} - -li { - display: inline; -} - -/* - * clearfix related - */ -.clearfix:after { - content: "."; - display: block; - clear: both; - visibility: hidden; - overflow: hidden; - line-height: 0; - height: 0; -} - -.clearfix { - display: inline-block; -} - -html[xmlns] .clearfix { - display: block; -} - -* html .clearfix { - height: 1%; - overflow: visible; -} - -/* - * Domain admin form - */ -#createfrom { - margin: 10px auto; - width: auto; -} - -#permform > #id_name { - width: 180px; -} - -/* - * Assign domains form - */ -#assignform #id_domains { - width: 310px; - height: 200px; -} - -/* - * Domain admins list - */ -#domainadmins { - width: 100%; - height: 140px; -} \ No newline at end of file diff --git a/static/css/calendar.css b/static/css/calendar.css deleted file mode 100644 index 969d1e185..000000000 --- a/static/css/calendar.css +++ /dev/null @@ -1,91 +0,0 @@ -/* - Document : calendar.css - Created on : 29.07.2009 - Author : jens klose - Description: - example styles for calendar -*/ -.calendarForm{ - float:right; -} -.calendar{ - font-family:verdana,sans-serif; - font-size:10px; - float:None; -} -button.calendar{ - position:relative; - top:-3px; - left:1px; - background-color:transparent; - background-image:url(../pics/calendar.gif); - border:none; - margin: 0px; - padding:0px; - margin-right:3px; - cursor:pointer; - width:20px; - height:20px; -} -input.calendar{ - cursor:pointer; - border: 1px solid #87847F; - height:12px; - width:100px; - color: #87847F; -} -div.calendar{ - background:white; - border: 1px solid #87847F; - width:180px -} -a.footer{ - color:#474747; - padding:2px; -} -.calendar table{ - width:100%; -} -.calendar th, -.calendar td{ - text-align:center; - font-size:10px; - padding: 0.1em 0.25em 0.1em 0.3em; -} -.calendar tbody td, -.calendar .invalid{ - color:silver; -} -.calendar .valid{ - color:blue; - font-weight:bold; - cursor:pointer; -} -.calendar .valid.hover{ - background:#FFFF77; - font-weight:bold; -} -.calendar caption a, -.calendar .next{ - color:blue; - font-weight:bold; - padding:2px; - cursor:pointer; -} -.calendar caption a, -.calendar .prev{ - color:blue; - font-weight:bold; - padding:2px; -} -.calendar .active, -.calendar .hilite, -.calendar .inactive{ - color:white; - background-color:#BE3916; - font-weight:bold; -} -.calendar .active, -.calendar .inactive{ - background-color:red; -} \ No newline at end of file diff --git a/static/css/closebox.png b/static/css/closebox.png deleted file mode 100644 index 4de4396d4a09677774f79289de2a09511baeea22..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1910 zcmV-+2Z{KJP)z^Q>-8KIssI20AY({UO#lFGm;eBCjsO7iaR2~=Yybd* zy8r;x$N&H_>;M26yq<`~KL7v*>`6pHRCwBAWWWy?p_swJ!GY=X=g*8ke*9qk`ST}Z zdU`qw7Z(@fv}x0Rf#f+jIR5kU^8P2LK7aoF=BZPs zUOIK^)MsF*`~o`QH`D=t(JcoEAauk84FygG1qFf7&``O#bLZLs4PE;m4ZL{q;_rh8 z5B{QyfBg7y1u$e3few}u6cpqKnFTeI3E3im07CaENCB6OjEqoBOpMCLjT`;`{{8y^ zWZ_n$O7C)YSBAZ*T9f@87@wgQ~r-ckkXvV`F1EUVpg2H5Y-eYuxnRM9M39EBU%&nZ#x+Xt8W|aVwYIj#h+{D^u`kKV z$v=Qu3ogdS#`gK?)2F{dW*t0uupVfZDo|VqWEaTS009KEm>Z}`3Ydy*?%%(^5TtnW z(;G*fzk0D zSSo?}K!do)4&2r=HI`6@Kg;DK;Y5@D8S6f$jA!R2#R=y z-@kw3EZXkgz5D;mmoF#|yK?2qKWvtQ08AWYDKNn@LoH?k2q3V5?BoZ76wef+J_SucZ|q}*4Eat0ppwvn;u{u2G>@=;`K8pCnvlB0SF)lXugsH z7X0Rlii*j=;_ekFPSCRwF!6l>MgdBk`uh5!#Cc9m&JXm$1Xy;1vlOsk$^_bB1{9YD zI&JkwlS1w(;^Z^)!@LCEWfM7lr1G>gQO-(Jludi=U<2%PtU4%`gn2LZ7@5Q~G#ZGZrRXD5)S*xA{=fSSvIdGkEBd`SvUNJ#h& zEi#`0(?mH?%mZWxEIR=N5WD~ZWo}@L!d6H~C^;o1WhN}%Kn9R%DX3Tl1uIBDuwk$s zD3%Dsw#WqtKmai#w>W@Fo)wt5IG#Lt!pzOh{RtR^ngIa;EI|DX!1DGl0}&uLHkM`0 znlZR5`~$4;!Ko9NO&EZQ5}bNLd|;LO3|NQX zDk&*B3FO`b6<$F66o}sdF(~nY(#>Ch0AfUH2(Un1!w0oo5@@*$Fu}2#z zG+4x;SOQq;hynAf5U?N-FflO^1=e1m<_rU{W%&_U5xfEBgts6ML*w!V5Wj@tchKm7 z#Vt550|XEwmS!y*Qm}(kyC5{X2|-gN4>0wE+fTqMj{(U315A|Pffy8wAD|Y29Pu8S zm|(&C3%QvK5I~Grnzu|)pR++TIyW=~c%hErLTWoPKn(*K{0(XeEWv#R;_pxkeqBrOP0*Db$8yndX$XyynEc#&m wB;+m&vSk?cJ3s(2G7t}lRr{Y9MF0T?00rpcocj25u>b%707*qoM6N<$g7Px#24YJ`L;(K){{a7>y{D4^000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2iOcB z4j3MdxfU7#000?uMObu0Z*6U5Zgc=ca%Ew3Wn>_CX>@2HM@dakSAh-}0003>Nkl!%1QTpl^X=s6Sobm^0BYz-Mao=*FFI z46p^(Qw<KWA++k?a_@mgc&837*J72%#%q-27rs&lm+hw2*!U$1V*sxeey0_FUm8ygztIX` lXoXi=;S_j*7kGh}`~di?BcNWtVvGO)002ovPDHLkV1kBd%d`Lh diff --git a/static/css/custom.css b/static/css/custom.css new file mode 100644 index 000000000..28121a8d2 --- /dev/null +++ b/static/css/custom.css @@ -0,0 +1,64 @@ +body { + padding-top: 60px; +} + +#bottom-bar { + position: fixed; + left: 0; + right: 0; + bottom: 0; +} + +#pagination_bar { + color: #999999 !important; +} + +.simplepagination ul { + display: inline-block; +} + +.simplepagination li { + display: inline; +} + +.simplepagination .disabled a, +.simplepagination .disabled a:hover { + background-color: transparent; + cursor: default; +} + +.sidebar-nav { + padding: 9px 0; +} + +.form-horizontal .param-label { + float: left; + padding-top: 5px; + text-align: left; + width: 200px; +} + +.form-horizontal .param-controls { + margin-left: 210px; +} + +.form-horizontal .control-group { + margin-bottom: 8px; +} + +.spaced select, +.spaced input[type="text"] { + margin-bottom: 8px; +} + +.padded5 { + padding: 5px; +} + +.buttons_list .bset { + display: none; +} + +.buttons_list .active { + display: block; +} diff --git a/static/css/default.css b/static/css/default.css index 2ea12f27d..3100fd50c 100644 --- a/static/css/default.css +++ b/static/css/default.css @@ -68,7 +68,7 @@ div#footer { #menubar { margin: 5px; - display: table; + /*display: table;*/ width: 735px; margin-top: 35px; } @@ -83,7 +83,7 @@ div#footer { font-weight: bold; } -#menubar a, #menubar a:visited { +/*#menubar a, #menubar a:visited { color: black; font-weight: bold; } @@ -91,7 +91,7 @@ div#footer { #menubar label { color: black; font-weight: bold; -} +}*/ #page { width: 100%; @@ -167,7 +167,7 @@ div#footer { width: 100%; } -.menubar a, .menubar a:visited { +/*.menubar a, .menubar a:visited { text-decoration: none; } @@ -201,7 +201,7 @@ div#footer { .menubar .title { color: white; -} +}*/ .menubardropdown ul { background: white; diff --git a/static/css/login.css b/static/css/login.css index 70fd8145c..fe14c6007 100644 --- a/static/css/login.css +++ b/static/css/login.css @@ -1,93 +1,19 @@ -html, body { - font-family: arial, helvetica, verdana, tahoma, sans-serif; - font-size: small; - background: #153F65; - margin: 0; - padding: 0; -} - -#loginbox { - margin: 100px auto 10px auto; - padding: 0 0 15px 0; - font-size: 1em; - width: 630px; -} - -#logo { - float: left; - text-align: center; - width: 300px; - margin-top: 10px; - border-right: 1px solid #ccc; +#logo { + margin-top: 60px; } #logo img { - height: 90px; + height: 100px; } -#test { - float: right; - width: 300px; +input[type="text"], +input[type="password"] { + width: 315px; + padding: 7px; } -.roundedbox { - margin: 10px auto; - padding: 10px; - width: 610px; - background: white; - font-size: 1.1em; - - border: 5px solid #333; - border-radius: 10px; +.form-actions { + padding-right: 0px; + padding-left: 0px; } -.roundedbox h1 { - padding: 0; - margin: 0; - font-size: 1.2em; - text-align: center; -} - -.roundedbox h2 { - margin: 30px 10px 0 10px; - font-size: small; -} - -.roundedbox h3 { - font-size: small; - text-align: center; -} - -.roundedbox a, a:visited { - font-weight: bold; - color: black; - text-decoration: none; -} - -.spacer { - clear: both; -} - -/* Mootools tooltips */ -.tip { - color: #fff; - width: 139px; - z-index: 13000; - border: 1px solid #cccccc; -} - -.tip-title { - font-weight: bold; - font-size: 11px; - margin: 0; - color: #9FD4FF; - padding: 8px 8px 4px; - background: #eee; -} - -.tip-text { - font-size: 11px; - padding: 4px 8px 8px; - background: #eee; - color: black; -} diff --git a/static/img/glyphicons-halflings-white.png b/static/img/glyphicons-halflings-white.png new file mode 100644 index 0000000000000000000000000000000000000000..a20760bfde58d1c92cee95116059fba03c68d689 GIT binary patch literal 4352 zcmd6r_dnEu|G?izMxtxU%uI5!l8nr)ZF&&*%FGe4jtO*5mbhJzhV&et11z&&^B?xH$MZ007{+ZK!Jj01(PQ zJBFS4pH$0DefCd1HM@h*JNkcsi%oOXzj>qsEle$eQ7ApHL(XYdn5Y$Lk_3-J9p9d) zFeVfl3J47_g1XaoDXWsnBp9ZzZ74CI9RN-Nw{>+8A&#rBpZgc9WX2H3Ssv6doZP?t zS!g}lGvW1<9%?dj_G_x}3WUMN(8(x{a6_pd0yiUsf^67GGS50uSB*ORe5x6}qAf1z z@Q;2y4G{Lb?f21p)uTpChN&4q%^blZ2IsusUOhk)pe0yxPD6oHKXWSjv8&2pMdnegiQUtoXt1U0MmWAWu2&>3j$eb^qKNV z_(`JQZP&mXLT@U%-2rPy!7r|*Y1oAdlarltaUyq+yq^|d{B9_>t@Rd#@_KW9w_6P$ z^Dv8(Hi8pDJK{r0Iqq*va$cL=isZh0=1)wIoQ^vYPs$(rBz$+DY z`y}1}`M%-da686`}zw_w>8 z!BcqxVTim*F)-}$segV$ON*!Zl~dhX@Rz^K2Xurh<1-vjImult%O z!-WXvkA_agVuhluW};J;#r>)?^uHS;G?a?j;(z?Y^FTwOA?tzLFvQDf&X8}9s7Wh< znEfd_vPyF_V`?>kR`w_h@+%59oKa;NPVGUo52QjisO-|$cYE(VNmm#+`#T5a;gh|Z z8A0^l3UwQMn0J3xXWL7tY~OxAu=_hGvp@_%SZKA)ec-h-dfwIhS3jGBLL6e6Os;1LR zRDG&3TF`HV*n{&*H!oTSsLq!U5xV5!Yr6I_!*VhmwC3a2BOYfWH13AtVY|n5jv49e zcb0xCCZnt0i$>-S$k9J@-c!8wG#siu(Lgy_r1nfy+}!W9g-ucwp=&Hs1=Vs4i_q;dQL$8~Uq2BVA4o4uY!6}S`xH(Qec+{mJD~qgg@6W8 zipi@Z!ZR+Kr_)u&G);pG$tg$8#KPrsl&N3(m($NAU&9ogH9rVfW<4Mw>^7$&96g<9 zHQzekG9T5SS7DVm7EFY%CjChhfRyap4+d;+^0ng^B)~xKFG^7d2oOo|R8uY&S|X0@ znAGMb^rFQwGPTzsFQ8ZK4S@WO(8`6T+$Yt9{jGMd?jrTeb|_!Un`n9xDZu-fW+_aJ z4Uyy_$)`Ot!~doWUHW`(?F!iYvc5+g-(W9X<-tX*h%6(f;+A(OQ@w{WYSiq&pjKnN z)tSH~5g)03sKk)U+&GyP*?86fusX1ttpH1ng8ruC6UOddM~t>0wvZh}1cW%&7{tT$ zze(TwkA~V|_~nL{6YE#^RUC__Mx26zo*w(EfK2Q@R6xo`VkJKs^Eax`&*O*bw~*ap zyaqA_p(~(POY{H5+NIgewtB{|(%ML_wR8o);^XGTQ|{*J>74v>{_iyU;U*NTN}A%` z`8ltg(&furYlb!j%1ra!KPSiGmJ>f4c!bkAtjb_qmQ+aVB(QohO zRo@%)1krVtMPgkT6&3T*u`XO8pE&-!!u((3qVnraj|gN5aDxvqtrPs*MCZcO3i^Qt zI7$&BFr)50exhv11)82?u`ab0FgUSw;dpbnAtmz4k^&Nx`xMQ$5(JW}ry%)ry+DV> zS)TWjtXz7V6iK5$ghFuPiT>;;fAp)oy%%7grs4UwqU5+Ms96%`wU=YU5W-UGw(6iq z2GhB=Zw49;Yu<#7=soc@tZvYFIVNfkRPsCT&;76cYOONMwv!v*e#(X?l7eB- z&pWvVcaO;IKDg7C8bZ-+Hm`g>n_WC6%BL=CZlc``M{0T;%eYQ4t}V%m20okR=HET) z@)@WU_}tJOqiH7w2K%lpe0P z^FhhCX$ufUPCq4?C1A8ZSrVz=$~!VZ>;=kb8eaI;S1TKb|E9j*muthJe2||9pYYI$ zR@lkEo?K76^_v{llrL+?Swi1koJYJqG_-g!v?$ITb=q4#Rk--)fABD zh4Ibu7+f~5HEzy@7xoP^f$=} z+D3gYZ3W>%>m=U)p#UNOPPd&2cD&; zxb{vXTzpCjcJAOEA_~=RX^_BM+_BYW*T{zzM(3TosvFOmf6Kp0IerP4`MuBgFdrkZ zf9X~m0O$toCckMn8klZDxWKr2%FHNk1VLQE)$!{Hz9{*a@TaZjC7kKsC1dIUx*6AQ zJFZc8p~!CewW(VvE@yaTPFt-6n+dZ@TM582m7=-#9JoDOH#zYPe{)-Lza89t+w#Zd zvQ3k$)Q)mPF)g)_+v$Gqgq~*RwGeBn{vhp!IPgkixW8WY)H`S{&~om!keO$Sum=oY zTatGW#*O^aVU<^!#et91z~$IYa;_C@J7+V)`<1b_lh`8FHOAgc=Az}lf)k%5xTMrv zr6uV%eKaU~wvi7pU)MeB7HK z2D;27Dik%)-q@hK-!I|N(cl`lAF^EIv0C-t$d1qtFnKIkcMW<4b%Lzf3Y+~~qB7`< zj);HTQS0Oex%zA170>?kRVA_m_*O?rZRpS3v{+O+cifN7Eb&>$Z==vGKh1V)C`qGu z_u8y<#N3Wp&$V^@T??GnE&RN^IyXM)r0h(gS3;b2pt0O!eNIt4{;3H~V5Ln7vs>8{ ziqqZL4Nwlvj4CtEv0>;Fw~D>LB_+-ecI)tiR%a!^GI3BawvNQGz4#b|_df&`e||2k;K}WnvU!Dx=0#ue(=U# zK&pYNNf5RQZOveUm+;dQ*FIA0&#`?@z*bBhUgr(n9_FpoHPB2pI8iMpW|sF*D{+75 z-k;nba~m^}=b7P$FAF1)S!oDKtNG-`%h{XQi6=SMH5GZ%8j?ugqt~!K zwvA_m(*=EIssFVW0EZ;o=u#R5gBB$CUL+->U32;2PM2O(drij20XBy|hH+=bu!0*KIKBj%c+ z^{)B`3$NB2yp-IHf02C#Fw!(;S&rR%2Pq(!<`Q=u&+_V4eCe z?!d0m@ndhMu%QZ`ERBCD+uU~%h>+E^Qd;Cz=IlGV(IwUrOz(+1Gkd7O z$HME|^+mAGBc4k(2jEj5$g30r-BUoK@Nn!*Td)5USoe+IZ-x9)#yd)sD}2Z?2{4@) zb|)xsK&pqOpB;+H#gbf^Pto29M<2Y>dU5pAF4p{+j=oBZ$2EXA*xI~AM@g20H7o_x z{2-Kc;SRpcxLXzU)a53ZoX%ndB^i8=>Sf&{i6CYkGSkvLj0<@C-!VKm#iX8dws__S zKp`T~rIAfaogJ!tV(~rs5)ctD#A};YXgPNI`<5=nWQjnIf<=1Pzn2y$C8yUkFKhwM z@%Ah?L`DM^@d<2evu->Oo=SVaiR<1GjYwe^G2)XY`l$Q%4H`|PpFA($N_8=6uOr0s zj+)C5xin zwn`&QQOr<`27|~lU*GNfe)r$+;%v`3=Q$VW;ymZMrG+ssw-7e~0K7L%46Ffwh5XNs z<6`?KHS^P-{ZmgZZ@~?jOs2~JH%~nY@PG5j1zTI#0Amn(L8qe2oETm=+B^jogFL!D zS!ISRHW3ybWQ6o&?2=byQi)JhfBSH9PzL~<0B#!S!^50cUq25lRnLyYPq06zWw>~J z`$KJG?wJet%MCZ1y81U)c?UzG;{mBi?no2aAHvt8L__Xy66K$DAupSD_4^VSeG;vA zGhrY7dmCA}Zg<=d*dvUYvYMo40k!iu>o|-n)q^ld6Q(6yBtUWr1GY<4vK2?uoeS|r zT(a}}&NC3;#Lv8{0Y$f=#j|95fZYUrx?foCUQ)KvUf$-LSb+6D%%)z#|1KO+ZTgw~ zNbE_n|4p~xYoc$edOQF-XOS;%evzdNi3 zk@(r9h#R5FpacG)j3VDRRz>g49u-o5A=@X`M=nQQ@W&MqFu3+}8)vIJyezf?(vDF#3iq72Yg1rU0$uCw``L1fzH6tU=MT zJ)FP#7~BMLoosB<>)Y`BnyxN?%PW`qwa_nrmk;P<^+|3lA$cC z!KnRdI-*8rENgl-h*t3^hviocbR?_BCX&(%?-)#H*`RRAUES@w^(0ey@bvFIq^EE0 zYIYPpa4Xz>{9(cUIq~=IuByDHtJskc@OXkoyhOvqjT$BRxhihe#hq<$(TaV?g(bYx zzk*$b_y4xdrKd-u!#@W)7x%!%FE62JOZu)fTpnAUKW94KXQKo9lR9BoI`nN#BVNL^WLc-2PBnDb`!FkQ6Yw zt8#VMCqN`vOx>8A-pqa3!sg7$vF4w|C29%3h5O_{d+D-|gED!U;S&A}5QU_Uz%?vp zmMBIPvj7qQQG74PJJYIU8KAgcJcJvNO0O6=%8w|@chXvpUX6O34cERMj)m?X)jwit zWYksusgx8zcrOv1Kd4Cm%yUoW#?wfM-ee=?*pXt7dUvyZrhI*Zx3!VQzm2&Dk2i(z zv;J?=_W|Z`2Nb*9*m`XJ^1ixr>GY^eNXXM8UzHKbJ%`E&g=nC-&t%U{b2>k}4 zM^eC8z9@VJ)NO6~zgW94x7psn_*GsP&AXPV>|c7+3V*`GDl?NuNHOr8_5jSBY+FrJ zxxFy&omakmacj-wPLUexLeI~s2^i^7jdiy$lDh;U-ze^bf8Wq&_j48xx9sRj~I0?AI|l`&NRKa0xj_M7{QQP8x>W$llZ# z^2}mA)Bep^+iA@Qw-LK1wT3nbnW#j??18HOX9M~EwO_4MW54*U(nB|yBja(g7FnMC zblZNR)Y{`EcNWNZ9&#=!$@W#;-?`_@7{fb;%BTGaNt!jg%h zP{`+<{G!`T5|=OLq>Z*{Z2O&8zMn16ACVB$Qm``DYk?tjJdb2uC7aci<-`J?E%OU+ zGrN5UtA#%|w#4Z;NP?k$>n!<|SrjF%qnK36 z-X#tb9{hRfZswTsPVZBN8H~75sHKLYIz~6u+pKzy#crwlQTpM#$E~+Abk)TD#sz#v zXX8Go`ZaF>B8Zu%M9U<;>RXE zbfFb@39Y9#&~E%DMKl*GIPjFwcNZ7nuMbVEpA0WbvBjM9QA!sp{YiDoe131&NawG0 z)w7{^`zTTBX*b%&r|n~U@dMgnxo!))g;D+Qg=`Xw5@VHk^{hiH?Dbc#u;gsXHzn0i z2)8o6*&Kl>6tpGG-xYvB-r`9coW<<#c<0|E=wQpY(XerrkkfVOt!t*N?wvbI|9F@&~JQ7q2jXe2H zCW^MvkWX8I-=%fo@BdI{A^py@pAB`shd&A{*amKE*X!a7A2Yu?Z%f;af$36@t#hgGI$UAqZQr>(vfUM3&C0L=d07kpTV z65hXXqa6SYLUvQ%beIm#w8HN~d3!4?$?iB2Owr|ut8l>>rMSqaZB}JGncrpN>H)eX z?`{XC$$(nou>9J>y&RJ_GCHrPS%%Jr+GeZ-p;^lV`1YLmyxKN-u#7+}dnx}N%zgXH z$CV1rQyi4eN)t(4&9Ix9{_jMeW*4;LYis@>9EQ2Es^gfy-VKyn0lc8i{7q3yuQV}F zD6Fom;2?qz@ukzYpge~g8?BAWbC}{;E82F=WrGc0;?er)DQ&9VG84bSn{>9B(k zwM%!e%*jQ~?@0DuS;yYC#^~O_E+}d7VN;GP%ockmCFlj4DNZ%yl_X-Hn$v_=+Er1z z)xF^ugN@xFweaki3bVXB3?uwjsn55RD1&YMi6B+jBAEU6|0Y1ne zLxbyOnkM9BHX2f}bHa<7WG>P_pz=aP(B)D(uo1i&yvId9DaA3GTsK?WdG%g5Q5z-% zUfT;wH`Xu@LDvM>F<4<`LiFUdk7UO)oS&1>Rnv!81;V#S1gZ^;byAIw5fmjY3m)nw z?+@SmlmBCWV>bFM8|-jGB{WLeI3o9DaWo<)11@8`kh*v=cN0DNB+st4sz6R#2I0qi z4c&8ZcAexDoiEyzoZJ((D9)8bG%^Z+MCs@_Q)++#Uvn&7#CI<7^ioFM{2qLTEAfMX z#1kD>oACS6EsTK8F}{R&pahvhyt|}$lX5-EzVP=!*jL*U(=7^7%UUF#`g>m(9)4uh zN+-O*&B&PgYQ520)x+!;$#)PXM`Kgq-o1CQLPsDGuSVi?k7|gIEtmv^WewHMkLAio zl1Us*ZM8T5*j_cED4OCIiNDZ{(dj&{3{g&T+~4Y*L((GimlI~v8Q&*2;zNurHxdEX zDgWY5T-u#~Rw6AH53<&eUOA_3sJa+<`S@61`0Z+&gPPC(dA9xY-3vCHs+QQ8y<*H| zq`~2~B6ACGIIhlq0$V=$vE_&HDcwxCpLD6$_1>ZT*h{SQByL1NMw0+fOj?Wz& zFvJdbQkbJBeJ=wX#hUle7%rUXR$4yPWhM|#t(`DrC+d#^K8*!sRn%{Eee5S%bqSan z?Gaxb6y6;Dw^4Ura3@7~UnV3ahsAZxfc!%uwqZbo@PGj7@>ji1sVn}8fiB(aiz~Jo zTDXK*@oVh~gVo^Iu~o8PQNMj6)RalL?o3^H@pnjZNLWoX&@@;gDJHvX&C-&SZCkAF z?Pux@B3eZQ037cWb&FZMuP+XLz1yG`s8)?SoCs!ygWlxG$PB`Eka2i37Fv)TK{|58 zJti;S=?xo)8?eTei(HD#f`Jq8j>vX~5NRzRU9sf_ z>oxtdr~$>ax+OJ;^X)vsSztp0JYJsoQlX{)JP`NN^%4mv6u3oW-hBTdM2W@5-Fze> z9n9nd!;qg7R6d&M#&&}CPAvA|mF^4XPltG`XZl9!t)5o^flxcEGJRDAZjOjF zQ0Iea%DG$E3bP&!(93|2RCY3l5t3s3J*JOik0=hGeaJ@3@H8tD7CVRqHg&`+R3j0a8@kqB}PI}{$m!yRab zvul5lL(>3*TF>n~)*#hsmwUTtKRAA2Fnk0PENdI!9GrZLu@zyKzs+&m-IKFviqv>& kg1Lm#gqI~e;$iYPkmG5c&N-g{UI@TVLkokN>#mRg2V?7pi2wiq literal 0 HcmV?d00001 diff --git a/static/js/Autocompleter.Local.js b/static/js/Autocompleter.Local.js deleted file mode 100644 index d7594b8da..000000000 --- a/static/js/Autocompleter.Local.js +++ /dev/null @@ -1,31 +0,0 @@ -/** - * Autocompleter.Local - * - * http://digitarald.de/project/autocompleter/ - * - * @version 1.1.2 - * - * @license MIT-style license - * @author Harald Kirschner - * @copyright Author - */ - -Autocompleter.Local = new Class({ - - Extends: Autocompleter, - - options: { - minLength: 0, - delay: 200 - }, - - initialize: function(element, tokens, options) { - this.parent(element, options); - this.tokens = tokens; - }, - - query: function() { - this.update(this.filter()); - } - -}); \ No newline at end of file diff --git a/static/js/Autocompleter.Request.js b/static/js/Autocompleter.Request.js deleted file mode 100644 index ffb7ddf2e..000000000 --- a/static/js/Autocompleter.Request.js +++ /dev/null @@ -1,109 +0,0 @@ -/** - * Autocompleter.Request - * - * http://digitarald.de/project/autocompleter/ - * - * @version 1.1.2 - * - * @license MIT-style license - * @author Harald Kirschner - * @copyright Author - */ - -Autocompleter.Request = new Class({ - - Extends: Autocompleter, - - options: {/* - indicator: null, - indicatorClass: null, - onRequest: $empty, - onComplete: $empty,*/ - postData: {}, - ajaxOptions: {}, - postVar: 'value' - - }, - - query: function(){ - var data = $unlink(this.options.postData) || {}; - data[this.options.postVar] = this.queryValue; - var indicator = $(this.options.indicator); - if (indicator) indicator.setStyle('display', ''); - var cls = this.options.indicatorClass; - if (cls) this.element.addClass(cls); - this.fireEvent('onRequest', [this.element, this.request, data, this.queryValue]); - this.request.send({'data': data}); - }, - - /** - * queryResponse - abstract - * - * Inherated classes have to extend this function and use this.parent() - */ - queryResponse: function() { - var indicator = $(this.options.indicator); - if (indicator) indicator.setStyle('display', 'none'); - var cls = this.options.indicatorClass; - if (cls) this.element.removeClass(cls); - return this.fireEvent('onComplete', [this.element, this.request]); - } - -}); - -Autocompleter.Request.JSON = new Class({ - - Extends: Autocompleter.Request, - - initialize: function(el, url, options) { - this.parent(el, options); - this.request = new Request.JSON($merge({ - 'url': url, - 'link': 'cancel' - }, this.options.ajaxOptions)).addEvent('onComplete', this.queryResponse.bind(this)); - }, - - queryResponse: function(response) { - this.parent(); - this.update(response); - } - -}); - -Autocompleter.Request.HTML = new Class({ - - Extends: Autocompleter.Request, - - initialize: function(el, url, options) { - this.parent(el, options); - this.request = new Request.HTML($merge({ - 'url': url, - 'link': 'cancel', - 'update': this.choices - }, this.options.ajaxOptions)).addEvent('onComplete', this.queryResponse.bind(this)); - }, - - queryResponse: function(tree, elements) { - this.parent(); - if (!elements || !elements.length) { - this.hideChoices(); - } else { - this.choices.getChildren(this.options.choicesMatch).each(this.options.injectChoice || function(choice) { - var value = choice.innerHTML; - choice.inputValue = value; - this.addChoiceEvents(choice.set('html', this.markQueryValue(value))); - }, this); - this.showChoices(); - } - - } - -}); - -/* compatibility */ - -Autocompleter.Ajax = { - Base: Autocompleter.Request, - Json: Autocompleter.Request.JSON, - Xhtml: Autocompleter.Request.HTML -}; diff --git a/static/js/Autocompleter.js b/static/js/Autocompleter.js deleted file mode 100644 index d9d78cbc9..000000000 --- a/static/js/Autocompleter.js +++ /dev/null @@ -1,442 +0,0 @@ -/** - * Autocompleter - * - * http://digitarald.de/project/autocompleter/ - * - * @version 1.1.2 - * - * @license MIT-style license - * @author Harald Kirschner - * @copyright Author - */ - -var Autocompleter = new Class({ - - Implements: [Options, Events], - - options: {/* - onOver: $empty, - onSelect: $empty, - onSelection: $empty, - onShow: $empty, - onHide: $empty, - onBlur: $empty, - onFocus: $empty,*/ - minLength: 1, - markQuery: true, - width: 'inherit', - maxChoices: 10, - injectChoice: null, - customChoices: null, - emptyChoices: null, - visibleChoices: true, - className: 'autocompleter-choices', - zIndex: 42, - delay: 400, - observerOptions: {}, - fxOptions: {}, - - autoSubmit: false, - overflow: false, - overflowMargin: 25, - selectFirst: false, - filter: null, - filterCase: false, - filterSubset: false, - forceSelect: false, - selectMode: true, - choicesMatch: null, - - multiple: false, - separator: ', ', - separatorSplit: /\s*[,;]\s*/, - autoTrim: false, - allowDupes: false, - - cache: true, - relative: false - }, - - initialize: function(element, options) { - this.element = $(element); - this.setOptions(options); - this.build(); - this.observer = new Observer(this.element, this.prefetch.bind(this), $merge({ - 'delay': this.options.delay - }, this.options.observerOptions)); - this.queryValue = null; - if (this.options.filter) this.filter = this.options.filter.bind(this); - var mode = this.options.selectMode; - this.typeAhead = (mode == 'type-ahead'); - this.selectMode = (mode === true) ? 'selection' : mode; - this.cached = []; - }, - - /** - * build - Initialize DOM - * - * Builds the html structure for choices and appends the events to the element. - * Override this function to modify the html generation. - */ - build: function() { - if ($(this.options.customChoices)) { - this.choices = this.options.customChoices; - } else { - this.choices = new Element('ul', { - 'class': this.options.className, - 'styles': { - 'zIndex': this.options.zIndex - } - }).inject(document.body); - this.relative = false; - if (this.options.relative) { - this.choices.inject(this.element, 'after'); - this.relative = this.element.getOffsetParent(); - } - this.fix = new OverlayFix(this.choices); - } - if (!this.options.separator.test(this.options.separatorSplit)) { - this.options.separatorSplit = this.options.separator; - } - this.fx = (!this.options.fxOptions) ? null : new Fx.Tween(this.choices, $merge({ - 'property': 'opacity', - 'link': 'cancel', - 'duration': 200 - }, this.options.fxOptions)).addEvent('onStart', Chain.prototype.clearChain).set(0); - this.element.setProperty('autocomplete', 'off') - .addEvent((Browser.Engine.trident || Browser.Engine.webkit) ? 'keydown' : 'keypress', this.onCommand.bind(this)) - .addEvent('click', this.onCommand.bind(this, [false])) - .addEvent('focus', this.toggleFocus.create({bind: this, arguments: true, delay: 100})) - .addEvent('blur', this.toggleFocus.create({bind: this, arguments: false, delay: 100})); - }, - - destroy: function() { - if (this.fix) this.fix.destroy(); - this.choices = this.selected = this.choices.destroy(); - }, - - toggleFocus: function(state) { - this.focussed = state; - if (!state) this.hideChoices(true); - this.fireEvent((state) ? 'onFocus' : 'onBlur', [this.element]); - }, - - onCommand: function(e) { - if (!e && this.focussed) return this.prefetch(); - if (e && e.key && !e.shift) { - switch (e.key) { - case 'enter': - if (this.element.value != this.opted) return true; - if (this.selected && this.visible) { - this.choiceSelect(this.selected); - return !!(this.options.autoSubmit); - } - break; - case 'up': case 'down': - if (!this.prefetch() && this.queryValue !== null) { - var up = (e.key == 'up'); - this.choiceOver((this.selected || this.choices)[ - (this.selected) ? ((up) ? 'getPrevious' : 'getNext') : ((up) ? 'getLast' : 'getFirst') - ](this.options.choicesMatch), true); - } - return false; - case 'esc': case 'tab': - this.hideChoices(true); - break; - } - } - return true; - }, - - setSelection: function(finish) { - var input = this.selected.inputValue, value = input; - var start = this.queryValue.length, end = input.length; - if (input.substr(0, start).toLowerCase() != this.queryValue.toLowerCase()) start = 0; - if (this.options.multiple) { - var split = this.options.separatorSplit; - value = this.element.value; - start += this.queryIndex; - end += this.queryIndex; - var old = value.substr(this.queryIndex).split(split, 1)[0]; - value = value.substr(0, this.queryIndex) + input + value.substr(this.queryIndex + old.length); - if (finish) { - var tokens = value.split(this.options.separatorSplit).filter(function(entry) { - return this.test(entry); - }, /[^\s,]+/); - if (!this.options.allowDupes) tokens = [].combine(tokens); - var sep = this.options.separator; - value = tokens.join(sep) + sep; - end = value.length; - } - } - this.observer.setValue(value); - this.opted = value; - if (finish || this.selectMode == 'pick') start = end; - this.element.selectRange(start, end); - this.fireEvent('onSelection', [this.element, this.selected, value, input]); - }, - - showChoices: function() { - var match = this.options.choicesMatch, first = this.choices.getFirst(match); - this.selected = this.selectedValue = null; - if (this.fix) { - var pos = this.element.getCoordinates(this.relative), width = this.options.width || 'auto'; - this.choices.setStyles({ - 'left': pos.left, - 'top': pos.bottom, - 'width': (width === true || width == 'inherit') ? pos.width : width - }); - } - if (!first) return; - if (!this.visible) { - this.visible = true; - this.choices.setStyle('display', ''); - if (this.fx) this.fx.start(1); - this.fireEvent('onShow', [this.element, this.choices]); - } - if (this.options.selectFirst || this.typeAhead || first.inputValue == this.queryValue) this.choiceOver(first, this.typeAhead); - var items = this.choices.getChildren(match), max = this.options.maxChoices; - var styles = {'overflowY': 'hidden', 'height': ''}; - this.overflown = false; - if (items.length > max) { - var item = items[max - 1]; - styles.overflowY = 'scroll'; - styles.height = item.getCoordinates(this.choices).bottom; - this.overflown = true; - }; - this.choices.setStyles(styles); - this.fix.show(); - if (this.options.visibleChoices) { - var scroll = document.getScroll(), - size = document.getSize(), - coords = this.choices.getCoordinates(); - if (coords.right > scroll.x + size.x) scroll.x = coords.right - size.x; - if (coords.bottom > scroll.y + size.y) scroll.y = coords.bottom - size.y; - window.scrollTo(Math.min(scroll.x, coords.left), Math.min(scroll.y, coords.top)); - } - }, - - hideChoices: function(clear) { - if (clear) { - var value = this.element.value; - if (this.options.forceSelect) value = this.opted; - if (this.options.autoTrim) { - value = value.split(this.options.separatorSplit).filter($arguments(0)).join(this.options.separator); - } - this.observer.setValue(value); - } - if (!this.visible) return; - this.visible = false; - if (this.selected) this.selected.removeClass('autocompleter-selected'); - this.observer.clear(); - var hide = function(){ - this.choices.setStyle('display', 'none'); - this.fix.hide(); - }.bind(this); - if (this.fx) this.fx.start(0).chain(hide); - else hide(); - this.fireEvent('onHide', [this.element, this.choices]); - }, - - prefetch: function() { - var value = this.element.value, query = value; - if (this.options.multiple) { - var split = this.options.separatorSplit; - var values = value.split(split); - var index = this.element.getSelectedRange().start; - var toIndex = value.substr(0, index).split(split); - var last = toIndex.length - 1; - index -= toIndex[last].length; - query = values[last]; - } - if (query.length < this.options.minLength) { - this.hideChoices(); - } else { - if (query === this.queryValue || (this.visible && query == this.selectedValue)) { - if (this.visible) return false; - this.showChoices(); - } else { - this.queryValue = query; - this.queryIndex = index; - if (!this.fetchCached()) this.query(); - } - } - return true; - }, - - fetchCached: function() { - return false; - if (!this.options.cache - || !this.cached - || !this.cached.length - || this.cached.length >= this.options.maxChoices - || this.queryValue) return false; - this.update(this.filter(this.cached)); - return true; - }, - - update: function(tokens) { - this.choices.empty(); - this.cached = tokens; - var type = tokens && $type(tokens); - if (!type || (type == 'array' && !tokens.length) || (type == 'hash' && !tokens.getLength())) { - (this.options.emptyChoices || this.hideChoices).call(this); - } else { - if (this.options.maxChoices < tokens.length && !this.options.overflow) tokens.length = this.options.maxChoices; - tokens.each(this.options.injectChoice || function(token){ - var choice = new Element('li', {'html': this.markQueryValue(token)}); - choice.inputValue = token; - this.addChoiceEvents(choice).inject(this.choices); - }, this); - this.showChoices(); - } - }, - - choiceOver: function(choice, selection) { - if (!choice || choice == this.selected) return; - if (this.selected) this.selected.removeClass('autocompleter-selected'); - this.selected = choice.addClass('autocompleter-selected'); - this.fireEvent('onSelect', [this.element, this.selected, selection]); - if (!this.selectMode) this.opted = this.element.value; - if (!selection) return; - this.selectedValue = this.selected.inputValue; - if (this.overflown) { - var coords = this.selected.getCoordinates(this.choices), margin = this.options.overflowMargin, - top = this.choices.scrollTop, height = this.choices.offsetHeight, bottom = top + height; - if (coords.top - margin < top && top) this.choices.scrollTop = Math.max(coords.top - margin, 0); - else if (coords.bottom + margin > bottom) this.choices.scrollTop = Math.min(coords.bottom - height + margin, bottom); - } - if (this.selectMode) this.setSelection(); - }, - - choiceSelect: function(choice) { - if (choice) this.choiceOver(choice); - this.setSelection(true); - this.queryValue = false; - this.hideChoices(); - }, - - filter: function(tokens) { - return (tokens || this.tokens).filter(function(token) { - return this.test(token); - }, new RegExp(((this.options.filterSubset) ? '' : '^') + this.queryValue.escapeRegExp(), (this.options.filterCase) ? '' : 'i')); - }, - - /** - * markQueryValue - * - * Marks the queried word in the given string with * - * Call this i.e. from your custom parseChoices, same for addChoiceEvents - * - * @param {String} Text - * @return {String} Text - */ - markQueryValue: function(str) { - return (!this.options.markQuery || !this.queryValue) ? str - : str.replace(new RegExp('(' + ((this.options.filterSubset) ? '' : '^') + this.queryValue.escapeRegExp() + ')', (this.options.filterCase) ? '' : 'i'), '$1'); - }, - - /** - * addChoiceEvents - * - * Appends the needed event handlers for a choice-entry to the given element. - * - * @param {Element} Choice entry - * @return {Element} Choice entry - */ - addChoiceEvents: function(el) { - return el.addEvents({ - 'mouseover': this.choiceOver.bind(this, [el]), - 'click': this.choiceSelect.bind(this, [el]) - }); - } -}); - -var OverlayFix = new Class({ - - initialize: function(el) { - if (Browser.Engine.trident) { - this.element = $(el); - this.relative = this.element.getOffsetParent(); - this.fix = new Element('iframe', { - 'frameborder': '0', - 'scrolling': 'no', - 'src': 'javascript:false;', - 'styles': { - 'position': 'absolute', - 'border': 'none', - 'display': 'none', - 'filter': 'progid:DXImageTransform.Microsoft.Alpha(opacity=0)' - } - }).inject(this.element, 'after'); - } - }, - - show: function() { - if (this.fix) { - var coords = this.element.getCoordinates(this.relative); - delete coords.right; - delete coords.bottom; - this.fix.setStyles($extend(coords, { - 'display': '', - 'zIndex': (this.element.getStyle('zIndex') || 1) - 1 - })); - } - return this; - }, - - hide: function() { - if (this.fix) this.fix.setStyle('display', 'none'); - return this; - }, - - destroy: function() { - if (this.fix) this.fix = this.fix.destroy(); - } - -}); - -Element.implement({ - - getSelectedRange: function() { - if (!Browser.Engine.trident) return {start: this.selectionStart, end: this.selectionEnd}; - var pos = {start: 0, end: 0}; - var range = this.getDocument().selection.createRange(); - if (!range || range.parentElement() != this) return pos; - var dup = range.duplicate(); - if (this.type == 'text') { - pos.start = 0 - dup.moveStart('character', -100000); - pos.end = pos.start + range.text.length; - } else { - var value = this.value; - var offset = value.length - value.match(/[\n\r]*$/)[0].length; - dup.moveToElementText(this); - dup.setEndPoint('StartToEnd', range); - pos.end = offset - dup.text.length; - dup.setEndPoint('StartToStart', range); - pos.start = offset - dup.text.length; - } - return pos; - }, - - selectRange: function(start, end) { - if (Browser.Engine.trident) { - var diff = this.value.substr(start, end - start).replace(/\r/g, '').length; - start = this.value.substr(0, start).replace(/\r/g, '').length; - var range = this.createTextRange(); - range.collapse(true); - range.moveEnd('character', start + diff); - range.moveStart('character', start); - range.select(); - } else { - this.focus(); - this.setSelectionRange(start, end); - } - return this; - } - -}); - -/* compatibility */ - -Autocompleter.Base = Autocompleter; \ No newline at end of file diff --git a/static/js/Observer.js b/static/js/Observer.js deleted file mode 100644 index 7724816b7..000000000 --- a/static/js/Observer.js +++ /dev/null @@ -1,69 +0,0 @@ -/** - * Observer - Observe formelements for changes - * - * - Additional code from clientside.cnet.com - * - * @version 1.1 - * - * @license MIT-style license - * @author Harald Kirschner - * @copyright Author - */ -var Observer = new Class({ - - Implements: [Options, Events], - - options: { - periodical: false, - delay: 1000 - }, - - initialize: function(el, onFired, options){ - this.element = $(el) || $$(el); - this.addEvent('onFired', onFired); - this.setOptions(options); - this.bound = this.changed.bind(this); - this.resume(); - }, - - changed: function() { - var value = this.element.get('value'); - if ($equals(this.value, value)) return; - this.clear(); - this.value = value; - this.timeout = this.onFired.delay(this.options.delay, this); - }, - - setValue: function(value) { - this.value = value; - this.element.set('value', value); - return this.clear(); - }, - - onFired: function() { - this.fireEvent('onFired', [this.value, this.element]); - }, - - clear: function() { - $clear(this.timeout || null); - return this; - }, - - pause: function(){ - if (this.timer) $clear(this.timer); - else this.element.removeEvent('keyup', this.bound); - return this.clear(); - }, - - resume: function(){ - this.value = this.element.get('value'); - if (this.options.periodical) this.timer = this.changed.periodical(this.options.periodical, this); - else this.element.addEvent('keyup', this.bound); - return this; - } - -}); - -var $equals = function(obj1, obj2) { - return (obj1 == obj2 || JSON.encode(obj1) == JSON.encode(obj2)); -}; \ No newline at end of file diff --git a/static/js/ProgressBar.js b/static/js/ProgressBar.js deleted file mode 100644 index 9ba418f21..000000000 --- a/static/js/ProgressBar.js +++ /dev/null @@ -1,19 +0,0 @@ -/* ---- -description: ProgressBar - -authors: - - David Walsh (http://davidwalsh.name) - -license: - - MIT-style license - -requires: - core/1.2.1: '*' - more/1.2.1: 'Fx.*' - -provides: - - ProgressBar -... -*/ -var ProgressBar=new Class({Implements:[Events,Options],options:{container:document.body,boxID:"progress-bar-box-id",percentageID:"progress-bar-percentage-id",displayID:"progress-bar-display-id",startPercentage:0,displayText:false,speed:10,step:1,allowMore:false,onComplete:$empty,onChange:$empty},initialize:function(a){this.setOptions(a);this.options.container=document.id(this.options.container);this.createElements();},createElements:function(){var b=new Element("div",{id:this.options.boxID});var a=new Element("div",{id:this.options.percentageID,style:"width:0px;"});a.inject(b);b.inject(this.options.container);if(this.options.displayText){var c=new Element("div",{id:this.options.displayID});c.inject(this.options.container);}this.set(this.options.startPercentage);},calculate:function(a){return(document.id(this.options.boxID).getStyle("width").replace("px","")*(a/100)).toInt();},animate:function(b){var c=false;var a=this;if(!a.options.allowMore&&b>100){b=100;}a.to=b.toInt();document.id(a.options.percentageID).set("morph",{duration:this.options.speed,link:"cancel",onComplete:function(){a.fireEvent("change",[a.to]);if(b>=100){a.fireEvent("complete",[a.to]);}}}).morph({width:a.calculate(b)});if(a.options.displayText){document.id(a.options.displayID).set("text",a.to+"%");}},set:function(a){this.animate(a);},step:function(){this.set(this.to+this.options.step);}}); \ No newline at end of file diff --git a/static/js/SqueezeBox.js b/static/js/SqueezeBox.js deleted file mode 100644 index 77cb7ac2c..000000000 --- a/static/js/SqueezeBox.js +++ /dev/null @@ -1,443 +0,0 @@ -/** - * SqueezeBox - Expandable Lightbox - * - * Allows to open various content as modal, - * centered and animated box. - * - * Dependencies: MooTools 1.3.1 - * - * Inspired by - * ... Lokesh Dhakar - The original Lightbox v2 - * - * @version 1.3 rc1 - * - * @license MIT-style license - * @author Harald Kirschner - * @copyright Author - */ - -var SqueezeBox = { - - presets: { - onOpen: function(){}, - onClose: function(){}, - onUpdate: function(){}, - onResize: function(){}, - onMove: function(){}, - onShow: function(){}, - onHide: function(){}, - size: {x: 600, y: 450}, - sizeLoading: {x: 200, y: 150}, - marginInner: {x: 20, y: 20}, - marginImage: {x: 50, y: 75}, - handler: false, - target: null, - closable: true, - closeBtn: true, - zIndex: 65555, - overlayOpacity: 0.7, - classWindow: '', - classOverlay: '', - overlayFx: {}, - resizeFx: {}, - contentFx: {}, - parse: false, // 'rel' - parseSecure: false, - shadow: true, - document: null, - ajaxOptions: {} - }, - - initialize: function(presets) { - if (this.options) return this; - - this.presets = Object.merge(this.presets, presets); - this.doc = this.presets.document || document; - this.options = {}; - this.setOptions(this.presets).build(); - this.bound = { - window: this.reposition.pass(null,this), - scroll: this.checkTarget.bind(this), - close: this.close.bind(this), - key: this.onKey.bind(this) - }; - this.isOpen = this.isLoading = false; - return this; - }, - - build: function() { - this.overlay = new Element('div', { - id: 'sbox-overlay', - styles: {display: 'none', zIndex: this.options.zIndex} - }); - this.win = new Element('div', { - id: 'sbox-window', - styles: {display: 'none', zIndex: this.options.zIndex + 2} - }); - if (this.options.shadow) { - if (Browser.safari || Browser.chrome) { - this.win.setStyle('-webkit-box-shadow', '0 0 10px rgba(0, 0, 0, 0.7)'); - } else if (!Browser.ie) { - var shadow = new Element('div', {'class': 'sbox-bg-wrap'}).inject(this.win); - var relay = function(e) { - this.overlay.fireEvent('click', [e]); - }.bind(this); - ['n', 'ne', 'e', 'se', 's', 'sw', 'w', 'nw'].each(function(dir) { - new Element('div', {'class': 'sbox-bg sbox-bg-' + dir}).inject(shadow).addEvent('click', relay); - }); - } - } - this.content = new Element('div', {id: 'sbox-content'}).inject(this.win); - if (this.options.closeBtn) { - this.closeBtn = new Element('a', {id: 'sbox-btn-close', href: '#'}).inject(this.win); - } - this.fx = { - overlay: new Fx.Tween(this.overlay, Object.merge({ - property: 'opacity', - onStart: Events.prototype.clearChain, - duration: 250, - link: 'cancel' - }, this.options.overlayFx)).set(0), - win: new Fx.Morph(this.win, Object.merge({ - onStart: Events.prototype.clearChain, - unit: 'px', - duration: 750, - transition: Fx.Transitions.Quint.easeOut, - link: 'cancel', - unit: 'px' - }, this.options.resizeFx)), - content: new Fx.Tween(this.content, Object.merge({ - property: 'opacity', - duration: 250, - link: 'cancel' - }, this.options.contentFx)).set(0) - }; - $(this.doc.body).adopt(this.overlay, this.win); - }, - - assign: function(to, options) { - return ($(to) || $$(to)).addEvent('click', function() { - return !SqueezeBox.fromElement(this, options); - }); - }, - - open: function(subject, options) { - this.initialize(); - - if (this.element != null) this.trash(); - this.element = $(subject) || false; - - this.setOptions(Object.merge(this.presets, options || {})); - if (this.element && this.options.parse) { - var obj = this.element.getProperty(this.options.parse); - if (obj && (obj = JSON.decode(obj, this.options.parseSecure))) this.setOptions(obj); - } - this.url = ((this.element) ? (this.element.get('href')) : subject) || this.options.url || ''; - - this.assignOptions(); - - var handler = handler || this.options.handler; - if (handler) return this.setContent(handler, this.parsers[handler].call(this, true)); - var ret = false; - return this.parsers.some(function(parser, key) { - var content = parser.call(this); - if (content) { - ret = this.setContent(key, content); - return true; - } - return false; - }, this); - }, - - fromElement: function(from, options) { - return this.open(from, options); - }, - - assignOptions: function() { - this.overlay.set('class', this.options.classOverlay); - this.win.set('class', this.options.classWindow); - if (Browser.ie) this.win.addClass('sbox-window-ie6'); - if (!this.options.closeBtn) { - this.closeBtn.dispose(); - } else { - this.closeBtn.inject(this.win); - } - }, - - close: function(e) { - var stoppable = (typeOf(e) == 'event'); - if (stoppable) e.stop(); - if (!this.isOpen || (stoppable && !Function.from(this.options.closable).call(this, e))) return this; - this.fx.overlay.start(0).chain(this.toggleOverlay.bind(this)); - this.win.setStyle('display', 'none'); - this.fireEvent('onClose', [this.content]); - this.trash(); - this.toggleListeners(false); - this.isOpen = false; - return this; - }, - - trash: function() { - this.element = this.asset = null; - this.content.empty(); - this.options = {}; - this.removeEvents().setOptions(this.presets).callChain(); - }, - - onError: function() { - this.asset = null; - this.setContent('string', this.options.errorMsg || 'An error occurred'); - }, - - setContent: function(handler, content) { - if (!this.handlers[handler]) return false; - this.content.className = 'sbox-content-' + handler; - this.applyTimer = this.applyContent.delay(this.fx.overlay.options.duration, this, this.handlers[handler].call(this, content)); - if (this.overlay.retrieve('opacity')) return this; - this.toggleOverlay(true); - this.fx.overlay.start(this.options.overlayOpacity); - return this.reposition(); - }, - - applyContent: function(content, size) { - if (!this.isOpen && !this.applyTimer) return; - this.applyTimer = clearTimeout(this.applyTimer); - this.hideContent(); - if (!content) { - this.toggleLoading(true); - } else { - if (this.isLoading) this.toggleLoading(false); - this.fireEvent('onUpdate', [this.content], 20); - } - if (content) { - if (['string', 'array'].contains(typeOf(content))) this.content.set('html', content); - else if ((!this.content.contains(content)) && content != this.content) this.content.adopt(content); - } - this.callChain(); - if (!this.isOpen) { - this.toggleListeners(true); - this.resize(size, true); - this.isOpen = true; - this.fireEvent('onOpen', [this.content]); - } else { - this.resize(size); - } - }, - - resize: function(size, instantly) { - this.showTimer = clearTimeout(this.showTimer || null); - var box = this.doc.getSize(), scroll = this.doc.getScroll(); - this.size = Object.merge((this.isLoading) ? this.options.sizeLoading : this.options.size, size); - var to = { - width: this.size.x, - height: this.size.y, - left: (scroll.x + (box.x - this.size.x - this.options.marginInner.x) / 2).toInt(), - top: (scroll.y + (box.y - this.size.y - this.options.marginInner.y) / 2).toInt() - }; - this.hideContent(); - if (!instantly) { - this.fx.win.start(to).chain(this.showContent.bind(this)); - } else { - this.win.setStyles(to).setStyle('display', ''); - this.showTimer = this.showContent.delay(50, this); - } - return this.reposition(); - }, - - toggleListeners: function(state) { - var fn = (state) ? 'addEvent' : 'removeEvent'; - if (this.options.closeBtn) { - this.closeBtn[fn]('click', this.bound.close); - } - this.overlay[fn]('click', this.bound.close); - this.doc[fn]('keydown', this.bound.key)[fn]('mousewheel', this.bound.scroll); - this.doc.getWindow()[fn]('resize', this.bound.window)[fn]('scroll', this.bound.window); - }, - - toggleLoading: function(state) { - this.isLoading = state; - this.win[(state) ? 'addClass' : 'removeClass']('sbox-loading'); - if (state) this.fireEvent('onLoading', [this.win]); - }, - - toggleOverlay: function(state) { - var full = this.doc.getSize().x; - this.overlay.setStyle('display', (state) ? '' : 'none'); - this.doc.body[(state) ? 'addClass' : 'removeClass']('body-overlayed'); - if (state) { - this.scrollOffset = this.doc.getWindow().getSize().x - full; - this.doc.body.setStyle('margin-right', this.scrollOffset); - } else { - this.doc.body.setStyle('margin-right', ''); - } - }, - - showContent: function() { - if (this.content.get('opacity')) this.fireEvent('onShow', [this.win]); - this.fx.content.start(1); - }, - - hideContent: function() { - if (!this.content.get('opacity')) this.fireEvent('onHide', [this.win]); - this.fx.content.cancel().set(0); - }, - - onKey: function(e) { - switch (e.key) { - case 'esc': this.close(e); - case 'up': case 'down': return false; - } - }, - - checkTarget: function(e) { - return (this.content.contains(e.target) && e.target != this.content); - }, - - reposition: function() { - var size = this.doc.getSize(), scroll = this.doc.getScroll(), ssize = this.doc.getScrollSize(); - this.overlay.setStyles({ - width: ssize.x + 'px', - height: ssize.y + 'px' - }); - this.win.setStyles({ - left: (scroll.x + (size.x - this.win.offsetWidth) / 2 - this.scrollOffset).toInt() + 'px', - top: (scroll.y + (size.y - this.win.offsetHeight) / 2).toInt() + 'px' - }); - return this.fireEvent('onMove', [this.overlay, this.win]); - }, - - removeEvents: function(type) { - if (!this.$events) return this; - if (!type) this.$events = null; - else if (this.$events[type]) this.$events[type] = null; - return this; - }, - - extend: function(properties) { - return Object.append(this, properties); - }, - - handlers: new Hash(), - - parsers: new Hash() - -}; - -SqueezeBox.extend(new Events(function(){})).extend(new Options(function(){})).extend(new Chain(function(){})); - -SqueezeBox.parsers.extend({ - - image: function(preset) { - return (preset || (/\.(?:jpg|png|gif)$/i).test(this.url)) ? this.url : false; - }, - - clone: function(preset) { - if ($(this.options.target)) return $(this.options.target); - if (this.element && !this.element.parentNode) return this.element; - var bits = this.url.match(/#([\w-]+)$/); - return (bits) ? $(bits[1]) : (preset ? this.element : false); - }, - - ajax: function(preset) { - return (preset || (this.url && !(/^(?:javascript|#)/i).test(this.url))) ? this.url : false; - }, - - iframe: function(preset) { - return (preset || this.url) ? this.url : false; - }, - - string: function(preset) { - return true; - } -}); - -SqueezeBox.handlers.extend({ - - image: function(url) { - var size, tmp = new Image(); - this.asset = null; - tmp.onload = tmp.onabort = tmp.onerror = (function() { - tmp.onload = tmp.onabort = tmp.onerror = null; - if (!tmp.width) { - this.onError.delay(10, this); - return; - } - var box = this.doc.getSize(); - box.x -= this.options.marginImage.x; - box.y -= this.options.marginImage.y; - size = {x: tmp.width, y: tmp.height}; - for (var i = 2; i--;) { - if (size.x > box.x) { - size.y *= box.x / size.x; - size.x = box.x; - } else if (size.y > box.y) { - size.x *= box.y / size.y; - size.y = box.y; - } - } - size.x = size.x.toInt(); - size.y = size.y.toInt(); - this.asset = $(tmp); - tmp = null; - this.asset.width = size.x; - this.asset.height = size.y; - this.applyContent(this.asset, size); - }).bind(this); - tmp.src = url; - if (tmp && tmp.onload && tmp.complete) tmp.onload(); - return (this.asset) ? [this.asset, size] : null; - }, - - clone: function(el) { - if (el) return el.clone(); - return this.onError(); - }, - - adopt: function(el) { - if (el) return el; - return this.onError(); - }, - - ajax: function(url) { - var options = this.options.ajaxOptions || {}; - this.asset = new Request.HTML(Object.merge({ - method: 'get', - evalScripts: false - }, this.options.ajaxOptions)).addEvents({ - onSuccess: function(resp) { - this.applyContent(resp); - if (options.evalScripts !== null && !options.evalScripts) Browser.exec(this.asset.response.javascript); - this.fireEvent('onAjax', [resp, this.asset]); - this.asset = null; - }.bind(this), - onFailure: this.onError.bind(this) - }); - this.asset.send.delay(10, this.asset, [{url: url}]); - }, - - iframe: function(url) { - this.asset = new Element('iframe', Object.merge({ - src: url, - frameBorder: 0, - width: this.options.size.x, - height: this.options.size.y - }, this.options.iframeOptions)); - if (this.options.iframePreload) { - this.asset.addEvent('load', function() { - this.applyContent(this.asset.setStyle('display', '')); - }.bind(this)); - this.asset.setStyle('display', 'none').inject(this.content); - return false; - } - return this.asset; - }, - - string: function(str) { - return str; - } - -}); - -SqueezeBox.handlers.url = SqueezeBox.handlers.ajax; -SqueezeBox.parsers.url = SqueezeBox.parsers.ajax; -SqueezeBox.parsers.adopt = SqueezeBox.parsers.clone; \ No newline at end of file diff --git a/static/js/admin.js b/static/js/admin.js new file mode 100644 index 000000000..740a099e0 --- /dev/null +++ b/static/js/admin.js @@ -0,0 +1,67 @@ +function domainform_cb() { + $('input:text:visible:first').focus(); + $("#id_aliases").dynamic_input(); + $(".submit").one('click', function(e) { + simple_ajax_form_post(e, domainform_cb); + }); +} + +function accountform_init() { + $("#id_aliases").dynamic_input(); + $("#id_email").autocompleter({ + from_character: "@", + choices: get_domains_list + }); + $("#id_domains") + .autocompleter({ + choices: get_domains_list + }) + .dynamic_input(); +} + +function newaccount_cb() { + $('input:text:visible:first').focus(); + accountform_init(); + $("#wizard").cwizard(); +} + +function editaccount_cb() { + accountform_init(); + $('#submit').click(function(e) { + simple_ajax_form_post(e, editaccount_cb); + }); +} + +function dlistform_cb() { + $("#id_email").autocompleter({ + from_character: "@", + choices: get_domains_list + }); + $("#id_recipients").dynamic_input(); + $(".submit").one('click', function(e) { + simple_ajax_form_post(e, dlistform_cb); + }); +} + +function importform_cb() { + $(".submit").one('click', function(e) { + if ($("#id_sourcefile").attr("value") == "") { + e.preventDefault(); + return; + } + $("#import_status").css("display", "block"); + $("form").submit(); + }); +} + +function importdone(status, msg) { + $("#import_status").css("display", "none"); + if (status == "ok") { + $("#modalbox").modal('hide'); + window.location.reload(); + } else { + $("#import_result").html(""); + $(".modal-header").notify({top_position: 10}); + $(".modal-header").notify('error', msg); + } +} \ No newline at end of file diff --git a/static/js/autocompleter.js b/static/js/autocompleter.js new file mode 100644 index 000000000..bc80df2f2 --- /dev/null +++ b/static/js/autocompleter.js @@ -0,0 +1,166 @@ +(function($) { + var Autocompleter = function(element, options) { + this.$element = $(element); + this.options = $.extend({}, $.fn.autocompleter.defaults, options); + this.choices = { + value: null + }; + if (typeof this.options.choices === "function") { + this.options.choices(this.choices); + } else { + this.choices.value = this.options.choices(); + } + this.$menu = + $('