Skip to content

Commit

Permalink
Merge pull request #482 from timeoff-management/tom-xxx-make-sort-loc…
Browse files Browse the repository at this point in the history
…ale-configurable

Add ability to define locale for sorting
  • Loading branch information
vpp authored Jul 22, 2021
2 parents 688cac3 + 3bd653f commit edc4ce4
Show file tree
Hide file tree
Showing 10 changed files with 98 additions and 85 deletions.
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,18 @@ There are some customizations available.
## How to amend or extend colours available for colour picker?
Follow instructions on [this page](docs/extend_colors_for_leave_type.md).

## Customization

There are few options to configure an installation.

### Make sorting sensitive to particular locale

Given the software could be installed for company with employees with non-English names there might be a need to
respect the alphabet while sorting customer entered content.

For that purpose the application config file has `locale_code_for_sorting` entry.
By default the value is `en` (English). One can override it with other locales such as `cs`, `fr`, `de` etc.


## Feedback

Expand Down
3 changes: 2 additions & 1 deletion config/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,6 @@
"ga_analytics_on" : false,
"crypto_secret" : "!2~`HswpPPLa22+=±§sdq qwe,appp qwwokDF_",
"application_domain" : "http://app.timeoff.management",
"promotion_website_domain" : "http://timeoff.management"
"promotion_website_domain" : "http://timeoff.management",
"locale_code_for_sorting": "en"
}
18 changes: 9 additions & 9 deletions lib/model/company/exporter/summary.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ const
users : Joi.array().required(),
});

const { sorter } = require("../../../util");

