Skip to content

Commit f7b5eba

Browse files
authored
Implement task uncompletion (#132)
1 parent e972f12 commit f7b5eba

File tree

3 files changed

+122
-0
lines changed

3 files changed

+122
-0
lines changed

internal/apis/task.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package apis
22

33
import (
4+
"context"
5+
"errors"
46
"fmt"
57
"net/http"
68
"strconv"
@@ -16,6 +18,7 @@ import (
1618
auth "dkhalife.com/tasks/core/internal/utils/auth"
1719
jwt "github.com/appleboy/gin-jwt/v2"
1820
"github.com/gin-gonic/gin"
21+
"gorm.io/gorm"
1922
)
2023

2124
type LabelReq struct {
@@ -533,6 +536,65 @@ func (h *TasksAPIHandler) completeTask(c *gin.Context) {
533536
})
534537
}
535538

539+
func (h *TasksAPIHandler) uncompleteTask(c *gin.Context) {
540+
currentIdentity := auth.CurrentIdentity(c)
541+
log := logging.FromContext(c)
542+
543+
rawID := c.Param("id")
544+
id, err := strconv.Atoi(rawID)
545+
if err != nil {
546+
c.JSON(http.StatusBadRequest, gin.H{
547+
"error": "Invalid task ID",
548+
})
549+
return
550+
}
551+
552+
task, err := h.tRepo.GetTask(c, id)
553+
if err != nil {
554+
log.Errorf("error getting task: %s", err.Error())
555+
c.JSON(http.StatusInternalServerError, gin.H{
556+
"error": "Error getting task",
557+
})
558+
return
559+
}
560+
561+
if currentIdentity.UserID != task.CreatedBy {
562+
c.JSON(http.StatusForbidden, gin.H{
563+
"error": "You are not allowed to update this task",
564+
})
565+
return
566+
}
567+
568+
if err := h.tRepo.UncompleteTask(c, id); err != nil {
569+
if errors.Is(err, gorm.ErrRecordNotFound) {
570+
c.JSON(http.StatusBadRequest, gin.H{"error": "Task was not completed already"})
571+
return
572+
}
573+
log.Errorf("error uncompleting task: %s", err.Error())
574+
c.JSON(http.StatusInternalServerError, gin.H{
575+
"error": "Error uncompleting task",
576+
})
577+
return
578+
}
579+
580+
updatedTask, err := h.tRepo.GetTask(c, id)
581+
if err != nil {
582+
log.Errorf("error getting updated task: %s", err.Error())
583+
c.JSON(http.StatusInternalServerError, gin.H{
584+
"error": "Error getting updated task",
585+
})
586+
return
587+
}
588+
589+
go func(task *models.Task) {
590+
h.nRepo.GenerateNotifications(context.Background(), task)
591+
}(updatedTask)
592+
593+
c.JSON(http.StatusOK, gin.H{
594+
"task": updatedTask,
595+
})
596+
}
597+
536598
func (h *TasksAPIHandler) GetTaskHistory(c *gin.Context) {
537599
currentIdentity := auth.CurrentIdentity(c)
538600
log := logging.FromContext(c)
@@ -578,6 +640,7 @@ func TaskRoutes(router *gin.Engine, h *TasksAPIHandler, auth *jwt.GinJWTMiddlewa
578640
tasksRoutes.GET("/:id", authMW.ScopeMiddleware(models.ApiTokenScopeTaskRead), h.getTask)
579641
tasksRoutes.GET("/:id/history", authMW.ScopeMiddleware(models.ApiTokenScopeTaskRead), h.GetTaskHistory)
580642
tasksRoutes.POST("/:id/do", authMW.ScopeMiddleware(models.ApiTokenScopeTaskWrite), h.completeTask)
643+
tasksRoutes.POST("/:id/undo", authMW.ScopeMiddleware(models.ApiTokenScopeTaskWrite), h.uncompleteTask)
581644
tasksRoutes.POST("/:id/skip", authMW.ScopeMiddleware(models.ApiTokenScopeTaskWrite), h.skipTask)
582645
tasksRoutes.PUT("/:id/dueDate", authMW.ScopeMiddleware(models.ApiTokenScopeTaskWrite), h.updateDueDate)
583646
tasksRoutes.DELETE("/:id", authMW.ScopeMiddleware(models.ApiTokenScopeTaskWrite), h.deleteTask)

internal/repos/task/task.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,30 @@ func (r *TaskRepository) CompleteTask(c context.Context, task *models.Task, user
107107
return err
108108
}
109109

110+
func (r *TaskRepository) UncompleteTask(c context.Context, taskID int) error {
111+
return r.db.WithContext(c).Transaction(func(tx *gorm.DB) error {
112+
var last models.TaskHistory
113+
if err := tx.Where("task_id = ?", taskID).Order("id desc").First(&last).Error; err != nil {
114+
return err
115+
}
116+
117+
if err := tx.Delete(&last).Error; err != nil {
118+
return err
119+
}
120+
121+
updates := map[string]interface{}{
122+
"next_due_date": last.DueDate,
123+
"is_active": true,
124+
}
125+
126+
if err := tx.Model(&models.Task{}).Where("id = ?", taskID).Updates(updates).Error; err != nil {
127+
return err
128+
}
129+
130+
return nil
131+
})
132+
}
133+
110134
func (r *TaskRepository) GetTaskHistory(c context.Context, taskID int) ([]*models.TaskHistory, error) {
111135
var histories []*models.TaskHistory
112136
if err := r.db.WithContext(c).Where("task_id = ?", taskID).Order("due_date desc").Find(&histories).Error; err != nil {

internal/repos/task/task_test.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,41 @@ func (s *TaskTestSuite) TestCompleteTask() {
367367
s.WithinDuration(*updatedRecurringTask.NextDueDate, nextDueDate, time.Second)
368368
}
369369

370+
func (s *TaskTestSuite) TestUncompleteTask() {
371+
ctx := context.Background()
372+
dueDate := time.Now().Add(24 * time.Hour)
373+
completedDate := time.Now()
374+
375+
task := &models.Task{
376+
Title: "Undo Task",
377+
CreatedBy: s.testUser.ID,
378+
NextDueDate: &dueDate,
379+
IsActive: true,
380+
Frequency: models.Frequency{
381+
Type: models.RepeatOnce,
382+
},
383+
}
384+
385+
err := s.DB.Create(task).Error
386+
s.Require().NoError(err)
387+
388+
err = s.repo.CompleteTask(ctx, task, s.testUser.ID, nil, &completedDate)
389+
s.Require().NoError(err)
390+
391+
err = s.repo.UncompleteTask(ctx, task.ID)
392+
s.Require().NoError(err)
393+
394+
var updatedTask models.Task
395+
err = s.DB.First(&updatedTask, task.ID).Error
396+
s.Require().NoError(err)
397+
s.True(updatedTask.IsActive)
398+
s.WithinDuration(dueDate, *updatedTask.NextDueDate, time.Second)
399+
400+
var count int64
401+
s.DB.Model(&models.TaskHistory{}).Where("task_id = ?", task.ID).Count(&count)
402+
s.Equal(int64(0), count)
403+
}
404+
370405
func (s *TaskTestSuite) TestGetTaskHistory() {
371406
ctx := context.Background()
372407
dueDate := time.Now().Add(-24 * time.Hour) // Due yesterday

0 commit comments

Comments
 (0)