diff --git a/README.md b/README.md index 597678ae..9016e119 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,98 @@ -# Файлы для итогового задания +# Task Scheduler -В директории `tests` находятся тесты для проверки API, которое должно быть реализовано в веб-сервере. +Task Scheduler - это веб-приложение на Go для управления задачами с поддержкой повторяющихся событий. Приложение использует SQLite для хранения задач и предоставляет API для взаимодействия с задачами. -Директория `web` содержит файлы фронтенда. \ No newline at end of file +## Установка + +### Требования + +- Go 1.16 или новее +- SQLite3 + +### Склонируйте репозиторий + +``` +git clone https://github.com/ваш-репозиторий/task-scheduler.git +cd task-scheduler +go mod tidy +``` + +## Настройка +### Параметры окружения +Приложение использует два параметра окружения: + +TODO_PORT: Порт, на котором будет работать сервер. По умолчанию используется порт 7540. +TODO_DBFILE: Путь к файлу базы данных SQLite. По умолчанию используется ./scheduler.db. + +## Инициализация базы данных + +Приложение автоматически создаст файл базы данных и необходимые таблицы при первом запуске, если файл базы данных не существует. + +## Запуск приложения +### Запуск сервера + +``` +go run main.go +``` +Сервер будет доступен по адресу http://localhost:7540 + +### Примеры использования API +## Добавление задачи + +``` +curl -X POST "http://localhost:7540/api/task" -H "Content-Type: application/json" -d '{ +"date": "20240201", +"title": "Подвести итог", +"comment": "Мой комментарий", +"repeat": "d 5" +}' +``` + +## Получение всех задач + +``` +curl -X GET "http://localhost:7540/api/tasks" +``` + +## Получение задачи по идентификатору +``` +curl -X GET "http://localhost:7540/api/task?id=185" +``` +## Обновление задачи +``` +curl -X PUT "http://localhost:7540/api/task" -H "Content-Type: application/json" -d '{ +"id": 185, +"date": "20240201", +"title": "Обновленный заголовок", +"comment": "Обновленный комментарий", +"repeat": "d 10" +}' +``` + +## Пометка задачи как выполненной +``` +curl -X POST "http://localhost:7540/api/task/done?id=185" +``` + +## Удаление задачи + +``` +curl -X DELETE "http://localhost:7540/api/task?id=185" +``` + +### Тестирование +Для запуска тестов выполните следующую команду: + +``` +go test ./... +``` + +Эта команда запустит все тесты в проекте и выведет результаты. + +### Структура проекта +- main.go: Главный файл приложения, точка входа сервера. +- database/: Пакет для инициализации базы данных. +- handlers/: Пакет с обработчиками API запросов. +- models/: Пакет с моделями данных. +- tasks/: Пакет с логикой обработки задач и вычисления следующей даты выполнения. +- web/: Директория для статических файлов (HTML, CSS, JS). diff --git a/database/add_task.go b/database/add_task.go new file mode 100644 index 00000000..3f12cf6c --- /dev/null +++ b/database/add_task.go @@ -0,0 +1,52 @@ +package database + +import ( + "errors" + "github.com/jmoiron/sqlx" + "github.com/qavaleria/go_final_project/models" + "github.com/qavaleria/go_final_project/tasks" + "time" +) + +func AddTask(db *sqlx.DB, task models.Task) (int64, error) { + if task.Title == "" { + return 0, errors.New("Не указан заголовок задачи") + } + + if task.Date == "" { + task.Date = time.Now().Format(tasks.FormatDate) + } else { + _, err := time.Parse(tasks.FormatDate, task.Date) + if err != nil { + return 0, errors.New("Дата представлена в неправильном формате") + } + } + + now := time.Now() + if task.Date < now.Format(tasks.FormatDate) { + if task.Repeat == "" { + task.Date = now.Format(tasks.FormatDate) + } else { + nextDate, err := tasks.NextDate(now, task.Date, task.Repeat) + if err != nil { + return 0, err + } + task.Date = nextDate + } + } + + result, err := db.Exec( + `INSERT INTO scheduler (date, title, comment, repeat) VALUES (?, ?, ?, ?)`, + task.Date, task.Title, task.Comment, task.Repeat, + ) + if err != nil { + return 0, err + } + + id, err := result.LastInsertId() + if err != nil { + return 0, err + } + + return id, nil +} diff --git a/database/database.go b/database/database.go new file mode 100644 index 00000000..be81aa53 --- /dev/null +++ b/database/database.go @@ -0,0 +1,47 @@ +package database + +import ( + "github.com/jmoiron/sqlx" + _ "github.com/mattn/go-sqlite3" + "os" +) + +const dbFileName = "scheduler.db" + +// InitializeDatabase проверяет существование файла базы данных и создает таблицу, если необходимо +func InitializeDatabase() (*sqlx.DB, error) { + if _, err := os.Stat(dbFileName); os.IsNotExist(err) { + file, err := os.Create(dbFileName) + if err != nil { + return nil, err + } + file.Close() + } + + db, err := sqlx.Connect("sqlite3", dbFileName) + if err != nil { + return nil, err + } + + createTableSQL := `CREATE TABLE IF NOT EXISTS scheduler ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + date CHAR(8) NOT NULL DEFAULT "", + title VARCHAR(128) NOT NULL DEFAULT "", + comment TEXT NOT NULL DEFAULT "", + repeat VARCHAR(128) NOT NULL DEFAULT "" + );` + + _, err = db.Exec(createTableSQL) + if err != nil { + return nil, err + } + + // Создаем индекс по полю date для сортировки задач по дате + createIndexSQL := `CREATE INDEX IF NOT EXISTS idx_scheduler_date ON scheduler(date);` + _, err = db.Exec(createIndexSQL) + if err != nil { + return nil, err + } + + return db, nil +} diff --git a/database/delete_task.go b/database/delete_task.go new file mode 100644 index 00000000..c384e59a --- /dev/null +++ b/database/delete_task.go @@ -0,0 +1,29 @@ +package database + +import ( + "errors" + "github.com/jmoiron/sqlx" + "log" +) + +// DeleteTask удаляет задачу из базы данных по идентификатору +func DeleteTask(db *sqlx.DB, id string) error { + deleteQuery := `DELETE FROM scheduler WHERE id = ?` + res, err := db.Exec(deleteQuery, id) + if err != nil { + log.Printf("Ошибка выполнения запроса: %v", err) + return errors.New("Ошибка выполнения запроса") + } + + rowsAffected, err := res.RowsAffected() + if err != nil { + log.Printf("Ошибка получения результата запроса: %v", err) + return errors.New("Ошибка получения результата запроса") + } + + if rowsAffected == 0 { + return errors.New("задача не найдена") + } + + return nil +} diff --git a/database/get_task.go b/database/get_task.go new file mode 100644 index 00000000..a9cc0fa1 --- /dev/null +++ b/database/get_task.go @@ -0,0 +1,24 @@ +package database + +import ( + "database/sql" + "errors" + "github.com/jmoiron/sqlx" + "github.com/qavaleria/go_final_project/models" + "log" +) + +// GetTaskByID извлекает задачу из базы данных по идентификатору +func GetTaskByID(db *sqlx.DB, id string) (models.Task, error) { + var task models.Task + query := `SELECT id, date, title, comment, repeat FROM scheduler WHERE id = ?` + err := db.QueryRow(query, id).Scan(&task.ID, &task.Date, &task.Title, &task.Comment, &task.Repeat) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return task, errors.New("задача не найдена") + } + log.Printf("Ошибка выполнения запроса: %v", err) + return task, errors.New("Ошибка выполнения запроса") + } + return task, nil +} diff --git a/database/get_tasks.go b/database/get_tasks.go new file mode 100644 index 00000000..8e634e22 --- /dev/null +++ b/database/get_tasks.go @@ -0,0 +1,80 @@ +package database + +import ( + "github.com/jmoiron/sqlx" + "github.com/qavaleria/go_final_project/models" + "github.com/qavaleria/go_final_project/tasks" + "log" + "strings" + "time" +) + +func GetTasks(db *sqlx.DB, search string, limit int) ([]models.Task, error) { + var rows *sqlx.Rows + var err error + + // Убираем лишние пробелы в поисковой строке + search = strings.TrimSpace(search) + if search != "" { + // Проверяем, является ли поисковая строка датой в формате "02.01.2006" + if searchDate, err := time.Parse("02.01.2006", search); err == nil { + searchDateStr := searchDate.Format(tasks.FormatDate) + rows, err = db.NamedQuery(`SELECT id, date, title, comment, repeat FROM scheduler WHERE date = :searchDate ORDER BY date LIMIT :limit`, map[string]interface{}{ + "searchDate": searchDateStr, + "limit": limit, + }) + if err != nil { + log.Printf("Ошибка выполнения запроса с датой: %v", err) + return nil, err + } + } else { + //search = "%" + strings.ToLower(search) + "%" + rows, err = db.NamedQuery(`SELECT id, date, title, comment, repeat FROM scheduler WHERE LOWER(title) LIKE :search OR LOWER(comment) LIKE :search ORDER BY date LIMIT :limit`, map[string]interface{}{ + "search": "%" + search + "%", + "limit": limit, + }) + if err != nil { + log.Printf("Ошибка выполнения запроса с поиском: %v", err) + return nil, err + } + } + } else { + // Если поисковая строка пустая, просто выбираем все задачи + rows, err = db.NamedQuery(`SELECT id, date, title, comment, repeat FROM scheduler ORDER BY date LIMIT :limit`, map[string]interface{}{ + "limit": limit, + }) + if err != nil { + log.Printf("Ошибка выполнения запроса без поиска: %v", err) + return nil, err + } + } + + defer func() { + if err := rows.Close(); err != nil { + log.Printf("Ошибка закрытия rows: %v", err) + } + }() + + var tasks []models.Task + for rows.Next() { + var task models.Task + err := rows.StructScan(&task) + if err != nil { + log.Printf("Ошибка сканирования строки: %v", err) + return nil, err + } + tasks = append(tasks, task) + } + + if err = rows.Err(); err != nil { + log.Printf("Ошибка чтения строк: %v", err) + return nil, err + } + + // Возвращаем пустой массив задач, если ни одной задачи не найдено + if tasks == nil { + tasks = []models.Task{} + } + + return tasks, nil +} diff --git a/database/update_task.go b/database/update_task.go new file mode 100644 index 00000000..e54fa27b --- /dev/null +++ b/database/update_task.go @@ -0,0 +1,30 @@ +package database + +import ( + "errors" + "github.com/jmoiron/sqlx" + "github.com/qavaleria/go_final_project/models" + "log" +) + +// UpdateTask обновляет задачу в базе данных +func UpdateTask(db *sqlx.DB, task models.Task) error { + query := `UPDATE scheduler SET date = ?, title = ?, comment = ?, repeat = ? WHERE id = ?` + res, err := db.Exec(query, task.Date, task.Title, task.Comment, task.Repeat, task.ID) + if err != nil { + log.Printf("Ошибка выполнения запроса: %v", err) + return errors.New("ошибка выполнения запроса") + } + + rowsAffected, err := res.RowsAffected() + if err != nil { + log.Printf("Ошибка получения результата запроса: %v", err) + return errors.New("ошибка получения результата запроса") + } + + if rowsAffected == 0 { + return errors.New("задача не найдена") + } + + return nil +} diff --git a/go.mod b/go.mod new file mode 100644 index 00000000..b97b1d01 --- /dev/null +++ b/go.mod @@ -0,0 +1,16 @@ +module github.com/qavaleria/go_final_project + +go 1.21 + +require ( + github.com/go-chi/chi/v5 v5.0.12 + github.com/jmoiron/sqlx v1.4.0 + github.com/mattn/go-sqlite3 v1.14.22 + github.com/stretchr/testify v1.9.0 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 00000000..a779bb6c --- /dev/null +++ b/go.sum @@ -0,0 +1,22 @@ +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s= +github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= +github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= +github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/handlers/add_task.go b/handlers/add_task.go new file mode 100644 index 00000000..99482be3 --- /dev/null +++ b/handlers/add_task.go @@ -0,0 +1,34 @@ +package handlers + +import ( + "encoding/json" + "github.com/jmoiron/sqlx" + "github.com/qavaleria/go_final_project/database" + "github.com/qavaleria/go_final_project/models" + "log" + "net/http" +) + +// HandleAddTask обработчик для добавления задачи +func HandleAddTask(db *sqlx.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json; charset=UTF-8") + + var task models.Task + err := json.NewDecoder(r.Body).Decode(&task) + if err != nil { + log.Printf("Ошибка десериализации JSON: %v", err) + http.Error(w, `{"error": "Ошибка десериализации JSON"}`, http.StatusBadRequest) + return + } + + id, err := database.AddTask(db, task) + if err != nil { + log.Printf("Ошибка добавления задачи: %v", err) + http.Error(w, `{"error": "`+err.Error()+`"}`, http.StatusInternalServerError) + return + } + + json.NewEncoder(w).Encode(map[string]interface{}{"id": id}) + } +} diff --git a/handlers/delete_task.go b/handlers/delete_task.go new file mode 100644 index 00000000..60c1cbf1 --- /dev/null +++ b/handlers/delete_task.go @@ -0,0 +1,30 @@ +package handlers + +import ( + "encoding/json" + "github.com/jmoiron/sqlx" + "github.com/qavaleria/go_final_project/database" + "net/http" +) + +func HandleDeleteTask(db *sqlx.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + id := r.URL.Query().Get("id") + if id == "" { + http.Error(w, `{"error": "Не указан идентификатор задачи"}`, http.StatusBadRequest) + return + } + + err := database.DeleteTask(db, id) + if err != nil { + if err.Error() == "задача не найдена" { + http.Error(w, `{"error": "Задача не найдена"}`, http.StatusNotFound) + } else { + http.Error(w, `{"error": "`+err.Error()+`"}`, http.StatusInternalServerError) + } + return + } + + json.NewEncoder(w).Encode(map[string]interface{}{}) + } +} diff --git a/handlers/get_task.go b/handlers/get_task.go new file mode 100644 index 00000000..98024dae --- /dev/null +++ b/handlers/get_task.go @@ -0,0 +1,30 @@ +package handlers + +import ( + "encoding/json" + "github.com/jmoiron/sqlx" + "github.com/qavaleria/go_final_project/database" + "net/http" +) + +func HandleGetTask(db *sqlx.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + id := r.URL.Query().Get("id") + if id == "" { + http.Error(w, `{"error": "Не указан идентификатор"}`, http.StatusBadRequest) + return + } + + task, err := database.GetTaskByID(db, id) + if err != nil { + if err.Error() == "задача не найдена" { + http.Error(w, `{"error": "Задача не найдена"}`, http.StatusNotFound) + } else { + http.Error(w, `{"error": "`+err.Error()+`"}`, http.StatusInternalServerError) + } + return + } + + json.NewEncoder(w).Encode(task) + } +} diff --git a/handlers/get_tasks.go b/handlers/get_tasks.go new file mode 100644 index 00000000..e1c4bd26 --- /dev/null +++ b/handlers/get_tasks.go @@ -0,0 +1,25 @@ +package handlers + +import ( + "encoding/json" + "github.com/jmoiron/sqlx" + "github.com/qavaleria/go_final_project/database" + "net/http" +) + +const Limit = 50 + +func HandleGetTasks(db *sqlx.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json; charset=UTF-8") + + search := r.URL.Query().Get("search") + tasks, err := database.GetTasks(db, search, Limit) + if err != nil { + http.Error(w, `{"error": "Ошибка выполнения запроса"}`, http.StatusInternalServerError) + return + } + + json.NewEncoder(w).Encode(map[string]interface{}{"tasks": tasks}) + } +} diff --git a/handlers/mark_task_done.go b/handlers/mark_task_done.go new file mode 100644 index 00000000..106294f6 --- /dev/null +++ b/handlers/mark_task_done.go @@ -0,0 +1,62 @@ +package handlers + +import ( + "encoding/json" + "github.com/jmoiron/sqlx" + "github.com/qavaleria/go_final_project/database" + "github.com/qavaleria/go_final_project/tasks" + "log" + "net/http" + "time" +) + +// HandleMarkTaskDone обработчик для пометки задачи выполненной +func HandleMarkTaskDone(db *sqlx.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json; charset=UTF-8") + + id := r.URL.Query().Get("id") + if id == "" { + http.Error(w, `{"error": "Не указан идентификатор задачи"}`, http.StatusBadRequest) + return + } + + task, err := database.GetTaskByID(db, id) + if err != nil { + if err.Error() == "задача не найдена" { + http.Error(w, `{"error": "Задача не найдена"}`, http.StatusNotFound) + } else { + http.Error(w, `{"error": "`+err.Error()+`"}`, http.StatusInternalServerError) + } + return + } + + if task.Repeat == "" { + // Удаляем одноразовую задачу + err := database.DeleteTask(db, id) + if err != nil { + http.Error(w, `{"error": "`+err.Error()+`"}`, http.StatusInternalServerError) + return + } + } else { + // Рассчитываем следующую дату для периодической задачи + now := time.Now() + nextDate, err := tasks.NextDate(now, task.Date, task.Repeat) + if err != nil { + log.Printf("Ошибка вычисления следующей даты: %v", err) + http.Error(w, `{"error": "Ошибка вычисления следующей даты"}`, http.StatusInternalServerError) + return + } + + // Обновляем задачу с новой датой + task.Date = nextDate + err = database.UpdateTask(db, task) + if err != nil { + http.Error(w, `{"error": "`+err.Error()+`"}`, http.StatusInternalServerError) + return + } + } + + json.NewEncoder(w).Encode(map[string]interface{}{}) + } +} diff --git a/handlers/next_date.go b/handlers/next_date.go new file mode 100644 index 00000000..0837d967 --- /dev/null +++ b/handlers/next_date.go @@ -0,0 +1,28 @@ +package handlers + +import ( + "github.com/qavaleria/go_final_project/tasks" + "net/http" + "time" +) + +// HandleNextDate обработчик для API запроса /api/nextdate +func HandleNextDate(w http.ResponseWriter, r *http.Request) { + nowStr := r.FormValue("now") + dateStr := r.FormValue("date") + repeat := r.FormValue("repeat") + + now, err := time.Parse(tasks.FormatDate, nowStr) + if err != nil { + http.Error(w, "Invalid now format", http.StatusBadRequest) + return + } + + nextDate, err := tasks.NextDate(now, dateStr, repeat) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + w.Write([]byte(nextDate)) +} diff --git a/handlers/update_task.go b/handlers/update_task.go new file mode 100644 index 00000000..6736b09b --- /dev/null +++ b/handlers/update_task.go @@ -0,0 +1,46 @@ +package handlers + +import ( + "encoding/json" + "github.com/jmoiron/sqlx" + "github.com/qavaleria/go_final_project/database" + "github.com/qavaleria/go_final_project/models" + "github.com/qavaleria/go_final_project/tasks" + "log" + "net/http" +) + +func HandleUpdateTask(db *sqlx.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var task models.Task + err := json.NewDecoder(r.Body).Decode(&task) + if err != nil { + log.Printf("Ошибка десериализации JSON: %v", err) + http.Error(w, `{"error": "Ошибка десериализации JSON"}`, http.StatusBadRequest) + return + } + + if task.ID == "" { + log.Printf("Не указан идентификатор задачи") + http.Error(w, `{"error": "Не указан идентификатор задачи"}`, http.StatusBadRequest) + return + } + + if err := tasks.ValidateTask(&task); err != nil { + http.Error(w, `{"error": "`+err.Error()+`"}`, http.StatusBadRequest) + return + } + + err = database.UpdateTask(db, task) + if err != nil { + if err.Error() == "задача не найдена" { + http.Error(w, `{"error": "Задача не найдена"}`, http.StatusNotFound) + } else { + http.Error(w, `{"error": "`+err.Error()+`"}`, http.StatusInternalServerError) + } + return + } + + json.NewEncoder(w).Encode(map[string]interface{}{}) + } +} diff --git a/main.go b/main.go new file mode 100644 index 00000000..8337f8b0 --- /dev/null +++ b/main.go @@ -0,0 +1,46 @@ +package main + +import ( + "github.com/go-chi/chi/v5" + "github.com/qavaleria/go_final_project/database" + "github.com/qavaleria/go_final_project/handlers" + "github.com/qavaleria/go_final_project/tests" + "log" + "net/http" + "os" + "strconv" +) + +func main() { + // Получаем порт из переменной окружения или используем значение по умолчанию + port := os.Getenv("TODO_PORT") + if port == "" { + port = strconv.Itoa(tests.Port) + } + + db, err := database.InitializeDatabase() + if err != nil { + log.Fatalf("Failed to initialize database: %v", err) + } + defer db.Close() + + r := chi.NewRouter() + fs := http.FileServer(http.Dir("./web")) + r.Handle("/*", fs) + + // Добавляем обработчик API для вычисления следующей даты + r.Get("/api/nextdate", handlers.HandleNextDate) + r.MethodFunc(http.MethodGet, "/api/task", handlers.HandleGetTask(db)) + r.MethodFunc(http.MethodPut, "/api/task", handlers.HandleUpdateTask(db)) + r.MethodFunc(http.MethodDelete, "/api/task", handlers.HandleDeleteTask(db)) + r.MethodFunc(http.MethodPost, "/api/task", handlers.HandleAddTask(db)) + r.MethodFunc(http.MethodGet, "/api/tasks", handlers.HandleGetTasks(db)) + r.MethodFunc(http.MethodPost, "/api/task/done", handlers.HandleMarkTaskDone(db)) + + // Запускаем сервер + log.Printf("Server is listening on port %s", port) + err = http.ListenAndServe(":"+port, r) + if err != nil { + log.Fatal(err) + } +} diff --git a/models/models.go b/models/models.go new file mode 100644 index 00000000..a5b499e6 --- /dev/null +++ b/models/models.go @@ -0,0 +1,9 @@ +package models + +type Task struct { + ID string `json:"id"` + Date string `json:"date"` + Title string `json:"title"` + Comment string `json:"comment"` + Repeat string `json:"repeat"` +} diff --git a/scheduler.db b/scheduler.db new file mode 100644 index 00000000..d18b030c Binary files /dev/null and b/scheduler.db differ diff --git a/tasks/service.go b/tasks/service.go new file mode 100644 index 00000000..bd796652 --- /dev/null +++ b/tasks/service.go @@ -0,0 +1,37 @@ +package tasks + +import ( + "errors" + "github.com/qavaleria/go_final_project/models" + "time" +) + +func ValidateTask(task *models.Task) error { + if task.Title == "" { + return errors.New("не указан заголовок задачи") + } + + now := time.Now() + if task.Date == "" { + task.Date = now.Format(FormatDate) + } else { + date, err := time.Parse(FormatDate, task.Date) + if err != nil { + return errors.New("Дата представлена в неправильном формате") + } + + if date.Before(now) { + if task.Repeat == "" { + task.Date = now.Format(FormatDate) + } else { + nextDate, err := NextDate(now, task.Date, task.Repeat) + if err != nil { + return errors.New("Ошибка вычисления следующей даты") + } + task.Date = nextDate + } + } + } + + return nil +} diff --git a/tasks/tasks.go b/tasks/tasks.go new file mode 100644 index 00000000..c17d48e1 --- /dev/null +++ b/tasks/tasks.go @@ -0,0 +1,69 @@ +package tasks + +import ( + "errors" + "fmt" + "strconv" + "strings" + "time" +) + +const FormatDate = "20060102" + +// NextDate вычисляет следующую дату для задачи в соответствии с правилом повторения +func NextDate(now time.Time, dateStr string, repeat string) (string, error) { + if repeat == "" { + return "", errors.New("Правило повторения не указано") + } + + date, err := time.Parse(FormatDate, dateStr) + if err != nil { + return "", fmt.Errorf("Неверный формат даты: %v", err) + } + + parts := strings.Fields(repeat) + rule := parts[0] + + var resultDate time.Time + switch rule { + case "": + if date.Before(now) { + resultDate = time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.UTC) + } else { + resultDate = date + } + case "d": + if len(parts) != 2 { + return "", errors.New("Неверный формат повторения для 'd'") + } + + daysToInt := make([]int, 0, 7) + days, err := strconv.Atoi(parts[1]) + if err != nil || days <= 0 || days > 400 { + return "", errors.New("Неверное кол-во дней") + } + daysToInt = append(daysToInt, days) + + if daysToInt[0] == 1 { + resultDate = date.AddDate(0, 0, 1) + } else { + resultDate = date.AddDate(0, 0, daysToInt[0]) + for resultDate.Before(now) { + resultDate = resultDate.AddDate(0, 0, daysToInt[0]) + } + } + case "y": + if len(parts) != 1 { + return "", errors.New("Неверный формат повторения для 'y'") + } + + resultDate = date.AddDate(1, 0, 0) + for resultDate.Before(now) { + resultDate = resultDate.AddDate(1, 0, 0) + } + default: + return "", errors.New("Не поддерживаемый формат повторения") + } + + return resultDate.Format(FormatDate), nil +} diff --git a/tests/settings.go b/tests/settings.go index 3908fdfe..e8a10b23 100644 --- a/tests/settings.go +++ b/tests/settings.go @@ -3,5 +3,5 @@ package tests var Port = 7540 var DBFile = "../scheduler.db" var FullNextDate = false -var Search = false +var Search = true var Token = `` diff --git a/tests/tasks_5_test.go b/tests/tasks_5_test.go index a12b5d19..b017680a 100644 --- a/tests/tasks_5_test.go +++ b/tests/tasks_5_test.go @@ -71,7 +71,7 @@ func TestTasks(t *testing.T) { repeat: "d 30", }) tasks = getTasks(t, "") - assert.Equal(t, len(tasks), 3) + assert.Equal(t, 3, len(tasks)) now = now.AddDate(0, 0, 2) date = now.Format(`20060102`) @@ -95,14 +95,14 @@ func TestTasks(t *testing.T) { }) tasks = getTasks(t, "") - assert.Equal(t, len(tasks), 6) + assert.Equal(t, 6, len(tasks)) if !Search { return } tasks = getTasks(t, "УК") - assert.Equal(t, len(tasks), 1) + assert.Equal(t, 1, len(tasks)) tasks = getTasks(t, now.Format(`02.01.2006`)) - assert.Equal(t, len(tasks), 3) + assert.Equal(t, 3, len(tasks)) }