Super user feature (#841)

* add ruby tests

* update ruby test

* backend part

* add get-supervised-ticket path and ruby tests

* update search-authors path and custom validation to get-supervised-tickets

* add supervised users components and css

* add supervised-users messages

* resolve minor bugs get super ticket, ruby tests and change file name

* add supervisor options on user panel

* change supervisor structure

* add pagination dashboard supervisor tickets

* change error name

* add error to path edit-supervised-list

* fix github comments

* resolve github comment backend

* add minor changes dashboard list tickets page
This commit is contained in:
Guillermo Giuliana 2020-07-21 06:52:34 -03:00 committed by GitHub
parent 507be7bff2
commit cd73606852
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 982 additions and 36 deletions

View File

@ -1,6 +1,6 @@
{
"name": "OpenSupports",
"version": "4.2.0",
"version": "4.7.0",
"author": "Ivan Diaz <contact@opensupports.com>",
"description": "Open source ticket system made with PHP and ReactJS",
"repository": {

View File

@ -92,7 +92,7 @@ class TicketQueryFilters extends React.Component {
}} />
</div>
<div className="ticket-query-filters__group__container">
<span className="ticket-query-filters__title">Authors</span>
<span className="ticket-query-filters__title">{i18n('AUTHORS')}</span>
<FormField
name="authors"
field="autocomplete"

View File

@ -12,6 +12,7 @@ import Header from 'core-components/header';
import Button from 'core-components/button';
import Message from 'core-components/message';
import InfoTooltip from 'core-components/info-tooltip';
import Autocomplete from 'core-components/autocomplete';
class AdminPanelViewUser extends React.Component {
@ -23,7 +24,9 @@ class AdminPanelViewUser extends React.Component {
customfields: [],
invalid: false,
loading: true,
disabled: false
disabled: false,
userList: [],
message: ''
};
componentDidMount() {
@ -80,6 +83,26 @@ class AdminPanelViewUser extends React.Component {
</div>
</div>
<span className="separator" />
<div className="admin-panel-view-user">
<div className="admin-panel-view-user__supervised-users-header">{i18n('SUPERVISED_USER')}</div>
<div className="admin-panel-view-user__supervised-users-content">
<Autocomplete
onChange={this.onChangeValues.bind(this)}
getItemListFromQuery={this.searchUsers}
values={this.transformUserListToAutocomplete()}
/>
<Button
disabled = {this.state.loading}
className="admin-panel-view-user__submit-button"
onClick={this.onClickSupervisorUserButton.bind(this)}
size="medium"
>
{i18n('UPDATE')}
</Button>
</div>
{this.renderSupervisedUserMessage()}
</div>
<span className="separator" />
<div className="admin-panel-view-user__tickets">
<div className="admin-panel-view-user__tickets-title">{i18n('TICKETS')}</div>
<TicketList {...this.getTicketListProps()}/>
@ -87,6 +110,92 @@ class AdminPanelViewUser extends React.Component {
</div>
);
}
renderSupervisedUserMessage(){
if(this.state.message) {
if(this.state.message != 'success'){
return(
<div className="admin-panel-view-user__supervised-users-message">
<Message type="error">{i18n(this.state.message)}</Message>
</div>
)
}else{
return(
<div className= "admin-panel-view-user__supervised-users-message">
<Message type="success">{i18n('SUPERVISED_USERS_UPDATED')}</Message>
</div>
)
}
}
}
onClickSupervisorUserButton(){
this.setState({
loading: true
});
const userIdList = this.state.userList.map((item) => {
return item.id;
});
API.call({
path: '/user/edit-supervised-list',
data: {
userIdList: JSON.stringify(userIdList),
userId: this.props.params.userId
}
}).then(r => {
this.setState({
loading: false,
message: 'success'
})
}).catch((r) => {
this.setState({
loading: false,
message: r.message
})
});
}
onChangeValues(newList) {
this.setState({
userList: newList
});
}
searchUsers(query, blacklist = []) {
return API.call({
path: '/ticket/search-authors',
data: {
query: query,
blackList: JSON.stringify(blacklist),
searchUsers: 1
}
}).then(r => {
return r.data.authors.map(author => {
return {
name: author.name,
color: "gray",
id: author.id*1,
content: <div>{author.name}</div>,
isStaff: false
}});
}).catch((r) => {
console.log(r);
});
}
transformUserListToAutocomplete() {
return(
this.state.userList.map((user) => {
return ({
id: user.id*1,
name: user.name,
content: <div>{user.name}</div>,
color: 'grey',
isStaff: false
});
})
);
}
renderNotVerified() {
return (
@ -129,7 +238,8 @@ class AdminPanelViewUser extends React.Component {
tickets: result.data.tickets,
disabled: result.data.disabled,
customfields: result.data.customfields,
loading: false
loading: false,
userList: result.data.userList
});
}

View File

@ -34,7 +34,28 @@
text-align: left;
}
&__supervised-users-header {
font-size: $font-size--md;
margin-bottom: 20px;
text-align: left;
}
&__supervised-users-content {
display:flex;
flex-direction: column;
align-items:flex-start;
}
&__supervised-users-message {
margin-top: 20px
}
&__submit-button {
margin-top: 20px;
}
&__unverified {
margin-left: 15px;
}
}

View File

@ -4,8 +4,13 @@ import {connect} from 'react-redux';
import SessionActions from 'actions/session-actions';
import i18n from 'lib-app/i18n';
import API from 'lib-app/api-call';
import Header from 'core-components/header';
import Form from 'core-components/form';
import FormField from 'core-components/form-field';
import Message from 'core-components/message';
import TicketList from 'app-components/ticket-list';
class DashboardListTicketsPage extends React.Component {
@ -16,28 +21,101 @@ class DashboardListTicketsPage extends React.Component {
static defaultProps = {
tickets: []
};
state = {
tickets: [],
pages: 1,
page: 1,
form:{
users: [],
ownTickets: true
},
message: ''
};
componentDidMount() {
this.retrieveUserData();
this.updateTicketList({users: [], ownTickets: true});
}
render() {
return (
<div className="dashboard-ticket-list">
<Header title={i18n('TICKET_LIST')} description={i18n('TICKET_LIST_DESCRIPTION')} />
<TicketList tickets={this.props.tickets} type="primary"/>
{this.props.userUsers.length ? this.showSupervisorOptions() : null}
<TicketList onPageChange={this.onPageChange.bind(this)} page={this.state.page} pages={this.state.pages} tickets={this.state.tickets} type="primary"/>
{this.state.message ? <Message type="error" >{i18n(this.state.message)}</Message> : null}
</div>
);
}
retrieveUserData() {
this.props.dispatch(SessionActions.getUserData());
}
onPageChange(e){
this.updateTicketList({...this.state.form, page: e.target.value});
}
showSupervisorOptions(){
return(
<Form className="dashboard-ticket-list__supervisor-form" values={this.state.form} onChange={this.onFormChange.bind(this)}>
<FormField label={i18n('SUPERVISED_USER')} name="users" field='autocomplete' fieldProps={{items: this.getUserItems()}}></FormField>
<FormField className="dashboard-ticket-list__supervisor-form-checkbox" label={i18n('SHOW_MY_TICKETS')} name="ownTickets" field="checkbox"/>
</Form>
)
}
getUserItems() {
let usersList = this.props.userUsers.map(user => {
return {
id: user.id*1,
name: user.name,
content: <div>{user.name}</div>,
color: 'grey',
isStaff: false
};
});
return usersList;
}
onFormChange(form){
this.updateTicketList(form);
this.setState({
form
});
}
updateTicketList({users, page, ownTickets}) {
let usersIds = users.map((item) => {
return item.id
})
API.call({
path: 'user/get-supervised-tickets',
data: {
page,
showOwnTickets: ownTickets*1,
supervisedUsers: JSON.stringify(usersIds)
}
}).then((r) => {
this.setState({
tickets: r.data.tickets,
pages: r.data.pages,
page: r.data.page,
message: ''
})
}).catch((r) => {
this.setState({
tickets: [],
message: r.message
})
});
}
}
export default connect((store) => {
return {
tickets: store.session.userTickets
userId: store.session.userId,
userUsers: store.session.userUsers || []
};
})(DashboardListTicketsPage);

View File

@ -0,0 +1,12 @@
.dashboard-ticket-list {
&__supervisor-form {
display: flex;
align-items: center;
justify-content: flex-start;
&-checkbox {
margin-left: 20px;
}
}
}

View File

@ -159,7 +159,6 @@ class Autocomplete extends React.Component {
if(this.getDropdownList().length) {
const selectedItem = this.getDropdownList()[e.index];
const newList = [...this.getSelectedItems(), selectedItem];
this.setState({
selectedItems: newList,
inputValue: "",

View File

@ -26,9 +26,11 @@ export default {
'CREATE_TICKET': 'Create Ticket',
'TICKET_LIST': 'Ticket List',
'SUPPORT_CENTER': 'Support Center',
'SUPERVISED_USER': 'Supervised users',
'DEPARTMENT': 'Department',
'DEFAULT_DEPARTMENT': 'Default Department',
'AUTHOR': 'Author',
'AUTHORS': 'Authors',
'DATE': 'Date',
'RESPOND': 'Respond',
'RESPOND_TICKET': 'Respond Ticket',
@ -198,6 +200,7 @@ export default {
'SUBJECT': 'Subject',
'SEND_EMAIL_ON_NEW_TICKET': 'Send email on new ticket',
'STAFF_UPDATED': 'Staff member has been updated',
'SUPERVISED_USERS_UPDATED': 'Supervised users updated',
'UPDATE': 'Update',
'NEVER': 'Never',
'HIMSELF': 'himself',
@ -210,6 +213,7 @@ export default {
'ENABLE_USER': 'Enable User',
'DISABLE_USER': 'Disable User',
'SHOW_CLOSED_TICKETS': 'Show Closed Tickets',
'SHOW_MY_TICKETS': 'Show my tickets',
'IMAGE_HEADER_URL': 'Image header URL',
'IMAGE_HEADER_DESCRIPTION': 'Image that will be used as header of the email',
'EMAIL_SETTINGS': 'Email Settings',
@ -390,6 +394,8 @@ export default {
'CURRENTLY_UNAVAILABLE': 'Currently unavailable',
'CAN_NOT_DELETE_DEFAULT_DEPARTMENT': 'Default department can not be deleted',
'INVALID_DEFAULT_DEPARTMENT': 'Default department choosen is invalid',
'INVALID_SUPERVISED_USERS': 'Invalid supervised users',
'SUPERVISOR_CAN_NOT_SUPERVISE_HIMSELF': 'Supervisor can not supervise himself',
//MESSAGES
'SIGNUP_SUCCESS': 'You have registered successfully in our support system.',

View File

@ -106,7 +106,6 @@ class SessionReducer extends Reducer {
onUserDataRetrieved(state, payload) {
let userData = payload.data;
sessionStore.storeUserData(payload.data);
return _.extend({}, state, {
staff: userData.staff,
userName: userData.name,
@ -116,7 +115,8 @@ class SessionReducer extends Reducer {
userDepartments: userData.departments,
userTickets: userData.tickets,
userSendEmailOnNewTicket: userData.sendEmailOnNewTicket * 1,
userCustomFields: userData.customfields
userCustomFields: userData.customfields,
userUsers: userData.users
});
}

View File

@ -76,7 +76,7 @@ class DeleteDepartmentController extends Controller {
public function transferDepartmentTickets() {
$tickets = Ticket::find('department_id = ?', [$this->departmentId]);
$newDepartment = Department::getDataStore($this->transferDepartmentId);;
$newDepartment = Department::getDataStore($this->transferDepartmentId);
foreach($tickets as $ticket) {
$staffOwner = $ticket->owner;

View File

@ -5,7 +5,7 @@ DataValidator::with('CustomValidations', true);
/**
* @api {post} /ticket/search-authors search authors of tickets
* @apiVersion 4.6.1
* @apiVersion 4.7
*
* @apiName Search authors
*
@ -41,6 +41,10 @@ class SearchAuthorsController extends Controller {
'blackList' => [
'validation' => DataValidator::oneOf(DataValidator::validAuthorsBlackList(),DataValidator::nullType()),
'error' => ERRORS::INVALID_BLACK_LIST
],
'searchUsers' => [
'validation' => DataValidator::oneOf(DataValidator::in(['0','1']),DataValidator::nullType()),
'error' => ERRORS::INVALID_USER_SEARCH_OPTION
]
]
];
@ -48,31 +52,44 @@ class SearchAuthorsController extends Controller {
public function handler() {
$query = Controller::request('query');
$searchUser = Controller::request('searchUsers') ? Controller::request('searchUsers') : 0;
$authorsQuery = "SELECT id,name,level FROM staff " . $this->generateAuthorsIdQuery($query) . " LIMIT 10";
$authorsMatch = RedBean::getAll($authorsQuery, [':query' => "%" .$query . "%",':queryAtBeginning' => $query . "%"] );
$authors = [];
if(!$searchUser){
$sqlQuery = $this->generateAuthorsIdQuery($query);
}else{
$sqlQuery = $this->generateUsersIdQuery($query);
}
foreach($authorsMatch as $authorMatch) {
if($authorMatch['level'] >=1 && $authorMatch['level'] <= 3){
$author = Staff::getDataStore($authorMatch['id']*1);
$dataStoresMatch = RedBean::getAll($sqlQuery, [':query' => "%" .$query . "%",':queryAtBeginning' => $query . "%"] );
$list = [];
foreach($dataStoresMatch as $dataStoreMatch) {
if(!$searchUser && $dataStoreMatch['level'] >=1 && $dataStoreMatch['level'] <= 3){
$dataStore = Staff::getDataStore($dataStoreMatch['id']*1);
} else {
$author = User::getDataStore($authorMatch['id']*1);
$dataStore = User::getDataStore($dataStoreMatch['id']*1);
}
array_push($authors, $author->toArray(true));
array_push($list, $dataStore->toArray(true));
}
Response::respondSuccess([
'authors' => $authors
'authors' => $list
]);
}
public function generateAuthorsIdQuery($query) {
if ($query){
return "WHERE name LIKE :query " . $this->generateStaffBlackListQuery() . " UNION SELECT id,name,signup_date FROM user WHERE name LIKE :query " . $this->generateUserBlackListQuery() . " ORDER BY CASE WHEN (name LIKE :queryAtBeginning) THEN 1 ELSE 2 END ASC ";
return "SELECT id,name, level FROM staff WHERE name LIKE :query " . $this->generateStaffBlackListQuery() . " UNION SELECT id,name,signup_date FROM user WHERE name LIKE :query " . $this->generateUserBlackListQuery() . " ORDER BY CASE WHEN (name LIKE :queryAtBeginning) THEN 1 ELSE 2 END ASC LIMIT 10";
} else {
return "WHERE 1=1 ". $this->generateStaffBlackListQuery() . " UNION SELECT id,name,signup_date FROM user WHERE 1=1". $this->generateUserBlackListQuery() ." ORDER BY id";
return "SELECT id,name, level FROM staff WHERE 1=1 ". $this->generateStaffBlackListQuery() . " UNION SELECT id,name,signup_date FROM user WHERE 1=1". $this->generateUserBlackListQuery() ." ORDER BY id LIMIT 10";
}
}
public function generateUsersIdQuery($query) {
if ($query){
return "SELECT id FROM user WHERE name LIKE :query " . $this->generateUserBlackListQuery() . " ORDER BY CASE WHEN (name LIKE :queryAtBeginning) THEN 1 ELSE 2 END ASC LIMIT 10";
} else {
return "SELECT id FROM user WHERE 1=1 ". $this->generateUserBlackListQuery() ." ORDER BY id LIMIT 10";
}
}
public function generateStaffBlackListQuery(){
$StaffBlackList = $this->getBlackListFiltered();
return $this->generateBlackListQuery($StaffBlackList);

View File

@ -27,6 +27,7 @@ DataValidator::with('CustomValidations', true);
* @apiParam {Number} page The number of the page of the tickets.
* @apiParam {Object} orderBy A object {value, asc}with string and boolean to make a especific order of the search.
* @apiParam {Number[]} owners The ids of the owners to make a custom search.
* @apiParam {Boolean} supervisor Boolean to deteminate if a super-user is making the call.
*
* @apiUse NO_PERMISSION
* @apiUse INVALID_TAG_FILTER
@ -48,6 +49,7 @@ DataValidator::with('CustomValidations', true);
class SearchController extends Controller {
const PATH = '/search';
const METHOD = 'POST';
private $ignoreDeparmentFilter;
public function validations() {
return [
@ -102,6 +104,7 @@ class SearchController extends Controller {
}
public function handler() {
$this->ignoreDeparmentFilter = (bool)Controller::request('supervisor');
$allowedDepartmentsId = [];
foreach (Controller::getLoggedUser()->sharedDepartmentList->toArray() as $department) {
@ -124,7 +127,6 @@ class SearchController extends Controller {
'staffId' => Controller::getLoggedUser()->id
];
$query = $this->getSQLQuery($inputs);
$queryWithOrder = $this->getSQLQueryWithOrder($inputs);
$totalCount = RedBean::getAll("SELECT COUNT(*) FROM (SELECT COUNT(*) " . $query . " ) AS T2", [':query' => "%" . $inputs['query'] . "%", ':queryAtBeginning' => $inputs['query'] . "%" ])[0]['COUNT(*)'];
@ -180,7 +182,7 @@ class SearchController extends Controller {
if(array_key_exists('unreadStaff',$inputs)) $this->setSeenFilter($inputs['unreadStaff'], $filters);
if(array_key_exists('dateRange',$inputs)) $this->setDateFilter($inputs['dateRange'], $filters);
if(array_key_exists('departments',$inputs) && array_key_exists('allowedDepartments',$inputs) && array_key_exists('staffId',$inputs)){
$this->setDepartmentFilter($inputs['departments'],$inputs['allowedDepartments'], $inputs['staffId'], $filters);
if(!$this->ignoreDeparmentFilter) $this->setDepartmentFilter($inputs['departments'],$inputs['allowedDepartments'], $inputs['staffId'], $filters);
}
if(array_key_exists('authors',$inputs)) $this->setAuthorFilter($inputs['authors'], $filters);
if(array_key_exists('owners',$inputs)) $this->setOwnerFilter($inputs['owners'], $filters);

View File

@ -22,5 +22,7 @@ $userControllers->addController(new VerifyController);
$userControllers->addController(new EnableUserController);
$userControllers->addController(new DisableUserController);
$userControllers->addController(new EditCustomFieldsController);
$userControllers->addController(new EditSupervisedListController);
$userControllers->addController(new GetSupervisedTicketController);
$userControllers->finalize();

View File

@ -0,0 +1,81 @@
<?php
use Respect\Validation\Validator as DataValidator;
/**
* @api {post} /user/edit-supervised-list Edit user list
* @apiVersion 4.7.0
*
* @apiName Edit user list
*
* @apiGroup User
*
* @apiDescription This path edits the user list of a user.
*
* @apiPermission staff1
*
* @apiParam {Number[]} userIdList The ids of the users.
* @apiParam {Number} userId Id of the supervisor user.
*
* @apiUse NO_PERMISSION
* @apiUse INVALID_LIST
* @apiUse INVALID_USER
* @apiUse SUPERVISOR_CAN_NOT_SUPERVISE_HIMSELF
*
* @apiSuccess {Object} data Empty object
*
*/
class EditSupervisedListController extends Controller {
const PATH = '/edit-supervised-list';
const METHOD = 'POST';
public function validations() {
return [
'permission' => 'staff_1',
'requestData' => [
'userIdList' => [
'validation' => DataValidator::validUsersId(),
'error' => ERRORS::INVALID_LIST
],
'userId' => [
'validation' => DataValidator::dataStoreId('user'),
'error' => ERRORS::INVALID_USER
]
]
];
}
public function handler() {
$userIdList = $this->getUserIdListCleared();
$superUser = User::getDataStore(Controller::request('userId'));
if(!$superUser->supervisedrelation) {
$superUser->supervisedrelation = new Supervisedrelation();
}
$superUser->supervisedrelation->sharedUserList->clear();
foreach($userIdList as $userId) {
$user = User::getDataStore($userId);
$superUser->supervisedrelation->sharedUserList->add($user);
}
$superUser->supervisedrelation->store();
$superUser->store();
Response::respondSuccess();
}
public function getUserIdListCleared(){
$clearedList = array_unique(json_decode(Controller::request('userIdList')));
$superUser = User::getDataStore(Controller::request('userId'));
foreach ($clearedList as $item) {
if($item == $superUser->id) throw new Exception(ERRORS::SUPERVISOR_CAN_NOT_SUPERVISE_HIMSELF);
}
return $clearedList;
}
}

View File

@ -0,0 +1,115 @@
<?php
use Respect\Validation\Validator as DataValidator;
/**
* @api {post} /user/get-supervised-tickets Get supervised tickets
* @apiVersion 4.8.0
*
* @apiName Get supervised tickets
*
* @apiGroup User
*
* @apiDescription This path retrieves own and user supervisated tickets.
*
* @apiPermission user
*
* @apiParam {id[]} supervisedUsers arrays of users Ids.
* @apiParam {boolean} showOwnTickets boolean to show or not current logged user tickets.
* @apiParam {Number} page The number of the page of the tickets.
*
* @apiUse NO_PERMISSION
* @apiUse INVALID_SUPERVISED_USERS
*
* @apiSuccess {Object} data Information about a tickets and quantity of pages.
* @apiSuccess {[Ticket](#api-Data_Structures-ObjectTicket)[]} data.tickets Array of tickets assigned to the staff of the current page.
* @apiSuccess {Number} data.page Number of current page.
* @apiSuccess {Number} data.pages Quantity of pages.
*
*/
class GetSupervisedTicketController extends Controller {
const PATH = '/get-supervised-tickets';
const METHOD = 'POST';
public function validations() {
return [
'permission' => 'user',
'requestData' => [
'supervisedUsers' => [
'validation' => DataValidator::oneOf(DataValidator::validUsersId(),DataValidator::nullType()),
'error' => ERRORS:: INVALID_SUPERVISED_USERS
],
'page' => [
'validation' => DataValidator::oneOf(DataValidator::numeric()->positive(),DataValidator::nullType()),
'error' => ERRORS::INVALID_PAGE
]
]
];
}
private $authors;
private $page;
private $showOwnTickets;
private $supervisedUserList;
public function handler() {
$this->page = Controller::request('page') ? Controller::request('page') : 1;
$this->showOwnTickets = (bool)Controller::request('showOwnTickets');
$this->supervisedUserList = Controller::request('supervisedUsers')? json_decode(Controller::request('supervisedUsers')) : [];
$this->authors = $this->createAuthorsArray();
if(!$this->canUserHandleSupervisedUsers()){
throw new Exception(ERRORS::INVALID_SUPERVISED_USERS);
}
$searchController = new SearchController(true);
Controller::setDataRequester(function ($key) {
switch ($key) {
case 'authors':
return json_encode($this->authors);
case 'page' :
return $this->page*1;
case 'supervisor':
return 1;
}
return null;
});
if(empty($this->authors)) {
Response::respondSuccess([]);
}else{
$searchController->handler();
}
}
public function canUserHandleSupervisedUsers() {
$user = Controller::getLoggedUser();
if(!$user->supervisedrelation && $this->supervisedUserList) return false;
if(!empty($this->supervisedUserList)){
foreach($this->supervisedUserList as $supevisedUser) {
if(!$user->supervisedrelation->sharedUserList->includesId($supevisedUser) && $supevisedUser != $user->id){
return false;
}
}
}
return true;
}
public function createAuthorsArray(){
$user = Controller::getLoggedUser();
$authors = [];
if(!empty($this->supervisedUserList)){
foreach(array_unique($this->supervisedUserList) as $supervised){
array_push($authors,['id'=> $supervised,'isStaff'=> 0]);
}
};
if(!in_array( $user->id, $this->supervisedUserList) && $this->showOwnTickets){
array_push($authors,['id'=> $user->id*1,'isStaff'=> 0]);
}
return $authors;
}
}

View File

@ -57,7 +57,6 @@ class GetUserByIdController extends Controller {
$tickets->add($ticket);
}
}
Response::respondSuccess([
'name' => $user->name,
'email' => $user->email,
@ -66,6 +65,7 @@ class GetUserByIdController extends Controller {
'verified' => !$user->verificationToken,
'disabled' => !!$user->disabled,
'customfields' => $user->xownCustomfieldvalueList->toArray(),
'userList' => $user->supervisedrelation ? $user->supervisedrelation->sharedUserList->toArray() : []
]);
}
}

View File

@ -38,6 +38,7 @@ class GetUserController extends Controller {
}
public function handler() {
if (Controller::isStaffLogged()) {
throw new RequestException(ERRORS::INVALID_CREDENTIALS);
return;
@ -50,7 +51,7 @@ class GetUserController extends Controller {
foreach($ticketList as $ticket) {
$parsedTicketList[] = $ticket->toArray(true);
}
Response::respondSuccess([
'name' => $user->name,
'email' => $user->email,
@ -58,6 +59,7 @@ class GetUserController extends Controller {
'verified' => !$user->verificationToken,
'tickets' => $parsedTicketList,
'customfields' => $user->xownCustomfieldvalueList->toArray(),
'users' => $user->supervisedrelation ? $user->supervisedrelation->sharedUserList->toArray() : null
]);
}
}

View File

@ -267,6 +267,10 @@
* @apiDefine DEFAULT_DEPARTMENT_CAN_NOT_BE_PRIVATE
* @apiError {String} DEFAULT_DEPARTMENT_CAN_NOT_BE_PRIVATE Default Department can not be private
*/
/**
* @apiDefine SUPERVISOR_CAN_NOT_SUPERVISE_HIMSELF
* @apiError {String} SUPERVISOR_CAN_NOT_SUPERVISE_HIMSELF The supervisor cannot select himself
*/
/**
* @apiDefine EMAIL_POLLING
* @apiError {String} EMAIL_POLLING Email polling was unsuccesful
@ -314,6 +318,10 @@
/**
* @apiDefine REGISTRATION_IS_DESACTIVATED
* @apiError {String} REGISTRATION_IS_DESACTIVATED Registration is disactivated
*/
/**
* @apiDefine INVALID_SUPERVISED_USERS
* @apiError {String} INVALID_SUPERVISED_USERS supervised users are invalid
*/
class ERRORS {
@ -386,6 +394,7 @@ class ERRORS {
const INVALID_TEXT_3 = 'INVALID_TEXT_3';
const DEPARTMENT_PRIVATE_TICKETS = 'DEPARTMENT_PRIVATE_TICKETS';
const DEFAULT_DEPARTMENT_CAN_NOT_BE_PRIVATE = 'DEFAULT_DEPARTMENT_CAN_NOT_BE_PRIVATE';
const SUPERVISOR_CAN_NOT_SUPERVISE_HIMSELF = 'SUPERVISOR_CAN_NOT_SUPERVISE_HIMSELF';
const EMAIL_POLLING = 'EMAIL_POLLING';
const CUSTOM_FIELD_ALREADY_EXISTS = 'CUSTOM_FIELD_ALREADY_EXISTS';
const INVALID_CUSTOM_FIELD = 'INVALID_CUSTOM_FIELD';
@ -397,4 +406,6 @@ class ERRORS {
const INVALID_API_KEY_TYPE = 'INVALID_API_KEY_TYPE';
const MANDATORY_LOGIN_IS_DESACTIVATED = 'MANDATORY_LOGIN_IS_DESACTIVATED';
const REGISTRATION_IS_DESACTIVATED = 'REGISTRATION_IS_DESACTIVATED';
const INVALID_SUPERVISED_USERS = 'INVALID_SUPERVISED_USERS';
const INVALID_USER_SEARCH_OPTION = 'INVALID_USER_SEARCH_OPTION';
}

View File

@ -52,6 +52,12 @@ class DataStoreList implements IteratorAggregate {
return empty($this->list);
}
public function clear() {
foreach($this->list as $index=>$item) {
unset($this->list[$index]);
}
}
public function toBeanList() {
$beanList = [];
@ -62,7 +68,7 @@ class DataStoreList implements IteratorAggregate {
return $beanList;
}
public function toArray($minimized = false) {
$array = [];

View File

@ -0,0 +1,18 @@
<?php
namespace CustomValidations;
use Respect\Validation\Rules\AbstractRule;
class ValidUsersId extends AbstractRule {
public function validate($userIdList) {
if(is_array(json_decode($userIdList))){
foreach (json_decode($userIdList) as $userItem) {
$author = \User::getDataStore($userItem);
if($author->isNull()) return false;
}
return true;
}
return false;
}
}

View File

@ -130,7 +130,7 @@ abstract class DataStore {
$parsedProp = $this->_bean[$prop];
}
if (strpos($prop, 'List')) {
if (strpos($prop, 'List') || strpos($prop, 'shared') === 0) {
$parsedProp = DataStoreList::getList($this->getListType($prop), $parsedProp);
} else if ($parsedProp instanceof \RedBeanPHP\OODBBean) {
$beanType = ucfirst($parsedProp->getPropertiesAndType()[1]);

View File

@ -0,0 +1,20 @@
<?php
class Supervisedrelation extends DataStore {
const TABLE = 'supervisedrelation';
public static function getProps() {
return[
'sharedUserList'
];
}
public function getDefaultProps() {
return array();
}
public function toArray() {
return [
'supervisedrelation' => $this->sharedUserList
];
}
}

View File

@ -33,7 +33,9 @@ class User extends DataStore {
'verificationToken',
'disabled',
'xownCustomfieldvalueList',
'notRegistered'
'notRegistered',
'sharedUserList',
'supervisedrelation'
];
}
@ -47,12 +49,17 @@ class User extends DataStore {
public function canManageTicket(Ticket $ticket){
$ticketNumberInstanceValidation = true;
$ticketOfSupervisedUser = false;
if($this->ticketNumber) {
$ticketNumberInstanceValidation = $this->ticketNumber == $ticket->ticketNumber;
}
return ($ticket->isAuthor($this) && $ticketNumberInstanceValidation);
if($this->supervisedrelation){
foreach( $this->supervisedrelation->sharedUserList as $user){
if($ticket->isAuthor($user)) $ticketOfSupervisedUser = true;
}
}
return (($ticket->isAuthor($this) || $ticketOfSupervisedUser) && $ticketNumberInstanceValidation);
}
public function toArray($minimal = false) {
@ -72,7 +79,8 @@ class User extends DataStore {
'verified' => !$this->verificationToken,
'disabled' => $this->disabled,
'customfields' => $this->xownCustomfieldvalueList->toArray(),
'notRegistered' => $this->notRegistered
'notRegistered' => $this->notRegistered,
'supervisedrelation' => $this->supervisedrelation
];
}
}

View File

@ -77,4 +77,6 @@ require './ticket/search.rb'
require './ticket/get-authors.rb'
require './system/mandatory-login.rb'
require './system/default-department.rb'
require './user/edit-supervised-list.rb'
require './user/get-supervised-tickets.rb'
# require './system/get-stats.rb'

View File

@ -0,0 +1,205 @@
describe '/staff/supervisor-user-list' do
request('/user/logout')
Scripts.createUser('supervisor@opensupports.com', 'passwordOfSupervisor', 'Supervisor Guy')
Scripts.createUser('usersupervised1@opensupports.com', 'usersupervised1', 'supervised Guy1')
Scripts.createUser('usersupervised2@opensupports.com', 'usersupervised2', 'supervised Guy2')
Scripts.createUser('usersupervised3@opensupports.com', 'usersupervised3', 'supervised Guy3')
Scripts.login('supervisor@opensupports.com', 'passwordOfSupervisor')
Scripts.createTicket('titlecreateadbySUpervisor','contentoftitlecreateadbySUpervisor',1)
Scripts.logout()
Scripts.login('usersupervised1@opensupports.com', 'usersupervised1')
Scripts.createTicket('titlecreateadbyusersupervised1','contentoftitlecreateadbyusersupervised1',3)
Scripts.logout()
Scripts.login('usersupervised2@opensupports.com', 'usersupervised2')
Scripts.createTicket('titlecreateadbyusersupervised2','contentoftitlecreateadbyusersupervised2',1)
Scripts.logout()
Scripts.login('usersupervised3@opensupports.com', 'usersupervised3')
Scripts.createTicket('titlecreateadbyusersupervised3','contentoftitlecreateadbyusersupervised3',1)
Scripts.logout()
supervisor = $database.getRow('user', 'supervisor@opensupports.com', 'email')
user1 = $database.getRow('user', 'usersupervised1@opensupports.com', 'email')
user2 = $database.getRow('user', 'usersupervised2@opensupports.com', 'email')
user3 = $database.getRow('user', 'usersupervised3@opensupports.com', 'email')
ticketsupervisor = $database.getRow('ticket', 'titlecreateadbySUpervisor', 'title')
ticketuser1 = $database.getRow('ticket', 'titlecreateadbyusersupervised1', 'title')
ticketuser2 = $database.getRow('ticket', 'titlecreateadbyusersupervised2', 'title')
ticketuser3 = $database.getRow('ticket', 'titlecreateadbyusersupervised3', 'title')
it'should fail if a no-staff tryes to make the request'do
request('/user/logout')
Scripts.login('supervisor@opensupports.com', 'passwordOfSupervisor')
result = request('/user/edit-supervised-list', {
userIdList: "[30,31,32]",
userId: supervisor['id'],
csrf_userid: $csrf_userid,
csrf_token: $csrf_token
})
(result['status']).should.equal('fail')
(result['message']).should.equal('NO_PERMISSION')
end
it 'should fail if userIdList is wrong' do
request('/user/logout')
Scripts.login($staff[:email], $staff[:password], true)
result = request('/user/edit-supervised-list', {
userIdList: "1",
userId: supervisor['id'],
csrf_userid: $csrf_userid,
csrf_token: $csrf_token
})
(result['status']).should.equal('fail')
(result['message']).should.equal('INVALID_LIST')
result = request('/user/edit-supervised-list', {
userIdList: "array",
userId: supervisor['id'],
csrf_userid: $csrf_userid,
csrf_token: $csrf_token
})
(result['status']).should.equal('fail')
(result['message']).should.equal('INVALID_LIST')
result = request('/user/edit-supervised-list', {
userIdList: "[30,31,32,666666]",
userId: supervisor['id'],
csrf_userid: $csrf_userid,
csrf_token: $csrf_token
})
(result['status']).should.equal('fail')
(result['message']).should.equal('INVALID_LIST')
end
it'should fail if userId is wrong'do
request('/user/logout')
Scripts.login($staff[:email], $staff[:password], true)
result = request('/user/edit-supervised-list', {
userIdList: "[30,31,32]",
userId: 666,
csrf_userid: $csrf_userid,
csrf_token: $csrf_token
})
(result['status']).should.equal('fail')
(result['message']).should.equal('INVALID_USER')
end
it'should fail if supervisor is included in user-id-List'do
request('/user/logout')
Scripts.login($staff[:email], $staff[:password], true)
result = request('/user/edit-supervised-list', {
userIdList: "[30,31,29]",
userId: supervisor['id'],
csrf_userid: $csrf_userid,
csrf_token: $csrf_token
})
(result['status']).should.equal('fail')
(result['message']).should.equal('SUPERVISOR_CAN_NOT_SUPERVISE_HIMSELF')
end
it'should create supervisor user'do
request('/user/logout')
Scripts.login($staff[:email], $staff[:password], true)
result = request('/user/edit-supervised-list', {
userIdList: "[30,31,32]",
userId: supervisor['id'],
csrf_userid: $csrf_userid,
csrf_token: $csrf_token
})
(result['status']).should.equal('success')
end
it 'should allow supervisor to access tickets from supervisated users' do
request('/user/logout')
Scripts.login('supervisor@opensupports.com', 'passwordOfSupervisor')
result = request('/ticket/get', {
ticketNumber: ticketsupervisor['ticket_number'],
csrf_userid: $csrf_userid,
csrf_token: $csrf_token
})
(result['status']).should.equal('success')
result = request('/ticket/get', {
ticketNumber: ticketuser1['ticket_number'],
csrf_userid: $csrf_userid,
csrf_token: $csrf_token
})
(result['status']).should.equal('success')
result = request('/ticket/get', {
ticketNumber: ticketuser2['ticket_number'],
csrf_userid: $csrf_userid,
csrf_token: $csrf_token
})
(result['status']).should.equal('success')
result = request('/ticket/get', {
ticketNumber: ticketuser3['ticket_number'],
csrf_userid: $csrf_userid,
csrf_token: $csrf_token
})
(result['status']).should.equal('success')
end
it 'should allow supervisor see only the new user list' do
request('/user/logout')
Scripts.login($staff[:email], $staff[:password], true)
request('/user/edit-supervised-list', {
userIdList: "[30]",
userId: supervisor['id'],
csrf_userid: $csrf_userid,
csrf_token: $csrf_token
})
request('/user/logout')
Scripts.login('supervisor@opensupports.com', 'passwordOfSupervisor')
result = request('/ticket/get', {
ticketNumber: ticketsupervisor['ticket_number'],
csrf_userid: $csrf_userid,
csrf_token: $csrf_token
})
(result['status']).should.equal('success')
result = request('/ticket/get', {
ticketNumber: ticketuser1['ticket_number'],
csrf_userid: $csrf_userid,
csrf_token: $csrf_token
})
(result['status']).should.equal('success')
result = request('/ticket/get', {
ticketNumber: ticketuser2['ticket_number'],
csrf_userid: $csrf_userid,
csrf_token: $csrf_token
})
(result['status']).should.equal('fail')
result = request('/ticket/get', {
ticketNumber: ticketuser3['ticket_number'],
csrf_userid: $csrf_userid,
csrf_token: $csrf_token
})
(result['status']).should.equal('fail')
end
end

View File

@ -0,0 +1,231 @@
describe '/user/get-supervised-tickets' do
request('/user/logout')
supervisor = $database.getRow('user', 'supervisor@opensupports.com', 'email')
user1 = $database.getRow('user', 'usersupervised1@opensupports.com', 'email')
user2 = $database.getRow('user', 'usersupervised2@opensupports.com', 'email')
ticketsupervisor = $database.getRow('ticket', 'titlecreateadbySUpervisor', 'title')
ticketuser1 = $database.getRow('ticket', 'titlecreateadbyusersupervised1', 'title')
ticketuser2 = $database.getRow('ticket', 'titlecreateadbyusersupervised2', 'title')
ticketuser3 = $database.getRow('ticket', 'titlecreateadbyusersupervised3', 'title')
it 'should fail if supervised users are not valid' do
request('/user/logout')
Scripts.login($staff[:email], $staff[:password], true)
result = request('/user/edit-supervised-list', {
userIdList: "[30,32,31]",
userId: supervisor['id'],
csrf_userid: $csrf_userid,
csrf_token: $csrf_token
})
(result['status']).should.equal('success')
request('/user/logout')
Scripts.login('supervisor@opensupports.com', 'passwordOfSupervisor')
result = request('/user/get-supervised-tickets', {
supervisedUsers: "[1000,30]",
showOwnTickets: 1,
page: 1,
csrf_userid: $csrf_userid,
csrf_token: $csrf_token
})
(result['status']).should.equal('fail')
(result['message']).should.equal('INVALID_SUPERVISED_USERS')
result = request('/user/get-supervised-tickets', {
supervisedUsers: "[32,30,1]",
showOwnTickets: 1,
page: 1,
csrf_userid: $csrf_userid,
csrf_token: $csrf_token
})
(result['status']).should.equal('fail')
(result['message']).should.equal('INVALID_SUPERVISED_USERS')
result = request('/user/get-supervised-tickets', {
supervisedUsers: "32",
showOwnTickets: 1,
page: 1,
csrf_userid: $csrf_userid,
csrf_token: $csrf_token
})
(result['status']).should.equal('fail')
(result['message']).should.equal('INVALID_SUPERVISED_USERS')
result = request('/user/get-supervised-tickets', {
supervisedUsers: "hello",
showOwnTickets: 1,
page: 1,
csrf_userid: $csrf_userid,
csrf_token: $csrf_token
})
(result['status']).should.equal('fail')
(result['message']).should.equal('INVALID_SUPERVISED_USERS')
result = request('/user/get-supervised-tickets', {
supervisedUsers: "[{'id' :29 , 'staff' true}]",
showOwnTickets: 1,
page: 1,
csrf_userid: $csrf_userid,
csrf_token: $csrf_token
})
(result['status']).should.equal('fail')
(result['message']).should.equal('INVALID_SUPERVISED_USERS')
end
it 'should return the tickets of the authors searched' do
result = request('/user/get-supervised-tickets', {
supervisedUsers: "[30,32,31]",
showOwnTickets: 0,
page: 1,
csrf_userid: $csrf_userid,
csrf_token: $csrf_token
})
(result['status']).should.equal('success')
(result['data']['tickets'].size).should.equal(3)
(result['data']['tickets'][0]['title']).should.equal(ticketuser3['title'])
(result['data']['tickets'][1]['title']).should.equal(ticketuser2['title'])
(result['data']['tickets'][2]['title']).should.equal(ticketuser1['title'])
end
it 'should return the tickets of the authors searched including logged user' do
result = request('/user/get-supervised-tickets', {
supervisedUsers: "[30,32]",
showOwnTickets: 1,
page: 1,
csrf_userid: $csrf_userid,
csrf_token: $csrf_token
})
(result['status']).should.equal('success')
(result['data']['tickets'].size).should.equal(3)
(result['data']['tickets'][0]['title']).should.equal(ticketuser3['title'])
(result['data']['tickets'][1]['title']).should.equal(ticketuser1['title'])
(result['data']['tickets'][2]['title']).should.equal(ticketsupervisor['title'])
result = request('/user/get-supervised-tickets', {
supervisedUsers: "[30,32,29]",
page: 1,
csrf_userid: $csrf_userid,
csrf_token: $csrf_token
})
(result['status']).should.equal('success')
(result['data']['tickets'].size).should.equal(3)
(result['data']['tickets'][0]['title']).should.equal(ticketuser3['title'])
(result['data']['tickets'][1]['title']).should.equal(ticketuser1['title'])
(result['data']['tickets'][2]['title']).should.equal(ticketsupervisor['title'])
end
it 'should return empty list if supervised users is empty and show own tickets off' do
result = request('/user/get-supervised-tickets', {
supervisedUsers: "[]",
showOwnTickets: 0,
page: 1,
csrf_userid: $csrf_userid,
csrf_token: $csrf_token
})
(result['status']).should.equal('success')
(result['data']).should.equal([])
end
it 'should works propertly if 2 supervisors has the same users' do
request('/user/logout')
Scripts.login($staff[:email], $staff[:password], true)
Scripts.createUser('supervisor2@opensupports.com', 'usersupervised2', 'supervisor Guy2')
supervisor2 = $database.getRow('user', 'supervisor2@opensupports.com', 'email')
result = request('/user/edit-supervised-list', {
userIdList: "[30,32,31]",
userId: supervisor2['id'],
csrf_userid: $csrf_userid,
csrf_token: $csrf_token
})
Scripts.login('supervisor@opensupports.com', 'passwordOfSupervisor')
result = request('/user/get-supervised-tickets', {
supervisedUsers: "[30,32,31]",
showOwnTickets: 0,
page: 1,
csrf_userid: $csrf_userid,
csrf_token: $csrf_token
})
(result['status']).should.equal('success')
(result['data']['tickets'].size).should.equal(3)
(result['data']['tickets'][0]['title']).should.equal(ticketuser3['title'])
(result['data']['tickets'][1]['title']).should.equal(ticketuser2['title'])
(result['data']['tickets'][2]['title']).should.equal(ticketuser1['title'])
Scripts.logout()
Scripts.login('supervisor2@opensupports.com', 'usersupervised2')
result = request('/user/get-supervised-tickets', {
supervisedUsers: "[30,32,31]",
showOwnTickets: 0,
page: 1,
csrf_userid: $csrf_userid,
csrf_token: $csrf_token
})
(result['status']).should.equal('success')
(result['data']['tickets'].size).should.equal(3)
(result['data']['tickets'][0]['title']).should.equal(ticketuser3['title'])
(result['data']['tickets'][1]['title']).should.equal(ticketuser2['title'])
(result['data']['tickets'][2]['title']).should.equal(ticketuser1['title'])
Scripts.logout()
end
it 'should if supervised Users tryes to handle supervisor-ticket' do
request('/user/logout')
Scripts.login('usersupervised1@opensupports.com', 'usersupervised1')
result = request('/user/get-supervised-tickets', {
supervisedUsers: "[29]",
page: 1,
csrf_userid: $csrf_userid,
csrf_token: $csrf_token
})
(result['status']).should.equal('fail')
(result['message']).should.equal('INVALID_SUPERVISED_USERS')
request('/user/logout')
Scripts.login('usersupervised2@opensupports.com', 'usersupervised2')
result = request('/user/get-supervised-tickets', {
supervisedUsers: "[29]",
page: 1,
csrf_userid: $csrf_userid,
csrf_token: $csrf_token
})
(result['status']).should.equal('fail')
(result['message']).should.equal('INVALID_SUPERVISED_USERS')
request('/user/logout')
Scripts.login('usersupervised3@opensupports.com', 'usersupervised3')
result = request('/user/get-supervised-tickets', {
supervisedUsers: "[29]",
page: 1,
csrf_userid: $csrf_userid,
csrf_token: $csrf_token
})
(result['status']).should.equal('fail')
(result['message']).should.equal('INVALID_SUPERVISED_USERS')
end
end

View File

@ -3,7 +3,7 @@ use RedBeanPHP\Facade as RedBean;
/**
* @api {OBJECT} MailTemplate MailTemplate
* @apiVersion 4.3.2
* @apiVersion 4.7
* @apiGroup Data Structures
* @apiParam {String} type The type of the mail template.
* @apiParam {String} subject The subject of the mail template.