From 865ae17785829f3d35ab281c024fc7b182d56adf Mon Sep 17 00:00:00 2001 From: Nikolai R Kristiansen Date: Sat, 4 Jan 2020 20:30:14 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Working=20create=20and=20delete?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mailadmin/api.py | 10 +- mailadmin/permissions.py | 8 +- mailadmin/static/mailadmin/.babelrc | 3 +- mailadmin/static/mailadmin/.eslintrc | 4 +- mailadmin/static/mailadmin/package-lock.json | 25 ++ mailadmin/static/mailadmin/package.json | 2 + mailadmin/static/mailadmin/src/App.js | 15 +- .../mailadmin/src/components/AliasList.js | 130 ++++++++++ .../src/components/AliasListCreate.js | 82 ++++++ .../src/components/EmailsTextarea.js | 62 +++++ .../mailadmin/src/components/MainArea.js | 241 ++---------------- mailadmin/static/mailadmin/src/mutations.js | 18 ++ mailadmin/static/mailadmin/src/queries.js | 3 +- mailadmin/static/mailadmin/src/utils.js | 27 +- mailadmin/urls.py | 14 +- mailadmin/views.py | 58 ++--- 16 files changed, 421 insertions(+), 281 deletions(-) create mode 100644 mailadmin/static/mailadmin/src/components/AliasList.js create mode 100644 mailadmin/static/mailadmin/src/components/AliasListCreate.js create mode 100644 mailadmin/static/mailadmin/src/components/EmailsTextarea.js create mode 100644 mailadmin/static/mailadmin/src/mutations.js diff --git a/mailadmin/api.py b/mailadmin/api.py index 0c96dbf..a54147a 100644 --- a/mailadmin/api.py +++ b/mailadmin/api.py @@ -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 diff --git a/mailadmin/permissions.py b/mailadmin/permissions.py index 749eb79..f810afd 100644 --- a/mailadmin/permissions.py +++ b/mailadmin/permissions.py @@ -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 diff --git a/mailadmin/static/mailadmin/.babelrc b/mailadmin/static/mailadmin/.babelrc index 2b7bafa..d301495 100644 --- a/mailadmin/static/mailadmin/.babelrc +++ b/mailadmin/static/mailadmin/.babelrc @@ -1,3 +1,4 @@ { - "presets": ["@babel/preset-env", "@babel/preset-react"] + "presets": ["@babel/preset-env", "@babel/preset-react"], + "plugins": ["@babel/plugin-transform-runtime"] } diff --git a/mailadmin/static/mailadmin/.eslintrc b/mailadmin/static/mailadmin/.eslintrc index 5525708..8bfa4d5 100644 --- a/mailadmin/static/mailadmin/.eslintrc +++ b/mailadmin/static/mailadmin/.eslintrc @@ -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": { diff --git a/mailadmin/static/mailadmin/package-lock.json b/mailadmin/static/mailadmin/package-lock.json index 836fd7b..6b0c323 100644 --- a/mailadmin/static/mailadmin/package-lock.json +++ b/mailadmin/static/mailadmin/package-lock.json @@ -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", diff --git a/mailadmin/static/mailadmin/package.json b/mailadmin/static/mailadmin/package.json index dd07b78..9b640b7 100644 --- a/mailadmin/static/mailadmin/package.json +++ b/mailadmin/static/mailadmin/package.json @@ -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", diff --git a/mailadmin/static/mailadmin/src/App.js b/mailadmin/static/mailadmin/src/App.js index 1cb67b9..e7bb507 100644 --- a/mailadmin/static/mailadmin/src/App.js +++ b/mailadmin/static/mailadmin/src/App.js @@ -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 = () => ( -
+
diff --git a/mailadmin/static/mailadmin/src/components/AliasList.js b/mailadmin/static/mailadmin/src/components/AliasList.js new file mode 100644 index 0000000..d8bf92e --- /dev/null +++ b/mailadmin/static/mailadmin/src/components/AliasList.js @@ -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 ( + + {destination} + +
+ { + setChecked(e.target.checked); + updateDeletionList(id, e.target.checked); + }} + /> +
+ + + ); +}; + +const ActionRow = ({ onAddEmailClick, deleteVisible, onDelete }) => { + return ( + + + + + + + + + + + ); +}; + +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 ( +
+
+
+ + + {aliases.map((alias) => ( + { + if (checked) { + const { __typename, ...aliasForDeletion } = alias; + setDeletionList([...deletionList, aliasForDeletion]); // add + } else { + setDeletionList(deletionList.filter(({ id }) => id !== aliasId)); // remove + } + }} + /> + ))} + { + e.preventDefault(); + setTextareaVisible(!textareaVisible); + }} + deleteVisible={Boolean(deletionList.length)} + onDelete={onDelete} + /> + + + + + +
+ +
+
+ ); +}; +export default AliasList; diff --git a/mailadmin/static/mailadmin/src/components/AliasListCreate.js b/mailadmin/static/mailadmin/src/components/AliasListCreate.js new file mode 100644 index 0000000..0d97d9d --- /dev/null +++ b/mailadmin/static/mailadmin/src/components/AliasListCreate.js @@ -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 ( +
+ +
+
+
+
+
+
+ +
+ {orgUnits.map((ou) => + ou.prefixes.map((prefix) => ( + { + e.preventDefault(); + setSelectedPrefix(prefix); + }} + > + {prefix}- + + )) + )} +
+
+ setName(e.target.value)} + /> +
+ {`@${domain.name}`} +
+
+ +
+
+ {/*
*/} +
+ ); +}; + +export default AliasListCreate; diff --git a/mailadmin/static/mailadmin/src/components/EmailsTextarea.js b/mailadmin/static/mailadmin/src/components/EmailsTextarea.js new file mode 100644 index 0000000..1784c2d --- /dev/null +++ b/mailadmin/static/mailadmin/src/components/EmailsTextarea.js @@ -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 ( + <> +
+ Eposter {!aliases.length ? '' : aliases.length} +
+