Support multiple prefixes
This commit is contained in:
1 parent
c7ee107e99
commit
7902767f3b
20 files changed
+581
-167
No files matched your search
+10
-4
@@ -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)
|
||||
admin.site.register(OrgUnit, OrgUnitAdmin)
|
||||
admin.site.register(Prefix)
|
||||
@@ -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',
|
||||
),
|
||||
]
|
||||
+17
-1
@@ -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()
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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); // <select>
|
||||
|
||||
/* Reset search field */
|
||||
$('.js-lists-filter-field').val('');
|
||||
}
|
||||
function set_prefixes_regex(ous) {
|
||||
ous = _.map(ous, function(ou) {
|
||||
var prefixes = _.map(ou.prefixes, function(prefix) {
|
||||
return '^'+ prefix +'-|^'+ prefix +'@';
|
||||
});
|
||||
ou.prefixes_regex = prefixes.join('|');
|
||||
|
||||
return ou;
|
||||
});
|
||||
|
||||
return ous;
|
||||
}
|
||||
function reset_active_org_unit() {
|
||||
$(".orgunit-list li").removeClass('active');
|
||||
$(".orgunit-list li:first-child").toggleClass('active');
|
||||
}
|
||||
|
||||
$(document).ready(function() {
|
||||
csrf_token = $('meta[name=x-csrf-token]').attr('content');
|
||||
@@ -102,16 +154,22 @@
|
||||
/* Lists view (home) */
|
||||
if( $('body.home').length ) {
|
||||
$.getJSON(api_urls.ou_list, function(data) {
|
||||
data = set_prefixes_regex(data);
|
||||
|
||||
/* Load orgunit list */
|
||||
var ou_html = nunjucks.render('orgunits.html', { orgunits: data });
|
||||
$(".orgunit-list").append(ou_html);
|
||||
var ous_html = nunjucks.render('orgunits_select.html', { orgunits: data });
|
||||
$(".orgunit-select-container").html(ous_html);
|
||||
var q_term = getParameterByName('q');
|
||||
if(q_term && q_term[0] === '^') {
|
||||
var prefix = q_term.slice(1, -1);
|
||||
$(".orgunit-list li").removeClass('active');
|
||||
$('.orgunit-list li a[data-prefix="'+prefix+'"]').parent().toggleClass('active');
|
||||
|
||||
/* If searching then set q */
|
||||
var query_string = queryString.parse(location.search);
|
||||
if(query_string.q) {
|
||||
reset_active_org_unit();
|
||||
$('.js-lists-filter-field').val(query_string.q);
|
||||
}
|
||||
if(query_string.orgunit) {
|
||||
set_active_org_unit(query_string.orgunit);
|
||||
}
|
||||
|
||||
$.getJSON(api_urls.email_domain, function(email_domain) {
|
||||
@@ -136,6 +194,7 @@
|
||||
}
|
||||
$('.js-new-list-preview').text(new_list_name);
|
||||
});
|
||||
|
||||
/* Prefix select */
|
||||
$('.new-list .prefix-select a').on('click', function() {
|
||||
$('.new-list .prefix-select a').parent().removeClass('active');
|
||||
@@ -143,6 +202,7 @@
|
||||
var prefix = $(this).parent().attr('data-value');
|
||||
$('.new-list .prefix-btn').html(prefix+' <span class="caret"></span>');
|
||||
});
|
||||
|
||||
$('.new-list textarea').on('keyup', update_num_emails);
|
||||
|
||||
$('.js-add-list').on('click', function(e) {
|
||||
@@ -163,8 +223,7 @@
|
||||
var prefix = $('.new-list .prefix-select .active').attr('data-value');
|
||||
var source = prefix + new_list_name + "@" + email_domain.name;
|
||||
|
||||
var new_aliases = {};
|
||||
new_aliases = _.map(emails, function(el) {
|
||||
var new_aliases = _.map(emails, function(el) {
|
||||
return {
|
||||
'source': source,
|
||||
'destination': el,
|
||||
@@ -196,26 +255,15 @@
|
||||
|
||||
/* Filter lists by orgunit */
|
||||
$(".orgunit-list a").on('click', function(e) {
|
||||
var prefix = $(this).attr('data-prefix');
|
||||
e.preventDefault();
|
||||
|
||||
/* Toggle selection */
|
||||
$(".orgunit-list li").removeClass('active');
|
||||
$(this).parent().toggleClass('active');
|
||||
$('.orgunits-select').val(prefix);
|
||||
|
||||
filter_lists('^'+prefix+'-', true);
|
||||
$('.js-lists-filter-field').val('');
|
||||
var id = $(this).attr('data-id');
|
||||
var prefix = $(this).attr('data-prefix');
|
||||
filter_lists_by_orgunit(id, prefix);
|
||||
});
|
||||
$(".orgunits-select").on('change', function() {
|
||||
var prefix = $(this).val();
|
||||
|
||||
/* Toggle selection */
|
||||
$(".orgunit-list li").removeClass('active');
|
||||
$(".orgunit-list li a[data-prefix="+prefix+"]").parent().toggleClass('active');
|
||||
|
||||
filter_lists('^'+prefix+'-', true);
|
||||
$('.js-lists-filter-field').val('');
|
||||
var id = $(this).val();
|
||||
var prefix = $(this).find(':selected').attr('data-prefix');
|
||||
filter_lists_by_orgunit(id, prefix);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -236,28 +284,16 @@
|
||||
return;
|
||||
}
|
||||
|
||||
var forwards = data;
|
||||
var lists =_.groupBy(forwards, function(fwd) { return fwd.source; });
|
||||
var lists = _.groupBy(data, function(alias) { return alias.source; });
|
||||
var fw_html = nunjucks.render('list.html', { lists: lists });
|
||||
|
||||
forwards_container.html(fw_html);
|
||||
|
||||
/* Load filter from query string */
|
||||
var q_term = getParameterByName('q');
|
||||
if(q_term) {
|
||||
if(q_term[0] === '^') {
|
||||
filter_lists(q_term, false);
|
||||
} else {
|
||||
filter_lists(q_term, false, true);
|
||||
}
|
||||
/* Filter lists by search term from query string */
|
||||
var query_string = queryString.parse(location.search);
|
||||
if(query_string.q) {
|
||||
filter_lists(query_string.q, true);
|
||||
}
|
||||
/* Filter by search query */
|
||||
$('.js-lists-filter-field').on('keyup', function() {
|
||||
var term = $(this).val();
|
||||
filter_lists(term, true, true);
|
||||
$(".orgunit-list li").removeClass('active');
|
||||
$(".orgunit-list li:first-child").toggleClass('active');
|
||||
});
|
||||
});
|
||||
|
||||
/* Mark row red for deletion and show delete button if applicable */
|
||||
@@ -271,7 +307,7 @@
|
||||
tbody.find('.link-del').removeClass('visible');
|
||||
}
|
||||
});
|
||||
/* Delete selected */
|
||||
/* Delete selected aliases */
|
||||
forwards_container.on('click', '.js-del-selected', function() {
|
||||
var list_name = $(this).attr('data-delete-list-name');
|
||||
var list_el = $('.fwdlist[data-list-name="'+list_name+'"]');
|
||||
@@ -281,7 +317,7 @@
|
||||
return {
|
||||
'source': $(el).attr('data-source'),
|
||||
'destination': $(el).attr('data-destination'),
|
||||
'domain': parseInt($(el).attr('data-domain'), 10),
|
||||
'domain': parseInt($(el).attr('data-domain'), 10)
|
||||
};
|
||||
});
|
||||
function success(res){
|
||||
@@ -308,7 +344,7 @@
|
||||
$.ajax(data);
|
||||
});
|
||||
|
||||
/* Toggles emails textarea */
|
||||
/* Toggle button for add emails textarea */
|
||||
forwards_container.on('click', '.js-toggle-email-textarea', function(e) {
|
||||
e.preventDefault();
|
||||
var row = $(this).closest('tbody').find('.textarea-row');
|
||||
@@ -357,9 +393,22 @@
|
||||
}
|
||||
);
|
||||
});
|
||||
/* Update num emails for all list textarea's */
|
||||
forwards_container.on('keyup', '.js-add-list-textarea', update_num_emails);
|
||||
|
||||
/* New list toggle display */
|
||||
/* Filter by search query */
|
||||
$('.js-lists-filter-field').on('keyup change', function(e) {
|
||||
var term = $(this).val();
|
||||
filter_lists(term, true);
|
||||
reset_active_org_unit();
|
||||
var query_params = {q: term};
|
||||
if(term === "") {
|
||||
query_params = null;
|
||||
}
|
||||
set_query_string(query_params);
|
||||
});
|
||||
|
||||
/* New list toggle display button */
|
||||
$('.new-list-btn').on('click', function() {
|
||||
$('.new-list').toggleClass('visible');
|
||||
});
|
||||
|
||||
@@ -141,6 +141,17 @@ $icon-font-path: "../fonts/";
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
.highlight .alias:before {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 50% 50%;
|
||||
background-color: #00d000;
|
||||
content: '';
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
left: 20px;
|
||||
margin-top: 7px;
|
||||
}
|
||||
/* Responsive fixes */
|
||||
@media(max-width: 500px){
|
||||
.panel-heading {
|
||||
@@ -153,6 +164,10 @@ $icon-font-path: "../fonts/";
|
||||
td {
|
||||
font-size: 13px;
|
||||
}
|
||||
.highlight .alias:before {
|
||||
margin-top: 5px;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.love {
|
||||
@@ -164,4 +179,4 @@ $icon-font-path: "../fonts/";
|
||||
text-align: center;
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
<li class="active"><a href="/lists/" data-prefix=".*">Alle</a></li>
|
||||
<li class="active"><a href="/lists/" data-id="" data-prefix=".+">Alle</a></li>
|
||||
{% for ou in orgunits %}
|
||||
<li><a href="/lists/?q={{ou.prefix }}" data-prefix="{{ou.prefix }}">{{ ou.name }} ({{ ou.prefix }})</a></li>
|
||||
<li><a href="/lists/?orgunit={{ ou.id }}" data-prefix="{{ ou.prefixes_regex }}" data-id="{{ ou.id }}">{{ ou.name }}</a></li>
|
||||
{% endfor %}
|
||||
@@ -1,6 +1,6 @@
|
||||
<select class="orgunits-select form-control">
|
||||
<option value=".+">Alle</option>
|
||||
<option value="" data-prefix=".+">Alle</option>
|
||||
{% for ou in orgunits %}
|
||||
<option value="{{ou.prefix }}">{{ ou.name }}</option>
|
||||
<option value="{{ ou.id }}" data-prefix="{{ou.prefixes_regex }}">{{ ou.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
@@ -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": {}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (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.
|
||||
@@ -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;
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,78 @@
|
||||
# query-string [](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)
|
||||
+116
-67
@@ -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); // <select>
|
||||
|
||||
/* Reset search field */
|
||||
$('.js-lists-filter-field').val('');
|
||||
}
|
||||
function set_prefixes_regex(ous) {
|
||||
ous = _.map(ous, function(ou) {
|
||||
var prefixes = _.map(ou.prefixes, function(prefix) {
|
||||
return '^'+ prefix +'-|^'+ prefix +'@';
|
||||
});
|
||||
ou.prefixes_regex = prefixes.join('|');
|
||||
|
||||
return ou;
|
||||
});
|
||||
|
||||
return ous;
|
||||
}
|
||||
function reset_active_org_unit() {
|
||||
$(".orgunit-list li").removeClass('active');
|
||||
$(".orgunit-list li:first-child").toggleClass('active');
|
||||
}
|
||||
|
||||
$(document).ready(function() {
|
||||
csrf_token = $('meta[name=x-csrf-token]').attr('content');
|
||||
@@ -102,16 +154,22 @@
|
||||
/* Lists view (home) */
|
||||
if( $('body.home').length ) {
|
||||
$.getJSON(api_urls.ou_list, function(data) {
|
||||
data = set_prefixes_regex(data);
|
||||
|
||||
/* Load orgunit list */
|
||||
var ou_html = nunjucks.render('orgunits.html', { orgunits: data });
|
||||
$(".orgunit-list").append(ou_html);
|
||||
var ous_html = nunjucks.render('orgunits_select.html', { orgunits: data });
|
||||
$(".orgunit-select-container").html(ous_html);
|
||||
var q_term = getParameterByName('q');
|
||||
if(q_term && q_term[0] === '^') {
|
||||
var prefix = q_term.slice(1, -1);
|
||||
$(".orgunit-list li").removeClass('active');
|
||||
$('.orgunit-list li a[data-prefix="'+prefix+'"]').parent().toggleClass('active');
|
||||
|
||||
/* If searching then set q */
|
||||
var query_string = queryString.parse(location.search);
|
||||
if(query_string.q) {
|
||||
reset_active_org_unit();
|
||||
$('.js-lists-filter-field').val(query_string.q);
|
||||
}
|
||||
if(query_string.orgunit) {
|
||||
set_active_org_unit(query_string.orgunit);
|
||||
}
|
||||
|
||||
$.getJSON(api_urls.email_domain, function(email_domain) {
|
||||
@@ -136,6 +194,7 @@
|
||||
}
|
||||
$('.js-new-list-preview').text(new_list_name);
|
||||
});
|
||||
|
||||
/* Prefix select */
|
||||
$('.new-list .prefix-select a').on('click', function() {
|
||||
$('.new-list .prefix-select a').parent().removeClass('active');
|
||||
@@ -143,6 +202,7 @@
|
||||
var prefix = $(this).parent().attr('data-value');
|
||||
$('.new-list .prefix-btn').html(prefix+' <span class="caret"></span>');
|
||||
});
|
||||
|
||||
$('.new-list textarea').on('keyup', update_num_emails);
|
||||
|
||||
$('.js-add-list').on('click', function(e) {
|
||||
@@ -163,8 +223,7 @@
|
||||
var prefix = $('.new-list .prefix-select .active').attr('data-value');
|
||||
var source = prefix + new_list_name + "@" + email_domain.name;
|
||||
|
||||
var new_aliases = {};
|
||||
new_aliases = _.map(emails, function(el) {
|
||||
var new_aliases = _.map(emails, function(el) {
|
||||
return {
|
||||
'source': source,
|
||||
'destination': el,
|
||||
@@ -196,26 +255,15 @@
|
||||
|
||||
/* Filter lists by orgunit */
|
||||
$(".orgunit-list a").on('click', function(e) {
|
||||
var prefix = $(this).attr('data-prefix');
|
||||
e.preventDefault();
|
||||
|
||||
/* Toggle selection */
|
||||
$(".orgunit-list li").removeClass('active');
|
||||
$(this).parent().toggleClass('active');
|
||||
$('.orgunits-select').val(prefix);
|
||||
|
||||
filter_lists('^'+prefix+'-', true);
|
||||
$('.js-lists-filter-field').val('');
|
||||
var id = $(this).attr('data-id');
|
||||
var prefix = $(this).attr('data-prefix');
|
||||
filter_lists_by_orgunit(id, prefix);
|
||||
});
|
||||
$(".orgunits-select").on('change', function() {
|
||||
var prefix = $(this).val();
|
||||
|
||||
/* Toggle selection */
|
||||
$(".orgunit-list li").removeClass('active');
|
||||
$(".orgunit-list li a[data-prefix="+prefix+"]").parent().toggleClass('active');
|
||||
|
||||
filter_lists('^'+prefix+'-', true);
|
||||
$('.js-lists-filter-field').val('');
|
||||
var id = $(this).val();
|
||||
var prefix = $(this).find(':selected').attr('data-prefix');
|
||||
filter_lists_by_orgunit(id, prefix);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -236,28 +284,16 @@
|
||||
return;
|
||||
}
|
||||
|
||||
var forwards = data;
|
||||
var lists =_.groupBy(forwards, function(fwd) { return fwd.source; });
|
||||
var lists = _.groupBy(data, function(alias) { return alias.source; });
|
||||
var fw_html = nunjucks.render('list.html', { lists: lists });
|
||||
|
||||
forwards_container.html(fw_html);
|
||||
|
||||
/* Load filter from query string */
|
||||
var q_term = getParameterByName('q');
|
||||
if(q_term) {
|
||||
if(q_term[0] === '^') {
|
||||
filter_lists(q_term, false);
|
||||
} else {
|
||||
filter_lists(q_term, false, true);
|
||||
}
|
||||
/* Filter lists by search term from query string */
|
||||
var query_string = queryString.parse(location.search);
|
||||
if(query_string.q) {
|
||||
filter_lists(query_string.q, true);
|
||||
}
|
||||
/* Filter by search query */
|
||||
$('.js-lists-filter-field').on('keyup', function() {
|
||||
var term = $(this).val();
|
||||
filter_lists(term, true, true);
|
||||
$(".orgunit-list li").removeClass('active');
|
||||
$(".orgunit-list li:first-child").toggleClass('active');
|
||||
});
|
||||
});
|
||||
|
||||
/* Mark row red for deletion and show delete button if applicable */
|
||||
@@ -271,7 +307,7 @@
|
||||
tbody.find('.link-del').removeClass('visible');
|
||||
}
|
||||
});
|
||||
/* Delete selected */
|
||||
/* Delete selected aliases */
|
||||
forwards_container.on('click', '.js-del-selected', function() {
|
||||
var list_name = $(this).attr('data-delete-list-name');
|
||||
var list_el = $('.fwdlist[data-list-name="'+list_name+'"]');
|
||||
@@ -281,7 +317,7 @@
|
||||
return {
|
||||
'source': $(el).attr('data-source'),
|
||||
'destination': $(el).attr('data-destination'),
|
||||
'domain': parseInt($(el).attr('data-domain'), 10),
|
||||
'domain': parseInt($(el).attr('data-domain'), 10)
|
||||
};
|
||||
});
|
||||
function success(res){
|
||||
@@ -308,7 +344,7 @@
|
||||
$.ajax(data);
|
||||
});
|
||||
|
||||
/* Toggles emails textarea */
|
||||
/* Toggle button for add emails textarea */
|
||||
forwards_container.on('click', '.js-toggle-email-textarea', function(e) {
|
||||
e.preventDefault();
|
||||
var row = $(this).closest('tbody').find('.textarea-row');
|
||||
@@ -357,9 +393,22 @@
|
||||
}
|
||||
);
|
||||
});
|
||||
/* Update num emails for all list textarea's */
|
||||
forwards_container.on('keyup', '.js-add-list-textarea', update_num_emails);
|
||||
|
||||
/* New list toggle display */
|
||||
/* Filter by search query */
|
||||
$('.js-lists-filter-field').on('keyup change', function(e) {
|
||||
var term = $(this).val();
|
||||
filter_lists(term, true);
|
||||
reset_active_org_unit();
|
||||
var query_params = {q: term};
|
||||
if(term === "") {
|
||||
query_params = null;
|
||||
}
|
||||
set_query_string(query_params);
|
||||
});
|
||||
|
||||
/* New list toggle display button */
|
||||
$('.new-list-btn').on('click', function() {
|
||||
$('.new-list').toggleClass('visible');
|
||||
});
|
||||
|
||||
+12
-10
@@ -233,7 +233,7 @@ var lineno = null;
|
||||
var colno = null;
|
||||
var output = "";
|
||||
try {
|
||||
output += "<li class=\"active\"><a href=\"/lists/\" data-prefix=\".*\">Alle</a></li>\n";
|
||||
output += "<li class=\"active\"><a href=\"/lists/\" data-id=\"\" data-prefix=\".+\">Alle</a></li>\n";
|
||||
frame = frame.push();
|
||||
var t_3 = runtime.contextOrFrameLookup(context, frame, "orgunits");
|
||||
if(t_3) {var t_2 = t_3.length;
|
||||
@@ -247,15 +247,15 @@ frame.set("loop.revindex0", t_2 - t_1 - 1);
|
||||
frame.set("loop.first", t_1 === 0);
|
||||
frame.set("loop.last", t_1 === t_2 - 1);
|
||||
frame.set("loop.length", t_2);
|
||||
output += "\n<li><a href=\"/lists/?q=";
|
||||
output += runtime.suppressValue(runtime.memberLookup((t_4),"prefix", env.autoesc), env.autoesc);
|
||||
output += "\n<li><a href=\"/lists/?orgunit=";
|
||||
output += runtime.suppressValue(runtime.memberLookup((t_4),"id", env.autoesc), env.autoesc);
|
||||
output += "\" data-prefix=\"";
|
||||
output += runtime.suppressValue(runtime.memberLookup((t_4),"prefix", env.autoesc), env.autoesc);
|
||||
output += runtime.suppressValue(runtime.memberLookup((t_4),"prefixes_regex", env.autoesc), env.autoesc);
|
||||
output += "\" data-id=\"";
|
||||
output += runtime.suppressValue(runtime.memberLookup((t_4),"id", env.autoesc), env.autoesc);
|
||||
output += "\">";
|
||||
output += runtime.suppressValue(runtime.memberLookup((t_4),"name", env.autoesc), env.autoesc);
|
||||
output += " (";
|
||||
output += runtime.suppressValue(runtime.memberLookup((t_4),"prefix", env.autoesc), env.autoesc);
|
||||
output += ")</a></li>\n";
|
||||
output += "</a></li>\n";
|
||||
;
|
||||
}
|
||||
}
|
||||
@@ -277,7 +277,7 @@ var lineno = null;
|
||||
var colno = null;
|
||||
var output = "";
|
||||
try {
|
||||
output += "<select class=\"orgunits-select form-control\">\n <option value=\".+\">Alle</option>\n\t";
|
||||
output += "<select class=\"orgunits-select form-control\">\n <option value=\"\" data-prefix=\".+\">Alle</option>\n\t";
|
||||
frame = frame.push();
|
||||
var t_3 = runtime.contextOrFrameLookup(context, frame, "orgunits");
|
||||
if(t_3) {var t_2 = t_3.length;
|
||||
@@ -291,8 +291,10 @@ frame.set("loop.revindex0", t_2 - t_1 - 1);
|
||||
frame.set("loop.first", t_1 === 0);
|
||||
frame.set("loop.last", t_1 === t_2 - 1);
|
||||
frame.set("loop.length", t_2);
|
||||
output += "\n\t<option value=\"";
|
||||
output += runtime.suppressValue(runtime.memberLookup((t_4),"prefix", env.autoesc), env.autoesc);
|
||||
output += "\n\t <option value=\"";
|
||||
output += runtime.suppressValue(runtime.memberLookup((t_4),"id", env.autoesc), env.autoesc);
|
||||
output += "\" data-prefix=\"";
|
||||
output += runtime.suppressValue(runtime.memberLookup((t_4),"prefixes_regex", env.autoesc), env.autoesc);
|
||||
output += "\">";
|
||||
output += runtime.suppressValue(runtime.memberLookup((t_4),"name", env.autoesc), env.autoesc);
|
||||
output += "</option>\n\t";
|
||||
|
||||
+16
@@ -6753,6 +6753,18 @@ html > body > .ui-pnotify {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.highlight .alias:before {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 50% 50%;
|
||||
background-color: #00d000;
|
||||
content: '';
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
left: 20px;
|
||||
margin-top: 7px;
|
||||
}
|
||||
|
||||
/* Responsive fixes */
|
||||
@media (max-width: 500px) {
|
||||
.panel-heading {
|
||||
@@ -6767,6 +6779,10 @@ html > body > .ui-pnotify {
|
||||
td {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.highlight .alias:before {
|
||||
margin-top: 5px;
|
||||
}
|
||||
}
|
||||
.love {
|
||||
color: #faafba;
|
||||
|
||||
@@ -25,7 +25,8 @@
|
||||
<script type="text/javascript" src="{% static 'bower_components/bootstrap-sass-official/vendor/assets/javascripts/bootstrap/dropdown.js' %}"></script>
|
||||
<script type="text/javascript" src="{% static 'bower_components/bootstrap-sass-official/vendor/assets/javascripts/bootstrap/collapse.js' %}"></script>
|
||||
<script type="text/javascript" src="{% static 'bower_components/pnotify/pnotify.core.js' %}"></script>
|
||||
<script type="text/javascript" src="{% static 'bower_components/query-string/query-string.js' %}"></script>
|
||||
<script type="text/javascript" src="{% static 'dist/scripts/templates.js' %}"></script>
|
||||
<script type="text/javascript" src="{% static 'dist/scripts/main.js' %}"></script>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
+8
-9
@@ -12,7 +12,7 @@ from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
|
||||
from mailadmin.api import DjangoPostfixDovecotAPI
|
||||
from mailadmin.models import OrgUnit
|
||||
from mailadmin.models import OrgUnit, Prefix
|
||||
from mailadmin.serializers import UserSerializer, OrgUnitSerializer, AliasSerializer
|
||||
from mailadmin.permissions import SourcePrefixOwner
|
||||
|
||||
@@ -69,10 +69,11 @@ class AliasesView(views.APIView):
|
||||
my_groups = self.get_groups(request)
|
||||
|
||||
# Look for mailinglist prefixes
|
||||
orgunits = OrgUnit.objects.exclude(prefix__isnull=True).exclude(is_active=False).distinct()
|
||||
orgunits = OrgUnit.objects.exclude(is_active=False).distinct()
|
||||
if not request.user.is_superuser:
|
||||
orgunits = orgunits.filter(admin_groups__in=my_groups)
|
||||
my_prefixes = orgunits.values_list('prefix', flat=True)
|
||||
|
||||
my_prefixes = Prefix.objects.filter(orgunit=orgunits)
|
||||
|
||||
return my_prefixes
|
||||
|
||||
@@ -80,12 +81,11 @@ class AliasesView(views.APIView):
|
||||
my_prefixes = self.get_user_prefixes(request)
|
||||
|
||||
# Prepare regular expression
|
||||
# FIXME: Move to method on orgunit manager
|
||||
regexp = '|'.join(['^{0}-|^{0}@'.format(p) for p in my_prefixes])
|
||||
regex = my_prefixes.as_regex()
|
||||
if len(my_prefixes) == 0:
|
||||
return Response({'result': 'No results'}) # Need at least one
|
||||
|
||||
res = self._api.list_aliases_regex(regexp)
|
||||
res = self._api.list_aliases_regex(regex)
|
||||
|
||||
return Response(res)
|
||||
|
||||
@@ -123,12 +123,11 @@ class MyOrgUnitViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get_queryset(self):
|
||||
orgunits = OrgUnit.objects.exclude(prefix__isnull=True).exclude(is_active=False).distinct()
|
||||
orgunits = OrgUnit.objects.exclude(prefixes__isnull=True).exclude(is_active=False).distinct()
|
||||
if self.request.user.is_superuser:
|
||||
return orgunits
|
||||
|
||||
group_ids = self.request.user.groups.values('id')
|
||||
return orgunits.filter(admin_groups__in=group_ids)
|
||||
return orgunits.filter(admin_groups=self.request.user.groups)
|
||||
|
||||
|
||||
class OrgUnitViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
|
||||
Reference in new issue
Block a user