From cff6443eca770c1127f0357f69ac1177ab6ce71b Mon Sep 17 00:00:00 2001 From: Sokurov Idar Date: Tue, 20 Apr 2021 22:55:22 +0300 Subject: [PATCH 01/10] Add notifications on work page, rebase already exists with notifications.js --- main/extra_func.py | 13 +- main/templates/base/success_messages.html | 14 -- main/templates/pages/adm_ruleset.html | 17 +- main/templates/pages/work.html | 11 +- main/views.py | 48 +++--- static/main/js/notifications.js | 14 ++ static/modules/notifications/.babelrc | 3 + static/modules/notifications/.eslintrc.js | 31 ++++ static/modules/notifications/.gitignore | 30 ++++ static/modules/notifications/.travis.yml | 3 + static/modules/notifications/LICENSE.md | 7 + .../notifications/__tests__/helpers.test.js | 104 ++++++++++++ .../notifications/__tests__/index.tests.js | 144 +++++++++++++++++ static/modules/notifications/demo/demo.js | 34 ++++ static/modules/notifications/demo/index.html | 101 ++++++++++++ static/modules/notifications/package.json | 58 +++++++ static/modules/notifications/readme.md | 82 ++++++++++ static/modules/notifications/src/helpers.js | 24 +++ static/modules/notifications/src/index.js | 148 ++++++++++++++++++ .../notifications/src/polyfills/classList.js | 68 ++++++++ static/modules/notifications/src/style.scss | 134 ++++++++++++++++ .../modules/notifications/webpack.config.js | 41 +++++ 22 files changed, 1083 insertions(+), 46 deletions(-) delete mode 100644 main/templates/base/success_messages.html create mode 100644 static/main/js/notifications.js create mode 100644 static/modules/notifications/.babelrc create mode 100644 static/modules/notifications/.eslintrc.js create mode 100644 static/modules/notifications/.gitignore create mode 100644 static/modules/notifications/.travis.yml create mode 100644 static/modules/notifications/LICENSE.md create mode 100644 static/modules/notifications/__tests__/helpers.test.js create mode 100644 static/modules/notifications/__tests__/index.tests.js create mode 100644 static/modules/notifications/demo/demo.js create mode 100644 static/modules/notifications/demo/index.html create mode 100644 static/modules/notifications/package.json create mode 100644 static/modules/notifications/readme.md create mode 100644 static/modules/notifications/src/helpers.js create mode 100644 static/modules/notifications/src/index.js create mode 100644 static/modules/notifications/src/polyfills/classList.js create mode 100644 static/modules/notifications/src/style.scss create mode 100644 static/modules/notifications/webpack.config.js diff --git a/main/extra_func.py b/main/extra_func.py index 3e54ae4..109f9c6 100644 --- a/main/extra_func.py +++ b/main/extra_func.py @@ -1,14 +1,13 @@ import logging -import os from datetime import timedelta, datetime, date from django.contrib.auth.models import User from django.core.exceptions import ObjectDoesNotExist +from django.shortcuts import redirect from django.utils import timezone from zenpy import Zenpy from zenpy.lib.exception import APIException - from access_controller.settings import ZENDESK_ROLES as ROLES, ONE_DAY, ZENDESK_GROUPS, SOLVED_TICKETS_EMAIL, \ ACTRL_API_EMAIL, ACTRL_API_TOKEN, ACTRL_API_PASSWORD, ACTRL_ZENDESK_SUBDOMAIN from main.models import UserProfile, RoleChangeLogs, UnassignedTicket, UnassignedTicketStatus @@ -666,3 +665,13 @@ def log(user, admin=0): logger.addHandler(csvhandler) logger.setLevel('INFO') logger.info(users) + + +def set_session_params_for_work_page(request, count=None, is_confirm=True): + """ + Функция для страницы получения прав + Устанавливает данные сессии о успешности запроса и количестве назначенных тикетов + """ + request.session['is_confirm'] = is_confirm + request.session['count_tickets'] = count + return redirect('work', request.user.id) diff --git a/main/templates/base/success_messages.html b/main/templates/base/success_messages.html deleted file mode 100644 index fdef313..0000000 --- a/main/templates/base/success_messages.html +++ /dev/null @@ -1,14 +0,0 @@ -
- {% for message in messages %} - - {% endfor %} -
diff --git a/main/templates/pages/adm_ruleset.html b/main/templates/pages/adm_ruleset.html index a951f80..2ee80f5 100644 --- a/main/templates/pages/adm_ruleset.html +++ b/main/templates/pages/adm_ruleset.html @@ -6,18 +6,16 @@ {% block heading %}Управление{% endblock %} -{% block extra_css %} - - -{% endblock %} - {% block extra_scripts %} -{% endblock%} + + + +{% endblock%} {% block content %}
@@ -25,6 +23,10 @@

Свободных Мест:

