Исправление отображения проектов в условиях
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m21s

This commit is contained in:
poignatov
2026-01-19 13:07:17 +03:00
parent e3c81a36de
commit 6d468d6967
6 changed files with 197 additions and 44 deletions

View File

@@ -334,6 +334,7 @@ type WishlistRequest struct {
}
type UnlockConditionRequest struct {
ID *int `json:"id,omitempty"` // ID существующего условия (для сохранения чужих условий)
Type string `json:"type"`
TaskID *int `json:"task_id,omitempty"`
ProjectID *int `json:"project_id,omitempty"`
@@ -2865,6 +2866,12 @@ func (a *App) initAuthDB() error {
// Не возвращаем ошибку, чтобы приложение могло запуститься
}
// Apply migration 025: Remove wishlist conditions without user_id
if err := a.applyMigration025(); err != nil {
log.Printf("Warning: Failed to apply migration 025: %v", err)
// Не возвращаем ошибку, чтобы приложение могло запуститься
}
// Clean up expired refresh tokens (only those with expiration date set)
a.DB.Exec("DELETE FROM refresh_tokens WHERE expires_at IS NOT NULL AND expires_at < NOW()")
@@ -3268,6 +3275,50 @@ func (a *App) applyMigration024() error {
return nil
}
// applyMigration025 применяет миграцию 025_remove_conditions_without_user_id.sql
func (a *App) applyMigration025() error {
log.Printf("Applying migration 025: Remove wishlist conditions without user_id")
// Проверяем, есть ли условия без user_id
var count int
err := a.DB.QueryRow(`
SELECT COUNT(*)
FROM wishlist_conditions
WHERE user_id IS NULL
`).Scan(&count)
if err != nil {
return fmt.Errorf("failed to check conditions without user_id: %w", err)
}
if count == 0 {
log.Printf("Migration 025 already applied (no conditions without user_id), skipping")
return nil
}
log.Printf("Found %d conditions without user_id, removing them", count)
// Читаем SQL из файла миграции
migrationPath := "migrations/025_remove_conditions_without_user_id.sql"
if _, err := os.Stat(migrationPath); os.IsNotExist(err) {
// Пробуем альтернативный путь (в Docker)
migrationPath = "/migrations/025_remove_conditions_without_user_id.sql"
}
migrationSQL, err := os.ReadFile(migrationPath)
if err != nil {
return fmt.Errorf("failed to read migration file %s: %w", migrationPath, err)
}
// Выполняем миграцию
if _, err := a.DB.Exec(string(migrationSQL)); err != nil {
return fmt.Errorf("failed to execute migration 025: %w", err)
}
log.Printf("Migration 025 applied successfully, removed %d conditions without user_id", count)
return nil
}
func (a *App) initPlayLifeDB() error {
// Создаем таблицу projects
createProjectsTable := `
@@ -9372,7 +9423,7 @@ func (a *App) getWishlistItemsWithConditions(userID int, includeCompleted bool)
LEFT JOIN task_conditions tc ON wc.task_condition_id = tc.id
LEFT JOIN tasks t ON tc.task_id = t.id
LEFT JOIN score_conditions sc ON wc.score_condition_id = sc.id
LEFT JOIN projects p ON sc.project_id = p.id
LEFT JOIN projects p ON sc.project_id = p.id AND p.deleted = FALSE
WHERE wi.user_id = $1
AND wi.deleted = FALSE
AND ($2 = TRUE OR wi.completed = FALSE)
@@ -9457,6 +9508,10 @@ func (a *App) getWishlistItemsWithConditions(userID int, includeCompleted bool)
if projectName.Valid {
condition.ProjectName = &projectName.String
}
if projectID.Valid {
projectIDVal := int(projectID.Int64)
condition.ProjectID = &projectIDVal
}
if requiredPoints.Valid {
condition.RequiredPoints = &requiredPoints.Float64
}
@@ -9633,18 +9688,43 @@ func (a *App) getWishlistItemsWithConditions(userID int, includeCompleted bool)
}
// saveWishlistConditions сохраняет условия для желания
// userID - автор условий (пользователь, который создает/обновляет условия)
func (a *App) saveWishlistConditions(
tx *sql.Tx,
wishlistItemID int,
userID int,
conditions []UnlockConditionRequest,
) error {
// Удаляем старые условия
_, err := tx.Exec(`
DELETE FROM wishlist_conditions
// Получаем все существующие условия с их user_id перед удалением
existingConditions := make(map[int]int) // map[conditionID]userID
rows, err := tx.Query(`
SELECT id, user_id
FROM wishlist_conditions
WHERE wishlist_item_id = $1
`, wishlistItemID)
if err != nil {
return err
return fmt.Errorf("error getting existing conditions: %w", err)
}
defer rows.Close()
for rows.Next() {
var condID int
var condUserID sql.NullInt64
if err := rows.Scan(&condID, &condUserID); err != nil {
return fmt.Errorf("error scanning existing condition: %w", err)
}
if condUserID.Valid {
existingConditions[condID] = int(condUserID.Int64)
}
}
// Удаляем только условия текущего пользователя
_, err = tx.Exec(`
DELETE FROM wishlist_conditions
WHERE wishlist_item_id = $1 AND user_id = $2
`, wishlistItemID, userID)
if err != nil {
return fmt.Errorf("error deleting user conditions: %w", err)
}
if len(conditions) == 0 {
@@ -9654,8 +9734,8 @@ func (a *App) saveWishlistConditions(
// Подготавливаем statement для вставки условий
stmt, err := tx.Prepare(`
INSERT INTO wishlist_conditions
(wishlist_item_id, task_condition_id, score_condition_id, display_order)
VALUES ($1, $2, $3, $4)
(wishlist_item_id, user_id, task_condition_id, score_condition_id, display_order)
VALUES ($1, $2, $3, $4, $5)
`)
if err != nil {
return err
@@ -9743,9 +9823,20 @@ func (a *App) saveWishlistConditions(
scoreConditionID = scID
}
// Определяем user_id для условия:
// - Если условие имеет id и это условие существовало - используем его оригинальный user_id
// - Иначе - используем userID текущего пользователя
conditionUserID := userID
if condition.ID != nil {
if originalUserID, exists := existingConditions[*condition.ID]; exists {
conditionUserID = originalUserID
}
}
// Создаём связь
_, err = stmt.Exec(
wishlistItemID,
conditionUserID,
taskConditionID,
scoreConditionID,
displayOrder,
@@ -9937,7 +10028,7 @@ func (a *App) createWishlistHandler(w http.ResponseWriter, r *http.Request) {
// Сохраняем условия
if len(req.UnlockConditions) > 0 {
err = a.saveWishlistConditions(tx, wishlistID, req.UnlockConditions)
err = a.saveWishlistConditions(tx, wishlistID, userID, req.UnlockConditions)
if err != nil {
log.Printf("Error saving wishlist conditions: %v", err)
sendErrorWithCORS(w, fmt.Sprintf("Error saving wishlist conditions: %v", err), http.StatusInternalServerError)
@@ -10088,7 +10179,7 @@ func (a *App) getWishlistItemHandler(w http.ResponseWriter, r *http.Request) {
LEFT JOIN task_conditions tc ON wc.task_condition_id = tc.id
LEFT JOIN tasks t ON tc.task_id = t.id
LEFT JOIN score_conditions sc ON wc.score_condition_id = sc.id
LEFT JOIN projects p ON sc.project_id = p.id
LEFT JOIN projects p ON sc.project_id = p.id AND p.deleted = FALSE
WHERE wi.id = $1
AND wi.deleted = FALSE
ORDER BY wc.display_order, wc.id
@@ -10185,6 +10276,12 @@ func (a *App) getWishlistItemHandler(w http.ResponseWriter, r *http.Request) {
if projectName.Valid {
condition.ProjectName = &projectName.String
}
if projectID.Valid {
projectIDVal := int(projectID.Int64)
condition.ProjectID = &projectIDVal
points, _ := a.calculateProjectPointsFromDate(int(projectID.Int64), startDate, conditionOwnerID)
condition.CurrentPoints = &points
}
if requiredPoints.Valid {
condition.RequiredPoints = &requiredPoints.Float64
}
@@ -10192,10 +10289,6 @@ func (a *App) getWishlistItemHandler(w http.ResponseWriter, r *http.Request) {
dateStr := startDate.Time.Format("2006-01-02")
condition.StartDate = &dateStr
}
if projectID.Valid {
points, _ := a.calculateProjectPointsFromDate(int(projectID.Int64), startDate, conditionOwnerID)
condition.CurrentPoints = &points
}
}
item.UnlockConditions = append(item.UnlockConditions, condition)
@@ -10358,7 +10451,7 @@ func (a *App) updateWishlistHandler(w http.ResponseWriter, r *http.Request) {
}
// Сохраняем условия
err = a.saveWishlistConditions(tx, itemID, req.UnlockConditions)
err = a.saveWishlistConditions(tx, itemID, userID, req.UnlockConditions)
if err != nil {
log.Printf("Error saving wishlist conditions: %v", err)
sendErrorWithCORS(w, fmt.Sprintf("Error saving wishlist conditions: %v", err), http.StatusInternalServerError)
@@ -10397,7 +10490,7 @@ func (a *App) updateWishlistHandler(w http.ResponseWriter, r *http.Request) {
LEFT JOIN task_conditions tc ON wc.task_condition_id = tc.id
LEFT JOIN tasks t ON tc.task_id = t.id
LEFT JOIN score_conditions sc ON wc.score_condition_id = sc.id
LEFT JOIN projects p ON sc.project_id = p.id
LEFT JOIN projects p ON sc.project_id = p.id AND p.deleted = FALSE
WHERE wi.id = $1
AND wi.deleted = FALSE
ORDER BY wc.display_order, wc.id
@@ -10500,6 +10593,12 @@ func (a *App) updateWishlistHandler(w http.ResponseWriter, r *http.Request) {
if projectName.Valid {
condition.ProjectName = &projectName.String
}
if projectID.Valid {
projectIDVal := int(projectID.Int64)
condition.ProjectID = &projectIDVal
points, _ := a.calculateProjectPointsFromDate(int(projectID.Int64), startDate, conditionOwnerID)
condition.CurrentPoints = &points
}
if requiredPoints.Valid {
condition.RequiredPoints = &requiredPoints.Float64
}
@@ -10507,10 +10606,6 @@ func (a *App) updateWishlistHandler(w http.ResponseWriter, r *http.Request) {
dateStr := startDate.Time.Format("2006-01-02")
condition.StartDate = &dateStr
}
if projectID.Valid {
points, _ := a.calculateProjectPointsFromDate(int(projectID.Int64), startDate, conditionOwnerID)
condition.CurrentPoints = &points
}
}
item.UnlockConditions = append(item.UnlockConditions, condition)
@@ -10944,11 +11039,13 @@ func (a *App) copyWishlistHandler(w http.ResponseWriter, r *http.Request) {
var link sql.NullString
var imagePath sql.NullString
var ownerID int
var boardID sql.NullInt64
var authorID sql.NullInt64
err = a.DB.QueryRow(`
SELECT user_id, name, price, link, image_path
SELECT user_id, name, price, link, image_path, board_id, author_id
FROM wishlist_items
WHERE id = $1 AND deleted = FALSE
`, itemID).Scan(&ownerID, &name, &price, &link, &imagePath)
`, itemID).Scan(&ownerID, &name, &price, &link, &imagePath, &boardID, &authorID)
if err == sql.ErrNoRows || ownerID != userID {
sendErrorWithCORS(w, "Wishlist item not found", http.StatusNotFound)
@@ -11039,11 +11136,23 @@ func (a *App) copyWishlistHandler(w http.ResponseWriter, r *http.Request) {
linkVal = link.String
}
// Определяем значения для board_id и author_id
var boardIDVal, authorIDVal interface{}
if boardID.Valid {
boardIDVal = int(boardID.Int64)
}
if authorID.Valid {
authorIDVal = int(authorID.Int64)
} else {
// Если author_id не был установлен, используем текущего пользователя
authorIDVal = userID
}
err = tx.QueryRow(`
INSERT INTO wishlist_items (user_id, name, price, link, completed, deleted)
VALUES ($1, $2, $3, $4, FALSE, FALSE)
INSERT INTO wishlist_items (user_id, board_id, author_id, name, price, link, completed, deleted)
VALUES ($1, $2, $3, $4, $5, $6, FALSE, FALSE)
RETURNING id
`, userID, name+" (копия)", priceVal, linkVal).Scan(&newWishlistID)
`, ownerID, boardIDVal, authorIDVal, name+" (копия)", priceVal, linkVal).Scan(&newWishlistID)
if err != nil {
log.Printf("Error creating wishlist copy: %v", err)
sendErrorWithCORS(w, fmt.Sprintf("Error creating wishlist copy: %v", err), http.StatusInternalServerError)
@@ -11052,7 +11161,7 @@ func (a *App) copyWishlistHandler(w http.ResponseWriter, r *http.Request) {
// Сохраняем условия
if len(conditions) > 0 {
err = a.saveWishlistConditions(tx, newWishlistID, conditions)
err = a.saveWishlistConditions(tx, newWishlistID, userID, conditions)
if err != nil {
log.Printf("Error saving wishlist conditions: %v", err)
sendErrorWithCORS(w, fmt.Sprintf("Error saving wishlist conditions: %v", err), http.StatusInternalServerError)
@@ -12022,7 +12131,7 @@ func (a *App) getBoardCompletedHandler(w http.ResponseWriter, r *http.Request) {
LEFT JOIN task_conditions tc ON wc.task_condition_id = tc.id
LEFT JOIN tasks t ON tc.task_id = t.id
LEFT JOIN score_conditions sc ON wc.score_condition_id = sc.id
LEFT JOIN projects p ON sc.project_id = p.id
LEFT JOIN projects p ON sc.project_id = p.id AND p.deleted = FALSE
LEFT JOIN users u ON wc.user_id = u.id
WHERE wi.board_id = $1
AND wi.deleted = FALSE
@@ -12178,6 +12287,7 @@ func (a *App) getWishlistItemsByBoard(boardID int, userID int) ([]WishlistItem,
wi.image_path,
wi.link,
wi.completed,
COALESCE(wi.author_id, wi.user_id) AS item_owner_id,
wc.id AS condition_id,
wc.display_order,
wc.task_condition_id,
@@ -12194,7 +12304,7 @@ func (a *App) getWishlistItemsByBoard(boardID int, userID int) ([]WishlistItem,
LEFT JOIN task_conditions tc ON wc.task_condition_id = tc.id
LEFT JOIN tasks t ON tc.task_id = t.id
LEFT JOIN score_conditions sc ON wc.score_condition_id = sc.id
LEFT JOIN projects p ON sc.project_id = p.id
LEFT JOIN projects p ON sc.project_id = p.id AND p.deleted = FALSE
WHERE wi.board_id = $1
AND wi.deleted = FALSE
AND wi.completed = FALSE
@@ -12216,6 +12326,7 @@ func (a *App) getWishlistItemsByBoard(boardID int, userID int) ([]WishlistItem,
var imagePath sql.NullString
var link sql.NullString
var completed bool
var itemOwnerID sql.NullInt64
var conditionID sql.NullInt64
var displayOrder sql.NullInt64
var taskConditionID sql.NullInt64
@@ -12229,7 +12340,7 @@ func (a *App) getWishlistItemsByBoard(boardID int, userID int) ([]WishlistItem,
var startDate sql.NullTime
err := rows.Scan(
&itemID, &name, &price, &imagePath, &link, &completed,
&itemID, &name, &price, &imagePath, &link, &completed, &itemOwnerID,
&conditionID, &displayOrder, &taskConditionID, &scoreConditionID, &conditionUserID,
&taskID, &taskName, &projectID, &projectName, &requiredPoints, &startDate,
)
@@ -12268,8 +12379,12 @@ func (a *App) getWishlistItemsByBoard(boardID int, userID int) ([]WishlistItem,
DisplayOrder: int(displayOrder.Int64),
}
// Используем user_id из условия, если он есть, иначе используем текущего пользователя
conditionOwnerID := userID
// Используем user_id из условия, если он есть, иначе используем владельца желания
if !itemOwnerID.Valid {
log.Printf("Warning: item_owner_id is NULL for wishlist item %d, skipping condition", itemID)
continue
}
conditionOwnerID := int(itemOwnerID.Int64)
if conditionUserID.Valid {
conditionOwnerID = int(conditionUserID.Int64)
}
@@ -12291,6 +12406,13 @@ func (a *App) getWishlistItemsByBoard(boardID int, userID int) ([]WishlistItem,
if projectName.Valid {
condition.ProjectName = &projectName.String
}
if projectID.Valid {
projectIDVal := int(projectID.Int64)
condition.ProjectID = &projectIDVal
// Считаем текущие баллы для владельца условия
points, _ := a.calculateProjectPointsFromDate(int(projectID.Int64), startDate, conditionOwnerID)
condition.CurrentPoints = &points
}
if requiredPoints.Valid {
condition.RequiredPoints = &requiredPoints.Float64
}
@@ -12298,11 +12420,6 @@ func (a *App) getWishlistItemsByBoard(boardID int, userID int) ([]WishlistItem,
dateStr := startDate.Time.Format("2006-01-02")
condition.StartDate = &dateStr
}
// Считаем текущие баллы для владельца условия
if projectID.Valid {
points, _ := a.calculateProjectPointsFromDate(int(projectID.Int64), startDate, conditionOwnerID)
condition.CurrentPoints = &points
}
}
item.UnlockConditions = append(item.UnlockConditions, condition)

View File

@@ -0,0 +1,13 @@
-- Migration: Remove wishlist conditions without user_id
-- These conditions should not exist as every condition must have an owner
-- This migration removes orphaned conditions that were created before the fix
-- ============================================
-- Remove conditions without user_id
-- ============================================
DELETE FROM wishlist_conditions WHERE user_id IS NULL;
-- ============================================
-- Comments
-- ============================================
COMMENT ON COLUMN wishlist_conditions.user_id IS 'Owner of this condition. Each user has their own goals on shared boards. Required field.';