✨ Working create and delete

This commit is contained in:
nikolark committed 2020-01-04 20:30:14 +01:00
1 parent 39942447ed
commit 865ae17785
16 files changed
+421 -281

No files matched your search

+5 -5
View File
@@ -30,7 +30,7 @@ class DjangoPostfixDovecotAPI(object):
self.username = username
self.password = password
def _api(self, method, path, params=None, json=None):
def _request(self, method, path, params=None, json=None):
headers = {'content-type': 'application/json'}
url = "{}{}".format(self.base_url, path)
@@ -64,10 +64,10 @@ class DjangoPostfixDovecotAPI(object):
return ret
def create_aliases(self, aliases):
return self._api('POST', '/aliases/create_bulk/', json=aliases)
return self._request('POST', '/aliases/create_bulk/', json=aliases)
def delete_aliases(self, aliases):
return self._api('DELETE', '/aliases/delete_bulk/', json=aliases)
return self._request('DELETE', '/aliases/delete_bulk/', json=aliases)
def list_aliases_regex(self, regex):
""" Limit aliases by regular expression and domain name
@@ -77,7 +77,7 @@ class DjangoPostfixDovecotAPI(object):
'domain__name': settings.NEUF_EMAIL_DOMAIN_NAME,
'source__iregex': regex
}
return self._api(
return self._request(
'GET',
'/aliases/',
params=params
@@ -89,7 +89,7 @@ class DjangoPostfixDovecotAPI(object):
if name is not None:
params['name'] = name
return self._api(
return self._request(
'GET',
'/domains/',
params=params
+4 -4
View File
@@ -8,7 +8,7 @@ class SourcePrefixOwner(permissions.BasePermission):
""" Ref: http://www.django-rest-framework.org/api-guide/permissions """
def has_permission(self, request, view):
assert hasattr(view, 'get_user_prefixes')
assert hasattr(view, 'user_prefixes')
# Read permissions (GET, HEAD or OPTIONS) are allowed for all authenticated request
if request.method in permissions.SAFE_METHODS or request.method not in ['DELETE', 'POST']:
@@ -22,13 +22,13 @@ class SourcePrefixOwner(permissions.BasePermission):
if not serializer.is_valid():
return True # Skip auth if not valid data
my_prefixes = view.get_user_prefixes(request)
if len(my_prefixes) == 0:
my_prefixes = view.user_prefixes(request)
if not my_prefixes:
return False
# Prepare regular expression
regexp = my_prefixes.as_regex()
# All source-adresses should match the prefix regexp
# All source adresses should match the prefix regexp
for alias in serializer.initial_data:
if re.search(regexp, alias.get('source', '')) is None:
return False
+2 -1
View File
@@ -1,3 +1,4 @@
{
"presets": ["@babel/preset-env", "@babel/preset-react"]
"presets": ["@babel/preset-env", "@babel/preset-react"],
"plugins": ["@babel/plugin-transform-runtime"]
}
+3 -1
View File
@@ -15,7 +15,9 @@
"no-underscore-dangle": "off",
"no-param-reassign": "off",
"no-restricted-syntax": "off",
"react/prop-types": "off"
"react/prop-types": "off",
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn"
},
"settings": {
"react": {
+25
View File
@@ -767,6 +767,18 @@
"@babel/helper-plugin-utils": "^7.0.0"
}
},
"@babel/plugin-transform-runtime": {
"version": "7.7.6",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.7.6.tgz",
"integrity": "sha512-tajQY+YmXR7JjTwRvwL4HePqoL3DYxpYXIHKVvrOIvJmeHe2y1w4tz5qz9ObUDC9m76rCzIMPyn4eERuwA4a4A==",
"dev": true,
"requires": {
"@babel/helper-module-imports": "^7.7.4",
"@babel/helper-plugin-utils": "^7.0.0",
"resolve": "^1.8.1",
"semver": "^5.5.1"
}
},
"@babel/plugin-transform-shorthand-properties": {
"version": "7.7.4",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.7.4.tgz",
@@ -896,6 +908,14 @@
"@babel/plugin-transform-react-jsx-source": "^7.7.4"
}
},
"@babel/runtime": {
"version": "7.7.7",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.7.7.tgz",
"integrity": "sha512-uCnC2JEVAu8AKB5do1WRIsvrdJ0flYx/A/9f/6chdacnEZ7LmavjdsDXr5ksYBegxtuTPR5Va9/+13QF/kFkCA==",
"requires": {
"regenerator-runtime": "^0.13.2"
}
},
"@babel/template": {
"version": "7.7.4",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.7.4.tgz",
@@ -9746,6 +9766,11 @@
"regenerate": "^1.4.0"
}
},
"regenerator-runtime": {
"version": "0.13.3",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.3.tgz",
"integrity": "sha512-naKIZz2GQ8JWh///G7L3X6LaQUAMp2lvb1rvwwsURe/VXwD6VMfr+/1NuNw3ag8v2kY1aQ/go5SNn79O9JU7yw=="
},
"regenerator-transform": {
"version": "0.14.1",
"resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.14.1.tgz",
+2
View File
@@ -12,6 +12,7 @@
],
"dependencies": {
"@apollo/react-hooks": "^3.1.3",
"@babel/runtime": "^7.7.7",
"@fortawesome/fontawesome-free": "^5.12.0",
"apollo-cache-inmemory": "^1.6.5",
"apollo-client": "^2.6.8",
@@ -34,6 +35,7 @@
"devDependencies": {
"@babel/core": "^7.7.7",
"@babel/plugin-syntax-dynamic-import": "^7.7.4",
"@babel/plugin-transform-runtime": "^7.7.6",
"@babel/preset-env": "^7.7.7",
"@babel/preset-react": "^7.7.4",
"autoprefixer": "^9.7.3",
+12 -3
View File
@@ -6,15 +6,24 @@ import { InMemoryCache } from 'apollo-cache-inmemory';
import { OrgUnitList, MainArea } from './components';
const restLink = new RestLink({ uri: '/api/' });
import { getCookie } from './utils';
const restLink = new RestLink({
uri: '/api/',
credentials: 'include',
headers: {
'x-csrftoken': getCookie('csrftoken')
}
});
const client = new ApolloClient({
link: restLink,
cache: new InMemoryCache()
cache: new InMemoryCache(),
typeDefs: {}
});
const App = () => (
<ApolloProvider client={client}>
<div className="container">
<div className="container" style={{ marginTop: '2rem' }}>
<div className="row">
<OrgUnitList />
<MainArea />
@@ -0,0 +1,130 @@
import React, { useState, useCallback } from 'react';
import ctx from 'classnames';
import { useMutation } from '@apollo/react-hooks';
import EmailsTextarea from './EmailsTextarea';
import { DELETE_ALIASES_MUTATION } from '../mutations';
import { notify } from '../utils';
import { GET_ALIASES } from '../queries';
const AliasRow = ({ id, destination, updateDeletionList }) => {
const [checked, setChecked] = useState(false);
return (
<tr className={ctx({ 'table-danger': checked })}>
<td>{destination}</td>
<td className="del-cell">
<div className="custom-control custom-checkbox custom-control-inline">
<input
id={`alias-delete-${id}`}
type="checkbox"
className="custom-control-input"
value={checked}
onChange={(e) => {
setChecked(e.target.checked);
updateDeletionList(id, e.target.checked);
}}
/>
<label className="custom-control-label" htmlFor={`alias-delete-${id}`} />
</div>
</td>
</tr>
);
};
const ActionRow = ({ onAddEmailClick, deleteVisible, onDelete }) => {
return (
<tr className="action-row">
<td>
<a href="#" className="link-add" onClick={onAddEmailClick}>
<span className="fas fa-plus" aria-hidden="true" /> Legg til
</a>
</td>
<td>
<a href="#" className={ctx('link-del', { visible: deleteVisible })} type="button" onClick={onDelete}>
<span className="fas fa-trash" aria-hidden="true" />
&nbsp;Slett
</a>
</td>
</tr>
);
};
const AliasList = ({ list, aliases, domain }) => {
const [textareaVisible, setTextareaVisible] = useState(false);
const [deletionList, setDeletionList] = useState([]);
const [deleteAliases] = useMutation(DELETE_ALIASES_MUTATION);
const onDelete = useCallback(
async (e) => {
e.preventDefault();
const input = deletionList.map((alias) => ({ ...alias, domain: domain.id }));
const errorMessage = `Kunne ikke slette e-poster fra ${list}`;
try {
const { errors } = await deleteAliases({
variables: { input },
update: (cache) => {
setDeletionList([]);
const deletedIds = deletionList.map(({ id }) => id);
const { aliases: aliasesFromCache } = cache.readQuery({ query: GET_ALIASES });
cache.writeQuery({
query: GET_ALIASES,
data: { aliases: aliasesFromCache.filter(({ id }) => !deletedIds.includes(id)) }
});
notify('Slettet e-poster', `${deletionList.map((alias) => alias.destination).join('\n')} slettet.`);
}
});
if (errors) {
notify('Feil', errorMessage, 'error');
console.error(errors);
}
} catch (err) {
notify('Feil', errorMessage, 'error');
console.error(err);
}
},
[deleteAliases, deletionList, domain, list]
);
return (
<div className="card mb-3">
<div className="card-header">
<span className="fas fa-list-alt icon-faded" aria-hidden="true" />
{list}
<span className="badge badge-secondary badge-list-total">{aliases.length}</span>
</div>
<table className="table table-striped table-hover table-condensed">
<tbody>
{aliases.map((alias) => (
<AliasRow
key={alias.id}
{...alias}
updateDeletionList={(aliasId, checked) => {
if (checked) {
const { __typename, ...aliasForDeletion } = alias;
setDeletionList([...deletionList, aliasForDeletion]); // add
} else {
setDeletionList(deletionList.filter(({ id }) => id !== aliasId)); // remove
}
}}
/>
))}
<ActionRow
onAddEmailClick={(e) => {
e.preventDefault();
setTextareaVisible(!textareaVisible);
}}
deleteVisible={Boolean(deletionList.length)}
onDelete={onDelete}
/>
<tr className={ctx('textarea-row', { visible: textareaVisible })}>
<td colSpan="2">
<EmailsTextarea source={list} domainId={domain.id} />
</td>
</tr>
</tbody>
</table>
</div>
);
};
export default AliasList;
@@ -0,0 +1,82 @@
import React, { useState } from 'react';
import ctx from 'classnames';
import EmailsTextarea from './EmailsTextarea';
const AliasListCreate = ({ domain, lists, orgUnits }) => {
const firstOrgUnit = orgUnits[0].prefixes[0];
const [textareaVisible, setTextareaVisible] = useState(false);
const [name, setName] = useState('');
const [selectedPrefix, setSelectedPrefix] = useState(firstOrgUnit);
const listName = `${selectedPrefix}-${name}@${domain.name}`;
const shouldCreate = Boolean(Object.entries(lists).filter(([list]) => list === listName).length);
return (
<div className="new-list-container">
<button
type="button"
className="btn btn-outline-primary new-list-btn"
onClick={() => {
setTextareaVisible(!textareaVisible);
}}
>
<span className="fas fa-plus" aria-hidden="true" /> Opprett ny liste
</button>
<div className={ctx('new-list fwdlist card', { visible: textareaVisible })}>
<div className="card-header">
<span className="fas fa-list-alt icon-faded" aria-hidden="true" />
{name ? listName : `Ny liste`}
{name && !shouldCreate && <span className="badge badge-success">Ny</span>}
{name && shouldCreate && <span className="badge badge-info">Eksisterende</span>}
</div>
<div className="card-body">
<div className="new-list-name input-group">
<div className="input-group-prepend">
<button
type="button"
className="btn btn btn-outline-secondary dropdown-toggle"
data-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false"
>
{selectedPrefix}- <span className="caret" />
</button>
<div className="dropdown-menu prefix-select">
{orgUnits.map((ou) =>
ou.prefixes.map((prefix) => (
<a
key={`${ou.id}-${prefix}`}
className="dropdown-item"
href="#"
onClick={(e) => {
e.preventDefault();
setSelectedPrefix(prefix);
}}
>
{prefix}-
</a>
))
)}
</div>
</div>
<input
type="text"
className="form-control js-new-list-name"
value={name}
onChange={(e) => setName(e.target.value)}
/>
<div className="input-group-append">
<span className="input-group-text">{`@${domain.name}`}</span>
</div>
</div>
<EmailsTextarea source={listName} domainId={domain.id} create={shouldCreate} />
</div>
</div>
{/* <div className="new-list-result"></div> */}
</div>
);
};
export default AliasListCreate;
@@ -0,0 +1,62 @@
import React, { useState, useCallback } from 'react';
import { useMutation } from '@apollo/react-hooks';
import { parseEmails, notify } from '../utils';
import { CREATE_ALIASES_MUTATION } from '../mutations';
import { GET_ALIASES } from '../queries';
const EmailsTextarea = ({ source, domainId, create = false }) => {
const [rawAliases, setRawAliases] = useState('');
const [createAliases] = useMutation(CREATE_ALIASES_MUTATION);
const aliases = parseEmails(rawAliases);
const onAddClick = useCallback(
async (e) => {
e.preventDefault();
const input = aliases.map((alias) => ({ source, destination: alias, domain: domainId }));
const { errors } = await createAliases({
variables: { input },
update: (cache, { data }) => {
setRawAliases('');
const { createAliasesResponse: newAliases } = data;
const { aliases: aliasesFromCache } = cache.readQuery({ query: GET_ALIASES });
cache.writeQuery({
query: GET_ALIASES,
data: { aliases: aliasesFromCache.concat(newAliases) }
});
const title = `${newAliases[0].source} ${create ? 'opprettet' : 'oppdatert'}`;
notify(title, `${newAliases.map((alias) => alias.destination).join('\n')} lagt til.`);
}
});
if (errors) {
notify('Feil', `Kunne ikke legge til e-poster til liste ${source}`, 'error');
console.error(errors);
}
},
[aliases, create, createAliases, domainId, source]
);
return (
<>
<h5>
Eposter <span className="badge badge-secondary email-counter">{!aliases.length ? '' : aliases.length}</span>
</h5>
<textarea
className="form-control"
placeholder="asdf@studentersamfundet.no, qwerty@studentersamfundet.no f.eks. Klipp og lim så mye du orker"
rows="4"
value={rawAliases}
onChange={(e) => {
setRawAliases(e.target.value);
}}
/>
<div className="text-right">
<button type="button" className="btn btn-primary btn-add-list" onClick={onAddClick}>
<span className="fas fa-plus" /> {create ? 'Opprett liste' : 'Legg til'}
</button>
</div>
</>
);
};
export default EmailsTextarea;
@@ -1,248 +1,43 @@
import React, { useState } from 'react';
import React from 'react';
import { groupBy, sortBy } from 'lodash';
import { useQuery } from '@apollo/react-hooks';
import ctx from 'classnames';
import { parseEmails } from '../utils';
import { GET_ALIASES, GET_DOMAINS, GET_ORG_UNITS } from '../queries';
import AliasList from './AliasList';
import AliasListCreate from './AliasListCreate';
const ListCreate = () => {
const { data, loading, error } = useQuery(GET_ORG_UNITS);
const { data: emailData, loading: emailLoading, error: emailError } = useQuery(GET_DOMAINS);
const [textareaVisible, setTextareaVisible] = useState(false);
const [listName, setListName] = useState('');
const [selectedPrefix, setSelectedPrefix] = useState(null);
if (loading || emailLoading || error || emailError) {
return null;
}
const { orgUnits } = data;
const { domains } = emailData;
const emailDomain = domains[0];
return (
<div className="new-list-container">
<button
type="button"
className="btn btn-outline-primary new-list-btn"
onClick={() => {
setTextareaVisible(!textareaVisible);
}}
>
<span className="fas fa-plus" aria-hidden="true" /> Opprett ny liste
</button>
<div className={ctx('new-list fwdlist card', { visible: textareaVisible })}>
<div className="card-header">
<span className="fas fa-list-alt icon-faded" aria-hidden="true" /> <span className="">Ny liste</span>
</div>
<div className="card-body">
<div className="new-list-name input-group">
<div className="input-group-prepend">
<button
type="button"
className="btn btn btn-outline-secondary dropdown-toggle"
data-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false"
>
{selectedPrefix || orgUnits[0].prefixes[0]}- <span className="caret" />
</button>
<div className="dropdown-menu prefix-select">
{orgUnits.map((ou) =>
ou.prefixes.map((prefix) => (
<a
key={`${ou.id}-${prefix}`}
className="dropdown-item"
href="#"
onClick={(e) => {
e.preventDefault();
setSelectedPrefix(prefix);
}}
>
{prefix}-
</a>
))
)}
</div>
</div>
<input
type="text"
className="form-control js-new-list-name"
value={listName}
onChange={(e) => setListName(e.target.value)}
/>
<div className="input-group-append">
<span className="input-group-text">{`@${emailDomain.name}`}</span>
</div>
</div>
<EmailsTextarea listName={listName} selectedPrefix={selectedPrefix} newList={true} />
</div>
</div>
{/* <div className="new-list-result"></div> */}
</div>
);
};
const AliasRow = ({ id, destination, updateDeletionList }) => {
const [checked, setChecked] = useState(false);
return (
<tr className="">
<td>{destination}</td>
<td className="del-cell">
<div className="custom-control custom-checkbox custom-control-inline">
<input
id={`alias-delete-${id}`}
type="checkbox"
className="custom-control-input"
value={checked}
onChange={(e) => {
setChecked(e.target.checked);
updateDeletionList(id, e.target.checked);
}}
/>
<label className="custom-control-label" htmlFor={`alias-delete-${id}`} />
</div>
</td>
</tr>
);
};
const ActionRow = ({ onAddEmailClick, deleteVisible, onDelete }) => {
return (
<tr className="action-row">
<td>
<a href="#" className="link-add" onClick={onAddEmailClick}>
<span className="fas fa-plus" aria-hidden="true" /> Legg til
</a>
</td>
<td>
<a href="#" className={ctx('link-del', { visible: deleteVisible })} type="button" onClick={onDelete}>
<span className="fas fa-trash" aria-hidden="true" />
&nbsp;Slett
</a>
</td>
</tr>
);
};
const EmailsTextarea = ({ list, selectedPrefix, newList = false }) => {
const [rawAliases, setRawAliases] = useState('');
const [aliases, setAliases] = useState([]);
const createAliases = () => {
console.log(list, aliases, selectedPrefix, newList);
// TODO: POST /aliases
};
return (
<>
<h5>
Eposter{' '}
<span className="badge badge-secondary email-counter">{aliases.length === 0 ? '' : aliases.length}</span>
</h5>
<textarea
className="form-control"
placeholder="asdf@studentersamfundet.no, qwerty@studentersamfundet.no f.eks. Klipp og lim så mye du orker"
rows="4"
value={rawAliases}
onChange={(e) => {
setRawAliases(e.target.value);
setAliases(parseEmails(e.target.value));
}}
/>
<div className="text-right">
<button type="button" className="btn btn-primary btn-add-list" onClick={createAliases}>
<span className="fas fa-plus" /> {newList ? 'Opprett liste' : 'Legg til'}
</button>
</div>
</>
);
};
const ForwardContainer = ({ filterQuery }) => {
const MainArea = ({ filterQuery }) => {
// TODO: Filter aliases by filterQuery
const { data, loading, error } = useQuery(GET_ALIASES);
const { data: ouData, loading: ouLoading, error: ouError } = useQuery(GET_ORG_UNITS);
const { data: domainData, loading: domainLoading, error: domainError } = useQuery(GET_DOMAINS);
const [textareaVisible, setTextareaVisible] = useState(false);
const [deletionList, setdeletionList] = useState([]);
const deleteAliases = (aliases) => {
// TODO: delete aliases
console.log(aliases);
};
if (loading) {
if (loading || ouLoading || domainLoading) {
return (
<div className="forwards-container">
<span className="fas fa-sync spin" aria-hidden="true" /> Laster...
</div>
);
}
if (error) {
if (error || ouError || domainError) {
return null;
}
const { orgUnits } = ouData;
const domain = domainData.domains[0];
const lists = groupBy(
sortBy(data.aliases, (alias) => {
return alias.source;
}),
sortBy(data.aliases, (alias) => alias.source),
(alias) => alias.source
);
return (
<div className="forwards-container">
{Object.entries(lists).map(([list, aliases]) => {
return (
<div key={list} className="card">
<div className="card-header">
<span className="fas fa-list-alt icon-faded" aria-hidden="true" /> {list}{' '}
<span className="badge badge-secondary badge-list-total">{aliases.length}</span>
</div>
<table className="table table-striped table-hover table-condensed">
<tbody>
{aliases.map((alias) => (
<AliasRow
key={alias.id}
{...alias}
updateDeletionList={(aliasId, checked) => {
if (checked) {
setdeletionList([...deletionList, aliasId]); // add
} else {
setdeletionList(deletionList.filter((id) => id !== aliasId)); // remove
}
}}
/>
))}
<ActionRow
onAddEmailClick={() => {
setTextareaVisible(!textareaVisible);
}}
deleteVisible={Boolean(deletionList.length)}
onDelete={(e) => {
e.preventDefault();
deleteAliases(deletionList);
}}
/>
<tr className={ctx('textarea-row', { visible: textareaVisible })}>
<td colSpan="2">
<EmailsTextarea list={list} />
</td>
</tr>
</tbody>
</table>
</div>
);
})}
</div>
);
};
const MainArea = () => {
return (
<div className="col-sm-8">
{/* <div className="orgunit-select-container"></div> */}
<ListCreate />
<ForwardContainer />
<AliasListCreate lists={lists} orgUnits={orgUnits} domain={domain} />
<div className="forwards-container">
{Object.entries(lists).map(([list, aliases]) => (
<AliasList key={list} list={list} aliases={aliases} domain={domain} />
))}
</div>
</div>
);
};
@@ -0,0 +1,18 @@
import gql from 'graphql-tag';
export const CREATE_ALIASES_MUTATION = gql`
mutation createAliases($input: [Alias]!) {
createAliasesResponse(input: $input) @rest(type: "Alias", path: "aliases/", method: "POST") {
id
source
destination
}
}
`;
export const DELETE_ALIASES_MUTATION = gql`
mutation deleteAliases($input: [Alias]!) {
deleteAliasesResponse(input: $input) @rest(type: "Alias", path: "aliases/delete/", method: "POST") {
id
}
}
`;
+1 -2
View File
@@ -10,7 +10,6 @@ export const GET_ORG_UNITS = gql`
}
`;
// FIXME: only a single domain
export const GET_DOMAINS = gql`
query Domains {
domains @rest(type: "Domain", path: "domains/") {
@@ -21,7 +20,7 @@ export const GET_DOMAINS = gql`
`;
export const GET_ALIASES = gql`
query OrgUnits {
query Aliases {
aliases @rest(type: "Alias", path: "aliases/") {
id
source
+24 -3
View File
@@ -1,4 +1,5 @@
import PNotify from 'pnotify/dist/es/PNotify';
import PNotifyButtons from 'pnotify/dist/es/PNotifyButtons';
import * as queryString from 'query-string';
export function parseEmails(text) {
@@ -31,9 +32,13 @@ export function setQueryString(obj) {
window.history.pushState(null, null, `/lists/${qs}`);
}
export function notify(title, text, type) {
/* types: info, success, error */
return new PNotify({
/**
* @param {string} title
* @param {string} text
* @param {('info'|'success'|'error')} type
* */
export function notify(title, text, type = 'success') {
PNotify.alert({
title,
text,
type,
@@ -42,3 +47,19 @@ export function notify(title, text, type) {
styling: 'bootstrap4'
});
}
export function getCookie(name) {
let cookieValue = null;
if (document.cookie && document.cookie !== '') {
const cookies = document.cookie.split(';');
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].trim();
// Does this cookie string begin with the name we want?
if (cookie.substring(0, name.length + 1) === `${name}=`) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
+8 -6
View File
@@ -1,11 +1,12 @@
from django.conf.urls import url
from django.urls import path, re_path
from rest_framework.routers import DefaultRouter
from mailadmin.views import MyUserViewSet, MyOrgUnitViewSet, OrgUnitViewSet, AliasesView, get_domains, index, lists
from mailadmin.views import MyUserViewSet, MyOrgUnitViewSet, OrgUnitViewSet, AliasesView, get_domains, index, lists, \
AliasesDeleteView
urlpatterns = [
url(r'^$', index, name='index'),
url(r'^lists/', lists, name='lists'),
re_path(r'^$', index, name='index'),
path('lists/', lists, name='lists'),
]
# API
@@ -16,6 +17,7 @@ router.register(r'api/orgunits/all', OrgUnitViewSet, basename='allous')
urlpatterns += router.urls
urlpatterns += [
url(r'^api/aliases/', AliasesView.as_view(), name='aliases'),
url(r'^api/domains/', get_domains, name='email-domains'),
path('api/aliases/', AliasesView.as_view(), name='aliases'),
path('api/aliases/delete/', AliasesDeleteView.as_view(), name='aliases-delete'),
path('api/domains/', get_domains, name='email-domains'),
]
+25 -33
View File
@@ -46,66 +46,58 @@ def logout(request):
return redirect('index')
class AliasesView(views.APIView):
""" List, create and delete aliases from django-postfix-dovecot-api """
class BaseAliasesView(views.APIView):
_api = DjangoPostfixDovecotAPI()
permission_classes = (IsAuthenticated, SourcePrefixOwner,)
def get_groups(self, request):
def _user_groups(self, request):
group_filter = request.query_params.get('groups', None)
if group_filter is None:
my_groups = request.user.groups.all()
user_groups = request.user.groups.all()
else:
# Filter groups
if ',' in group_filter:
group_filter = group_filter.split(',')
else:
group_filter = [group_filter]
my_groups = request.user.groups.filter(pk__in=group_filter)
group_filter = group_filter.split(',') if ',' in group_filter else [group_filter]
user_groups = request.user.groups.filter(pk__in=group_filter)
return my_groups
return user_groups
def get_user_prefixes(self, request):
my_groups = self.get_groups(request)
def user_prefixes(self, request):
"""Look for mailinglist prefixes"""
orgunits = OrgUnit.objects.filter(is_active=True)
if request.user.is_superuser:
return Prefix.objects.filter(orgunit__in=orgunits)
# Look for mailinglist prefixes
orgunits = OrgUnit.objects.exclude(is_active=False).distinct()
if not request.user.is_superuser:
orgunits = orgunits.filter(admin_groups__in=my_groups)
orgunits = orgunits.filter(admin_groups__in=self._user_groups(request)).distinct()
return Prefix.objects.filter(orgunit__in=orgunits)
my_prefixes = Prefix.objects.filter(orgunit__in=orgunits)
return my_prefixes
class AliasesView(BaseAliasesView):
""" List and create aliases from django-postfix-dovecot-api """
def get(self, request):
my_prefixes = self.get_user_prefixes(request)
user_prefixes = self.user_prefixes(request)
# Prepare regular expression
regex = my_prefixes.as_regex()
if len(my_prefixes) == 0:
regex = user_prefixes.as_regex()
if len(user_prefixes) == 0:
return Response({'result': 'No results'}) # Need at least one
res = self._api.list_aliases_regex(regex)
return Response(res)
return Response(self._api.list_aliases_regex(regex))
def post(self, request):
aliases = AliasSerializer(data=request.data, many=True)
if not aliases.is_valid():
return Response(aliases.errors, status=status.HTTP_400_BAD_REQUEST)
res = self._api.create_aliases(aliases.data)
return Response(self._api.create_aliases(aliases.data))
return Response(res)
def delete(self, request):
class AliasesDeleteView(BaseAliasesView):
""" Delete aliases (using POST) from django-postfix-dovecot-api """
def post(self, request):
aliases = AliasSerializer(data=request.data, many=True)
if not aliases.is_valid():
return Response(aliases.errors, status=status.HTTP_400_BAD_REQUEST)
res = self._api.delete_aliases(aliases.data)
return Response(res)
return Response(self._api.delete_aliases(aliases.data))
class MyUserViewSet(viewsets.ReadOnlyModelViewSet):
@@ -123,7 +115,7 @@ class MyOrgUnitViewSet(viewsets.ReadOnlyModelViewSet):
permission_classes = [IsAuthenticated]
def get_queryset(self):
orgunits = OrgUnit.objects.exclude(prefixes__isnull=True).exclude(is_active=False).distinct()
orgunits = OrgUnit.objects.filter(prefixes__isnull=False, is_active=True).distinct()
if self.request.user.is_superuser:
return orgunits