+ {% for message in messages %} + + {% endfor %} + {% block form %}
{% csrf_token %} @@ -100,9 +102,6 @@
{% endblock %} - {% include 'base/success_messages.html' %} - - {% endblock %} diff --git a/main/templates/pages/work.html b/main/templates/pages/work.html index d9043b4..57c6cab 100644 --- a/main/templates/pages/work.html +++ b/main/templates/pages/work.html @@ -7,7 +7,12 @@ {% block heading %}Управление правами{% endblock %} {% block extra_css %} - + + +{% endblock %} +{% block extra_scripts %} + + {% endblock %} {% block content %} @@ -66,8 +71,10 @@ + {% for message in messages %} + + {% endfor %} - {% include 'base/success_messages.html' %} {% endblock %} diff --git a/main/views.py b/main/views.py index 0c89f11..6d6ce05 100644 --- a/main/views.py +++ b/main/views.py @@ -23,7 +23,7 @@ from zenpy.lib.api_objects import User as ZenpyUser from access_controller.settings import DEFAULT_FROM_EMAIL, ZENDESK_ROLES, ZENDESK_MAX_AGENTS from main.extra_func import check_user_exist, update_profile, get_user_organization, \ make_engineer, make_light_agent, get_users_list, update_users_in_model, count_users, \ - StatisticData, log, ZendeskAdmin + StatisticData, log, ZendeskAdmin, set_session_params_for_work_page from main.forms import AdminPageUsers, CustomRegistrationForm, CustomAuthenticationForm, StatisticForm from main.serializers import ProfileSerializer, ZendeskUserSerializer from .models import UserProfile @@ -31,7 +31,6 @@ from .models import UserProfile def setup_context(profile_lit: bool = False, control_lit: bool = False, work_lit: bool = False, registration_lit: bool = False, login_lit: bool = False, stats_lit: bool = False): - print(profile_lit, control_lit, work_lit, registration_lit, login_lit) context = { @@ -182,29 +181,37 @@ def work_page(request: WSGIRequest, id: int) -> HttpResponse: """ users = get_users_list() if request.user.id == id: + if request.session.get('is_confirm', None): + messages.success(request, 'Изменения были применены') + elif request.session.get('is_confirm', None) is not None: + messages.error(request, 'Изменения не были применены') + count = request.session.get('count_tickets', None) + if count is not None: + messages.success(request, f'{count} тикетов назначено') + request.session['is_confirm'] = None + request.session['count_tickets'] = None + engineers = [] light_agents = [] for user in users: - if user.custom_role_id == ZENDESK_ROLES['engineer']: engineers.append(user) elif user.custom_role_id == ZENDESK_ROLES['light_agent']: light_agents.append(user) - context = setup_context(work_lit=True) context.update({ 'engineers': engineers, 'agents': light_agents, 'messages': messages.get_messages(request), 'licences_remaining': max(0, ZENDESK_MAX_AGENTS - len(engineers)), - 'pagename': 'Управление правами' + 'pagename': 'Управление правами', }) return render(request, 'pages/work.html', context) return redirect("login") @login_required() -def work_hand_over(request: WSGIRequest) -> HttpResponseRedirect: +def work_hand_over(request: WSGIRequest): """ Функция позволяет текущему пользователю (login_required) сдать права, а именно сменить в Zendesk роль с "engineer" на "light agent" и установить роль "agent" в БД. Действия выполняются, если исходная роль пользователя "engineer". @@ -212,8 +219,8 @@ def work_hand_over(request: WSGIRequest) -> HttpResponseRedirect: :param request: данные текущего пользователя (login_required) :return: перезагрузка текущей страницы после выполнения смены роли """ - make_light_agent(request.user.userprofile,request.user) - return HttpResponseRedirect(reverse('work', args=(request.user.id,))) + make_light_agent(request.user.userprofile, request.user) + return set_session_params_for_work_page(request) @login_required() @@ -224,22 +231,25 @@ def work_become_engineer(request: WSGIRequest) -> HttpResponseRedirect: :param request: данные текущего пользователя (login_required) :return: перезагрузка текущей страницы после выполнения смены роли """ - zenpy_user, admin = auth_user(request) + make_engineer(request.user.userprofile, request.user) + return set_session_params_for_work_page(request) - make_engineer(request.user.userprofile,request.user) - return HttpResponseRedirect(reverse('work', args=(request.user.id,))) @login_required() def work_get_tickets(request): zenpy_user, admin = auth_user(request) - count_tickets = int(request.GET["count_tickets"]) - tickets = [ticket for ticket in admin.search(type="ticket") if ticket.group.name == 'Сменная группа' and ticket.assignee is None] - for i in range(len(tickets)): - if i == count_tickets: - return HttpResponseRedirect(reverse('work', args=(request.user.id,))) - tickets[i].assignee = zenpy_user - admin.tickets.update(tickets[i]) - return HttpResponseRedirect(reverse('work', args=(request.user.id,))) + if zenpy_user.role == 'admin' or zenpy_user.custom_role_id == ZENDESK_ROLES['engineer']: + tickets = [ticket for ticket in admin.search(type="ticket") if + ticket.group.name == 'Сменная группа' and ticket.assignee is None] + count = 0 + for i in range(len(tickets)): + if i == request.GET.get('count_tickets'): + return set_session_params_for_work_page(request, count) + tickets[i].assignee = zenpy_user + admin.tickets.update(tickets[i]) + count += 1 + return set_session_params_for_work_page(request, count) + return set_session_params_for_work_page(request, is_confirm=False) def main_page(request: WSGIRequest) -> HttpResponse: diff --git a/static/main/js/notifications.js b/static/main/js/notifications.js new file mode 100644 index 0000000..cef9887 --- /dev/null +++ b/static/main/js/notifications.js @@ -0,0 +1,14 @@ +"use strict"; +function create_notification(title,description,theme,time){ + const myNotification = window.createNotification({ + closeOnClick: true, + displayCloseButton: true, + positionClass: 'nfc-top-right', + theme: theme, + showDuration: Number(time), + }); + myNotification({ + title: title, + message: description + }); +}; diff --git a/static/modules/notifications/.babelrc b/static/modules/notifications/.babelrc new file mode 100644 index 0000000..af0f0c3 --- /dev/null +++ b/static/modules/notifications/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["es2015"] +} \ No newline at end of file diff --git a/static/modules/notifications/.eslintrc.js b/static/modules/notifications/.eslintrc.js new file mode 100644 index 0000000..5bebe58 --- /dev/null +++ b/static/modules/notifications/.eslintrc.js @@ -0,0 +1,31 @@ +module.exports = { + "env": { + "browser": true, + "commonjs": true, + "es6": true + }, + "extends": "eslint:recommended", + "parserOptions": { + "sourceType": "module" + }, + "rules": { + "indent": [ + "error", + "tab" + ], + "linebreak-style": [ + "error", + "windows" + ], + "quotes": [ + "error", + "single" + ], + "semi": [ + "error", + "always" + ], + "no-console": 0, + "no-undef": 0 + } +}; \ No newline at end of file diff --git a/static/modules/notifications/.gitignore b/static/modules/notifications/.gitignore new file mode 100644 index 0000000..6909f31 --- /dev/null +++ b/static/modules/notifications/.gitignore @@ -0,0 +1,30 @@ +# IDE files +.idea/ +.DS_Store + +# Build directories +build/ + +# Dependency directories +node_modules/ +jspm_packages/ + +# Lock files +yarn.lock +package-lock.json + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Yarn Integrity file +.yarn-integrity diff --git a/static/modules/notifications/.travis.yml b/static/modules/notifications/.travis.yml new file mode 100644 index 0000000..0fe294a --- /dev/null +++ b/static/modules/notifications/.travis.yml @@ -0,0 +1,3 @@ +language: node_js +node_js: + - "7" diff --git a/static/modules/notifications/LICENSE.md b/static/modules/notifications/LICENSE.md new file mode 100644 index 0000000..50ec29c --- /dev/null +++ b/static/modules/notifications/LICENSE.md @@ -0,0 +1,7 @@ +# Notifications license + +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. \ No newline at end of file diff --git a/static/modules/notifications/__tests__/helpers.test.js b/static/modules/notifications/__tests__/helpers.test.js new file mode 100644 index 0000000..1ac9938 --- /dev/null +++ b/static/modules/notifications/__tests__/helpers.test.js @@ -0,0 +1,104 @@ +const { partial, append, isString, createElement, createParagraph } = require('../src/helpers'); + +const addNumbers = (x, y) => x + y; + +const sum = (...numbers) => numbers.reduce((total, current) => total + current, 0); + +describe('Helpers', () => { + beforeEach(() => { + document.body.innerHTML = ''; + }); + + describe('Partial', () => { + it('should return a partially applied function', () => { + expect(typeof partial(addNumbers, 10)).toEqual('function'); + }); + + it('should execute function when partially applied function is called', () => { + expect(partial(addNumbers, 20)(10)).toEqual(30); + }); + + it('should gather argument', () => { + expect(partial(sum, 5, 10)(15, 20, 25)).toEqual(75); + }); + }); + + describe('Append', () => { + const container = document.createElement('div'); + document.body.appendChild(container); + + const elementToAppend = document.createElement('h1'); + elementToAppend.classList.add('heading'); + elementToAppend.innerText = 'working'; + + append(container, elementToAppend); + + const element = document.querySelector('.heading'); + expect(element); + + expect(element.innerText).toEqual('working'); + }); + + describe('Is string', () => { + expect(isString(1)).toEqual(false); + expect(isString(null)).toEqual(false); + expect(isString(undefined)).toEqual(false); + expect(isString({})).toEqual(false); + + expect(isString('')).toEqual(true); + expect(isString('a')).toEqual(true); + expect(isString('1')).toEqual(true); + expect(isString('some string')).toEqual(true); + }); + + describe('Create element', () => { + it('should create an element', () => { + expect(createElement('p')).toEqual(document.createElement('p')); + expect(createElement('h1')).toEqual(document.createElement('h1')); + expect(createElement('ul')).toEqual(document.createElement('ul')); + expect(createElement('li')).toEqual(document.createElement('li')); + expect(createElement('div')).toEqual(document.createElement('div')); + expect(createElement('span')).toEqual(document.createElement('span')); + }); + + it('should add class names', () => { + expect(createElement('div', 'someclass1', 'someclass2').classList.contains('someclass2')); + expect(createElement('p', 'para', 'test').classList.contains('para')); + + const mockUl = document.createElement('ul'); + mockUl.classList.add('nav'); + mockUl.classList.add('foo'); + + expect(createElement('ul', 'nav', 'foo').classList).toEqual(mockUl.classList); + }); + }); + + describe('Create paragraph', () => { + it('should create a paragraph', () => { + const p = document.createElement('p'); + p.innerText = 'Some text'; + expect(createParagraph()('Some text')).toEqual(p); + }); + + it('should add class names', () => { + const p = document.createElement('p'); + p.classList.add('body-text'); + p.classList.add('para'); + + expect(createParagraph('body-text', 'para')('')).toEqual(p); + }); + + it('should set inner text', () => { + const p = document.createElement('p'); + p.innerText = 'Hello world!'; + p.classList.add('text'); + + expect(createParagraph('text')('Hello world!')).toEqual(p); + }); + + it('should append to DOM', () => { + append(document.body, createParagraph('text')('hello')); + expect(document.querySelector('.text').innerText).toEqual('hello'); + }); + }); +}); diff --git a/static/modules/notifications/__tests__/index.tests.js b/static/modules/notifications/__tests__/index.tests.js new file mode 100644 index 0000000..071597c --- /dev/null +++ b/static/modules/notifications/__tests__/index.tests.js @@ -0,0 +1,144 @@ +require('../src/index'); + +describe('Notifications', () => { + beforeEach(() => { + document.body.innerHTML = ''; + }); + + it('should display a console warning if no title or message is passed', () => { + jest.spyOn(global.console, 'warn'); + window.createNotification()(); + expect(console.warn).toBeCalled(); + }); + + it('should render a default notification', () => { + const notification = window.createNotification(); + + const title = 'I am a title'; + + // Should initially not contain any notifications + expect(document.querySelectorAll('.ncf').length).toEqual(0); + + // Create a notification instance with a title + notification({ title }); + + // Should be one notification with the title passed in + expect(document.querySelectorAll('.ncf').length).toEqual(1); + expect(document.querySelector('.ncf-title').innerText).toEqual(title); + + // Create a second instance so there should now be two instances + notification({ title }); + expect(document.querySelectorAll('.ncf').length).toEqual(2); + }); + + it('should close on click if the option is enabled', () => { + const notification = window.createNotification({ + closeOnClick: true + }); + + // Create a notification with a generic body + notification({ message: 'some text' }); + + // Should be one notification instance + expect(document.querySelectorAll('.ncf').length).toEqual(1); + + // Click the notification + document.querySelector('.ncf').click(); + + expect(document.querySelectorAll('.ncf').length).toEqual(0); + }); + + it('should not close on click if the option is disabled', () => { + const notification = window.createNotification({ + closeOnClick: false + }); + + // Create a notification with a generic body + notification({ message: 'some text' }); + + // Should be one notification instance + expect(document.querySelectorAll('.ncf').length).toEqual(1); + + // Click the notification + document.querySelector('.ncf').click(); + + expect(document.querySelectorAll('.ncf').length).toEqual(1); + }); + + it('should set position class if valid', () => { + const validPositions = [ + 'nfc-top-left', + 'nfc-top-right', + 'nfc-bottom-left', + 'nfc-bottom-right' + ]; + + validPositions.forEach(position => { + const notification = window.createNotification({ + positionClass: position + }); + + notification({ title: 'title here' }); + + const className = `.${position}`; + + expect(document.querySelectorAll(className).length).toEqual(1); + + const container = document.querySelector(className); + expect(container.querySelectorAll('.ncf').length).toEqual(1); + }); + }); + + it('should revert to default to default position and warn if class is invalid', () => { + const notification = window.createNotification({ + positionClass: 'invalid-name' + }); + + jest.spyOn(global.console, 'warn'); + + notification({ message: 'test' }); + + expect(console.warn).toBeCalled(); + + expect(document.querySelectorAll('.nfc-top-right').length).toEqual(1); + }); + + it('should allow a custom onclick callback', () => { + let a = 'not clicked'; + + const notification = window.createNotification({ + onclick: () => { + a = 'clicked'; + } + }); + + notification({ message: 'click test' }); + + expect(a).toEqual('not clicked'); + + // Click the notification + document.querySelector('.ncf').click(); + + expect(a).toEqual('clicked'); + }); + + it('should show for correct duration', () => { + const notification = window.createNotification({ + showDuration: 500 + }); + + notification({ message: 'test' }); + + expect(document.querySelectorAll('.ncf').length).toEqual(1); + + // Should exist after 400ms + setTimeout(() => { + expect(document.querySelectorAll('.ncf').length).toEqual(1); + }, 400); + + // Should delete after 500ms + setTimeout(() => { + expect(document.querySelectorAll('.ncf').length).toEqual(0); + }); + }, 501); +}); diff --git a/static/modules/notifications/demo/demo.js b/static/modules/notifications/demo/demo.js new file mode 100644 index 0000000..d809fe2 --- /dev/null +++ b/static/modules/notifications/demo/demo.js @@ -0,0 +1,34 @@ +'use strict'; + +// Written using ES5 JS for browser support +window.addEventListener('DOMContentLoaded', function () { + var form = document.querySelector('form'); + + form.addEventListener('submit', function (e) { + e.preventDefault(); + + // Form elements + var title = form.querySelector('#title').value; + var message = form.querySelector('#message').value; + var position = form.querySelector('#position').value; + var duration = form.querySelector('#duration').value; + var theme = form.querySelector('#theme').value; + var closeOnClick = form.querySelector('#close').checked; + var displayClose = form.querySelector('#closeButton').checked; + + if(!message) { + message = 'You did not enter a message...'; + } + + window.createNotification({ + closeOnClick: closeOnClick, + displayCloseButton: displayClose, + positionClass: position, + showDuration: duration, + theme: theme + })({ + title: title, + message: message + }); + }); +}); \ No newline at end of file diff --git a/static/modules/notifications/demo/index.html b/static/modules/notifications/demo/index.html new file mode 100644 index 0000000..d5dd6a6 --- /dev/null +++ b/static/modules/notifications/demo/index.html @@ -0,0 +1,101 @@ + + + + +Notifications + + + + + + + + + +