class CompanySummary {
constructor(args) {
args = Joi.attempt(
Expand Down Expand Up @@ -50,16 +52,14 @@ class CompanySummary {

self.users
// Sort users by departments and by last names
.sort(
(a,b) => (
// primary criteria is department name
departmentsMap[ a.DepartmentId ].name
.localeCompare( departmentsMap[ b.DepartmentId ].name )
||
// secondary criteria is employee's lastname
a.lastname.localeCompare( b.lastname )
.sort((a, b) => (
sorter(
departmentsMap[ a.DepartmentId ].name,
departmentsMap[ b.DepartmentId ].name
)
)
||
sorter(a.lastname, b.lastname)
))
// Get a row per every leave
.forEach(u => u.my_leaves.forEach(l => {

Expand Down
71 changes: 28 additions & 43 deletions lib/model/db/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ const
withCompanyAwareness = require('../mixin/user/company_aware'),
withAbsenceAwareness = require('../mixin/user/absence_aware');

const { sorter } = require("../../util");

const LeaveCollectionUtil = require('../leave_collection')();

module.exports = function(sequelize, DataTypes) {
Expand Down Expand Up @@ -155,52 +157,36 @@ function get_instance_methods(sequelize) {
// "promise_all_supervised_users_plus_me"
// In fact this method probably have to be ditched in favour of more granular ones
//
promise_users_I_can_manage : function(){
var this_user = this;

// Check if current user is admin, then fetch all users form company
if ( this_user.is_admin() ) {

return this_user
.getCompany({
scope : ['with_all_users'],
})
.then(function(company){
return Promise.resolve( company.users );
});
}
promise_users_I_can_manage : async function(){
const self = this;

// If current user has any departments under supervision then get
// all users from those departments plus user himself,
// if no supervised users an array with only current user is returned
return this_user.promise_supervised_departments()
.then(function(departments){
var users = _.flatten(
_.map(
departments,
function(department){ return department.users; }
)
);
let users = [];

// Make sure current user is considered as well
users.push(this_user);
if ( self.is_admin() ) {
// Check if current user is admin, then fetch all users form company
const company = await self.getCompany({
scope : ['with_all_users'],
});

// Remove duplicates
users = _.uniq(
users,
function(user){ return user.id; }
);
users = company.users;

// Order by last name
users = _.sortBy(
users,
function(user){ return user.lastname; }
);
} else {
// If current user has any departments under supervision then get
// all users from those departments plus user himself,
// if no supervised users an array with only current user is returned
const departments = await self.promise_supervised_departments();

return users;
});
users = departments.map(({users}) => users).flat();
}

// Make sure current user is considered as well
users.push(self);

}, // promise_users_I_can_manage
users = _.uniq(users, ({id}) => id);
users = users.sort((a, b) => sorter(a.lastname, b.lastname));

return users;
},

/*
* Return user's boss, the head of department user belongs to
Expand Down Expand Up @@ -301,11 +287,10 @@ function get_instance_methods(sequelize) {
self.get_company_with_all_leave_types(),
self.promise_schedule_I_obey(),
function(users, company, schedule){
const supervisedUsers = users || [];
self.supervised_users = supervisedUsers.sort((a,b) => a.name.localeCompare(b.name));
self.supervised_users = users || [];
self.company = company;

// Note: we do not do anithing with scheduler as "promise_schedule_I_obey"
// Note: we do not do anything with scheduler as "promise_schedule_I_obey"
// sets the "cached_schedule" attribute under the hood, which is used in
// synchronous code afterwards. Yes... it is silly, but it is where we are
// at thi moment after mixing non blocking and blocking code together...
Expand Down
10 changes: 5 additions & 5 deletions lib/model/mixin/user/absence_aware.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@

'use strict';

const { sorter } = require('../../../util');

var
_ = require('underscore'),
Promise = require("bluebird"),
Expand Down Expand Up @@ -459,18 +461,16 @@ module.exports = function(sequelize){
}
);

statistic_arr = _.sortBy(
statistic_arr,
'days_taken'
)
statistic_arr = statistic_arr
.sort((a,b) => sorter(a.days_taken, b.days_taken))
.reverse();


if (limit_by_top) {
statistic_arr = _.first(statistic_arr, 4);
}

return _.sortBy(statistic_arr, function(rec){ return rec.leave_type.name; });
return statistic_arr.sort((a,b) => sorter(a.leave_type.name, b.leave_type.name));
},


Expand Down
15 changes: 9 additions & 6 deletions lib/model/team_view.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const
Joi = require('joi'),
Exception = require('../error'),
_ = require('underscore');
const { sorter } = require('../util');

function TeamView(args) {
var me = this;
Expand Down Expand Up @@ -62,7 +63,7 @@ TeamView.prototype.promise_team_view_details = function(args){

// Copy all available departments for current user into closured variable
// to pass it into template
related_departments = _.sortBy(supervised_departments, 'name');
related_departments = supervised_departments.sort((a, b) => sorter(a.name, b.name))

// Find out what particular department is active now
if (current_department_id) {
Expand Down Expand Up @@ -99,10 +100,12 @@ TeamView.prototype.promise_team_view_details = function(args){

return promise_users_and_leaves.then(users_and_leaves => {

users_and_leaves = _.sortBy(
_.flatten(users_and_leaves),
item => item.user.lastname + item.user.name
);
users_and_leaves = users_and_leaves
.flat(Infinity)
.sort((a,b) => sorter(
a.user.lastname + a.user.name,
b.user.lastname + b.user.name
));

return Promise.resolve({
users_and_leaves : users_and_leaves,
Expand Down Expand Up @@ -204,7 +207,7 @@ TeamView.prototype.inject_statistics = function(args){
// format for rendering to end users
pretty_version : leave_types
// Sort by name
.sort((a,b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0)
.sort((a,b) => sorter(a.name, b.name))
.map(lt => ({ name : lt.name, stat : leave_type_stat[ lt.id ], id : lt.id })),
}
};
Expand Down
15 changes: 8 additions & 7 deletions lib/route/reports.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const
_ = require('underscore');

const { fetchLeavesForLeavesReport } = require('../model/Report');
const { sorter } = require('../util');

// Make sure that current user is authorized to deal with settings
router.all(/.*/, require('../middleware/ensure_user_is_admin'));
Expand Down Expand Up @@ -204,14 +205,14 @@ const renderLeavesReportAsCsv = async ({res, company, startDate, endDate, leaves

const defaultSortAttributeForLeaveReport = 'employeeFullName';
const sortersForLeavesReport = {
employeeFullName: (a,b) => a.employeeFullName.localeCompare(b.employeeFullName),
departmentName: (a,b) => a.departmentName.localeCompare(b.departmentName),
type: (a,b) => a.type.localeCompare(b.type),
employeeFullName: (a,b) => sorter(a.employeeFullName, b.employeeFullName),
departmentName: (a,b) => sorter(a.departmentName, b.departmentName),
type: (a,b) => sorter(a.type, b.type),
startDate: (a,b) => moment.utc(a.startDate).toDate().valueOf() - moment.utc(b.startDate).toDate().valueOf(),
endDate: (a,b) => moment.utc(a.endDate).toDate().valueOf() - moment.utc(b.endDate).toDate().valueOf(),
status: (a,b) => {a.status.localeCompare(b.status)},
status: (a,b) => sorter(a.status, b.status),
createdAt: (a,b) => moment.utc(a.createdAt).toDate().valueOf() - moment.utc(b.createdAt).toDate().valueOf(),
approver: (a,b) => a.approver.localeCompare(b.approver),
approver: (a,b) => sorter(a.approver, b.approver),
};

const getSorterForLeaves = (attribute = defaultSortAttributeForLeaveReport) => {
Expand Down Expand Up @@ -275,15 +276,15 @@ router.get('/leaves/', async (req, res) => {
? company.leave_types
.map(lt => lt.toJSON())
.map(lt => ({...lt, id: `${lt.id}`}))
.sort((a,b) => a.name.localeCompare(b.name))
.sort((a,b) => sorter(a.name, b.name))
: []
),
departments: (
company.departments
? company.departments
.map(d => d.toJSON())
.map(d => ({...d, id: `${d.id}`}))
.sort((a,b) => a.name.localeCompare(b.name))
.sort((a,b) => sorter(a.name, b.name))
: []
),
});
Expand Down
23 changes: 10 additions & 13 deletions lib/route/users/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ const
EmailTransport = require('../../email'),
{getAuditCaptureForUser} = require('../../model/audit');

const { sorter } = require("../../util");

// Make sure that current user is authorized to deal with settings
router.all(/.*/, require('../../middleware/ensure_user_is_admin'));

Expand Down Expand Up @@ -788,14 +790,6 @@ router.get('/', function(req, res) {
],
},
],
order : [
[{ model : model.User, as : 'users' }, 'lastname'],
[
{ model : model.User, as : 'users' },
{ model : model.Department, as : 'department'},
model.Department.default_order_field()
],
]
})

// Make sure that objects have all necessary attributes to render page
Expand Down Expand Up @@ -862,11 +856,11 @@ router.get('/', function(req, res) {
)

// We are moving away from passing complex objects into templates
// for callting complicated methods from within templates
// Now only basic simple objects to be sent over to tamples,
// for calling complicated methods from within templates
// Now only basic simple objects to be sent over to the template,
// all preparation to be done before rendering.
//
// So prepare special rendering datastructure here
// So prepare special rendering data structure here
.then(args => promise_user_list_data_for_rendering(args))

.then(function(args){
Expand Down Expand Up @@ -897,7 +891,7 @@ function promise_user_list_data_for_rendering(args) {
company = args[0],
users_info = args[1];

let users_info_for_rendering = users_info.map(ui => ({
const usersInfoForRendering = users_info.map(ui => ({
user_id : ui.user_row.id,
user_email : ui.user_row.email,
user_name : ui.user_row.name,
Expand All @@ -911,7 +905,10 @@ function promise_user_list_data_for_rendering(args) {
is_active : ui.user_row.is_active(),
}));

return Promise.resolve([company, users_info_for_rendering]);
const sortedUsersInfoForRendering = usersInfoForRendering
.sort((a, b) => sorter(a.user_full_name, b.user_full_name));

return Promise.resolve([company, sortedUsersInfoForRendering]);
}

function users_list_as_csv(args) {
Expand Down
14 changes: 14 additions & 0 deletions lib/util/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@


const config = require('../config');

const defaultLocale = config.get("locale_code_for_sorting") || "en";

/**
* Local aware comparator to be used as compare function for Array's `sort` function
*/
const sorter = (a, b) => String(a).localeCompare(String(b), defaultLocale);

module.exports = {
sorter,
};
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"private": false,
"description": "Simple yet powerful absence management software for small and medium size business",
"engines": {
"node": ">=12.18.0"
"node": ">=13.0.0"
},
"dependencies": {
"bluebird": "^2.10.2",
Expand Down

0 comments on commit edc4ce4

Please sign in to comment.