diff --git a/mailadmin/admin.py b/mailadmin/admin.py index c757ea9..a755b0b 100644 --- a/mailadmin/admin.py +++ b/mailadmin/admin.py @@ -1,12 +1,18 @@ from django.contrib import admin -from mailadmin.models import OrgUnit +from mailadmin.models import OrgUnit, Prefix + + +class PrefixInline(admin.TabularInline): + model = Prefix + extra = 0 class OrgUnitAdmin(admin.ModelAdmin): - list_display = ('name', 'prefix', 'is_active') + list_display = ('name', 'is_active') list_filter = ('is_active',) - list_editable = ('prefix',) filter_horizontal = ('admin_groups', 'member_groups') + inlines = [PrefixInline] -admin.site.register(OrgUnit, OrgUnitAdmin) \ No newline at end of file +admin.site.register(OrgUnit, OrgUnitAdmin) +admin.site.register(Prefix) \ No newline at end of file diff --git a/mailadmin/migrations/0004_auto_20150125_1401.py b/mailadmin/migrations/0004_auto_20150125_1401.py new file mode 100644 index 0000000..36fa849 --- /dev/null +++ b/mailadmin/migrations/0004_auto_20150125_1401.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('mailadmin', '0003_auto_20141105_2153'), + ] + + operations = [ + migrations.CreateModel( + name='Prefix', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('name', models.CharField(max_length=500, null=True, blank=True)), + ('orgunit', models.ForeignKey(related_name='prefixes', to='mailadmin.OrgUnit')), + ], + options={ + }, + bases=(models.Model,), + ), + migrations.RemoveField( + model_name='orgunit', + name='prefix', + ), + ] diff --git a/mailadmin/models.py b/mailadmin/models.py index 37beabe..8881d92 100644 --- a/mailadmin/models.py +++ b/mailadmin/models.py @@ -7,7 +7,6 @@ class OrgUnit(models.Model): return self.name name = models.CharField(max_length=500) - prefix = models.CharField(max_length=256, blank=True, null=True) is_active = models.BooleanField(default=True) admin_groups = models.ManyToManyField(Group, null=True, blank=True, related_name='admin_orgunits') member_groups = models.ManyToManyField(Group, null=True, blank=True, related_name='member_orgunits') @@ -17,3 +16,20 @@ class OrgUnit(models.Model): class Meta: ordering = ['name'] + + +class PrefixQueryset(models.QuerySet): + + def as_regex(self): + prefixes = self.values_list('name', flat=True) + return '|'.join(['^{0}-|^{0}@'.format(p) for p in prefixes]) + + +class Prefix(models.Model): + def __unicode__(self): + return self.name + + name = models.CharField(max_length=500, blank=True, null=True) + orgunit = models.ForeignKey('mailadmin.OrgUnit', related_name='prefixes') + + objects = PrefixQueryset.as_manager() diff --git a/mailadmin/permissions.py b/mailadmin/permissions.py index d881d94..ca9bac2 100644 --- a/mailadmin/permissions.py +++ b/mailadmin/permissions.py @@ -27,10 +27,9 @@ class SourcePrefixOwner(permissions.BasePermission): return False # Prepare regular expression - regexp = '|'.join(['^{0}-|^{0}@'.format(p) for p in my_prefixes]) + regexp = my_prefixes.as_regex() # All source-adresses should match the prefix regexp for alias in serializer.initial_data: - print alias, re.search(regexp, alias.get('source', '')) if re.search(regexp, alias.get('source', '')) is None: return False diff --git a/mailadmin/serializers.py b/mailadmin/serializers.py index 94f8fdc..f28415d 100644 --- a/mailadmin/serializers.py +++ b/mailadmin/serializers.py @@ -3,6 +3,7 @@ from django.contrib.auth.models import User, Group from rest_framework import serializers from mailadmin.models import OrgUnit +from rest_framework.relations import SlugRelatedField class GroupSerializer(serializers.ModelSerializer): @@ -22,6 +23,10 @@ class UserSerializer(serializers.ModelSerializer): class OrgUnitSerializer(serializers.ModelSerializer): admin_groups = GroupSerializer(many=True) member_groups = GroupSerializer(many=True) + prefixes = SlugRelatedField( + many=True, + read_only=True, + slug_field='name') class Meta: model = OrgUnit diff --git a/mailadmin/static/app/scripts/main.js b/mailadmin/static/app/scripts/main.js index 728f9fa..102b415 100644 --- a/mailadmin/static/app/scripts/main.js +++ b/mailadmin/static/app/scripts/main.js @@ -1,6 +1,6 @@ (function() { 'use strict'; - /*global moment, jQuery, document, _, $, window, List */ + /*global moment, jQuery, document, _, $, window, List, queryString */ var api_urls = { aliases: '/api/aliases/', ou_list: '/api/orgunits/', @@ -9,13 +9,6 @@ }; var csrf_token; - function getParameterByName(name) { - name = name.replace(/[\[]/, '\\[').replace(/[\]]/, '\\]'); - var regex = new RegExp('[\\?&]' + name + '=([^&#]*)'), - results = regex.exec(location.search); - return results === null ? '' : decodeURIComponent(results[1].replace(/\+/g, ' ')); - } - function parseEmails(text) { if(text === undefined || text === "") { return []; @@ -37,28 +30,60 @@ return _.unique(emails); } - function filter_lists(term, pushState, with_members) { - if(pushState) { - /* Update query string */ - window.history.pushState(null, null, '/lists/?q=' + term); - } + function set_query_string(obj) { + var qs = ''; + if(obj !== null) { + qs = '?' + queryString.stringify(obj); + } + window.history.pushState(null, null, '/lists/'+qs); + } + + function filter_lists(query_term, with_members) { + var lists = $(".fwdlist").not('.new-list'); + if($.trim(query_term) === "") { + $(lists).find('.alias-row').removeClass('highlight'); + $(lists).removeClass('hidden'); + return; + } /* Filter lists */ - var re_term = new RegExp(term); - $(".fwdlist").not('.new-list').each(function() { - var cur_name = $(this).attr('data-list-name'); - var members = $(this).find('.member'); - var hits = _.filter(members, function(el) { - return re_term.test($(el).text()); - }); - if(re_term.test(cur_name) || (hits.length !== 0 && with_members) ) { + var re_term = new RegExp(query_term); + lists.each(function() { + var list_name = $(this).attr('data-list-name'); + var members = $(this).find('.alias'); + var hits = 0; + if(with_members) { + hits = _.filter(members, function(el) { + var matches = re_term.test($(el).text()); + if(matches) { + $(el).parent().addClass('highlight'); + } else { + $(el).parent().removeClass('highlight'); + } + return matches; + }); + } else { + $('.alias-row').removeClass('highlight'); + } + + if(re_term.test(list_name) || (hits.length !== 0 && with_members) ) { $(this).removeClass('hidden'); } else { $(this).addClass('hidden'); } }); - } + + function filter_lists_by_orgunit(id, prefix) { + set_active_org_unit(id); + filter_lists(prefix); + var query_params = {orgunit: id}; + if(id === "") { + query_params = null; + } + set_query_string(query_params); + } + function notify(title, text, type) { /* types: info, success, error */ new PNotify({ @@ -93,6 +118,33 @@ error: onError }); } + function set_active_org_unit(id) { + /* Reset active */ + $(".orgunit-list li").removeClass('active'); + /* Set current */ + var org_unit = $('.orgunit-list li a[data-id="'+ id +'"]'); + org_unit.parent().toggleClass('active'); + $('.orgunits-select').val(id); // - + {% for ou in orgunits %} - + {% endfor %} \ No newline at end of file diff --git a/mailadmin/static/bower.json b/mailadmin/static/bower.json index e7ed933..f6130dc 100644 --- a/mailadmin/static/bower.json +++ b/mailadmin/static/bower.json @@ -6,7 +6,8 @@ "jquery": "~1.11.0", "underscore": "~1.7.0", "nunjucks": "~1.1.0", - "pnotify": "~2.0.1" + "pnotify": "~2.0.1", + "query-string": "~1.0.0" }, "devDependencies": {} } diff --git a/mailadmin/static/bower_components/query-string/.bower.json b/mailadmin/static/bower_components/query-string/.bower.json new file mode 100644 index 0000000..f24af12 --- /dev/null +++ b/mailadmin/static/bower_components/query-string/.bower.json @@ -0,0 +1,37 @@ +{ + "name": "query-string", + "description": "Parse and stringify URL query strings", + "main": "query-string.js", + "keywords": [ + "querystring", + "query", + "string", + "qs", + "param", + "parameter", + "url", + "uri", + "parse", + "stringify", + "encode", + "decode" + ], + "ignore": [ + ".*", + "test.js", + "component.json", + "package.json" + ], + "homepage": "https://github.com/sindresorhus/query-string", + "version": "1.0.0", + "_release": "1.0.0", + "_resolution": { + "type": "version", + "tag": "v1.0.0", + "commit": "f49a95c792543f38c76773f8c0031a488b34f1d0" + }, + "_source": "git://github.com/sindresorhus/query-string.git", + "_target": "~1.0.0", + "_originalSource": "query-string", + "_direct": true +} \ No newline at end of file diff --git a/mailadmin/static/bower_components/query-string/bower.json b/mailadmin/static/bower_components/query-string/bower.json new file mode 100644 index 0000000..1d694f8 --- /dev/null +++ b/mailadmin/static/bower_components/query-string/bower.json @@ -0,0 +1,25 @@ +{ + "name": "query-string", + "description": "Parse and stringify URL query strings", + "main": "query-string.js", + "keywords": [ + "querystring", + "query", + "string", + "qs", + "param", + "parameter", + "url", + "uri", + "parse", + "stringify", + "encode", + "decode" + ], + "ignore": [ + ".*", + "test.js", + "component.json", + "package.json" + ] +} diff --git a/mailadmin/static/bower_components/query-string/license b/mailadmin/static/bower_components/query-string/license new file mode 100644 index 0000000..654d0bf --- /dev/null +++ b/mailadmin/static/bower_components/query-string/license @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/mailadmin/static/bower_components/query-string/query-string.js b/mailadmin/static/bower_components/query-string/query-string.js new file mode 100644 index 0000000..e55243d --- /dev/null +++ b/mailadmin/static/bower_components/query-string/query-string.js @@ -0,0 +1,66 @@ +/*! + query-string + Parse and stringify URL query strings + https://github.com/sindresorhus/query-string + by Sindre Sorhus + MIT License +*/ +(function () { + 'use strict'; + var queryString = {}; + + queryString.parse = function (str) { + if (typeof str !== 'string') { + return {}; + } + + str = str.trim().replace(/^(\?|#)/, ''); + + if (!str) { + return {}; + } + + return str.trim().split('&').reduce(function (ret, param) { + var parts = param.replace(/\+/g, ' ').split('='); + var key = parts[0]; + var val = parts[1]; + + key = decodeURIComponent(key); + // missing `=` should be `null`: + // http://w3.org/TR/2012/WD-url-20120524/#collect-url-parameters + val = val === undefined ? null : decodeURIComponent(val); + + if (!ret.hasOwnProperty(key)) { + ret[key] = val; + } else if (Array.isArray(ret[key])) { + ret[key].push(val); + } else { + ret[key] = [ret[key], val]; + } + + return ret; + }, {}); + }; + + queryString.stringify = function (obj) { + return obj ? Object.keys(obj).map(function (key) { + var val = obj[key]; + + if (Array.isArray(val)) { + return val.map(function (val2) { + return encodeURIComponent(key) + '=' + encodeURIComponent(val2); + }).join('&'); + } + + return encodeURIComponent(key) + '=' + encodeURIComponent(val); + }).join('&') : ''; + }; + + if (typeof define === 'function' && define.amd) { + define(function() { return queryString; }); + } else if (typeof module !== 'undefined' && module.exports) { + module.exports = queryString; + } else { + window.queryString = queryString; + } +})(); diff --git a/mailadmin/static/bower_components/query-string/readme.md b/mailadmin/static/bower_components/query-string/readme.md new file mode 100644 index 0000000..a7a4e38 --- /dev/null +++ b/mailadmin/static/bower_components/query-string/readme.md @@ -0,0 +1,78 @@ +# query-string [![Build Status](https://travis-ci.org/sindresorhus/query-string.svg?branch=master)](https://travis-ci.org/sindresorhus/query-string) + +> Parse and stringify URL [query strings](http://en.wikipedia.org/wiki/Query_string) + + +## Install + +```sh +$ npm install --save query-string +``` + +```sh +$ bower install --save query-string +``` + +```sh +$ component install sindresorhus/query-string +``` + + +## Usage + +```js +console.log(location.search); +// ?foo=bar + +var parsed = queryString.parse(location.search); +console.log(parsed); +// {foo: 'bar'} + +console.log(location.hash); +// #token=bada55cafe + +var parsedHash = queryString.parse(location.hash); +console.log(parsedHash); +// {token: 'bada55cafe'} + +parsed.foo = 'unicorn'; +parsed.ilike = 'pizza'; + +location.search = queryString.stringify(parsed); + +console.log(location.search); +// ?foo=unicorn&ilike=pizza +``` + + +## API + +### queryString.parse(*string*) + +Parse a query string into an object. Leading `?` or `#` are ignored, so you can pass `location.search` or `location.hash` directly. + +### queryString.stringify(*object*) + +Stringify an object into a query string. + + +## Nesting + +This module intentionally doesn't support nesting as it's not specced and varies between implementations, which causes a lot of [edge cases](https://github.com/visionmedia/node-querystring/issues). + +You're much better off just converting the object to a JSON string: + +```js +queryString.stringify({ + foo: 'bar', + nested: JSON.stringify({ + unicorn: 'cake' + }) +}); +// foo=bar&nested=%7B%22unicorn%22%3A%22cake%22%7D +``` + + +## License + +MIT © [Sindre Sorhus](http://sindresorhus.com) diff --git a/mailadmin/static/dist/scripts/main.js b/mailadmin/static/dist/scripts/main.js index 728f9fa..102b415 100644 --- a/mailadmin/static/dist/scripts/main.js +++ b/mailadmin/static/dist/scripts/main.js @@ -1,6 +1,6 @@ (function() { 'use strict'; - /*global moment, jQuery, document, _, $, window, List */ + /*global moment, jQuery, document, _, $, window, List, queryString */ var api_urls = { aliases: '/api/aliases/', ou_list: '/api/orgunits/', @@ -9,13 +9,6 @@ }; var csrf_token; - function getParameterByName(name) { - name = name.replace(/[\[]/, '\\[').replace(/[\]]/, '\\]'); - var regex = new RegExp('[\\?&]' + name + '=([^&#]*)'), - results = regex.exec(location.search); - return results === null ? '' : decodeURIComponent(results[1].replace(/\+/g, ' ')); - } - function parseEmails(text) { if(text === undefined || text === "") { return []; @@ -37,28 +30,60 @@ return _.unique(emails); } - function filter_lists(term, pushState, with_members) { - if(pushState) { - /* Update query string */ - window.history.pushState(null, null, '/lists/?q=' + term); - } + function set_query_string(obj) { + var qs = ''; + if(obj !== null) { + qs = '?' + queryString.stringify(obj); + } + window.history.pushState(null, null, '/lists/'+qs); + } + + function filter_lists(query_term, with_members) { + var lists = $(".fwdlist").not('.new-list'); + if($.trim(query_term) === "") { + $(lists).find('.alias-row').removeClass('highlight'); + $(lists).removeClass('hidden'); + return; + } /* Filter lists */ - var re_term = new RegExp(term); - $(".fwdlist").not('.new-list').each(function() { - var cur_name = $(this).attr('data-list-name'); - var members = $(this).find('.member'); - var hits = _.filter(members, function(el) { - return re_term.test($(el).text()); - }); - if(re_term.test(cur_name) || (hits.length !== 0 && with_members) ) { + var re_term = new RegExp(query_term); + lists.each(function() { + var list_name = $(this).attr('data-list-name'); + var members = $(this).find('.alias'); + var hits = 0; + if(with_members) { + hits = _.filter(members, function(el) { + var matches = re_term.test($(el).text()); + if(matches) { + $(el).parent().addClass('highlight'); + } else { + $(el).parent().removeClass('highlight'); + } + return matches; + }); + } else { + $('.alias-row').removeClass('highlight'); + } + + if(re_term.test(list_name) || (hits.length !== 0 && with_members) ) { $(this).removeClass('hidden'); } else { $(this).addClass('hidden'); } }); - } + + function filter_lists_by_orgunit(id, prefix) { + set_active_org_unit(id); + filter_lists(prefix); + var query_params = {orgunit: id}; + if(id === "") { + query_params = null; + } + set_query_string(query_params); + } + function notify(title, text, type) { /* types: info, success, error */ new PNotify({ @@ -93,6 +118,33 @@ error: onError }); } + function set_active_org_unit(id) { + /* Reset active */ + $(".orgunit-list li").removeClass('active'); + /* Set current */ + var org_unit = $('.orgunit-list li a[data-id="'+ id +'"]'); + org_unit.parent().toggleClass('active'); + $('.orgunits-select').val(id); // \n \n\t"; +output += "