Notifications

+
+ +
+ + + +
+ + + +
+ + + +
+ + + +
+ + + + + +
+ + + + + +
+ + + + + \ No newline at end of file diff --git a/static/modules/notifications/package.json b/static/modules/notifications/package.json new file mode 100644 index 0000000..c9de18d --- /dev/null +++ b/static/modules/notifications/package.json @@ -0,0 +1,58 @@ +{ + "name": "styled-notifications", + "version": "1.0.1", + "description": "A simple JavaScript notifications library", + "main": "dist/notifications.js", + "scripts": { + "start": "webpack --watch", + "build": "webpack -p", + "test": "jest", + "prepare": "yarn run test && yarn run build" + }, + "pre-commit": [ + "prepare" + ], + "files": [ + "dist" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/JamieLivingstone/Notifications.git" + }, + "keywords": [ + "notification", + "popup", + "alert", + "toast" + ], + "author": "Jamie Livingstone", + "contributors": [ + { + "name": "Jamie Livingstone (https://github.com/JamieLivingstone)" + }, + { + "name": "cavebeavis (https://github.com/cavebeavis)" + } + ], + "license": "ISC", + "bugs": { + "url": "https://github.com/JamieLivingstone/Notifications/issues" + }, + "homepage": "https://github.com/JamieLivingstone/Notifications#readme", + "devDependencies": { + "babel-core": "^6.26.0", + "babel-jest": "^21.0.2", + "babel-loader": "^7.1.2", + "babel-preset-es2015": "^6.24.1", + "babel-preset-es2015-ie": "^6.7.0", + "css-loader": "^0.28.7", + "eslint": "^4.6.1", + "extract-text-webpack-plugin": "^3.0.0", + "jest": "^21.0.2", + "node-sass": "^4.5.3", + "pre-commit": "^1.2.2", + "sass-loader": "^6.0.6", + "style-loader": "^0.18.2", + "webpack": "^3.5.6" + } +} diff --git a/static/modules/notifications/readme.md b/static/modules/notifications/readme.md new file mode 100644 index 0000000..2bb4235 --- /dev/null +++ b/static/modules/notifications/readme.md @@ -0,0 +1,82 @@ +[![Build Status](https://travis-ci.org/JamieLivingstone/Notifications.svg?branch=master)](https://travis-ci.org/JamieLivingstone/Notifications) + +# Notifications +**Notifications** is a Javascript library for notifications heavily inspired by toastr but does not require any dependencies such as jQuery. + +Works on browsers: IE9+, Safari, Chrome, FireFox, opera, edge + +## npm Installation +Do either +``` +npm i styled-notifications +``` +or add the following to your `package.json`: +``` +"dependencies": { + "styled-notifications": "^1.0.1" +}, +``` + +## Installation +Download files from the dist folder and then: +1. Link to notifications.css `` + +2. Link to notifications.js `` + +## Usage +### Custom options +- closeOnClick - Close the notification dialog when a click is invoked. +- displayCloseButton - Display a close button in the top right hand corner of the notification. +- positionClass - Set the position of the notification dialog. Accepted positions: ('nfc-top-right', 'nfc-bottom-right', 'nfc-bottom-left', 'nfc-top-left'). +- onClick - Call a callback function when a click is invoked on a notification. +- showDuration - Milliseconds the notification should be visible (0 for a notification that will remain open until clicked) +- theme - Set the position of the notification dialog. Accepted positions: ('success', 'info', 'warning', 'error', 'A custom clasName'). +``` +const defaultOptions = { + closeOnClick: true, + displayCloseButton: false, + positionClass: 'nfc-top-right', + onclick: false, + showDuration: 3500, + theme: 'success' +}; +``` + +## Example + +### Success notification +``` +// Create a success notification instance +const successNotification = window.createNotification({ + theme: 'success', + showDuration: 5000 +}); + +// Invoke success notification +successNotification({ + message: 'Simple success notification' +}); + +// Use the same instance but pass a title +successNotification({ + title: 'Working', + message: 'Simple success notification' +}); +``` + +### Information notification +``` +// Only running it once? Invoke immediately like this +window.createNotification({ + theme: 'success', + showDuration: 5000 +})({ + message: 'I have some information for you...' +}); +``` + +### Todo +~~1. Add to NPM~~ +2. Improve documentation +3. Further device testing +4. Add contributor instructions \ No newline at end of file diff --git a/static/modules/notifications/src/helpers.js b/static/modules/notifications/src/helpers.js new file mode 100644 index 0000000..7a9f0dc --- /dev/null +++ b/static/modules/notifications/src/helpers.js @@ -0,0 +1,24 @@ +export const partial = (fn, ...presetArgs) => (...laterArgs) => fn(...presetArgs, ...laterArgs); + +export const append = (el, ...children) => children.forEach(child => el.appendChild(child)); + +export const isString = input => typeof input === 'string'; + +export const createElement = (elementType, ...classNames) => { + const element = document.createElement(elementType); + + if(classNames.length) { + classNames.forEach(currentClass => element.classList.add(currentClass)); + } + + return element; +}; + +const setInnerText = (element, text) => { + element.innerText = text; + return element; +}; + +const createTextElement = (elementType, ...classNames) => partial(setInnerText, createElement(elementType, ...classNames)); + +export const createParagraph = (...classNames) => createTextElement('p', ...classNames); \ No newline at end of file diff --git a/static/modules/notifications/src/index.js b/static/modules/notifications/src/index.js new file mode 100644 index 0000000..e3375cd --- /dev/null +++ b/static/modules/notifications/src/index.js @@ -0,0 +1,148 @@ +'use strict'; + +// Polyfills +import './polyfills/classList'; + +import { + append, + createElement, + createParagraph, + isString +} from './helpers'; + +(function Notifications(window) { + // Default notification options + const defaultOptions = { + closeOnClick: true, + displayCloseButton: false, + positionClass: 'nfc-top-right', + onclick: false, + showDuration: 3500, + theme: 'success' + }; + + function configureOptions(options) { + // Create a copy of options and merge with defaults + options = Object.assign({}, defaultOptions, options); + + // Validate position class + function validatePositionClass(className) { + const validPositions = [ + 'nfc-top-left', + 'nfc-top-right', + 'nfc-bottom-left', + 'nfc-bottom-right' + ]; + + return validPositions.indexOf(className) > -1; + } + + // Verify position, if invalid reset to default + if (!validatePositionClass(options.positionClass)) { + console.warn('An invalid notification position class has been specified.'); + options.positionClass = defaultOptions.positionClass; + } + + // Verify onClick is a function + if (options.onclick && typeof options.onclick !== 'function') { + console.warn('Notification on click must be a function.'); + options.onclick = defaultOptions.onclick; + } + + // Verify show duration + if(typeof options.showDuration !== 'number') { + options.showDuration = defaultOptions.showDuration; + } + + // Verify theme + if(!isString(options.theme) || options.theme.length === 0) { + console.warn('Notification theme must be a string with length'); + options.theme = defaultOptions.theme; + } + + return options; + } + + // Create a new notification instance + function createNotification(options) { + // Validate options and set defaults + options = configureOptions(options); + + // Return a notification function + return function notification({ title, message } = {}) { + const container = createNotificationContainer(options.positionClass); + + if(!title && !message) { + return console.warn('Notification must contain a title or a message!'); + } + + // Create the notification wrapper + const notificationEl = createElement('div', 'ncf', options.theme); + + // Close on click + if(options.closeOnClick === true) { + notificationEl.addEventListener('click', () => container.removeChild(notificationEl)); + } + + // Custom click callback + if(options.onclick) { + notificationEl.addEventListener('click', (e) => options.onclick(e)); + } + + // Display close button + if(options.displayCloseButton) { + const closeButton = createElement('button'); + closeButton.innerText = 'X'; + + // Use the wrappers close on click to avoid useless event listeners + if(options.closeOnClick === false){ + closeButton.addEventListener('click', () =>container.removeChild(notificationEl)); + } + + append(notificationEl, closeButton); + } + + // Append title and message + isString(title) && title.length && append(notificationEl, createParagraph('ncf-title')(title)); + isString(message) && message.length && append(notificationEl, createParagraph('nfc-message')(message)); + + // Append to container + append(container, notificationEl); + + // Remove element after duration + if(options.showDuration && options.showDuration > 0) { + const timeout = setTimeout(() => { + container.removeChild(notificationEl); + + // Remove container if empty + if(container.querySelectorAll('.ncf').length === 0) { + document.body.removeChild(container); + } + }, options.showDuration); + + // If close on click is enabled and the user clicks, cancel timeout + if(options.closeOnClick || options.displayCloseButton) { + notificationEl.addEventListener('click', () => clearTimeout(timeout)); + } + } + }; + } + + function createNotificationContainer(position) { + let container = document.querySelector(`.${position}`); + + if(!container) { + container = createElement('div', 'ncf-container', position); + append(document.body, container); + } + + return container; + } + + // Add Notifications to window to make globally accessible + if (window.createNotification) { + console.warn('Window already contains a create notification function. Have you included the script twice?'); + } else { + window.createNotification = createNotification; + } +})(window); diff --git a/static/modules/notifications/src/polyfills/classList.js b/static/modules/notifications/src/polyfills/classList.js new file mode 100644 index 0000000..cd8a786 --- /dev/null +++ b/static/modules/notifications/src/polyfills/classList.js @@ -0,0 +1,68 @@ +(function () { + if (typeof window.Element === 'undefined' || 'classList' in document.documentElement) return; + + var prototype = Array.prototype, + push = prototype.push, + splice = prototype.splice, + join = prototype.join; + + function DOMTokenList(el) { + this.el = el; + // The className needs to be trimmed and split on whitespace + // to retrieve a list of classes. + var classes = el.className.replace(/^\s+|\s+$/g,'').split(/\s+/); + for (var i = 0; i < classes.length; i++) { + push.call(this, classes[i]); + } + } + + DOMTokenList.prototype = { + add: function(token) { + if(this.contains(token)) return; + push.call(this, token); + this.el.className = this.toString(); + }, + contains: function(token) { + return this.el.className.indexOf(token) != -1; + }, + item: function(index) { + return this[index] || null; + }, + remove: function(token) { + if (!this.contains(token)) return; + for (var i = 0; i < this.length; i++) { + if (this[i] == token) break; + } + splice.call(this, i, 1); + this.el.className = this.toString(); + }, + toString: function() { + return join.call(this, ' '); + }, + toggle: function(token) { + if (!this.contains(token)) { + this.add(token); + } else { + this.remove(token); + } + + return this.contains(token); + } + }; + + window.DOMTokenList = DOMTokenList; + + function defineElementGetter (obj, prop, getter) { + if (Object.defineProperty) { + Object.defineProperty(obj, prop,{ + get : getter + }); + } else { + obj.__defineGetter__(prop, getter); + } + } + + defineElementGetter(Element.prototype, 'classList', function () { + return new DOMTokenList(this); + }); +})(); \ No newline at end of file diff --git a/static/modules/notifications/src/style.scss b/static/modules/notifications/src/style.scss new file mode 100644 index 0000000..13c37b9 --- /dev/null +++ b/static/modules/notifications/src/style.scss @@ -0,0 +1,134 @@ +// Base colors +$success: #51A351; +$info: #2F96B4; +$warning: #f87400; +$error: #BD362F; +$grey: #999999; + +.ncf-container { + font-size: 14px; + box-sizing: border-box; + position: fixed; + z-index: 999999; + + &.nfc-top-left { + top: 12px; + left: 12px; + } + + &.nfc-top-right { + top: 12px; + right: 12px; + } + + &.nfc-bottom-right { + bottom: 12px; + right: 12px; + } + + &.nfc-bottom-left { + bottom: 12px; + left: 12px; + } + + @media (max-width: 767px) { + left: 0; + right: 0; + } + + .ncf { + background: #ffffff; + transition: .3s ease; + position: relative; + pointer-events: auto; + overflow: hidden; + margin: 0 0 6px; + padding: 30px; + width: 300px; + border-radius: 3px 3px 3px 3px; + box-shadow: 0 0 12px $grey; + color: #000000; + opacity: 0.9; + -ms-filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=90); + filter: alpha(opacity=90); + background-position: 15px center !important; + background-repeat: no-repeat !important; + + // Prevent annoying text selection + -webkit-user-select: none; /* Chrome all / Safari all */ + -moz-user-select: none; /* Firefox all */ + -ms-user-select: none; /* IE 10+ */ + user-select: none; /* Likely future */ + + &:hover { + box-shadow: 0 0 12px #000000; + opacity: 1; + cursor: pointer; + } + + .ncf-title { + font-weight: bold; + font-size: 16px; + text-align: left; + margin-top: 0; + margin-bottom: 6px; + word-wrap: break-word; + } + + .nfc-message { + margin: 0; + text-align: left; + word-wrap: break-word; + } + } + + // Themes + .success { + background: $success; + color: #ffffff; + padding: 15px 15px 15px 50px; + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAADsSURBVEhLY2AYBfQMgf///3P8+/evAIgvA/FsIF+BavYDDWMBGroaSMMBiE8VC7AZDrIFaMFnii3AZTjUgsUUWUDA8OdAH6iQbQEhw4HyGsPEcKBXBIC4ARhex4G4BsjmweU1soIFaGg/WtoFZRIZdEvIMhxkCCjXIVsATV6gFGACs4Rsw0EGgIIH3QJYJgHSARQZDrWAB+jawzgs+Q2UO49D7jnRSRGoEFRILcdmEMWGI0cm0JJ2QpYA1RDvcmzJEWhABhD/pqrL0S0CWuABKgnRki9lLseS7g2AlqwHWQSKH4oKLrILpRGhEQCw2LiRUIa4lwAAAABJRU5ErkJggg=="); + } + + .info { + background: $info; + color: #ffffff; + padding: 15px 15px 15px 50px; + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAGwSURBVEhLtZa9SgNBEMc9sUxxRcoUKSzSWIhXpFMhhYWFhaBg4yPYiWCXZxBLERsLRS3EQkEfwCKdjWJAwSKCgoKCcudv4O5YLrt7EzgXhiU3/4+b2ckmwVjJSpKkQ6wAi4gwhT+z3wRBcEz0yjSseUTrcRyfsHsXmD0AmbHOC9Ii8VImnuXBPglHpQ5wwSVM7sNnTG7Za4JwDdCjxyAiH3nyA2mtaTJufiDZ5dCaqlItILh1NHatfN5skvjx9Z38m69CgzuXmZgVrPIGE763Jx9qKsRozWYw6xOHdER+nn2KkO+Bb+UV5CBN6WC6QtBgbRVozrahAbmm6HtUsgtPC19tFdxXZYBOfkbmFJ1VaHA1VAHjd0pp70oTZzvR+EVrx2Ygfdsq6eu55BHYR8hlcki+n+kERUFG8BrA0BwjeAv2M8WLQBtcy+SD6fNsmnB3AlBLrgTtVW1c2QN4bVWLATaIS60J2Du5y1TiJgjSBvFVZgTmwCU+dAZFoPxGEEs8nyHC9Bwe2GvEJv2WXZb0vjdyFT4Cxk3e/kIqlOGoVLwwPevpYHT+00T+hWwXDf4AJAOUqWcDhbwAAAAASUVORK5CYII="); + } + + .warning { + background: $warning; + color: #ffffff; + padding: 15px 15px 15px 50px; + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAGYSURBVEhL5ZSvTsNQFMbXZGICMYGYmJhAQIJAICYQPAACiSDB8AiICQQJT4CqQEwgJvYASAQCiZiYmJhAIBATCARJy+9rTsldd8sKu1M0+dLb057v6/lbq/2rK0mS/TRNj9cWNAKPYIJII7gIxCcQ51cvqID+GIEX8ASG4B1bK5gIZFeQfoJdEXOfgX4QAQg7kH2A65yQ87lyxb27sggkAzAuFhbbg1K2kgCkB1bVwyIR9m2L7PRPIhDUIXgGtyKw575yz3lTNs6X4JXnjV+LKM/m3MydnTbtOKIjtz6VhCBq4vSm3ncdrD2lk0VgUXSVKjVDJXJzijW1RQdsU7F77He8u68koNZTz8Oz5yGa6J3H3lZ0xYgXBK2QymlWWA+RWnYhskLBv2vmE+hBMCtbA7KX5drWyRT/2JsqZ2IvfB9Y4bWDNMFbJRFmC9E74SoS0CqulwjkC0+5bpcV1CZ8NMej4pjy0U+doDQsGyo1hzVJttIjhQ7GnBtRFN1UarUlH8F3xict+HY07rEzoUGPlWcjRFRr4/gChZgc3ZL2d8oAAAAASUVORK5CYII="); + } + + .error { + background: $error; + color: #ffffff; + padding: 15px 15px 15px 50px; + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAHOSURBVEhLrZa/SgNBEMZzh0WKCClSCKaIYOED+AAKeQQLG8HWztLCImBrYadgIdY+gIKNYkBFSwu7CAoqCgkkoGBI/E28PdbLZmeDLgzZzcx83/zZ2SSXC1j9fr+I1Hq93g2yxH4iwM1vkoBWAdxCmpzTxfkN2RcyZNaHFIkSo10+8kgxkXIURV5HGxTmFuc75B2RfQkpxHG8aAgaAFa0tAHqYFfQ7Iwe2yhODk8+J4C7yAoRTWI3w/4klGRgR4lO7Rpn9+gvMyWp+uxFh8+H+ARlgN1nJuJuQAYvNkEnwGFck18Er4q3egEc/oO+mhLdKgRyhdNFiacC0rlOCbhNVz4H9FnAYgDBvU3QIioZlJFLJtsoHYRDfiZoUyIxqCtRpVlANq0EU4dApjrtgezPFad5S19Wgjkc0hNVnuF4HjVA6C7QrSIbylB+oZe3aHgBsqlNqKYH48jXyJKMuAbiyVJ8KzaB3eRc0pg9VwQ4niFryI68qiOi3AbjwdsfnAtk0bCjTLJKr6mrD9g8iq/S/B81hguOMlQTnVyG40wAcjnmgsCNESDrjme7wfftP4P7SP4N3CJZdvzoNyGq2c/HWOXJGsvVg+RA/k2MC/wN6I2YA2Pt8GkAAAAASUVORK5CYII=") !important; + } + + button { + position: relative; + right: -0.3em; + top: -0.3em; + float: right; + font-weight: bold; + color: #FFFFFF; + text-shadow: 0 1px 0 #ffffff; + opacity: 0.8; + line-height: 1; + font-size: 16px; + padding: 0; + cursor: pointer; + background: transparent; + border: 0; + + &:hover { + opacity: 1; + } + } +} diff --git a/static/modules/notifications/webpack.config.js b/static/modules/notifications/webpack.config.js new file mode 100644 index 0000000..beba2e3 --- /dev/null +++ b/static/modules/notifications/webpack.config.js @@ -0,0 +1,41 @@ +const webpack = require('webpack'); +const ExtractTextPlugin = require('extract-text-webpack-plugin'); + +const extractSass = new ExtractTextPlugin({ + filename: 'notifications.css', + disable: process.env.NODE_ENV === 'development' +}); + +module.exports = { + entry: ['./src/index.js', './src/style.scss'], + output: { + path: __dirname + '/dist', + filename: 'notifications.js' + }, + module: { + rules: [ + { + test: /\.js$/, + loader: 'babel-loader', + query: { + presets: ['babel-preset-es2015', 'es2015-ie'] + } + }, + { + test: /\.scss$/, + use: extractSass.extract({ + use: [{ + loader: 'css-loader' + }, { + loader: 'sass-loader' + }], + // use style-loader in development + fallback: 'style-loader' + }) + } + ], + }, + plugins: [ + extractSass + ] +}; \ No newline at end of file From 05a4bc70f22a2f52404a65554d6d1519aa3c94b8 Mon Sep 17 00:00:00 2001 From: Sokurov Idar Date: Tue, 20 Apr 2021 23:01:31 +0300 Subject: [PATCH 02/10] minor fix --- main/templates/pages/adm_ruleset.html | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/main/templates/pages/adm_ruleset.html b/main/templates/pages/adm_ruleset.html index 2ee80f5..dd077c4 100644 --- a/main/templates/pages/adm_ruleset.html +++ b/main/templates/pages/adm_ruleset.html @@ -6,6 +6,11 @@ {% block heading %}Управление{% endblock %} +{% block extra_css %} + + +{% endblock %} + {% block extra_scripts %} From 750371e595b3007d5c87bc5d8ceadeb02ecc4c3e Mon Sep 17 00:00:00 2001 From: Iurii Tatishchev Date: Tue, 20 Apr 2021 13:29:46 -0700 Subject: [PATCH 03/10] move ZendeskAdmin to separate file --- main/extra_func.py | 154 ++++-------------------------------------- main/views.py | 3 +- main/zendesk_admin.py | 138 +++++++++++++++++++++++++++++++++++++ 3 files changed, 153 insertions(+), 142 deletions(-) create mode 100644 main/zendesk_admin.py diff --git a/main/extra_func.py b/main/extra_func.py index 3e54ae4..4aaee13 100644 --- a/main/extra_func.py +++ b/main/extra_func.py @@ -1,5 +1,4 @@ import logging -import os from datetime import timedelta, datetime, date from django.contrib.auth.models import User @@ -7,139 +6,12 @@ from django.core.exceptions import ObjectDoesNotExist from django.utils import timezone from zenpy import Zenpy from zenpy.lib.exception import APIException - +from zenpy.lib.api_objects import User as ZenpyUser from access_controller.settings import ZENDESK_ROLES as ROLES, ONE_DAY, ZENDESK_GROUPS, SOLVED_TICKETS_EMAIL, \ - ACTRL_API_EMAIL, ACTRL_API_TOKEN, ACTRL_API_PASSWORD, ACTRL_ZENDESK_SUBDOMAIN + ACTRL_ZENDESK_SUBDOMAIN from main.models import UserProfile, RoleChangeLogs, UnassignedTicket, UnassignedTicketStatus - - -class ZendeskAdmin: - """ - Класс **ZendeskAdmin** существует, чтобы в каждой функции отдельно не проверять аккаунт администратора. - - :param credentials: Полномочия (первым указывается учетная запись организации в Zendesk) - :type credentials: :class:`dict` - :param email: Email администратора, указанный в env - :type email: :class:`str` - :param token: Токен администратора (формируется в Zendesk, указывается в env) - :type token: :class:`str` - :param password: Пароль администратора, указанный в env - :type password: :class:`str` - """ - - credentials: dict = { - 'subdomain': ACTRL_ZENDESK_SUBDOMAIN - } - email: str = ACTRL_API_EMAIL - token: str = ACTRL_API_TOKEN - password: str = ACTRL_API_PASSWORD - - def __init__(self): - self.create_admin() - - def check_user(self, email: str) -> bool: - """ - Функция осуществляет проверку существования пользователя в Zendesk по email. - - :param email: Email пользователя - :return: Является ли зарегистрированным - """ - return True if self.admin.search(email, type='user') else False - - def get_user_name(self, email: str) -> str: - """ - Функция **get_user_name** возвращает имя пользователя по его email - """ - user = self.admin.users.search(email).values[0] - return user.name - - def get_user_role(self, email: str) -> str: - """ - Функция возвращает роль пользователя по его email. - - :param email: Email пользователя - :return: Роль пользователя - """ - user = self.admin.users.search(email).values[0] - return user.role - - def get_user_id(self, email: str) -> str: - """ - Функция возвращает id пользователя по его email - - :param email: Email пользователя - :return: ID пользователя - """ - user = self.admin.users.search(email).values[0] - return user.id - - def get_user_image(self, email: str) -> str: - """ - Функция возвращает url-ссылку на аватар пользователя по его email. - - :param email: Email пользователя - :return: Аватар пользователя - """ - user = self.admin.users.search(email).values[0] - return user.photo['content_url'] if user.photo else None - - def get_user(self, email: str): - """ - Функция возвращает пользователя (объект) по его email. - - :param email: Email пользователя - :return: Объект пользователя, найденного в БД - """ - return self.admin.users.search(email).values[0] - - def get_group(self, name: str) -> str: - """ - Функция возвращает группу по названию - - :param name: Имя пользователя - :return: Группы пользователя (в случае отсутствия None) - """ - groups = self.admin.search(name, type='group') - for group in groups: - return group - return None - - def get_user_org(self, email: str) -> str: - """ - Функция возвращает организацию, к которой относится пользователь по его email. - - :param email: Email пользователя - :return: Организация пользователя - """ - user = self.admin.users.search(email).values[0] - return user.organization.name if user.organization else None - - def create_admin(self) -> None: - """ - Функция создает администратора, проверяя наличие вводимых данных в env. - - :param credentials: В список полномочий администратора вносятся email, token, password из env - :type credentials: :class:`dict` - :raise: :class:`ValueError`: исключение, вызываемое если email не введен в env - :raise: :class:`APIException`: исключение, вызываемое если пользователя с таким email не существует в Zendesk - """ - - if self.email is None: - raise ValueError('access_controller email not in env') - self.credentials['email'] = self.email - - if self.token: - self.credentials['token'] = self.token - elif self.password: - self.credentials['password'] = self.password - else: - raise ValueError('access_controller token or password not in env') - self.admin = Zenpy(**self.credentials) - try: - self.admin.search(self.email, type='user') - except APIException: - raise ValueError('invalid access_controller`s login data') +from main.zendesk_admin import zenpy def update_role(user_profile: UserProfile, role: int) -> None: @@ -150,7 +22,7 @@ def update_role(user_profile: UserProfile, role: int) -> None: :param role: Новая роль :return: Пользователь с обновленной ролью """ - zendesk = ZendeskAdmin() + zendesk = zenpy user = zendesk.get_user(user_profile.user.email) user.custom_role_id = role user_profile.custom_role_id = role @@ -183,11 +55,11 @@ def make_light_agent(user_profile: UserProfile, who_changes: User) -> None: status=UnassignedTicketStatus.SOLVED if ticket.status == 'solved' else UnassignedTicketStatus.UNASSIGNED ) if ticket.status == 'solved': - ticket.assignee = ZendeskAdmin().get_user(SOLVED_TICKETS_EMAIL) + ticket.assignee = zenpy.get_user(SOLVED_TICKETS_EMAIL) else: ticket.assignee = None - ticket.group = ZendeskAdmin().get_group(ZENDESK_GROUPS['buffer']) - ZendeskAdmin().admin.tickets.update(ticket) + ticket.group = zenpy.get_group(ZENDESK_GROUPS['buffer']) + zenpy.admin.tickets.update(ticket) update_role(user_profile, ROLES['light_agent']) @@ -195,7 +67,7 @@ def get_users_list() -> list: """ Функция **get_users_list** возвращает список пользователей Zendesk, относящихся к организации SYSTEM. """ - zendesk = ZendeskAdmin() + zendesk = zenpy # У пользователей должна быть организация SYSTEM org = next(zendesk.admin.search(type='organization', name='SYSTEM')) @@ -207,7 +79,7 @@ def get_tickets_list(email): """ Функция возвращает список тикетов пользователя Zendesk """ - return ZendeskAdmin().admin.search(assignee=email, type='ticket') + return zenpy.admin.search(assignee=email, type='ticket') def update_profile(user_profile: UserProfile) -> UserProfile: @@ -217,7 +89,7 @@ def update_profile(user_profile: UserProfile) -> UserProfile: :param user_profile: Профиль пользователя :return: Обновленный, в соответствие с текущими данными в Zendesk, профиль пользователя """ - user = ZendeskAdmin().get_user(user_profile.user.email) + user = zenpy.get_user(user_profile.user.email) user_profile.name = user.name user_profile.role = user.role user_profile.custom_role_id = user.custom_role_id if user.custom_role_id else 0 @@ -232,7 +104,7 @@ def check_user_exist(email: str) -> bool: :param email: Email пользователя :return: Зарегистрирован ли пользователь в Zendesk """ - return ZendeskAdmin().check_user(email) + return zenpy.check_user(email) def get_user_organization(email: str) -> str: @@ -242,7 +114,7 @@ def get_user_organization(email: str) -> str: :param email: Email пользователя :return: Организация пользователя """ - return ZendeskAdmin().get_user_org(email) + return zenpy.get_user_org(email) def check_user_auth(email: str, password: str) -> bool: @@ -264,7 +136,7 @@ def check_user_auth(email: str, password: str) -> bool: return True -def update_user_in_model(profile: UserProfile, zendesk_user: User): +def update_user_in_model(profile: UserProfile, zendesk_user: ZenpyUser): """ Функция обновляет профиль пользователя при изменении данных пользователя на Zendesk. diff --git a/main/views.py b/main/views.py index 0c89f11..c7eec58 100644 --- a/main/views.py +++ b/main/views.py @@ -23,7 +23,8 @@ from zenpy.lib.api_objects import User as ZenpyUser from access_controller.settings import DEFAULT_FROM_EMAIL, ZENDESK_ROLES, ZENDESK_MAX_AGENTS from main.extra_func import check_user_exist, update_profile, get_user_organization, \ make_engineer, make_light_agent, get_users_list, update_users_in_model, count_users, \ - StatisticData, log, ZendeskAdmin + StatisticData, log +from main.zendesk_admin import ZendeskAdmin from main.forms import AdminPageUsers, CustomRegistrationForm, CustomAuthenticationForm, StatisticForm from main.serializers import ProfileSerializer, ZendeskUserSerializer from .models import UserProfile diff --git a/main/zendesk_admin.py b/main/zendesk_admin.py new file mode 100644 index 0000000..e0d1674 --- /dev/null +++ b/main/zendesk_admin.py @@ -0,0 +1,138 @@ +from typing import Optional + +from zenpy import Zenpy +from zenpy.lib.api_objects import User as ZenpyUser +from zenpy.lib.exception import APIException + +from access_controller.settings import ACTRL_ZENDESK_SUBDOMAIN, ACTRL_API_EMAIL, ACTRL_API_TOKEN, ACTRL_API_PASSWORD + + +class ZendeskAdmin: + """ + Класс **ZendeskAdmin** существует, чтобы в каждой функции отдельно не проверять аккаунт администратора. + + :param credentials: Полномочия (первым указывается учетная запись организации в Zendesk) + :type credentials: :class:`dict` + :param email: Email администратора, указанный в env + :type email: :class:`str` + :param token: Токен администратора (формируется в Zendesk, указывается в env) + :type token: :class:`str` + :param password: Пароль администратора, указанный в env + :type password: :class:`str` + """ + + credentials: dict = { + 'subdomain': ACTRL_ZENDESK_SUBDOMAIN + } + email: str = ACTRL_API_EMAIL + token: str = ACTRL_API_TOKEN + password: str = ACTRL_API_PASSWORD + + def __init__(self): + self.create_admin() + + def check_user(self, email: str) -> bool: + """ + Функция осуществляет проверку существования пользователя в Zendesk по email. + + :param email: Email пользователя + :return: Является ли зарегистрированным + """ + return True if self.admin.search(email, type='user') else False + + def get_user_name(self, email: str) -> str: + """ + Функция **get_user_name** возвращает имя пользователя по его email + """ + user = self.admin.users.search(email).values[0] + return user.name + + def get_user_role(self, email: str) -> str: + """ + Функция возвращает роль пользователя по его email. + + :param email: Email пользователя + :return: Роль пользователя + """ + user = self.admin.users.search(email).values[0] + return user.role + + def get_user_id(self, email: str) -> str: + """ + Функция возвращает id пользователя по его email + + :param email: Email пользователя + :return: ID пользователя + """ + user = self.admin.users.search(email).values[0] + return user.id + + def get_user_image(self, email: str) -> str: + """ + Функция возвращает url-ссылку на аватар пользователя по его email. + + :param email: Email пользователя + :return: Аватар пользователя + """ + user = self.admin.users.search(email).values[0] + return user.photo['content_url'] if user.photo else None + + def get_user(self, email: str) -> ZenpyUser: + """ + Функция возвращает пользователя (объект) по его email. + + :param email: Email пользователя + :return: Объект пользователя, найденного в БД + """ + return self.admin.users.search(email).values[0] + + def get_group(self, name: str) -> Optional[str]: + """ + Функция возвращает группу по названию + + :param name: Имя пользователя + :return: Группы пользователя (в случае отсутствия None) + """ + groups = self.admin.search(name, type='group') + for group in groups: + return group + return None + + def get_user_org(self, email: str) -> str: + """ + Функция возвращает организацию, к которой относится пользователь по его email. + + :param email: Email пользователя + :return: Организация пользователя + """ + user = self.admin.users.search(email).values[0] + return user.organization.name if user.organization else None + + def create_admin(self) -> None: + """ + Функция создает администратора, проверяя наличие вводимых данных в env. + + :param credentials: В список полномочий администратора вносятся email, token, password из env + :type credentials: :class:`dict` + :raise: :class:`ValueError`: исключение, вызываемое если email не введен в env + :raise: :class:`APIException`: исключение, вызываемое если пользователя с таким email не существует в Zendesk + """ + + if self.email is None: + raise ValueError('access_controller email not in env') + self.credentials['email'] = self.email + + if self.token: + self.credentials['token'] = self.token + elif self.password: + self.credentials['password'] = self.password + else: + raise ValueError('access_controller token or password not in env') + self.admin = Zenpy(**self.credentials) + try: + self.admin.search(self.email, type='user') + except APIException: + raise ValueError('invalid access_controller`s login data') + + +zenpy = ZendeskAdmin() From 30e4d93c8166d91c6abe5114d8c677049d3d47b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D0=BE=D0=BA=D1=83=D1=80=D0=BE=D0=B2=20=D0=98=D0=B4?= =?UTF-8?q?=D0=B0=D1=80?= Date: Tue, 20 Apr 2021 20:30:11 +0000 Subject: [PATCH 04/10] Update views.py --- main/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main/views.py b/main/views.py index 6d6ce05..e299607 100644 --- a/main/views.py +++ b/main/views.py @@ -243,7 +243,7 @@ def work_get_tickets(request): ticket.group.name == 'Сменная группа' and ticket.assignee is None] count = 0 for i in range(len(tickets)): - if i == request.GET.get('count_tickets'): + if i == int(request.GET.get('count_tickets')): return set_session_params_for_work_page(request, count) tickets[i].assignee = zenpy_user admin.tickets.update(tickets[i]) From 045d2cfb2b7b3cbeb87fbe77a8b400798cc50c68 Mon Sep 17 00:00:00 2001 From: Iurii Tatishchev Date: Tue, 20 Apr 2021 13:30:15 -0700 Subject: [PATCH 05/10] bulk request for tickets to buffer group --- main/extra_func.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/main/extra_func.py b/main/extra_func.py index 4aaee13..f8d95b8 100644 --- a/main/extra_func.py +++ b/main/extra_func.py @@ -48,6 +48,8 @@ def make_light_agent(user_profile: UserProfile, who_changes: User) -> None: :return: Вызов функции **update_role** с параметрами: профиль пользователя, роль "light_agent" """ tickets = get_tickets_list(user_profile.user.email) + tickets_to_update = [] + buffer_group = zenpy.get_group(ZENDESK_GROUPS['buffer']) for ticket in tickets: UnassignedTicket.objects.create( assignee=user_profile.user, @@ -58,8 +60,11 @@ def make_light_agent(user_profile: UserProfile, who_changes: User) -> None: ticket.assignee = zenpy.get_user(SOLVED_TICKETS_EMAIL) else: ticket.assignee = None - ticket.group = zenpy.get_group(ZENDESK_GROUPS['buffer']) - zenpy.admin.tickets.update(ticket) + ticket.group = buffer_group + tickets_to_update.append(ticket) + + print(tickets_to_update) + zenpy.admin.tickets.update(tickets.values) update_role(user_profile, ROLES['light_agent']) From a5d0c222bd0a5c4b19bb693d6b2eec8ce752c4c9 Mon Sep 17 00:00:00 2001 From: Iurii Tatishchev Date: Tue, 20 Apr 2021 14:39:27 -0700 Subject: [PATCH 06/10] Multiple attempts to change role to compensate Zendesk API delay --- main/extra_func.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/main/extra_func.py b/main/extra_func.py index f8d95b8..162adaf 100644 --- a/main/extra_func.py +++ b/main/extra_func.py @@ -48,8 +48,8 @@ def make_light_agent(user_profile: UserProfile, who_changes: User) -> None: :return: Вызов функции **update_role** с параметрами: профиль пользователя, роль "light_agent" """ tickets = get_tickets_list(user_profile.user.email) - tickets_to_update = [] buffer_group = zenpy.get_group(ZENDESK_GROUPS['buffer']) + solved_tickets_user = zenpy.get_user(SOLVED_TICKETS_EMAIL) for ticket in tickets: UnassignedTicket.objects.create( assignee=user_profile.user, @@ -57,15 +57,22 @@ def make_light_agent(user_profile: UserProfile, who_changes: User) -> None: status=UnassignedTicketStatus.SOLVED if ticket.status == 'solved' else UnassignedTicketStatus.UNASSIGNED ) if ticket.status == 'solved': - ticket.assignee = zenpy.get_user(SOLVED_TICKETS_EMAIL) + ticket.assignee = solved_tickets_user else: ticket.assignee = None ticket.group = buffer_group - tickets_to_update.append(ticket) - print(tickets_to_update) zenpy.admin.tickets.update(tickets.values) - update_role(user_profile, ROLES['light_agent']) + + attempts, success = 5, False + while not success and attempts != 0: + try: + update_role(user_profile, ROLES['light_agent']) + success = True + except APIException as e: + attempts -= 1 + if attempts == 0: + raise e def get_users_list() -> list: From 95d0db24b6b2d6dee97d5805db87c08679cb957a Mon Sep 17 00:00:00 2001 From: Iurii Tatishchev Date: Tue, 20 Apr 2021 15:02:17 -0700 Subject: [PATCH 07/10] Find buffer group id and solved tickets user id in ZendeskAdmin constructor --- main/extra_func.py | 12 +++++------- main/zendesk_admin.py | 5 ++++- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/main/extra_func.py b/main/extra_func.py index 162adaf..fd59968 100644 --- a/main/extra_func.py +++ b/main/extra_func.py @@ -6,10 +6,9 @@ from django.core.exceptions import ObjectDoesNotExist from django.utils import timezone from zenpy import Zenpy from zenpy.lib.exception import APIException -from zenpy.lib.api_objects import User as ZenpyUser +from zenpy.lib.api_objects import User as ZenpyUser, Ticket as ZenpyTicket -from access_controller.settings import ZENDESK_ROLES as ROLES, ONE_DAY, ZENDESK_GROUPS, SOLVED_TICKETS_EMAIL, \ - ACTRL_ZENDESK_SUBDOMAIN +from access_controller.settings import ZENDESK_ROLES as ROLES, ONE_DAY, ACTRL_ZENDESK_SUBDOMAIN from main.models import UserProfile, RoleChangeLogs, UnassignedTicket, UnassignedTicketStatus from main.zendesk_admin import zenpy @@ -48,8 +47,7 @@ def make_light_agent(user_profile: UserProfile, who_changes: User) -> None: :return: Вызов функции **update_role** с параметрами: профиль пользователя, роль "light_agent" """ tickets = get_tickets_list(user_profile.user.email) - buffer_group = zenpy.get_group(ZENDESK_GROUPS['buffer']) - solved_tickets_user = zenpy.get_user(SOLVED_TICKETS_EMAIL) + ticket: ZenpyTicket for ticket in tickets: UnassignedTicket.objects.create( assignee=user_profile.user, @@ -57,10 +55,10 @@ def make_light_agent(user_profile: UserProfile, who_changes: User) -> None: status=UnassignedTicketStatus.SOLVED if ticket.status == 'solved' else UnassignedTicketStatus.UNASSIGNED ) if ticket.status == 'solved': - ticket.assignee = solved_tickets_user + ticket.assignee_id = zenpy.solved_tickets_user_id else: ticket.assignee = None - ticket.group = buffer_group + ticket.group_id = zenpy.buffer_group_id zenpy.admin.tickets.update(tickets.values) diff --git a/main/zendesk_admin.py b/main/zendesk_admin.py index e0d1674..70a461f 100644 --- a/main/zendesk_admin.py +++ b/main/zendesk_admin.py @@ -4,7 +4,8 @@ from zenpy import Zenpy from zenpy.lib.api_objects import User as ZenpyUser from zenpy.lib.exception import APIException -from access_controller.settings import ACTRL_ZENDESK_SUBDOMAIN, ACTRL_API_EMAIL, ACTRL_API_TOKEN, ACTRL_API_PASSWORD +from access_controller.settings import ACTRL_ZENDESK_SUBDOMAIN, ACTRL_API_EMAIL, ACTRL_API_TOKEN, ACTRL_API_PASSWORD, \ + ZENDESK_GROUPS, SOLVED_TICKETS_EMAIL class ZendeskAdmin: @@ -30,6 +31,8 @@ class ZendeskAdmin: def __init__(self): self.create_admin() + self.buffer_group_id: int = self.get_group(ZENDESK_GROUPS['buffer']).id + self.solved_tickets_user_id: int = self.get_user(SOLVED_TICKETS_EMAIL).id def check_user(self, email: str) -> bool: """ From 8d14f2e8b49b7d612fb31eccbe2fd2d09d2ced3e Mon Sep 17 00:00:00 2001 From: Iurii Tatishchev Date: Tue, 20 Apr 2021 15:13:55 -0700 Subject: [PATCH 08/10] Fix some type hints, small ZendeskAdmin refactor --- main/extra_func.py | 5 ++-- main/views.py | 4 ++-- main/zendesk_admin.py | 53 +++++++++++++++++-------------------------- 3 files changed, 26 insertions(+), 36 deletions(-) diff --git a/main/extra_func.py b/main/extra_func.py index fd59968..2ebb384 100644 --- a/main/extra_func.py +++ b/main/extra_func.py @@ -1,5 +1,6 @@ import logging from datetime import timedelta, datetime, date +from typing import Optional from django.contrib.auth.models import User from django.core.exceptions import ObjectDoesNotExist @@ -92,7 +93,7 @@ def get_tickets_list(email): return zenpy.admin.search(assignee=email, type='ticket') -def update_profile(user_profile: UserProfile) -> UserProfile: +def update_profile(user_profile: UserProfile): """ Функция обновляет профиль пользователя в соответствии с текущим в Zendesk. @@ -318,7 +319,7 @@ class StatisticData: self.display = display_format return True - def get_data(self) -> list: + def get_data(self) -> Optional[dict]: """ Функция возвращает данные - список объектов RoleChangeLogs. """ diff --git a/main/views.py b/main/views.py index c7eec58..818de06 100644 --- a/main/views.py +++ b/main/views.py @@ -24,7 +24,7 @@ from access_controller.settings import DEFAULT_FROM_EMAIL, ZENDESK_ROLES, ZENDES from main.extra_func import check_user_exist, update_profile, get_user_organization, \ make_engineer, make_light_agent, get_users_list, update_users_in_model, count_users, \ StatisticData, log -from main.zendesk_admin import ZendeskAdmin +from main.zendesk_admin import zenpy from main.forms import AdminPageUsers, CustomRegistrationForm, CustomAuthenticationForm, StatisticForm from main.serializers import ProfileSerializer, ZendeskUserSerializer from .models import UserProfile @@ -167,7 +167,7 @@ def auth_user(request: WSGIRequest) -> ZenpyUser: :param request: email, subdomain и token пользователя :return: объект пользователя Zendesk """ - admin = ZendeskAdmin().admin + admin = zenpy.admin zenpy_user: ZenpyUser = admin.users.search(request.user.email).values[0] return zenpy_user, admin diff --git a/main/zendesk_admin.py b/main/zendesk_admin.py index 70a461f..7692e0e 100644 --- a/main/zendesk_admin.py +++ b/main/zendesk_admin.py @@ -1,7 +1,7 @@ -from typing import Optional +from typing import Optional, Dict from zenpy import Zenpy -from zenpy.lib.api_objects import User as ZenpyUser +from zenpy.lib.api_objects import User as ZenpyUser, Group as ZenpyGroup from zenpy.lib.exception import APIException from access_controller.settings import ACTRL_ZENDESK_SUBDOMAIN, ACTRL_API_EMAIL, ACTRL_API_TOKEN, ACTRL_API_PASSWORD, \ @@ -13,24 +13,12 @@ class ZendeskAdmin: Класс **ZendeskAdmin** существует, чтобы в каждой функции отдельно не проверять аккаунт администратора. :param credentials: Полномочия (первым указывается учетная запись организации в Zendesk) - :type credentials: :class:`dict` - :param email: Email администратора, указанный в env - :type email: :class:`str` - :param token: Токен администратора (формируется в Zendesk, указывается в env) - :type token: :class:`str` - :param password: Пароль администратора, указанный в env - :type password: :class:`str` + :type credentials: :class:`Dict[str, str]` """ - credentials: dict = { - 'subdomain': ACTRL_ZENDESK_SUBDOMAIN - } - email: str = ACTRL_API_EMAIL - token: str = ACTRL_API_TOKEN - password: str = ACTRL_API_PASSWORD - - def __init__(self): - self.create_admin() + def __init__(self, credentials: Dict[str, str]): + self.credentials = credentials + self.admin = self.create_admin() self.buffer_group_id: int = self.get_group(ZENDESK_GROUPS['buffer']).id self.solved_tickets_user_id: int = self.get_user(SOLVED_TICKETS_EMAIL).id @@ -89,7 +77,7 @@ class ZendeskAdmin: """ return self.admin.users.search(email).values[0] - def get_group(self, name: str) -> Optional[str]: + def get_group(self, name: str) -> Optional[ZenpyGroup]: """ Функция возвращает группу по названию @@ -111,31 +99,32 @@ class ZendeskAdmin: user = self.admin.users.search(email).values[0] return user.organization.name if user.organization else None - def create_admin(self) -> None: + def create_admin(self) -> Zenpy: """ Функция создает администратора, проверяя наличие вводимых данных в env. - :param credentials: В список полномочий администратора вносятся email, token, password из env - :type credentials: :class:`dict` :raise: :class:`ValueError`: исключение, вызываемое если email не введен в env :raise: :class:`APIException`: исключение, вызываемое если пользователя с таким email не существует в Zendesk """ - if self.email is None: + if self.credentials.get('email') is None: raise ValueError('access_controller email not in env') - self.credentials['email'] = self.email - if self.token: - self.credentials['token'] = self.token - elif self.password: - self.credentials['password'] = self.password - else: + if self.credentials.get('token') is None and self.credentials.get('password') is None: raise ValueError('access_controller token or password not in env') - self.admin = Zenpy(**self.credentials) + + admin = Zenpy(**self.credentials) try: - self.admin.search(self.email, type='user') + admin.search(self.credentials['email'], type='user') except APIException: raise ValueError('invalid access_controller`s login data') + return admin -zenpy = ZendeskAdmin() + +zenpy = ZendeskAdmin({ + 'subdomain': ACTRL_ZENDESK_SUBDOMAIN, + 'email': ACTRL_API_EMAIL, + 'token': ACTRL_API_TOKEN, + 'password': ACTRL_API_PASSWORD, +}) From 7f837d99441dd1c2bfc7e65626fc13e986ac0bc8 Mon Sep 17 00:00:00 2001 From: Iurii Tatishchev Date: Tue, 20 Apr 2021 15:34:10 -0700 Subject: [PATCH 09/10] Remove unnecessary ZendeskAdmin methods --- main/zendesk_admin.py | 37 ------------------------------------- 1 file changed, 37 deletions(-) diff --git a/main/zendesk_admin.py b/main/zendesk_admin.py index 7692e0e..8ecb877 100644 --- a/main/zendesk_admin.py +++ b/main/zendesk_admin.py @@ -31,43 +31,6 @@ class ZendeskAdmin: """ return True if self.admin.search(email, type='user') else False - def get_user_name(self, email: str) -> str: - """ - Функция **get_user_name** возвращает имя пользователя по его email - """ - user = self.admin.users.search(email).values[0] - return user.name - - def get_user_role(self, email: str) -> str: - """ - Функция возвращает роль пользователя по его email. - - :param email: Email пользователя - :return: Роль пользователя - """ - user = self.admin.users.search(email).values[0] - return user.role - - def get_user_id(self, email: str) -> str: - """ - Функция возвращает id пользователя по его email - - :param email: Email пользователя - :return: ID пользователя - """ - user = self.admin.users.search(email).values[0] - return user.id - - def get_user_image(self, email: str) -> str: - """ - Функция возвращает url-ссылку на аватар пользователя по его email. - - :param email: Email пользователя - :return: Аватар пользователя - """ - user = self.admin.users.search(email).values[0] - return user.photo['content_url'] if user.photo else None - def get_user(self, email: str) -> ZenpyUser: """ Функция возвращает пользователя (объект) по его email. From f65eb546524d9fe04b25c4cf2e2952a5b6f789d5 Mon Sep 17 00:00:00 2001 From: Iurii Tatishchev Date: Tue, 20 Apr 2021 15:39:55 -0700 Subject: [PATCH 10/10] Small views.py refactor --- main/views.py | 58 +++++++++++++++++++-------------------------------- 1 file changed, 21 insertions(+), 37 deletions(-) diff --git a/main/views.py b/main/views.py index 818de06..2dc6636 100644 --- a/main/views.py +++ b/main/views.py @@ -1,4 +1,5 @@ from smtplib import SMTPException +from typing import Dict, Any from django.contrib import messages from django.contrib.auth.decorators import login_required @@ -11,14 +12,13 @@ from django.contrib.contenttypes.models import ContentType from django.contrib.messages.views import SuccessMessageMixin from django.core.handlers.wsgi import WSGIRequest from django.http import HttpResponseRedirect, HttpResponse -from django.shortcuts import render, redirect, get_list_or_404 +from django.shortcuts import render, redirect from django.urls import reverse_lazy, reverse from django.views.generic import FormView from django_registration.views import RegistrationView # Django REST from rest_framework import viewsets from rest_framework.response import Response -from zenpy.lib.api_objects import User as ZenpyUser from access_controller.settings import DEFAULT_FROM_EMAIL, ZENDESK_ROLES, ZENDESK_MAX_AGENTS from main.extra_func import check_user_exist, update_profile, get_user_organization, \ @@ -31,9 +31,7 @@ from .models import UserProfile def setup_context(profile_lit: bool = False, control_lit: bool = False, work_lit: bool = False, - registration_lit: bool = False, login_lit: bool = False, stats_lit: bool = False): - - print(profile_lit, control_lit, work_lit, registration_lit, login_lit) + registration_lit: bool = False, login_lit: bool = False, stats_lit: bool = False) -> Dict[str, Any]: context = { 'profile_lit': profile_lit, @@ -160,18 +158,6 @@ def profile_page(request: WSGIRequest) -> HttpResponse: return render(request, 'pages/profile.html', context) -def auth_user(request: WSGIRequest) -> ZenpyUser: - """ - Функция возвращает профиль пользователя на Zendesk. - - :param request: email, subdomain и token пользователя - :return: объект пользователя Zendesk - """ - admin = zenpy.admin - zenpy_user: ZenpyUser = admin.users.search(request.user.email).values[0] - return zenpy_user, admin - - @login_required() def work_page(request: WSGIRequest, id: int) -> HttpResponse: """ @@ -186,7 +172,6 @@ def work_page(request: WSGIRequest, id: int) -> HttpResponse: engineers = [] light_agents = [] for user in users: - if user.custom_role_id == ZENDESK_ROLES['engineer']: engineers.append(user) elif user.custom_role_id == ZENDESK_ROLES['light_agent']: @@ -207,39 +192,38 @@ def work_page(request: WSGIRequest, id: int) -> HttpResponse: @login_required() def work_hand_over(request: WSGIRequest) -> HttpResponseRedirect: """ - Функция позволяет текущему пользователю (login_required) сдать права, а именно сменить в Zendesk роль с "engineer" на "light agent" - и установить роль "agent" в БД. Действия выполняются, если исходная роль пользователя "engineer". + Функция позволяет текущему пользователю сдать права, а именно сменить в Zendesk роль с "engineer" на "light_agent" :param request: данные текущего пользователя (login_required) :return: перезагрузка текущей страницы после выполнения смены роли """ - make_light_agent(request.user.userprofile,request.user) + make_light_agent(request.user.userprofile, request.user) return HttpResponseRedirect(reverse('work', args=(request.user.id,))) @login_required() def work_become_engineer(request: WSGIRequest) -> HttpResponseRedirect: """ - Функция меняет роль пользователя в Zendesk на "engineer" и присваивает роль "agent" в БД (в случае, если исходная роль пользователя была "light_agent"). + Функция позволяет текущему пользователю получить права, а именно сменить в Zendesk роль с "light_agent" на "engineer" :param request: данные текущего пользователя (login_required) :return: перезагрузка текущей страницы после выполнения смены роли """ - zenpy_user, admin = auth_user(request) - make_engineer(request.user.userprofile,request.user) + make_engineer(request.user.userprofile, request.user) return HttpResponseRedirect(reverse('work', args=(request.user.id,))) + @login_required() def work_get_tickets(request): - zenpy_user, admin = auth_user(request) count_tickets = int(request.GET["count_tickets"]) - tickets = [ticket for ticket in admin.search(type="ticket") if ticket.group.name == 'Сменная группа' and ticket.assignee is None] + tickets = [ticket for ticket in zenpy.admin.search(type="ticket") if + ticket.group.name == 'Сменная группа' and ticket.assignee is None] for i in range(len(tickets)): if i == count_tickets: return HttpResponseRedirect(reverse('work', args=(request.user.id,))) - tickets[i].assignee = zenpy_user - admin.tickets.update(tickets[i]) + tickets[i].assignee = zenpy.get_user(request.user.email) + zenpy.admin.tickets.update(tickets[i]) return HttpResponseRedirect(reverse('work', args=(request.user.id,))) @@ -378,16 +362,16 @@ def statistic_page(request: WSGIRequest) -> HttpResponse: if form.is_valid(): start_date, end_date = form.cleaned_data['range_start'], form.cleaned_data['range_end'] interval, show = form.cleaned_data['interval'], form.cleaned_data['display_format'] - Data = StatisticData(start_date, end_date, form.cleaned_data['email']) - Data.set_display(show) - Data.set_interval(interval) - stats = Data.get_statistic() - if Data.errors: - context['errors'] = Data.errors - if Data.warnings: - context['warnings'] = Data.warnings + data = StatisticData(start_date, end_date, form.cleaned_data['email']) + data.set_display(show) + data.set_interval(interval) + stats = data.get_statistic() + if data.errors: + context['errors'] = data.errors + if data.warnings: + context['warnings'] = data.warnings context['log_stats'] = stats if not context['errors'] else None - if request.method == 'GET': + elif request.method == 'GET': form = StatisticForm() context['form'] = form return render(request, 'pages/statistic.html', context)