Skip to content
Merged
50 changes: 34 additions & 16 deletions api/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ import { Router } from "express";

import db from "./db.js";
import { decideStatus } from "./functions/decideStatus.js";
import { getBoxPlotData } from "./functions/getBoxPlotData.js";
import { lookupEmail } from "./functions/lookupEmail.js";
import { processImportFiles } from "./functions/processImportFiles.js";
import { scoreNormaliser } from "./functions/scoreNormaliser.js";
import { updateDbUsers } from "./functions/updateDbUsers.js";
import { updateUsersActivity } from "./functions/updateUsersActivity.js";
import messageRouter from "./messages/messageRouter.js";
Expand Down Expand Up @@ -116,24 +118,18 @@ api.get("/users/status-counts", async (req, res) => {
const rawConfigTable = await db.query("SELECT * FROM config_table");
const configTable = rawConfigTable.rows[0];

const overalStatus = { low: 0, medium: 0, high: 0, inactive: 0 };

for (const user of allusers) {
const status = await decideStatus(
configTable,
user.user_id,
userActivities,
);
const normalisedScores = scoreNormaliser(
allusers,
userActivities,
configTable,
);

if (status.success) {
overalStatus[status.status] += 1;
} else {
logger.warn(status.message);
return res.status(404).json({});
}
}
const boxPlotData = getBoxPlotData(normalisedScores);
const totalStatus = await decideStatus(normalisedScores, configTable);

return res.status(200).json({ status: overalStatus });
return res
.status(200)
.json({ status: totalStatus, boxPlotData: boxPlotData });
} catch (error) {
res.status(500).json({ msg: "server error" });
}
Expand All @@ -157,6 +153,28 @@ api.put("/config", async (req, res) => {
!Number.isFinite(reactionsWeighting) ||
!Number.isFinite(reactionsReceivedWeighting)
) {
logger.error("input values are inavalid");
return res.status(400).json({ message: "total weight must be 100" });
}

const totalWeights =
messagesWeighting + reactionsWeighting + reactionsReceivedWeighting;

if (totalWeights !== 100) {
res.status(400).json({});
}

if (lowTreshholds + mediumTreshholds + highTreshHolds !== 100) {
logger.error("Thresholds must add up to 100.");
return res.status(400).json({});
}

if (
!(lowTreshholds < mediumTreshholds && mediumTreshholds < highTreshHolds)
) {
logger.error(
"Thresholds must be in increasing order: low < medium < high.",
);
return res.status(400).json({});
}

Expand Down
10 changes: 6 additions & 4 deletions api/functions/decideScore.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,18 +23,20 @@ export const decideScore = ({
messages,
reactions,
reactionsReceived,
messageWeight = 3,
reactionWeight = 1,
reactionsReceivedWeight = 1,
configTable,
}) => {
if (!Number.isFinite(messages)) messages = 0;
if (!Number.isFinite(reactions)) reactions = 0;
if (!Number.isFinite(reactionsReceived)) reactionsReceived = 0;

const messageWeight = configTable.message_weighting;
const reactionWeight = configTable.reactions_weighting;
const reactionsReceivedWeight = configTable.reactions_received_weighting;

if (messages <= 0 && reactions <= 0) return 0;

// if no messages then no reactions received.
if (!messages) return reactionWeight * reactions;
if (!messages) return (reactionWeight * reactions) / 100;

if (!reactions)
return (
Expand Down
130 changes: 0 additions & 130 deletions api/functions/decideScore.test.js

This file was deleted.

84 changes: 31 additions & 53 deletions api/functions/decideStatus.js
Original file line number Diff line number Diff line change
@@ -1,63 +1,41 @@
import logger from "../utils/logger.js";

import { aggregateUserActivity } from "./aggregateUserActivity.js";
import { decideScore } from "./decideScore.js";

/**
* Determines the status of a user based on their activity and the provided configuration table.
*
* This function aggregates the user's activity, calculates a score based on messages and reactions,
* and then returns a status based on predefined threshold values. If any part of the process fails,
* an error message will be returned. This is an asynchronous function.
*
* @param {Array<Object>} configTable - An array of configuration objects, each containing thresholds and weights for score calculation. retrieved from DB.
* @param {number} configTable[].low_threshold - The threshold for the "inactive" status.
* @param {number} configTable[].medium_threshold - The threshold for the "low" status.
* @param {number} configTable[].high_threshold - The threshold for the "medium" status.
* @param {number} configTable[].message_weighting - The weight given to messages in the score calculation.
* @param {number} configTable[].reactions_weighting - The weight given to reactions in the score calculation.
*
* @param {string} userId - The ID of the user whose status is to be determined.
* @param {Array<Object>} userActivity - The array of user activity objects returned from another function.
* @param {string} userActivity[].user_id - The user ID associated with the activity.
* @param {number} userActivity[].messages - The number of messages sent by the user.
* @param {number} userActivity[].reactions - The number of reactions sent by the user.
* @param {number} userActivity[].reactions_received - The number of reactions received by the user.
*
* @returns {Object} An object containing the success status and the user's activity status.
* @returns {boolean} return.success - Whether the operation was successful.
* @returns {string} return.status - The status of the user: "inactive", "low", "medium", or "high", or an error message if the process failed.
* @throws {Error} If the user activity could not be aggregated or if there is an issue with the score calculation.

* Determines the status of users based on their normalised scores.
*
* This function takes an array of normalised scores and compares each score with
* the provided thresholds in the `configTable`. It categorises users into
* different status groups: inactive, low, medium, and high activity based on
* the score ranges defined in the `configTable`.
*
* @param {number[]} normalisedScores - An array of normalised scores representing user activity levels.
* @param {Object} configTable - An object containing threshold values for determining user status.
* @param {number} configTable.low_threshold - The threshold below which users are considered inactive.
* @param {number} configTable.medium_threshold - The threshold for users to be categorised as low activity.
* @param {number} configTable.high_threshold - The threshold above which users are considered to have high activity.
*
* @returns {Object} An object representing the count of users in each activity status category:
* `inactive`, `low`, `medium`, and `high`.
* Example: `{ inactive: 5, low: 10, medium: 15, high: 20 }`.
*
* @throws {Error} Throws an error if there is an issue during the status determination process.
*/
export const decideStatus = async (configTable, userId, userActivity) => {
export const decideStatus = async (normalisedScores, configTable) => {
try {
const aggregatedActivity = aggregateUserActivity(userId, userActivity);

if (!aggregatedActivity.success) {
logger.error("activity has not been fetched correctly");
throw Error("activity has not been fetched correctly");
const finalStatus = { low: 0, medium: 0, high: 0, inactive: 0 };
for (const score of normalisedScores) {
if (score < configTable.low_threshold) {
finalStatus.inactive += 1;
} else if (score < configTable.medium_threshold) {
finalStatus.low += 1;
} else if (score < configTable.high_threshold) {
finalStatus.medium += 1;
} else {
finalStatus.high += 1;
}
}

const score = decideScore({
messages: aggregatedActivity.countActivity.messagesCount,
reactions: aggregatedActivity.countActivity.reactionsCount,
reactionsReceived:
aggregatedActivity.countActivity.reactionsReceivedCount,
messageWeight: configTable.message_weighting,
reactionWeight: configTable.reactions_weighting,
reactionsReceivedWeight: configTable.reactions_received_weighting,
});

if (score < configTable.low_threshold) {
return { success: true, status: "inactive" };
} else if (score < configTable.medium_threshold) {
return { success: true, status: "low" };
} else if (score < configTable.high_threshold) {
return { success: true, status: "medium" };
} else {
return { success: true, status: "high" };
}
return finalStatus;
} catch (error) {
logger.error(error);
throw error;
Expand Down
63 changes: 63 additions & 0 deletions api/functions/getBoxPlotData.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/**
* Calculates box plot statistics for an array of scores.
*
* This function computes the minimum, first quartile (Q1), median, third quartile (Q3),
* maximum, and outliers based on the given scores. It returns these values to give
* insight into the distribution of the data.
*
* @param {number[]} scores - An array of numerical scores to analyse.
* @returns {Object|null} An object containing min, Q1, median, Q3, max, and outliers, or null if the array is empty.
*/
export const getBoxPlotData = (scores) => {
if (!scores.length) return null;

const sorted = [...scores].sort((a, b) => a - b);
const n = sorted.length;

// Function to calculate the percentile value for a given percentile (p).
const percentile = (p) => {
// Calculate the index of the desired percentile.
const index = (p / 100) * (n - 1);

// Find the lower and upper bounds of the index.
const lower = Math.floor(index);
const upper = Math.ceil(index);

// If the lower and upper indices are the same, return the value at that index.
if (lower === upper) return sorted[lower];

// Otherwise, interpolate between the values at the lower and upper indices.
return sorted[lower] + (sorted[upper] - sorted[lower]) * (index - lower);
};

// Calculate the first quartile (Q1), median, and third quartile (Q3) percentiles.
const q1 = percentile(25);
const median = percentile(50);
const q3 = percentile(75);

// Calculate the Interquartile Range (IQR) as the difference between Q3 and Q1.
const iqr = q3 - q1;

// Calculate the minimum score, ensuring it is not lower than Q1 - 1.5 * IQR.
// This ensures that we do not count extreme outliers as part of the "min".
const min = Math.max(Math.min(...sorted), q1 - 1.5 * iqr);

// Calculate the maximum score, ensuring it is not higher than Q3 + 1.5 * IQR.
// This ensures that we do not count extreme outliers as part of the "max".
const max = Math.min(Math.max(...sorted), q3 + 1.5 * iqr);

// Identify any scores that are considered outliers, based on being lower than
// Q1 - 1.5 * IQR or higher than Q3 + 1.5 * IQR.
const outliers = sorted.filter(
(score) => score < q1 - 1.5 * iqr || score > q3 + 1.5 * iqr,
);

return {
min,
q1,
median,
q3,
max,
outliers,
};
};
Loading