2024-02-12 12:40:49 +01:00
package database
import (
2024-02-28 03:30:05 +01:00
"embed"
2024-03-17 20:30:55 +01:00
"errors"
2024-03-20 11:09:48 +01:00
"fmt"
2024-02-28 03:21:13 +01:00
"path/filepath"
2024-03-14 19:48:49 +01:00
"ttime/internal/types"
2024-02-12 12:40:49 +01:00
"github.com/jmoiron/sqlx"
2024-03-15 11:02:33 +01:00
_ "modernc.org/sqlite"
2024-02-12 12:40:49 +01:00
)
2024-02-29 20:33:20 +01:00
// Interface for the database
type Database interface {
2024-03-12 20:44:40 +01:00
// Insert a new user into the database, password should be hashed before calling
2024-02-29 20:33:20 +01:00
AddUser ( username string , password string ) error
2024-03-17 01:32:10 +01:00
CheckUser ( username string , password string ) bool
2024-02-29 20:33:20 +01:00
RemoveUser ( username string ) error
2024-03-07 13:21:47 +01:00
PromoteToAdmin ( username string ) error
2024-02-29 20:33:20 +01:00
GetUserId ( username string ) ( int , error )
AddProject ( name string , description string , username string ) error
2024-03-20 11:22:33 +01:00
DeleteProject ( name string , username string ) error
2024-03-17 14:38:20 +01:00
Migrate ( ) error
2024-03-18 22:07:02 +01:00
MigrateSampleData ( ) error
2024-03-14 13:27:57 +01:00
GetProjectId ( projectname string ) ( int , error )
2024-03-16 22:47:19 +01:00
AddWeeklyReport ( projectName string , userName string , week int , developmentTime int , meetingTime int , adminTime int , ownWorkTime int , studyTime int , testingTime int ) error
2024-03-14 13:39:56 +01:00
AddUserToProject ( username string , projectname string , role string ) error
2024-03-14 13:47:04 +01:00
ChangeUserRole ( username string , projectname string , role string ) error
2024-03-20 12:11:05 +01:00
ChangeUserName ( username string , newname string ) error
2024-03-14 16:01:56 +01:00
GetAllUsersProject ( projectname string ) ( [ ] UserProjectMember , error )
2024-03-14 16:25:54 +01:00
GetAllUsersApplication ( ) ( [ ] string , error )
2024-03-14 19:48:49 +01:00
GetProjectsForUser ( username string ) ( [ ] types . Project , error )
GetAllProjects ( ) ( [ ] types . Project , error )
2024-03-15 15:14:45 +01:00
GetProject ( projectId int ) ( types . Project , error )
2024-03-14 19:48:49 +01:00
GetUserRole ( username string , projectname string ) ( string , error )
2024-03-17 17:58:02 +01:00
GetWeeklyReport ( username string , projectName string , week int ) ( types . WeeklyReport , error )
2024-04-03 17:31:39 +02:00
GetAllWeeklyReports ( username string , projectname string ) ( [ ] types . WeeklyReportList , error )
2024-03-21 01:37:39 +01:00
GetUnsignedWeeklyReports ( projectName string ) ( [ ] types . WeeklyReport , error )
2024-03-17 20:30:55 +01:00
SignWeeklyReport ( reportId int , projectManagerId int ) error
2024-03-18 13:32:55 +01:00
IsSiteAdmin ( username string ) ( bool , error )
2024-03-19 19:14:55 +01:00
IsProjectManager ( username string , projectname string ) ( bool , error )
2024-03-20 23:49:55 +01:00
GetProjectTimes ( projectName string ) ( map [ string ] int , error )
2024-03-21 02:48:26 +01:00
UpdateWeeklyReport ( projectName string , userName string , week int , developmentTime int , meetingTime int , adminTime int , ownWorkTime int , studyTime int , testingTime int ) error
2024-03-27 21:18:44 +01:00
RemoveProject ( projectname string ) error
2024-02-29 20:33:20 +01:00
}
2024-02-27 05:00:04 +01:00
// This struct is a wrapper type that holds the database connection
// Internally DB holds a connection pool, so it's safe for concurrent use
type Db struct {
* sqlx . DB
}
2024-03-14 16:01:56 +01:00
type UserProjectMember struct {
Username string ` db:"username" `
UserRole string ` db:"p_role" `
}
2024-02-28 03:30:05 +01:00
//go:embed migrations
var scripts embed . FS
2024-03-18 22:07:02 +01:00
//go:embed sample_data
var sampleData embed . FS
2024-03-14 13:39:56 +01:00
// TODO: Possibly break these out into separate files bundled with the embed package?
2024-02-27 05:00:04 +01:00
const userInsert = "INSERT INTO users (username, password) VALUES (?, ?)"
2024-03-20 21:51:36 +01:00
const projectInsert = "INSERT INTO projects (name, description, owner_user_id) VALUES (?, ?, (SELECT id FROM users WHERE username = ?))"
2024-03-07 13:21:47 +01:00
const promoteToAdmin = "INSERT INTO site_admin (admin_id) SELECT id FROM users WHERE username = ?"
2024-03-16 22:47:19 +01:00
const addWeeklyReport = ` WITH UserLookup AS ( SELECT id FROM users WHERE username = ? ) ,
2024-03-14 13:39:56 +01:00
ProjectLookup AS ( SELECT id FROM projects WHERE name = ? )
2024-03-16 22:47:19 +01:00
INSERT INTO weekly_reports ( project_id , user_id , week , development_time , meeting_time , admin_time , own_work_time , study_time , testing_time )
VALUES ( ( SELECT id FROM ProjectLookup ) , ( SELECT id FROM UserLookup ) , ? , ? , ? , ? , ? , ? , ? ) ; `
2024-03-20 21:51:36 +01:00
const addUserToProject = ` INSERT OR IGNORE INTO user_roles ( user_id , project_id , p_role )
VALUES ( ( SELECT id FROM users WHERE username = ? ) ,
( SELECT id FROM projects WHERE name = ? ) , ? ) `
const changeUserRole = "UPDATE user_roles SET p_role = ? WHERE user_id = (SELECT id FROM users WHERE username = ?) AND project_id = (SELECT id FROM projects WHERE name = ?)"
2024-03-18 22:46:53 +01:00
const getProjectsForUser = ` SELECT p . id , p . name , p . description FROM projects p
JOIN user_roles ur ON p . id = ur . project_id
JOIN users u ON ur . user_id = u . id
WHERE u . username = ? `
2024-03-20 11:09:48 +01:00
const deleteProject = ` DELETE FROM projects
WHERE id = ? AND owner_username = ? `
2024-03-14 19:48:49 +01:00
2024-03-20 21:51:36 +01:00
const isProjectManagerQuery = ` SELECT COUNT ( * ) > 0 FROM user_roles
JOIN users ON user_roles . user_id = users . id
JOIN projects ON user_roles . project_id = projects . id
WHERE users . username = ? AND projects . name = ? AND user_roles . p_role = ' project_manager ' `
2024-02-27 05:00:04 +01:00
// DbConnect connects to the database
2024-02-29 20:33:20 +01:00
func DbConnect ( dbpath string ) Database {
2024-02-12 12:40:49 +01:00
// Open the database
2024-03-15 11:02:33 +01:00
db , err := sqlx . Connect ( "sqlite" , dbpath )
2024-02-12 12:40:49 +01:00
if err != nil {
panic ( err )
}
2024-02-27 05:00:04 +01:00
// Ping forces the connection to be established
2024-02-12 12:40:49 +01:00
err = db . Ping ( )
if err != nil {
panic ( err )
}
2024-02-27 05:00:04 +01:00
return & Db { db }
}
2024-03-17 01:32:10 +01:00
func ( d * Db ) CheckUser ( username string , password string ) bool {
var dbPassword string
err := d . Get ( & dbPassword , "SELECT password FROM users WHERE username = ?" , username )
if err != nil {
return false
}
return dbPassword == password
}
2024-03-15 16:57:42 +01:00
// GetProjectsForUser retrieves all projects associated with a specific user.
2024-03-14 19:48:49 +01:00
func ( d * Db ) GetProjectsForUser ( username string ) ( [ ] types . Project , error ) {
var projects [ ] types . Project
err := d . Select ( & projects , getProjectsForUser , username )
return projects , err
}
2024-03-15 16:57:42 +01:00
// GetAllProjects retrieves all projects from the database.
2024-03-14 19:48:49 +01:00
func ( d * Db ) GetAllProjects ( ) ( [ ] types . Project , error ) {
var projects [ ] types . Project
err := d . Select ( & projects , "SELECT * FROM projects" )
return projects , err
}
2024-03-15 16:57:42 +01:00
// GetProject retrieves a specific project by its ID.
2024-03-15 15:14:45 +01:00
func ( d * Db ) GetProject ( projectId int ) ( types . Project , error ) {
var project types . Project
2024-03-18 16:42:35 +01:00
err := d . Get ( & project , "SELECT * FROM projects WHERE id = ?" , projectId )
if err != nil {
println ( "Error getting project: " , err )
}
2024-03-15 15:14:45 +01:00
return project , err
}
2024-03-16 22:47:19 +01:00
func ( d * Db ) AddWeeklyReport ( projectName string , userName string , week int , developmentTime int , meetingTime int , adminTime int , ownWorkTime int , studyTime int , testingTime int ) error {
_ , err := d . Exec ( addWeeklyReport , userName , projectName , week , developmentTime , meetingTime , adminTime , ownWorkTime , studyTime , testingTime )
2024-03-07 21:57:27 +01:00
return err
}
2024-03-14 13:27:57 +01:00
2024-03-15 16:57:42 +01:00
// AddUserToProject adds a user to a project with a specified role.
2024-03-20 16:46:17 +01:00
func ( d * Db ) AddUserToProject ( username string , projectname string , role string ) error {
2024-03-20 21:51:36 +01:00
_ , err := d . Exec ( addUserToProject , username , projectname , role )
return err
2024-03-07 21:57:27 +01:00
}
2024-03-07 20:58:50 +01:00
2024-03-15 16:57:42 +01:00
// ChangeUserRole changes the role of a user within a project.
2024-03-14 13:39:56 +01:00
func ( d * Db ) ChangeUserRole ( username string , projectname string , role string ) error {
2024-03-15 16:57:42 +01:00
// Execute the SQL query to change the user's role
2024-03-20 21:51:36 +01:00
_ , err := d . Exec ( changeUserRole , role , username , projectname )
return err
2024-03-07 23:24:54 +01:00
}
2024-03-07 20:58:50 +01:00
2024-03-20 16:46:17 +01:00
// ChangeUserName changes the username of a user.
2024-03-20 12:11:05 +01:00
func ( d * Db ) ChangeUserName ( username string , newname string ) error {
2024-03-20 16:46:17 +01:00
// Execute the SQL query to update the username
_ , err := d . Exec ( "UPDATE users SET username = ? WHERE username = ?" , newname , username )
return err
2024-03-20 12:11:05 +01:00
}
2024-03-15 16:57:42 +01:00
// GetUserRole retrieves the role of a user within a project.
2024-03-14 19:48:49 +01:00
func ( d * Db ) GetUserRole ( username string , projectname string ) ( string , error ) {
var role string
err := d . Get ( & role , "SELECT p_role FROM user_roles WHERE user_id = (SELECT id FROM users WHERE username = ?) AND project_id = (SELECT id FROM projects WHERE name = ?)" , username , projectname )
return role , err
}
2024-02-27 05:00:04 +01:00
// AddUser adds a user to the database
func ( d * Db ) AddUser ( username string , password string ) error {
_ , err := d . Exec ( userInsert , username , password )
return err
}
// Removes a user from the database
func ( d * Db ) RemoveUser ( username string ) error {
_ , err := d . Exec ( "DELETE FROM users WHERE username = ?" , username )
return err
2024-02-12 12:40:49 +01:00
}
2024-02-27 05:51:16 +01:00
2024-03-07 13:21:47 +01:00
func ( d * Db ) PromoteToAdmin ( username string ) error {
_ , err := d . Exec ( promoteToAdmin , username )
return err
}
2024-02-27 05:51:16 +01:00
func ( d * Db ) GetUserId ( username string ) ( int , error ) {
var id int
2024-03-07 23:24:54 +01:00
err := d . Get ( & id , "SELECT id FROM users WHERE username = ?" , username ) // Borde det inte vara "user" i singular
return id , err
}
2024-03-14 13:27:57 +01:00
func ( d * Db ) GetProjectId ( projectname string ) ( int , error ) {
2024-03-07 23:24:54 +01:00
var id int
2024-03-14 13:27:57 +01:00
err := d . Get ( & id , "SELECT id FROM projects WHERE name = ?" , projectname )
2024-02-27 05:51:16 +01:00
return id , err
}
2024-02-27 07:59:42 +01:00
// Creates a new project in the database, associated with a user
2024-02-27 05:51:16 +01:00
func ( d * Db ) AddProject ( name string , description string , username string ) error {
2024-03-19 01:38:40 +01:00
tx := d . MustBegin ( )
2024-03-20 21:51:36 +01:00
// Insert the project into the database
2024-03-19 01:38:40 +01:00
_ , err := tx . Exec ( projectInsert , name , description , username )
if err != nil {
2024-03-19 02:14:09 +01:00
if err := tx . Rollback ( ) ; err != nil {
return err
}
2024-03-19 01:38:40 +01:00
return err
2024-03-19 01:10:02 +01:00
}
2024-03-20 21:51:36 +01:00
// Add creator to project as project manager
_ , err = tx . Exec ( addUserToProject , username , name , "project_manager" )
2024-03-19 01:38:40 +01:00
if err != nil {
2024-03-19 02:14:09 +01:00
if err := tx . Rollback ( ) ; err != nil {
return err
}
2024-03-19 01:38:40 +01:00
return err
}
2024-03-19 02:14:09 +01:00
if err := tx . Commit ( ) ; err != nil {
return err
}
2024-03-19 01:38:40 +01:00
return err
2024-02-27 05:51:16 +01:00
}
2024-02-28 03:21:13 +01:00
2024-03-20 11:09:48 +01:00
func ( d * Db ) DeleteProject ( projectID string , username string ) error {
tx := d . MustBegin ( )
_ , err := tx . Exec ( deleteProject , projectID , username )
if err != nil {
if rollbackErr := tx . Rollback ( ) ; rollbackErr != nil {
return fmt . Errorf ( "error rolling back transaction: %v, delete error: %v" , rollbackErr , err )
}
panic ( err )
}
return err
}
2024-03-14 16:01:56 +01:00
func ( d * Db ) GetAllUsersProject ( projectname string ) ( [ ] UserProjectMember , error ) {
// Define the SQL query to fetch users and their roles for a given project
query := `
SELECT u . username , ur . p_role
FROM users u
INNER JOIN user_roles ur ON u . id = ur . user_id
INNER JOIN projects p ON ur . project_id = p . id
WHERE p . name = ?
`
// Execute the query
rows , err := d . Queryx ( query , projectname )
if err != nil {
return nil , err
}
defer rows . Close ( )
// Iterate over the rows and populate the result slice
var users [ ] UserProjectMember
for rows . Next ( ) {
var user UserProjectMember
if err := rows . StructScan ( & user ) ; err != nil {
return nil , err
}
users = append ( users , user )
}
if err := rows . Err ( ) ; err != nil {
return nil , err
}
return users , nil
}
2024-03-14 16:25:54 +01:00
// GetAllUsersApplication retrieves all usernames from the database
func ( d * Db ) GetAllUsersApplication ( ) ( [ ] string , error ) {
// Define the SQL query to fetch all usernames
query := `
SELECT username FROM users
`
// Execute the query
rows , err := d . Queryx ( query )
if err != nil {
return nil , err
}
defer rows . Close ( )
// Iterate over the rows and populate the result slice
var usernames [ ] string
for rows . Next ( ) {
var username string
if err := rows . Scan ( & username ) ; err != nil {
return nil , err
}
usernames = append ( usernames , username )
}
if err := rows . Err ( ) ; err != nil {
return nil , err
}
return usernames , nil
}
2024-03-17 17:58:02 +01:00
func ( d * Db ) GetWeeklyReport ( username string , projectName string , week int ) ( types . WeeklyReport , error ) {
var report types . WeeklyReport
query := `
SELECT
report_id ,
user_id ,
project_id ,
week ,
development_time ,
meeting_time ,
admin_time ,
own_work_time ,
study_time ,
2024-03-17 20:30:55 +01:00
testing_time ,
signed_by
2024-03-17 17:58:02 +01:00
FROM
weekly_reports
WHERE
user_id = ( SELECT id FROM users WHERE username = ? )
AND project_id = ( SELECT id FROM projects WHERE name = ? )
AND week = ?
`
err := d . Get ( & report , query , username , projectName , week )
return report , err
}
2024-03-17 20:30:55 +01:00
// SignWeeklyReport signs a weekly report by updating the signed_by field
// with the provided project manager's ID, but only if the project manager
// is in the same project as the report
func ( d * Db ) SignWeeklyReport ( reportId int , projectManagerId int ) error {
// Retrieve the project ID associated with the report
var reportProjectID int
err := d . Get ( & reportProjectID , "SELECT project_id FROM weekly_reports WHERE report_id = ?" , reportId )
if err != nil {
return err
}
// Retrieve the project ID associated with the project manager
var managerProjectID int
err = d . Get ( & managerProjectID , "SELECT project_id FROM user_roles WHERE user_id = ? AND p_role = 'project_manager'" , projectManagerId )
if err != nil {
return err
}
// Check if the project manager is in the same project as the report
if reportProjectID != managerProjectID {
return errors . New ( "project manager doesn't have permission to sign the report" )
}
// Update the signed_by field of the specified report
_ , err = d . Exec ( "UPDATE weekly_reports SET signed_by = ? WHERE report_id = ?" , projectManagerId , reportId )
return err
}
2024-03-21 01:37:39 +01:00
func ( d * Db ) GetUnsignedWeeklyReports ( projectName string ) ( [ ] types . WeeklyReport , error ) {
// Define the SQL query to fetch unsigned reports for a given user
query := `
SELECT
report_id ,
user_id ,
project_id ,
week ,
development_time ,
meeting_time ,
admin_time ,
own_work_time ,
study_time ,
testing_time ,
signed_by
FROM
weekly_reports
WHERE
signed_by IS NULL
AND project_id = ( SELECT id FROM projects WHERE name = ? )
`
// Execute the query
rows , err := d . Queryx ( query , projectName )
if err != nil {
return nil , err
}
defer rows . Close ( )
// Iterate over the rows and populate the result slice
var reports [ ] types . WeeklyReport
for rows . Next ( ) {
var report types . WeeklyReport
if err := rows . StructScan ( & report ) ; err != nil {
return nil , err
}
reports = append ( reports , report )
}
if err := rows . Err ( ) ; err != nil {
return nil , err
}
return reports , nil
}
2024-03-18 13:32:55 +01:00
// IsSiteAdmin checks if a given username is a site admin
func ( d * Db ) IsSiteAdmin ( username string ) ( bool , error ) {
// Define the SQL query to check if the user is a site admin
query := `
SELECT COUNT ( * ) FROM site_admin
JOIN users ON site_admin . admin_id = users . id
WHERE users . username = ?
`
// Execute the query
var count int
err := d . Get ( & count , query , username )
if err != nil {
return false , err
}
// If count is greater than 0, the user is a site admin
return count > 0 , nil
}
2024-02-28 03:21:13 +01:00
// Reads a directory of migration files and applies them to the database.
// This will eventually be used on an embedded directory
2024-03-17 14:38:20 +01:00
func ( d * Db ) Migrate ( ) error {
2024-02-28 03:30:05 +01:00
// Read the embedded scripts directory
files , err := scripts . ReadDir ( "migrations" )
2024-02-28 03:21:13 +01:00
if err != nil {
return err
}
2024-03-17 14:38:20 +01:00
if len ( files ) == 0 {
println ( "No migration files found" )
return nil
}
2024-02-28 03:21:13 +01:00
tr := d . MustBegin ( )
// Iterate over each SQL file and execute it
for _ , file := range files {
if file . IsDir ( ) || filepath . Ext ( file . Name ( ) ) != ".sql" {
continue
}
2024-02-28 03:30:05 +01:00
// This is perhaps not the most elegant way to do this
2024-03-17 14:38:20 +01:00
sqlBytes , err := scripts . ReadFile ( "migrations/" + file . Name ( ) )
2024-02-28 03:21:13 +01:00
if err != nil {
return err
}
sqlQuery := string ( sqlBytes )
_ , err = tr . Exec ( sqlQuery )
if err != nil {
return err
}
}
2024-03-02 04:29:50 +01:00
if tr . Commit ( ) != nil {
return err
}
2024-02-28 03:21:13 +01:00
return nil
}
2024-03-18 22:07:02 +01:00
2024-04-03 17:31:39 +02:00
// GetAllWeeklyReports retrieves weekly reports for a specific user and project.
func ( d * Db ) GetAllWeeklyReports ( username string , projectName string ) ( [ ] types . WeeklyReportList , error ) {
2024-03-19 19:04:45 +01:00
query := `
SELECT
wr . week ,
wr . development_time ,
wr . meeting_time ,
wr . admin_time ,
wr . own_work_time ,
wr . study_time ,
wr . testing_time ,
wr . signed_by
FROM
weekly_reports wr
INNER JOIN
users u ON wr . user_id = u . id
INNER JOIN
projects p ON wr . project_id = p . id
WHERE
u . username = ? AND p . name = ?
`
var reports [ ] types . WeeklyReportList
if err := d . Select ( & reports , query , username , projectName ) ; err != nil {
return nil , err
}
return reports , nil
}
2024-03-19 19:14:55 +01:00
// IsProjectManager checks if a given username is a project manager for the specified project
func ( d * Db ) IsProjectManager ( username string , projectname string ) ( bool , error ) {
2024-03-20 21:51:36 +01:00
var manager bool
err := d . Get ( & manager , isProjectManagerQuery , username , projectname )
return manager , err
2024-03-19 19:14:55 +01:00
}
2024-03-21 02:48:26 +01:00
func ( d * Db ) UpdateWeeklyReport ( projectName string , userName string , week int , developmentTime int , meetingTime int , adminTime int , ownWorkTime int , studyTime int , testingTime int ) error {
query := `
UPDATE weekly_reports
SET
development_time = ? ,
meeting_time = ? ,
admin_time = ? ,
own_work_time = ? ,
study_time = ? ,
testing_time = ?
WHERE
user_id = ( SELECT id FROM users WHERE username = ? )
AND project_id = ( SELECT id FROM projects WHERE name = ? )
AND week = ?
`
_ , err := d . Exec ( query , developmentTime , meetingTime , adminTime , ownWorkTime , studyTime , testingTime , userName , projectName , week )
return err
}
2024-03-18 22:07:02 +01:00
// MigrateSampleData applies sample data to the database.
func ( d * Db ) MigrateSampleData ( ) error {
// Insert sample data
files , err := sampleData . ReadDir ( "sample_data" )
if err != nil {
return err
}
if len ( files ) == 0 {
println ( "No sample data files found" )
}
tr := d . MustBegin ( )
// Iterate over each SQL file and execute it
for _ , file := range files {
if file . IsDir ( ) || filepath . Ext ( file . Name ( ) ) != ".sql" {
continue
}
// This is perhaps not the most elegant way to do this
sqlBytes , err := sampleData . ReadFile ( "sample_data/" + file . Name ( ) )
if err != nil {
return err
}
sqlQuery := string ( sqlBytes )
_ , err = tr . Exec ( sqlQuery )
if err != nil {
return err
}
}
if tr . Commit ( ) != nil {
return err
}
return nil
}
2024-03-20 17:40:10 +01:00
2024-03-20 23:49:55 +01:00
// GetProjectTimes retrieves a map with times per "Activity" for a given project
func ( d * Db ) GetProjectTimes ( projectName string ) ( map [ string ] int , error ) {
2024-03-20 21:51:36 +01:00
query := `
2024-03-20 17:40:10 +01:00
SELECT development_time , meeting_time , admin_time , own_work_time , study_time , testing_time
2024-03-20 23:49:55 +01:00
FROM weekly_reports
2024-03-20 17:40:10 +01:00
JOIN projects ON weekly_reports . project_id = projects . id
WHERE projects . name = ?
`
2024-03-20 21:51:36 +01:00
rows , err := d . DB . Query ( query , projectName )
if err != nil {
return nil , err
}
defer rows . Close ( )
totalTime := make ( map [ string ] int )
for rows . Next ( ) {
var developmentTime , meetingTime , adminTime , ownWorkTime , studyTime , testingTime int
if err := rows . Scan ( & developmentTime , & meetingTime , & adminTime , & ownWorkTime , & studyTime , & testingTime ) ; err != nil {
return nil , err
}
totalTime [ "development" ] += developmentTime
totalTime [ "meeting" ] += meetingTime
totalTime [ "admin" ] += adminTime
totalTime [ "own_work" ] += ownWorkTime
totalTime [ "study" ] += studyTime
totalTime [ "testing" ] += testingTime
}
if err := rows . Err ( ) ; err != nil {
return nil , err
}
return totalTime , nil
2024-03-20 17:40:10 +01:00
}
2024-03-27 21:18:44 +01:00
func ( d * Db ) RemoveProject ( projectname string ) error {
_ , err := d . Exec ( "DELETE FROM projects WHERE name = ?" , projectname )
return err
}