Compare commits
No commits in common. "master" and "gruppDM" have entirely different histories.
12 changed files with 7 additions and 403 deletions
|
@ -47,8 +47,6 @@ type Database interface {
|
||||||
GetUserName(id int) (string, error)
|
GetUserName(id int) (string, error)
|
||||||
UnsignWeeklyReport(reportId int, projectManagerId int) error
|
UnsignWeeklyReport(reportId int, projectManagerId int) error
|
||||||
DeleteReport(reportID int) error
|
DeleteReport(reportID int) error
|
||||||
ChangeProjectName(projectName string, newProjectName string) error
|
|
||||||
ChangeUserPassword(username string, password string) error
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// This struct is a wrapper type that holds the database connection
|
// This struct is a wrapper type that holds the database connection
|
||||||
|
@ -672,14 +670,3 @@ func (d *Db) DeleteReport(reportID int) error {
|
||||||
_, err := d.Exec("DELETE FROM weekly_reports WHERE report_id = ?", reportID)
|
_, err := d.Exec("DELETE FROM weekly_reports WHERE report_id = ?", reportID)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// ChangeProjectName is a handler that changes the name of a project
|
|
||||||
func (d *Db) ChangeProjectName(projectName string, newProjectName string) error {
|
|
||||||
_, err := d.Exec("UPDATE projects SET name = ? WHERE name = ?", newProjectName, projectName)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Db) ChangeUserPassword(username string, password string) error {
|
|
||||||
_, err := d.Exec("UPDATE users SET password = ? WHERE username = ?", password, username)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
|
@ -1092,53 +1092,3 @@ func TestDeleteReport(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestChangeProjectName(t *testing.T) {
|
|
||||||
db, err := setupAdvancedState()
|
|
||||||
if err != nil {
|
|
||||||
t.Error("setupState failed:", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Promote user to Admin
|
|
||||||
err = db.PromoteToAdmin("demouser")
|
|
||||||
if err != nil {
|
|
||||||
t.Error("PromoteToAdmin failed:", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Change project name
|
|
||||||
err = db.ChangeProjectName("projecttest", "newprojectname")
|
|
||||||
if err != nil {
|
|
||||||
t.Error("ChangeProjectName failed:", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the project name was changed
|
|
||||||
projects, err := db.GetAllProjects()
|
|
||||||
if err != nil {
|
|
||||||
t.Error("GetAllProjects failed:", err)
|
|
||||||
}
|
|
||||||
if projects[0].Name != "newprojectname" {
|
|
||||||
t.Error("ChangeProjectName failed: expected newprojectname, got", projects[0].Name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestChangeUserPassword(t *testing.T) {
|
|
||||||
db, err := setupState()
|
|
||||||
if err != nil {
|
|
||||||
t.Error("setupState failed:", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add a user
|
|
||||||
_ = db.AddUser("testuser", "password")
|
|
||||||
|
|
||||||
// Change user password
|
|
||||||
err = db.ChangeUserPassword("testuser", "newpassword")
|
|
||||||
if err != nil {
|
|
||||||
t.Error("ChangeUserPassword failed:", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the password was changed
|
|
||||||
if !db.CheckUser("testuser", "newpassword") {
|
|
||||||
t.Error("ChangeUserPassword failed: password not changed")
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,43 +0,0 @@
|
||||||
package projects
|
|
||||||
|
|
||||||
import (
|
|
||||||
db "ttime/internal/database"
|
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
"github.com/gofiber/fiber/v2/log"
|
|
||||||
"github.com/golang-jwt/jwt/v5"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ChangeProjectName is a handler that changes the name of a project
|
|
||||||
func ChangeProjectName(c *fiber.Ctx) error {
|
|
||||||
|
|
||||||
//check token and get username of current user
|
|
||||||
user := c.Locals("user").(*jwt.Token)
|
|
||||||
claims := user.Claims.(jwt.MapClaims)
|
|
||||||
username := claims["name"].(string)
|
|
||||||
|
|
||||||
// Extract the necessary parameters from the request
|
|
||||||
projectName := c.Params("projectName")
|
|
||||||
newProjectName := c.Query("newProjectName")
|
|
||||||
|
|
||||||
// Check if user is site admin
|
|
||||||
issiteadmin, err := db.GetDb(c).IsSiteAdmin(username)
|
|
||||||
if err != nil {
|
|
||||||
log.Warn("Error checking if siteadmin:", err)
|
|
||||||
return c.Status(500).SendString(err.Error())
|
|
||||||
} else if !issiteadmin {
|
|
||||||
log.Warn("User is not siteadmin")
|
|
||||||
return c.Status(401).SendString("User is not siteadmin")
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Perform the project name change
|
|
||||||
err = db.GetDb(c).ChangeProjectName(projectName, newProjectName)
|
|
||||||
if err != nil {
|
|
||||||
log.Warn("Error changing project name:", err)
|
|
||||||
return c.Status(500).SendString(err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return a success message
|
|
||||||
return c.Status(200).SendString("Project name changed successfully")
|
|
||||||
}
|
|
|
@ -1,42 +0,0 @@
|
||||||
package users
|
|
||||||
|
|
||||||
import (
|
|
||||||
db "ttime/internal/database"
|
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
"github.com/gofiber/fiber/v2/log"
|
|
||||||
"github.com/golang-jwt/jwt/v5"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ChangeUserPassword is a handler that changes the password of a user
|
|
||||||
func ChangeUserPassword(c *fiber.Ctx) error {
|
|
||||||
|
|
||||||
//Check token and get username of current user
|
|
||||||
user := c.Locals("user").(*jwt.Token)
|
|
||||||
claims := user.Claims.(jwt.MapClaims)
|
|
||||||
admin := claims["name"].(string)
|
|
||||||
|
|
||||||
// Extract the necessary parameters from the request
|
|
||||||
username := c.Params("username")
|
|
||||||
newPassword := c.Query("newPassword")
|
|
||||||
|
|
||||||
// Check if user is site admin
|
|
||||||
issiteadmin, err := db.GetDb(c).IsSiteAdmin(admin)
|
|
||||||
if err != nil {
|
|
||||||
log.Warn("Error checking if siteadmin:", err)
|
|
||||||
return c.Status(500).SendString(err.Error())
|
|
||||||
} else if !issiteadmin {
|
|
||||||
log.Warn("User is not siteadmin")
|
|
||||||
return c.Status(401).SendString("User is not siteadmin")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Perform the password change
|
|
||||||
err = db.GetDb(c).ChangeUserPassword(username, newPassword)
|
|
||||||
if err != nil {
|
|
||||||
log.Warn("Error changing password:", err)
|
|
||||||
return c.Status(500).SendString(err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return a success message
|
|
||||||
return c.Status(200).SendString("Password changed successfully")
|
|
||||||
}
|
|
|
@ -110,7 +110,6 @@ func main() {
|
||||||
api.Post("/promoteToAdmin", users.PromoteToAdmin)
|
api.Post("/promoteToAdmin", users.PromoteToAdmin)
|
||||||
api.Put("/changeUserName", users.ChangeUserName)
|
api.Put("/changeUserName", users.ChangeUserName)
|
||||||
api.Delete("/userdelete/:username", users.UserDelete) // Perhaps just use POST to avoid headaches
|
api.Delete("/userdelete/:username", users.UserDelete) // Perhaps just use POST to avoid headaches
|
||||||
api.Put("/changeUserPassword/:username", users.ChangeUserPassword)
|
|
||||||
|
|
||||||
// All project related routes
|
// All project related routes
|
||||||
// projectGroup := api.Group("/project") // Not currently in use
|
// projectGroup := api.Group("/project") // Not currently in use
|
||||||
|
@ -126,7 +125,6 @@ func main() {
|
||||||
api.Delete("/removeUserFromProject/:projectName", projects.RemoveUserFromProject)
|
api.Delete("/removeUserFromProject/:projectName", projects.RemoveUserFromProject)
|
||||||
api.Delete("/removeProject/:projectName", projects.RemoveProject)
|
api.Delete("/removeProject/:projectName", projects.RemoveProject)
|
||||||
api.Delete("/project/:projectID", projects.DeleteProject)
|
api.Delete("/project/:projectID", projects.DeleteProject)
|
||||||
api.Put("/changeProjectName/:projectName", projects.ChangeProjectName)
|
|
||||||
|
|
||||||
// All report related routes
|
// All report related routes
|
||||||
// reportGroup := api.Group("/report") // Not currently in use
|
// reportGroup := api.Group("/report") // Not currently in use
|
||||||
|
|
|
@ -271,30 +271,6 @@ interface API {
|
||||||
token: string,
|
token: string,
|
||||||
userName?: string,
|
userName?: string,
|
||||||
): Promise<APIResponse<Statistics>>;
|
): Promise<APIResponse<Statistics>>;
|
||||||
|
|
||||||
/**
|
|
||||||
* Changes the name of a project
|
|
||||||
* @param {string} projectName The name of the project
|
|
||||||
* @param {string} newProjectName The new name of the project
|
|
||||||
* @param {string} token The authentication token
|
|
||||||
*/
|
|
||||||
changeProjectName(
|
|
||||||
projectName: string,
|
|
||||||
newProjectName: string,
|
|
||||||
token: string,
|
|
||||||
): Promise<APIResponse<string>>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Changes the password of a user
|
|
||||||
* @param {string} username The username of the user
|
|
||||||
* @param {string} newPassword The new password
|
|
||||||
* @param {string} token The authentication token
|
|
||||||
*/
|
|
||||||
changeUserPassword(
|
|
||||||
username: string,
|
|
||||||
newPassword: string,
|
|
||||||
token: string,
|
|
||||||
): Promise<APIResponse<string>>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** An instance of the API */
|
/** An instance of the API */
|
||||||
|
@ -1030,58 +1006,4 @@ export const api: API = {
|
||||||
return { success: false, message: "Failed to get statistics" };
|
return { success: false, message: "Failed to get statistics" };
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async changeProjectName(
|
|
||||||
projectName: string,
|
|
||||||
newProjectName: string,
|
|
||||||
token: string,
|
|
||||||
): Promise<APIResponse<string>> {
|
|
||||||
try {
|
|
||||||
const response = await fetch(
|
|
||||||
`/api/changeProjectName/${projectName}?newProjectName=${newProjectName}`,
|
|
||||||
{
|
|
||||||
method: "PUT",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Authorization: "Bearer " + token,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
return { success: false, message: "Failed to change project name" };
|
|
||||||
} else {
|
|
||||||
return { success: true, message: "Project name changed" };
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
return { success: false, message: "Failed to change project name" };
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async changeUserPassword(
|
|
||||||
username: string,
|
|
||||||
newPassword: string,
|
|
||||||
token: string,
|
|
||||||
): Promise<APIResponse<string>> {
|
|
||||||
try {
|
|
||||||
const response = await fetch(
|
|
||||||
`/api/changeUserPassword/${username}?newPassword=${newPassword}`,
|
|
||||||
{
|
|
||||||
method: "PUT",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Authorization: "Bearer " + token,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
return { success: false, message: "Failed to change password" };
|
|
||||||
} else {
|
|
||||||
return { success: true, message: "Password changed" };
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
return { success: false, message: "Failed to change password" };
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,36 +0,0 @@
|
||||||
import { APIResponse, api } from "../API/API";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Changes the name of a project
|
|
||||||
* @param {string} props.projectName - Current project name
|
|
||||||
* @param {string} props.newProjectName - New project name
|
|
||||||
* @returns {void} - Nothing
|
|
||||||
*/
|
|
||||||
export default function ChangeProjectName(props: {
|
|
||||||
projectName: string;
|
|
||||||
newProjectName: string;
|
|
||||||
}): void {
|
|
||||||
if (props.projectName === "" || props.projectName === props.newProjectName) {
|
|
||||||
alert("You have to give a new name\n\nName not changed");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
api
|
|
||||||
.changeProjectName(
|
|
||||||
props.projectName,
|
|
||||||
props.newProjectName,
|
|
||||||
localStorage.getItem("accessToken") ?? "",
|
|
||||||
)
|
|
||||||
.then((response: APIResponse<string>) => {
|
|
||||||
if (response.success) {
|
|
||||||
alert("Name changed successfully");
|
|
||||||
location.reload();
|
|
||||||
} else {
|
|
||||||
alert("Name not changed, name could be taken");
|
|
||||||
console.error(response.message);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
alert("Name not changed");
|
|
||||||
console.error("An error occurred during change:", error);
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -1,36 +0,0 @@
|
||||||
import { APIResponse, api } from "../API/API";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Changes the password of a user
|
|
||||||
* @param {string} props.username - The username of the user
|
|
||||||
* @param {string} props.newPassword - The new password
|
|
||||||
* @returns {void} - Nothing
|
|
||||||
*/
|
|
||||||
export default function ChangeUserPassword(props: {
|
|
||||||
username: string;
|
|
||||||
newPassword: string;
|
|
||||||
}): void {
|
|
||||||
if (props.username === localStorage.getItem("username")) {
|
|
||||||
alert("You cannot change admin password");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
api
|
|
||||||
.changeUserPassword(
|
|
||||||
props.username,
|
|
||||||
props.newPassword,
|
|
||||||
localStorage.getItem("accessToken") ?? "",
|
|
||||||
)
|
|
||||||
.then((response: APIResponse<string>) => {
|
|
||||||
if (response.success) {
|
|
||||||
alert("Password changed successfully");
|
|
||||||
location.reload();
|
|
||||||
} else {
|
|
||||||
alert("Password not changed");
|
|
||||||
console.error(response.message);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
alert("Password not changed");
|
|
||||||
console.error("An error occurred during change:", error);
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -8,7 +8,6 @@ import InputField from "./InputField";
|
||||||
import ProjectNameInput from "./Inputs/ProjectNameInput";
|
import ProjectNameInput from "./Inputs/ProjectNameInput";
|
||||||
import { alphanumeric } from "../Data/regex";
|
import { alphanumeric } from "../Data/regex";
|
||||||
import { projNameHighLimit, projNameLowLimit } from "../Data/constants";
|
import { projNameHighLimit, projNameLowLimit } from "../Data/constants";
|
||||||
import ChangeProjectName from "./ChangeProjectName";
|
|
||||||
|
|
||||||
function ProjectInfoModal(props: {
|
function ProjectInfoModal(props: {
|
||||||
projectname: string;
|
projectname: string;
|
||||||
|
@ -51,10 +50,9 @@ function ProjectInfoModal(props: {
|
||||||
`Are you sure you want to change name of ${props.projectname} to ${newProjName}?`,
|
`Are you sure you want to change name of ${props.projectname} to ${newProjName}?`,
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
ChangeProjectName({
|
//TODO: change and insert change name functionality
|
||||||
projectName: props.projectname,
|
alert("Not implemented yet");
|
||||||
newProjectName: newProjName,
|
setNewProjName("");
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
alert("Name was not changed!");
|
alert("Name was not changed!");
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,6 @@ import {
|
||||||
usernameLowLimit,
|
usernameLowLimit,
|
||||||
usernameUpLimit,
|
usernameUpLimit,
|
||||||
} from "../Data/constants";
|
} from "../Data/constants";
|
||||||
import ChangeUserPassword from "./ChangeUserPassword";
|
|
||||||
|
|
||||||
function UserInfoModal(props: {
|
function UserInfoModal(props: {
|
||||||
isVisible: boolean;
|
isVisible: boolean;
|
||||||
|
@ -95,10 +94,9 @@ function UserInfoModal(props: {
|
||||||
if (
|
if (
|
||||||
confirm(`Are you sure you want to change password of ${props.username}?`)
|
confirm(`Are you sure you want to change password of ${props.username}?`)
|
||||||
) {
|
) {
|
||||||
ChangeUserPassword({
|
//TODO: insert change password functionality
|
||||||
username: props.username,
|
alert("Not implemented yet");
|
||||||
newPassword: newPassword,
|
setNewPassword("");
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
alert("Password was not changed!");
|
alert("Password was not changed!");
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,8 +37,6 @@ promoteToPmPath = base_url + "/api/promoteToPm"
|
||||||
unsignReportPath = base_url + "/api/unsignReport"
|
unsignReportPath = base_url + "/api/unsignReport"
|
||||||
deleteReportPath = base_url + "/api/deleteReport"
|
deleteReportPath = base_url + "/api/deleteReport"
|
||||||
getStatisticsPath = base_url + "/api/getStatistics"
|
getStatisticsPath = base_url + "/api/getStatistics"
|
||||||
changeProjectNamePath = base_url + "/api/changeProjectName"
|
|
||||||
changeUserPasswordPath = base_url + "/api/changeUserPassword"
|
|
||||||
|
|
||||||
debug_output = False
|
debug_output = False
|
||||||
|
|
||||||
|
@ -173,19 +171,3 @@ def getStatistics(token: string, projectName: string):
|
||||||
params={"projectName": projectName}
|
params={"projectName": projectName}
|
||||||
)
|
)
|
||||||
return response.json()
|
return response.json()
|
||||||
|
|
||||||
def changeProjectName(token: string, projectName: string, newProjectName: string):
|
|
||||||
response = requests.put(
|
|
||||||
changeProjectNamePath + "/" + projectName,
|
|
||||||
headers = {"Authorization": "Bearer " + token},
|
|
||||||
params={"newProjectName": newProjectName}
|
|
||||||
)
|
|
||||||
return response
|
|
||||||
|
|
||||||
def changeUserPassword(token: string, username: string, newPassword: string):
|
|
||||||
response = requests.put(
|
|
||||||
changeUserPasswordPath + "/" + username,
|
|
||||||
headers = {"Authorization": "Bearer " + token},
|
|
||||||
params={"newPassword": newPassword}
|
|
||||||
)
|
|
||||||
return response
|
|
|
@ -666,82 +666,8 @@ def test_get_statistics():
|
||||||
assert stats["totalDevelopmentTime"] == 20, "Total development time is not correct"
|
assert stats["totalDevelopmentTime"] == 20, "Total development time is not correct"
|
||||||
gprint("test_get_statistics successful")
|
gprint("test_get_statistics successful")
|
||||||
|
|
||||||
def test_project_name_change():
|
|
||||||
# Create admin
|
|
||||||
admin_username = randomString()
|
|
||||||
admin_password = randomString()
|
|
||||||
|
|
||||||
project_name = "project" + randomString()
|
|
||||||
|
|
||||||
token = register_and_login(admin_username, admin_password)
|
|
||||||
|
|
||||||
# Promote to admin
|
|
||||||
response = requests.post(
|
|
||||||
promoteToAdminPath,
|
|
||||||
json={"username": admin_username},
|
|
||||||
headers={"Authorization": "Bearer " + token},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
response = create_project(token, project_name)
|
|
||||||
assert response.status_code == 200, "Create project failed"
|
|
||||||
|
|
||||||
response = requests.get(
|
|
||||||
getUserProjectsPath + "/" + admin_username,
|
|
||||||
headers={"Authorization": "Bearer " + token},
|
|
||||||
)
|
|
||||||
|
|
||||||
dprint(response.json())
|
|
||||||
|
|
||||||
new_project_name = "new project name " + randomString()
|
|
||||||
dprint("Changing project name from ", project_name, " to ", new_project_name)
|
|
||||||
response = changeProjectName(token, project_name, new_project_name)
|
|
||||||
|
|
||||||
response = requests.get(
|
|
||||||
getUserProjectsPath + "/" + admin_username,
|
|
||||||
headers={"Authorization": "Bearer " + token},
|
|
||||||
)
|
|
||||||
|
|
||||||
dprint(response.json())
|
|
||||||
|
|
||||||
if (response.json()[0]["name"] != new_project_name):
|
|
||||||
assert False, "Project name change failed"
|
|
||||||
|
|
||||||
|
|
||||||
assert response.status_code == 200, "Project name change failed"
|
|
||||||
gprint("test_projectNameChange successful")
|
|
||||||
|
|
||||||
def test_change_user_password():
|
|
||||||
# Create admin
|
|
||||||
admin_username = randomString()
|
|
||||||
admin_password = randomString()
|
|
||||||
|
|
||||||
user = randomString()
|
|
||||||
password = randomString()
|
|
||||||
|
|
||||||
token = register_and_login(admin_username, admin_password)
|
|
||||||
|
|
||||||
# Promote to admin
|
|
||||||
response = requests.post(
|
|
||||||
promoteToAdminPath,
|
|
||||||
json={"username": admin_username},
|
|
||||||
headers={"Authorization": "Bearer " + token},
|
|
||||||
)
|
|
||||||
|
|
||||||
_ = register_and_login(user, password)
|
|
||||||
|
|
||||||
response = changeUserPassword(token, user, "new_password")
|
|
||||||
assert response.status_code == 200, "Change user password failed"
|
|
||||||
|
|
||||||
response = login(user, "new_password")
|
|
||||||
assert response.status_code == 200, "Login failed with new password"
|
|
||||||
|
|
||||||
gprint("test_change_user_password successful")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
test_change_user_password()
|
|
||||||
test_project_name_change();
|
|
||||||
test_delete_report()
|
test_delete_report()
|
||||||
test_unsign_report()
|
test_unsign_report()
|
||||||
test_promote_to_manager()
|
test_promote_to_manager()
|
||||||
|
|
Loading…
Reference in a new issue