diff --git a/src/Project/model.go b/src/Project/model.go deleted file mode 100644 index 858689c14a6849b76340919bea2bed9176832551..0000000000000000000000000000000000000000 --- a/src/Project/model.go +++ /dev/null @@ -1,168 +0,0 @@ -package project - -import ( - "time" - - "github.com/google/uuid" - "go.mongodb.org/mongo-driver/bson/primitive" -) - -// Project represents the main project entity stored in MongoDB -// This struct defines the complete data structure for a project including -// metadata fields for tracking creation and modification times -type Project struct { - // ID is the unique MongoDB ObjectID that serves as the primary key - // The omitempty tag excludes this field if it's empty during insertion - ID primitive.ObjectID bson:"_id,omitempty" json:"id" - - // ProjectID is a human-readable unique identifier (e.g., "PRJ-2025-001") - // This allows users to reference projects with meaningful IDs - ProjectID string bson:"project_id" json:"projectId" - - // Name is the display name of the project (e.g., "HARMONY Mobile") - // This is what users see in the interface as the project title - Name string bson:"name" json:"name" - - // Description provides detailed information about the project's purpose - // This field is optional and can be empty for simple projects - Description string bson:"description" json:"description" - - // StartDate defines when the project begins - // Used for project timeline calculations and reporting - StartDate time.Time bson:"start_date" json:"startDate" - - // EndDate defines when the project is scheduled to complete - // Must be after StartDate as enforced by business logic - EndDate time.Time bson:"end_date" json:"endDate" - - // CreatedBy stores the UUID of the user who created this project - // This establishes ownership and is used for access control - CreatedBy uuid.UUID bson:"created_by" json:"createdBy" - - // CreatedAt records when this project was first created - // Automatically set during project creation for audit trails - CreatedAt time.Time bson:"created_at" json:"createdAt" - - // UpdatedAt tracks the last modification time - // Uses pointer to allow nil values when no updates have occurred - // The omitempty tag excludes this field if it's nil - UpdatedAt *time.Time bson:"updated_at,omitempty" json:"updatedAt,omitempty" -} - -// ProjectToCreate represents the data structure used when creating new projects -// This struct contains only the fields that users can specify during creation -// System-generated fields like ID and timestamps are handled separately -type ProjectToCreate struct { - // ProjectID must be provided by the user and must be unique - // Validation ensures this field is not empty - ProjectID string json:"projectId" hvalidate:"required" - - // Name is required and serves as the display title - // Validation ensures this field is not empty - Name string json:"name" hvalidate:"required" - - // Description is optional and can be left empty - // No validation constraints are applied to this field - Description string json:"description" - - // StartDate is required and must be a valid date - // Validation ensures this field is provided - StartDate time.Time json:"startDate" hvalidate:"required" - - // EndDate is required and must be after StartDate - // Basic validation ensures this field is provided - // Business logic validation ensures it's after StartDate - EndDate time.Time json:"endDate" hvalidate:"required" - - // CreatedBy is set by the system based on the current user - // The - tag excludes this from JSON serialization as it's not user input - CreatedBy uuid.UUID json:"-" -} - -// ProjectToUpdate represents the data structure used when updating existing projects -// This struct includes the project ID to identify which project to update -// Note: ProjectID is not included as it cannot be changed after creation -type ProjectToUpdate struct { - // ID identifies which project to update using the MongoDB ObjectID - // This field is populated from the URL parameter - ID primitive.ObjectID bson:"_id" json:"id" - - // Name can be updated and is required to be non-empty - // Users can change the display name of their projects - Name string json:"name" hvalidate:"required" - - // Description can be updated and can be empty - // Users can modify or clear the project description - Description string json:"description" - - // StartDate can be updated but must remain valid - // Users can adjust project timelines as needed - StartDate time.Time json:"startDate" hvalidate:"required" - - // EndDate can be updated but must remain after StartDate - // Business logic validation ensures timeline consistency - EndDate time.Time json:"endDate" hvalidate:"required" -} - -// ToUpdate converts a Project instance to a ProjectToUpdate structure -// This method is useful when pre-filling edit forms with existing project data -// It extracts only the fields that can be modified during updates -func (p *Project) ToUpdate() *ProjectToUpdate { - return &ProjectToUpdate{ - ID: p.ID, // Preserve the project identifier - Name: p.Name, // Current name as default - Description: p.Description, // Current description as default - StartDate: p.StartDate, // Current start date as default - EndDate: p.EndDate, // Current end date as default - } -} - -// ProjectWithRequirements represents a project along with its associated requirements -// This structure is used for project detail views where both project information -// and requirement summaries need to be displayed together -type ProjectWithRequirements struct { - // Embed the base Project struct to inherit all project fields - // This allows access to all project properties directly - *Project - - // Requirements contains summary information for all requirements in this project - // This is optimized for display purposes rather than full requirement details - Requirements []*RequirementSummary json:"requirements" - - // RequirementCount provides a quick count of total requirements - // This avoids the need to calculate len(Requirements) in templates - RequirementCount int json:"requirementCount" -} - -// RequirementSummary provides essential requirement information for project views -// This structure contains only the fields needed for requirement lists and summaries -// Full requirement details would be loaded separately when viewing individual requirements -type RequirementSummary struct { - // ID is the unique MongoDB ObjectID for this requirement - // Used for linking to detailed requirement views - ID primitive.ObjectID bson:"_id" json:"id" - - // RequirementID is a human-readable identifier for this requirement - // Generated automatically if not provided (e.g., "REQ-abc123") - RequirementID string bson:"requirement_id" json:"requirementId" - - // Condition describes the circumstances under which this requirement applies - // This field comes from the requirement parsing and validation process - Condition string bson:"condition" json:"condition" - - // System identifies which system or component this requirement affects - // This helps organize requirements by their target systems - System string bson:"system" json:"system" - - // Requirement contains the actual requirement text - // This is the main content that describes what must be implemented - Requirement string bson:"requirement" json:"requirement" - - // Status tracks the current state of this requirement - // Possible values include "Entwurf", "In Prüfung", "Genehmigt", "Abgelehnt" - Status string bson:"status" json:"status" - - // CreatedAt records when this requirement was first created - // Used for sorting and audit purposes - CreatedAt time.Time bson:"created_at" json:"createdAt" -} \ No newline at end of file diff --git a/src/Project/mongodb.go b/src/Project/mongodb.go deleted file mode 100644 index 33bd24ab8b3b06df8fd9788b87f9cfa8e3af69e3..0000000000000000000000000000000000000000 --- a/src/Project/mongodb.go +++ /dev/null @@ -1,97 +0,0 @@ -package project - -import ( - "context" - "fmt" - "log" - "time" - - "go.mongodb.org/mongo-driver/mongo" - "go.mongodb.org/mongo-driver/mongo/options" -) - -// MongoManager handles MongoDB connection management and database operations -// This struct encapsulates the MongoDB client and database references -// It provides a centralized way to manage database connections throughout the application -type MongoManager struct { - client *mongo.Client // The MongoDB client for connection management - db *mongo.Database // The specific database instance for this application -} - -// NewMongoManager creates and initializes a new MongoDB connection manager -// This function establishes a connection to MongoDB with optimized settings -// and returns a manager instance that can be used throughout the application -func NewMongoManager(uri, dbName string) (*MongoManager, error) { - log.Printf("INFO: Connecting to MongoDB at %s, database: %s", uri, dbName) - - // Configure MongoDB client options for optimal performance - clientOptions := options.Client().ApplyURI(uri) - - // Set connection pool parameters for handling concurrent requests efficiently - // MaxPoolSize: Maximum number of connections in the pool (100 connections) - // This allows the application to handle many concurrent database operations - clientOptions.SetMaxPoolSize(100) - - // MinPoolSize: Minimum number of connections to maintain (10 connections) - // This ensures quick response times by keeping connections ready - clientOptions.SetMinPoolSize(10) - - // MaxConnIdleTime: How long idle connections stay open (30 seconds) - // This balances resource usage with connection availability - clientOptions.SetMaxConnIdleTime(30 * time.Second) - - log.Printf("DEBUG: MongoDB connection pool configured - max: 100, min: 10, idle timeout: 30s") - - // Attempt to create a connection to MongoDB using the configured options - client, err := mongo.Connect(context.TODO(), clientOptions) - if err != nil { - log.Printf("ERROR: Failed to connect to MongoDB: %v", err) - return nil, fmt.Errorf("failed to connect to MongoDB: %w", err) - } - - log.Println("DEBUG: MongoDB client created, testing connection...") - - // Test the connection by pinging the MongoDB server - // This ensures that the connection is actually working before proceeding - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() // Ensure the context is cleaned up after the ping - - if err := client.Ping(ctx, nil); err != nil { - log.Printf("ERROR: MongoDB ping failed: %v", err) - return nil, fmt.Errorf("failed to ping MongoDB: %w", err) - } - - log.Printf("INFO: Successfully connected to MongoDB database '%s'", dbName) - - // Return a configured MongoManager instance - return &MongoManager{ - client: client, // Store the client for connection management - db: client.Database(dbName), // Store the database reference for operations - }, nil -} - -// GetDatabase returns the MongoDB database instance for performing operations -// This method provides access to the database for repositories and other components -// that need to interact with MongoDB collections -func (m *MongoManager) GetDatabase() *mongo.Database { - log.Printf("DEBUG: Returning database instance: %s", m.db.Name()) - return m.db -} - -// Close gracefully shuts down the MongoDB connection -// This method should be called when the application is shutting down -// to ensure all connections are properly closed and resources are released -func (m *MongoManager) Close() error { - log.Println("INFO: Closing MongoDB connection...") - - // Disconnect the MongoDB client using a background context - // This ensures that ongoing operations can complete before disconnection - err := m.client.Disconnect(context.Background()) - if err != nil { - log.Printf("ERROR: Failed to close MongoDB connection: %v", err) - return err - } - - log.Println("INFO: MongoDB connection closed successfully") - return nil -} diff --git a/src/Project/repository.go b/src/Project/repository.go deleted file mode 100644 index 164ff09bcff81560690b25aac0b4fad14d1f55a8..0000000000000000000000000000000000000000 --- a/src/Project/repository.go +++ /dev/null @@ -1,393 +0,0 @@ -package project - -import ( - "context" - "fmt" - "log" - "time" - - "github.com/google/uuid" - "go.mongodb.org/mongo-driver/bson" - "go.mongodb.org/mongo-driver/bson/primitive" - "go.mongodb.org/mongo-driver/mongo" - "go.mongodb.org/mongo-driver/mongo/options" -) - -// MongoDB collection names used throughout the application -// These constants ensure consistent collection naming and make changes easier -const ( - ProjectCollectionName = "projects" // Main projects collection - RequirementCollectionName = "requirements" // Requirements associated with projects -) - -// ProjectRepository defines the interface for project data access operations -// This interface abstracts the underlying database technology and allows for -// easy testing with mock implementations or switching database providers -type ProjectRepository interface { - // Create inserts a new project into the database - Create(ctx context.Context, project *ProjectToCreate) (*Project, error) - - // FindByID retrieves a project using its MongoDB ObjectID - FindByID(ctx context.Context, id primitive.ObjectID) (*Project, error) - - // FindByProjectID retrieves a project using its human-readable project ID - FindByProjectID(ctx context.Context, projectID string) (*Project, error) - - // FindByCreatedBy retrieves all projects created by a specific user - FindByCreatedBy(ctx context.Context, userID uuid.UUID) ([]*Project, error) - - // FindWithRequirements retrieves a project along with all its requirements - FindWithRequirements(ctx context.Context, id primitive.ObjectID) (*ProjectWithRequirements, error) - - // Update modifies an existing project with new data - Update(ctx context.Context, update *ProjectToUpdate) (*Project, error) - - // Delete removes a project and all associated requirements - Delete(ctx context.Context, id primitive.ObjectID) error - - // Search finds projects matching a text query for a specific user - Search(ctx context.Context, query string, userID uuid.UUID) ([]*Project, error) -} - -// MongoProjectRepository implements ProjectRepository using MongoDB -// This struct contains MongoDB-specific implementation details and connection handling -type MongoProjectRepository struct { - projectCollection *mongo.Collection // MongoDB collection for projects - requirementCollection *mongo.Collection // MongoDB collection for requirements - timeout time.Duration // Default timeout for database operations -} - -// NewMongoProjectRepository creates a new MongoDB-based repository instance -// This constructor initializes the repository with references to the required collections -func NewMongoProjectRepository(db *mongo.Database) ProjectRepository { - log.Printf("INFO: Creating MongoProjectRepository with database %s", db.Name()) - return &MongoProjectRepository{ - projectCollection: db.Collection(ProjectCollectionName), - requirementCollection: db.Collection(RequirementCollectionName), - timeout: 10 * time.Second, // Standard timeout for all operations - } -} - -// Create inserts a new project document into the MongoDB projects collection -// This method handles the conversion from ProjectToCreate to Project and sets system fields -func (r *MongoProjectRepository) Create(ctx context.Context, toCreate *ProjectToCreate) (*Project, error) { - // Set a timeout for this operation to prevent hanging connections - ctx, cancel := context.WithTimeout(ctx, r.timeout) - defer cancel() - - log.Printf("DEBUG: Creating project document for '%s'", toCreate.ProjectID) - - // Convert the creation request to a full project document - // Set system-generated fields like ID and creation timestamp - project := &Project{ - ID: primitive.NewObjectID(), // Generate new MongoDB ObjectID - ProjectID: toCreate.ProjectID, // User-provided project identifier - Name: toCreate.Name, // Project display name - Description: toCreate.Description, // Optional project description - StartDate: toCreate.StartDate, // Project start date - EndDate: toCreate.EndDate, // Project end date - CreatedBy: toCreate.CreatedBy, // User who created this project - CreatedAt: time.Now(), // Current timestamp - } - - log.Printf("DEBUG: Inserting project into MongoDB collection %s", ProjectCollectionName) - - // Insert the project document into MongoDB - result, err := r.projectCollection.InsertOne(ctx, project) - if err != nil { - log.Printf("ERROR: MongoDB insert failed for project '%s': %v", toCreate.ProjectID, err) - return nil, fmt.Errorf("failed to create project: %w", err) - } - - // Update the project with the actual ObjectID assigned by MongoDB - if oid, ok := result.InsertedID.(primitive.ObjectID); ok { - project.ID = oid - log.Printf("INFO: Project '%s' inserted with MongoDB ID %s", project.ProjectID, oid.Hex()) - } - - return project, nil -} - -// FindByID retrieves a project using its MongoDB ObjectID -// This is the primary method for finding projects when you have the database ID -func (r *MongoProjectRepository) FindByID(ctx context.Context, id primitive.ObjectID) (*Project, error) { - ctx, cancel := context.WithTimeout(ctx, r.timeout) - defer cancel() - - log.Printf("DEBUG: Finding project by MongoDB ID %s", id.Hex()) - - var project Project - // Query MongoDB using the _id field (primary key) - err := r.projectCollection.FindOne(ctx, bson.M{"_id": id}).Decode(&project) - if err != nil { - if err == mongo.ErrNoDocuments { - log.Printf("WARN: Project with ID %s not found", id.Hex()) - return nil, fmt.Errorf("project not found") - } - log.Printf("ERROR: MongoDB query failed for project ID %s: %v", id.Hex(), err) - return nil, fmt.Errorf("failed to find project: %w", err) - } - - log.Printf("INFO: Found project '%s' (ProjectID: %s)", project.Name, project.ProjectID) - return &project, nil -} - -// FindByProjectID retrieves a project using its human-readable project identifier -// This method is used when checking for duplicate project IDs during creation -func (r *MongoProjectRepository) FindByProjectID(ctx context.Context, projectID string) (*Project, error) { - ctx, cancel := context.WithTimeout(ctx, r.timeout) - defer cancel() - - log.Printf("DEBUG: Finding project by ProjectID '%s'", projectID) - - var project Project - // Query using the project_id field (user-defined identifier) - err := r.projectCollection.FindOne(ctx, bson.M{"project_id": projectID}).Decode(&project) - if err != nil { - if err == mongo.ErrNoDocuments { - log.Printf("DEBUG: Project with ProjectID '%s' not found (this may be expected)", projectID) - return nil, fmt.Errorf("project not found") - } - log.Printf("ERROR: MongoDB query failed for ProjectID '%s': %v", projectID, err) - return nil, fmt.Errorf("failed to find project: %w", err) - } - - log.Printf("INFO: Found project '%s' by ProjectID '%s'", project.Name, projectID) - return &project, nil -} - -// FindByCreatedBy retrieves all projects created by a specific user -// Results are sorted by creation date (newest first) for better user experience -func (r *MongoProjectRepository) FindByCreatedBy(ctx context.Context, userID uuid.UUID) ([]*Project, error) { - ctx, cancel := context.WithTimeout(ctx, r.timeout) - defer cancel() - - log.Printf("DEBUG: Finding projects for user %s", userID) - - // Query for all projects where created_by matches the user ID - // Sort by created_at in descending order (newest first) - cursor, err := r.projectCollection.Find( - ctx, - bson.M{"created_by": userID}, // Filter condition - options.Find().SetSort(bson.D{{"created_at", -1}}), // Sort options - ) - if err != nil { - log.Printf("ERROR: MongoDB query failed for user %s: %v", userID, err) - return nil, fmt.Errorf("failed to find projects: %w", err) - } - defer cursor.Close(ctx) // Ensure cursor is closed to free resources - - // Iterate through the cursor and decode each document - var projects []*Project - for cursor.Next(ctx) { - var project Project - if err := cursor.Decode(&project); err != nil { - log.Printf("ERROR: Failed to decode project document: %v", err) - return nil, fmt.Errorf("failed to decode project: %w", err) - } - projects = append(projects, &project) - } - - log.Printf("INFO: Found %d projects for user %s", len(projects), userID) - return projects, cursor.Err() // Return any cursor iteration errors -} - -// FindWithRequirements retrieves a project along with all its associated requirements -// This method is optimized for project detail pages where both sets of data are needed -func (r *MongoProjectRepository) FindWithRequirements(ctx context.Context, id primitive.ObjectID) (*ProjectWithRequirements, error) { - ctx, cancel := context.WithTimeout(ctx, r.timeout) - defer cancel() - - log.Printf("DEBUG: Finding project with requirements for ID %s", id.Hex()) - - // First, load the base project information - project, err := r.FindByID(ctx, id) - if err != nil { - log.Printf("ERROR: Failed to find base project for ID %s: %v", id.Hex(), err) - return nil, err - } - - log.Printf("DEBUG: Loading requirements for project '%s' (ProjectID: %s)", project.Name, project.ProjectID) - - // Load all requirements associated with this project - // Requirements are linked by the project_id field (not MongoDB ObjectID) - cursor, err := r.requirementCollection.Find( - ctx, - bson.M{"project_id": project.ProjectID}, // Link by project_id - options.Find().SetSort(bson.D{{"created_at", -1}}), // Newest first - ) - if err != nil { - log.Printf("ERROR: Failed to query requirements for project '%s': %v", project.ProjectID, err) - return nil, fmt.Errorf("failed to find requirements: %w", err) - } - defer cursor.Close(ctx) - - // Process each requirement document - var requirements []*RequirementSummary - for cursor.Next(ctx) { - var req RequirementSummary - if err := cursor.Decode(&req); err != nil { - log.Printf("WARN: Skipping invalid requirement document: %v", err) - continue // Skip documents that can't be decoded rather than failing entirely - } - - // Extract additional fields from nested parsing_result if available - // This handles requirements that have been processed by the EIFFEL system - var doc bson.M - cursor.Decode(&doc) - if parsingResult, ok := doc["parsing_result"].(bson.M); ok { - if requirement, ok := parsingResult["requirement"].(string); ok { - req.Requirement = requirement - } - } - - // Set default values for missing fields to ensure consistency - if req.Status == "" { - req.Status = "Entwurf" // Default status for new requirements - } - if req.RequirementID == "" { - // Generate a default requirement ID from the MongoDB ObjectID - req.RequirementID = fmt.Sprintf("REQ-%s", req.ID.Hex()[:6]) - } - - requirements = append(requirements, &req) - } - - log.Printf("INFO: Loaded project '%s' with %d requirements", project.Name, len(requirements)) - - // Return the combined structure with project and requirements - return &ProjectWithRequirements{ - Project: project, - Requirements: requirements, - RequirementCount: len(requirements), - }, nil -} - -// Update modifies an existing project with new data and sets the updated timestamp -// This method only updates the fields that can be modified, preserving system fields -func (r *MongoProjectRepository) Update(ctx context.Context, update *ProjectToUpdate) (*Project, error) { - ctx, cancel := context.WithTimeout(ctx, r.timeout) - defer cancel() - - log.Printf("DEBUG: Updating project %s with name '%s'", update.ID.Hex(), update.Name) - - // Prepare the update document with new values and current timestamp - now := time.Now() - updateDoc := bson.M{ - "$set": bson.M{ - "name": update.Name, // Updated project name - "description": update.Description, // Updated description - "start_date": update.StartDate, // Updated start date - "end_date": update.EndDate, // Updated end date - "updated_at": now, // Set current time as update timestamp - }, - } - - // Execute the update operation - result, err := r.projectCollection.UpdateOne( - ctx, - bson.M{"_id": update.ID}, // Find the project by ObjectID - updateDoc, // Apply the updates - ) - if err != nil { - log.Printf("ERROR: MongoDB update failed for project %s: %v", update.ID.Hex(), err) - return nil, fmt.Errorf("failed to update project: %w", err) - } - - log.Printf("INFO: Project %s updated - matched: %d, modified: %d", - update.ID.Hex(), result.MatchedCount, result.ModifiedCount) - - // Return the updated project by fetching it again - // This ensures we return the most current data including the updated timestamp - return r.FindByID(ctx, update.ID) -} - -// Delete removes a project and all its associated requirements from the database -// This method ensures referential integrity by cleaning up related data -func (r *MongoProjectRepository) Delete(ctx context.Context, id primitive.ObjectID) error { - ctx, cancel := context.WithTimeout(ctx, r.timeout) - defer cancel() - - log.Printf("DEBUG: Starting deletion process for project %s", id.Hex()) - - // First, find the project to get its ProjectID for requirement cleanup - project, err := r.FindByID(ctx, id) - if err != nil { - log.Printf("ERROR: Cannot find project %s for deletion: %v", id.Hex(), err) - return err - } - - log.Printf("INFO: Deleting project '%s' and all its requirements", project.Name) - - // Delete all requirements associated with this project - // Requirements are linked by project_id (string) not MongoDB ObjectID - reqDeleteResult, err := r.requirementCollection.DeleteMany(ctx, bson.M{"project_id": project.ProjectID}) - if err != nil { - log.Printf("ERROR: Failed to delete requirements for project '%s': %v", project.ProjectID, err) - return fmt.Errorf("failed to delete project requirements: %w", err) - } - - log.Printf("INFO: Deleted %d requirements for project '%s'", reqDeleteResult.DeletedCount, project.ProjectID) - - // Delete the project itself - projDeleteResult, err := r.projectCollection.DeleteOne(ctx, bson.M{"_id": id}) - if err != nil { - log.Printf("ERROR: Failed to delete project %s: %v", id.Hex(), err) - return fmt.Errorf("failed to delete project: %w", err) - } - - // Verify that the project was actually deleted - if projDeleteResult.DeletedCount == 0 { - log.Printf("WARN: No project was deleted for ID %s", id.Hex()) - } else { - log.Printf("INFO: Project '%s' deleted successfully", project.Name) - } - - return nil -} - -// Search finds projects matching a text query across multiple fields -// This method provides flexible search functionality for the project list interface -func (r *MongoProjectRepository) Search(ctx context.Context, query string, userID uuid.UUID) ([]*Project, error) { - ctx, cancel := context.WithTimeout(ctx, r.timeout) - defer cancel() - - log.Printf("DEBUG: Searching projects for user %s with query '%s'", userID, query) - - // Build a MongoDB query that searches across multiple fields - // Uses case-insensitive regular expressions for flexible matching - filter := bson.M{ - "created_by": userID, // Only search projects owned by this user - "$or": []bson.M{ // Search across multiple fields - {"name": bson.M{"$regex": query, "$options": "i"}}, // Project name - {"description": bson.M{"$regex": query, "$options": "i"}}, // Project description - {"project_id": bson.M{"$regex": query, "$options": "i"}}, // Project ID - }, - } - - // Execute the search query with sorting - cursor, err := r.projectCollection.Find( - ctx, - filter, - options.Find().SetSort(bson.D{{"created_at", -1}}), // Newest first - ) - if err != nil { - log.Printf("ERROR: MongoDB search query failed for '%s': %v", query, err) - return nil, fmt.Errorf("failed to search projects: %w", err) - } - defer cursor.Close(ctx) - - // Decode the search results - var projects []*Project - for cursor.Next(ctx) { - var project Project - if err := cursor.Decode(&project); err != nil { - log.Printf("ERROR: Failed to decode project in search results: %v", err) - return nil, fmt.Errorf("failed to decode project: %w", err) - } - projects = append(projects, &project) - } - - log.Printf("INFO: Search completed - found %d projects for query '%s'", len(projects), query) - return projects, cursor.Err() -} diff --git a/src/Project/service.go b/src/Project/service.go deleted file mode 100644 index 03691430ffd83bab2f88810799d2e545ad34cac4..0000000000000000000000000000000000000000 --- a/src/Project/service.go +++ /dev/null @@ -1,176 +0,0 @@ -package project - -import ( - "context" - "fmt" - "log" - - "github.com/google/uuid" - "go.mongodb.org/mongo-driver/bson/primitive" -) - -// ProjectService contains the business logic for project management operations -// This service layer sits between the web controllers and the data repository -// It enforces business rules, handles validation, and coordinates data operations -type ProjectService struct { - repo ProjectRepository // Repository interface for data access operations -} - -// NewProjectService creates and initializes a new ProjectService instance -// The service requires a repository implementation to handle data persistence -func NewProjectService(repo ProjectRepository) *ProjectService { - log.Println("INFO: ProjectService initialized") - return &ProjectService{repo: repo} -} - -// CreateProject handles the creation of new projects with business logic validation -// This method ensures project uniqueness, validates business rules, and creates the project -func (s *ProjectService) CreateProject(ctx context.Context, toCreate *ProjectToCreate) (*Project, error) { - log.Printf("INFO: Creating project with ID '%s' and name '%s'", toCreate.ProjectID, toCreate.Name) - - // Business Rule 1: Project IDs must be unique across the entire system - // Check if a project with this ID already exists before creating a new one - log.Printf("DEBUG: Checking if project ID '%s' already exists", toCreate.ProjectID) - existing, err := s.repo.FindByProjectID(ctx, toCreate.ProjectID) - if err == nil && existing != nil { - // If we found an existing project, the creation should fail - log.Printf("WARN: Project creation failed - ID '%s' already exists", toCreate.ProjectID) - return nil, fmt.Errorf("project with ID '%s' already exists", toCreate.ProjectID) - } - - // Business Rule 2: Project end date must be after the start date - // This prevents creating projects with invalid date ranges - if toCreate.EndDate.Before(toCreate.StartDate) { - log.Printf("WARN: Project creation failed - end date (%s) before start date (%s)", - toCreate.EndDate.Format("2006-01-02"), toCreate.StartDate.Format("2006-01-02")) - return nil, fmt.Errorf("end date must be after start date") - } - - log.Printf("DEBUG: Project validation passed, creating in repository") - - // All business rules passed, delegate to repository for actual creation - project, err := s.repo.Create(ctx, toCreate) - if err != nil { - log.Printf("ERROR: Failed to create project '%s' in repository: %v", toCreate.ProjectID, err) - return nil, err - } - - log.Printf("INFO: Project '%s' created successfully with MongoDB ID %s", - project.ProjectID, project.ID.Hex()) - return project, nil -} - -// GetProjectsByUser retrieves all projects belonging to a specific user -// This method provides a simple interface for fetching user-specific projects -func (s *ProjectService) GetProjectsByUser(ctx context.Context, userID uuid.UUID) ([]*Project, error) { - log.Printf("INFO: Getting projects for user %s", userID) - - // Delegate to repository for data retrieval - projects, err := s.repo.FindByCreatedBy(ctx, userID) - if err != nil { - log.Printf("ERROR: Failed to get projects for user %s: %v", userID, err) - return nil, err - } - - log.Printf("INFO: Found %d projects for user %s", len(projects), userID) - - // Debug logging: Show project names for troubleshooting - if len(projects) > 0 { - log.Printf("DEBUG: Projects found: %v", func() []string { - names := make([]string, len(projects)) - for i, p := range projects { - names[i] = p.Name - } - return names - }()) - } - - return projects, nil -} - -// GetProjectWithRequirements retrieves a project along with all its associated requirements -// This method is optimized for displaying project detail pages where both -// project information and requirements are needed simultaneously -func (s *ProjectService) GetProjectWithRequirements(ctx context.Context, id primitive.ObjectID) (*ProjectWithRequirements, error) { - log.Printf("INFO: Getting project with requirements for ID %s", id.Hex()) - - // Use the repository method that efficiently loads project and requirements together - // This is more efficient than making separate calls for project and requirements - projectWithReqs, err := s.repo.FindWithRequirements(ctx, id) - if err != nil { - log.Printf("ERROR: Failed to get project with requirements for ID %s: %v", id.Hex(), err) - return nil, err - } - - log.Printf("INFO: Loaded project '%s' with %d requirements", - projectWithReqs.Project.Name, projectWithReqs.RequirementCount) - - return projectWithReqs, nil -} - -// UpdateProject handles project updates with business rule validation -// This method ensures that updates maintain data integrity and business rules -func (s *ProjectService) UpdateProject(ctx context.Context, update *ProjectToUpdate) (*Project, error) { - log.Printf("INFO: Updating project %s with name '%s'", update.ID.Hex(), update.Name) - - // Business Rule: End date must still be after start date after the update - // This validation is applied to updates just like it is for creation - if update.EndDate.Before(update.StartDate) { - log.Printf("WARN: Project update failed - end date (%s) before start date (%s)", - update.EndDate.Format("2006-01-02"), update.StartDate.Format("2006-01-02")) - return nil, fmt.Errorf("end date must be after start date") - } - - log.Printf("DEBUG: Project update validation passed, updating in repository") - - // Delegate to repository for the actual update operation - project, err := s.repo.Update(ctx, update) - if err != nil { - log.Printf("ERROR: Failed to update project %s: %v", update.ID.Hex(), err) - return nil, err - } - - log.Printf("INFO: Project '%s' updated successfully", project.Name) - return project, nil -} - -// DeleteProject handles the complete removal of a project and all associated data -// This method ensures that all related requirements are also deleted to maintain data consistency -func (s *ProjectService) DeleteProject(ctx context.Context, id primitive.ObjectID) error { - log.Printf("INFO: Deleting project with ID %s", id.Hex()) - - // The repository handles the cascading deletion of requirements - // This ensures that no orphaned requirements remain in the database - err := s.repo.Delete(ctx, id) - if err != nil { - log.Printf("ERROR: Failed to delete project %s: %v", id.Hex(), err) - return err - } - - log.Printf("INFO: Project %s and all its requirements deleted successfully", id.Hex()) - return nil -} - -// SearchProjects provides flexible project search functionality -// This method handles both empty queries (return all projects) and text-based searches -func (s *ProjectService) SearchProjects(ctx context.Context, query string, userID uuid.UUID) ([]*Project, error) { - // Handle empty search queries by returning all projects for the user - // This provides a consistent interface regardless of whether a search term is provided - if query == "" { - log.Printf("INFO: Empty search query, returning all projects for user %s", userID) - return s.repo.FindByCreatedBy(ctx, userID) - } - - log.Printf("INFO: Searching projects for user %s with query '%s'", userID, query) - - // Delegate to repository for the actual search implementation - // The repository handles the specifics of text searching across project fields - projects, err := s.repo.Search(ctx, query, userID) - if err != nil { - log.Printf("ERROR: Search failed for query '%s' and user %s: %v", query, userID, err) - return nil, err - } - - log.Printf("INFO: Search completed - found %d projects for query '%s'", len(projects), query) - return projects, nil -} diff --git a/src/Project/web/web.go b/src/Project/web/web.go deleted file mode 100644 index 198aff1e64ff14e07a472e95cdc3ba65d93d5a49..0000000000000000000000000000000000000000 --- a/src/Project/web/web.go +++ /dev/null @@ -1,492 +0,0 @@ -package web - -import ( - "errors" - "fmt" - "log" - "net/http" - "time" - - "github.com/org-harmony/harmony/src/app/user" - "github.com/org-harmony/harmony/src/core/hctx" - "github.com/org-harmony/harmony/src/core/validation" - "github.com/org-harmony/harmony/src/core/web" - "go.mongodb.org/mongo-driver/bson/primitive" -) - -// ProjectFormData represents the data structure needed for rendering project forms -// This struct is used for both creating new projects and editing existing ones -type ProjectFormData struct { - Project any // Can hold either *project.ProjectToCreate or *project.ProjectToUpdate - IsEditForm bool // Flag to determine if this is an edit form (true) or create form (false) -} - -// ProjectListData contains the list of projects for template rendering -// This structure is passed to the project list template -type ProjectListData struct { - Projects []*project.Project // Array of all projects belonging to a user -} - -// ProjectDetailData holds all necessary data for displaying detailed project information -// including tab navigation and requirement management -type ProjectDetailData struct { - Project *project.ProjectWithRequirements // The project with all its requirements loaded - ActiveTab string // Currently active tab: "general" or "requirements" - RequirementStatuses []string // Available status options for requirements -} - -// RegisterController sets up all HTTP routes and handlers for project management -// This function is called during application startup to configure the web routing -func RegisterController(appCtx *hctx.AppCtx, webCtx *web.Ctx, mongoManager *project.MongoManager) { - log.Println("INFO: Registering project controllers...") - - // Initialize the data access layer (repository) and business logic layer (service) - // Repository handles MongoDB operations, Service handles business rules - projectRepo := project.NewMongoProjectRepository(mongoManager.GetDatabase()) - projectService := project.NewProjectService(projectRepo) - log.Println("INFO: Project repository and service initialized") - - // Create a router group that requires user authentication - // All routes defined here will require a logged-in user - router := webCtx.Router.With(user.LoggedInMiddleware(appCtx)) - - // Define all HTTP routes for project management: - - // GET /project/list - Display all projects for the current user - router.Get("/project/list", projectListController(appCtx, webCtx, projectService).ServeHTTP) - - // GET /project/new - Show form for creating a new project - router.Get("/project/new", projectNewController(appCtx, webCtx).ServeHTTP) - - // POST /project/new - Process new project creation form submission - router.Post("/project/new", projectCreateController(appCtx, webCtx, projectService).ServeHTTP) - - // GET /project/{id} - Display detailed view of a specific project - router.Get("/project/{id}", projectDetailController(appCtx, webCtx, projectService).ServeHTTP) - - // GET /project/{id}/edit - Show form for editing an existing project - router.Get("/project/{id}/edit", projectEditFormController(appCtx, webCtx, projectService).ServeHTTP) - - // PUT /project/{id} - Process project update form submission - router.Put("/project/{id}", projectUpdateController(appCtx, webCtx, projectService).ServeHTTP) - - // DELETE /project/{id} - Delete a project and all its requirements - router.Delete("/project/{id}", projectDeleteController(appCtx, webCtx, projectService).ServeHTTP) - - // POST /project/search - Handle project search functionality - router.Post("/project/search", projectSearchController(appCtx, webCtx, projectService).ServeHTTP) - - log.Println("INFO: Project routes registered successfully") -} - -// projectListController handles requests to display all projects belonging to the current user -// This controller fetches projects from the database and renders them in a list view -func projectListController(appCtx *hctx.AppCtx, webCtx *web.Ctx, service *project.ProjectService) http.Handler { - return web.NewController(appCtx, webCtx, func(io web.IO) error { - ctx := io.Context() - // Extract the currently logged-in user from the request context - // This user information was added by the authentication middleware - usr := user.MustCtxUser(ctx) - - log.Printf("INFO: Loading project list for user %s", usr.ID) - - // Fetch all projects created by this user from the database - // This calls through the service layer to maintain separation of concerns - projects, err := service.GetProjectsByUser(ctx, usr.ID) - if err != nil { - log.Printf("ERROR: Failed to get projects for user %s: %v", usr.ID, err) - return io.Error(web.ErrInternal, err) - } - - log.Printf("INFO: Found %d projects for user %s", len(projects), usr.ID) - - // Prepare data structure for template rendering - data := ProjectListData{Projects: projects} - log.Printf("DEBUG: Rendering project list with %d projects", len(data.Projects)) - - // Render the template with the project data - // The template system will wrap this data in BaseTemplateData automatically - err = io.Render( - data, - "project.list.page", // Template name to execute - "project/list-page.go.html", // Main page template file - "project/_list.go.html", // Partial template for the project list - ) - - if err != nil { - log.Printf("ERROR: Failed to render project list: %v", err) - return err - } - - log.Println("INFO: Project list rendered successfully") - return nil - }) -} - -// projectNewController displays an empty form for creating a new project -// This controller prepares default values and renders the project creation form -func projectNewController(appCtx *hctx.AppCtx, webCtx *web.Ctx) http.Handler { - return web.NewController(appCtx, webCtx, func(io web.IO) error { - log.Println("INFO: Showing new project form") - - // Create a new project structure with sensible default values - // Start date is today, end date is 6 months in the future - toCreate := &project.ProjectToCreate{ - StartDate: time.Now(), - EndDate: time.Now().AddDate(0, 6, 0), // Add 6 months to current date - } - - // Render the project form with empty/default values - return renderProjectForm(io, &ProjectFormData{ - Project: toCreate, - IsEditForm: false, // This is a creation form, not an edit form - }, nil, nil) - }) -} - -// projectCreateController processes the form submission for creating a new project -// This controller validates the input, creates the project, and handles success/error cases -func projectCreateController(appCtx *hctx.AppCtx, webCtx *web.Ctx, service *project.ProjectService) http.Handler { - return web.NewController(appCtx, webCtx, func(io web.IO) error { - ctx := io.Context() - // Get the current user to associate the new project with them - usr := user.MustCtxUser(ctx) - - log.Printf("INFO: Creating new project for user %s", usr.ID) - - // Initialize a new project creation object with the user ID - toCreate := &project.ProjectToCreate{CreatedBy: usr.ID} - - // Parse and validate the form data from the HTTP request - // This extracts form fields and validates them against defined rules - err, validationErrs := web.ReadForm(io.Request(), toCreate, appCtx.Validator) - if err != nil { - log.Printf("ERROR: Failed to read project form: %v", err) - return io.Error(web.ErrInternal, err) - } - - // If there are validation errors, redisplay the form with error messages - if validationErrs != nil { - log.Printf("WARN: Project creation validation failed: %d errors", len(validationErrs)) - return renderProjectForm(io, &ProjectFormData{ - Project: toCreate, - IsEditForm: false, - }, nil, validationErrs) - } - - log.Printf("INFO: Creating project with ID '%s' and name '%s'", toCreate.ProjectID, toCreate.Name) - - // Attempt to create the project using the business logic service - // The service will handle additional validation and database operations - newProject, err := service.CreateProject(ctx, toCreate) - if err != nil { - log.Printf("ERROR: Failed to create project '%s': %v", toCreate.ProjectID, err) - // If creation fails, redisplay the form with the error message - return renderProjectForm(io, &ProjectFormData{ - Project: toCreate, - IsEditForm: false, - }, nil, []error{validation.Error{Msg: err.Error()}}) - } - - log.Printf("INFO: Project '%s' created successfully with MongoDB ID %s", newProject.ProjectID, newProject.ID.Hex()) - - // On successful creation, redirect to the project detail page - return io.Redirect(fmt.Sprintf("/project/%s", newProject.ID.Hex()), http.StatusFound) - }) -} - -// projectDetailController displays detailed information about a specific project -// This includes general project info and requirements, organized in tabs -func projectDetailController(appCtx *hctx.AppCtx, webCtx *web.Ctx, service *project.ProjectService) http.Handler { - return web.NewController(appCtx, webCtx, func(io web.IO) error { - ctx := io.Context() - - // Extract the project ID from the URL parameters - id, err := parseObjectID(io.Request(), "id") - if err != nil { - log.Printf("ERROR: Invalid project ID in URL: %v", err) - return io.Error(web.ErrInternal, err) - } - - log.Printf("INFO: Loading project details for ID %s", id.Hex()) - - // Fetch the project along with all its requirements from the database - // This is more efficient than making separate calls for project and requirements - projectWithReqs, err := service.GetProjectWithRequirements(ctx, id) - if err != nil { - log.Printf("ERROR: Failed to get project with requirements for ID %s: %v", id.Hex(), err) - return io.Error(web.ErrInternal, err) - } - - // Determine which tab should be active (from URL query parameter) - // Default to "general" tab if no tab is specified - activeTab := io.Request().URL.Query().Get("tab") - if activeTab == "" { - activeTab = "general" - } - - log.Printf("INFO: Loaded project '%s' with %d requirements, showing tab '%s'", - projectWithReqs.Project.Name, projectWithReqs.RequirementCount, activeTab) - - // Prepare all data needed for the project detail template - data := ProjectDetailData{ - Project: projectWithReqs, - ActiveTab: activeTab, - RequirementStatuses: []string{"Entwurf", "In Prüfung", "Genehmigt", "Abgelehnt"}, // Available status options - } - - // Render the project detail page with multiple template files - err = io.Render( - data, - "project.detail.page", // Main template name - "templates/project/detail-page.go.html", // Page wrapper template - "templates/project/_detail.go.html", // Project detail content - "templates/project/_requirements-list.go.html", // Requirements list partial - ) - - if err != nil { - log.Printf("ERROR: Failed to render project detail page: %v", err) - return err - } - - log.Println("INFO: Project detail page rendered successfully") - return nil - }) -} - -// projectEditFormController displays a form pre-filled with existing project data for editing -// This controller loads the current project data and renders it in an editable form -func projectEditFormController(appCtx *hctx.AppCtx, webCtx *web.Ctx, service *project.ProjectService) http.Handler { - return web.NewController(appCtx, webCtx, func(io web.IO) error { - ctx := io.Context() - - // Parse the project ID from the URL - id, err := parseObjectID(io.Request(), "id") - if err != nil { - log.Printf("ERROR: Invalid project ID for edit: %v", err) - return io.InlineError(web.ErrInternal, err) - } - - log.Printf("INFO: Loading project edit form for ID %s", id.Hex()) - - // Get all projects for the current user to find the requested project - // This ensures the user can only edit their own projects - proj, err := service.GetProjectsByUser(ctx, user.MustCtxUser(ctx).ID) - if err != nil { - log.Printf("ERROR: Failed to get projects for edit: %v", err) - return io.InlineError(web.ErrInternal, err) - } - - // Search through the user's projects to find the one being edited - var targetProject *project.Project - for _, p := range proj { - if p.ID == id { - targetProject = p - break - } - } - - // If the project is not found, it either doesn't exist or doesn't belong to this user - if targetProject == nil { - log.Printf("ERROR: Project with ID %s not found for edit", id.Hex()) - return io.InlineError(web.ErrInternal, errors.New("project not found")) - } - - log.Printf("INFO: Showing edit form for project '%s'", targetProject.Name) - - // Convert the project to an update structure and render the edit form - return renderProjectEditForm(io, targetProject.ToUpdate(), nil, nil) - }) -} - -// projectUpdateController processes form submissions for updating existing projects -// This controller validates changes and applies them to the database -func projectUpdateController(appCtx *hctx.AppCtx, webCtx *web.Ctx, service *project.ProjectService) http.Handler { - return web.NewController(appCtx, webCtx, func(io web.IO) error { - ctx := io.Context() - - // Extract project ID from URL parameters - id, err := parseObjectID(io.Request(), "id") - if err != nil { - log.Printf("ERROR: Invalid project ID for update: %v", err) - return io.InlineError(web.ErrInternal, err) - } - - log.Printf("INFO: Updating project with ID %s", id.Hex()) - - // Create an update structure with the project ID - toUpdate := &project.ProjectToUpdate{ID: id} - - // Parse and validate the form data from the request - err, validationErrs := web.ReadForm(io.Request(), toUpdate, appCtx.Validator) - if err != nil { - log.Printf("ERROR: Failed to read update form: %v", err) - return io.InlineError(web.ErrInternal, err) - } - - // If validation fails, redisplay the form with error messages - if validationErrs != nil { - log.Printf("WARN: Project update validation failed: %d errors", len(validationErrs)) - return renderProjectEditForm(io, toUpdate, nil, validationErrs) - } - - log.Printf("INFO: Updating project '%s' with new name '%s'", id.Hex(), toUpdate.Name) - - // Apply the updates through the service layer - updatedProject, err := service.UpdateProject(ctx, toUpdate) - if err != nil { - log.Printf("ERROR: Failed to update project %s: %v", id.Hex(), err) - return renderProjectEditForm(io, toUpdate, nil, []error{validation.Error{Msg: err.Error()}}) - } - - log.Printf("INFO: Project '%s' updated successfully", updatedProject.Name) - - // Redisplay the form with a success message - return renderProjectEditForm(io, updatedProject.ToUpdate(), []string{"Projekt erfolgreich aktualisiert"}, nil) - }) -} - -// projectDeleteController handles project deletion requests -// This controller removes the project and all associated requirements from the database -func projectDeleteController(appCtx *hctx.AppCtx, webCtx *web.Ctx, service *project.ProjectService) http.Handler { - return web.NewController(appCtx, webCtx, func(io web.IO) error { - ctx := io.Context() - usr := user.MustCtxUser(ctx) - - // Parse the project ID from the URL - id, err := parseObjectID(io.Request(), "id") - if err != nil { - log.Printf("ERROR: Invalid project ID for deletion: %v", err) - return io.InlineError(web.ErrInternal, err) - } - - log.Printf("INFO: Deleting project with ID %s for user %s", id.Hex(), usr.ID) - - // Delete the project and all its requirements through the service - err = service.DeleteProject(ctx, id) - if err != nil { - log.Printf("ERROR: Failed to delete project %s: %v", id.Hex(), err) - return io.InlineError(web.ErrInternal, err) - } - - log.Printf("INFO: Project %s deleted successfully", id.Hex()) - - // After deletion, fetch and return the updated project list - // This provides immediate feedback to the user about the deletion - projects, err := service.GetProjectsByUser(ctx, usr.ID) - if err != nil { - log.Printf("ERROR: Failed to get updated project list after deletion: %v", err) - return io.InlineError(web.ErrInternal, err) - } - - log.Printf("INFO: Returning updated project list with %d projects", len(projects)) - - // Return the updated project list as HTML to replace the current list - data := ProjectListData{Projects: projects} - return io.Render( - data, - "project.list", // Template name for just the list part - "project/_list.go.html", // Partial template for project list - ) - }) -} - -// projectSearchController handles search requests for projects -// This controller filters projects based on user input and returns matching results -func projectSearchController(appCtx *hctx.AppCtx, webCtx *web.Ctx, service *project.ProjectService) http.Handler { - return web.NewController(appCtx, webCtx, func(io web.IO) error { - ctx := io.Context() - usr := user.MustCtxUser(ctx) - - request := io.Request() - - // Parse the form data to extract search parameters - err := request.ParseForm() - if err != nil { - log.Printf("ERROR: Failed to parse search form: %v", err) - return io.InlineError(web.ErrInternal, err) - } - - // Extract the search query from the form - query := request.FormValue("search") - log.Printf("INFO: Searching projects for user %s with query '%s'", usr.ID, query) - - // Perform the search using the service layer - // The service handles both empty queries (return all) and actual searches - projects, err := service.SearchProjects(ctx, query, usr.ID) - if err != nil { - log.Printf("ERROR: Project search failed for query '%s': %v", query, err) - return io.InlineError(web.ErrInternal, err) - } - - log.Printf("INFO: Search found %d projects for query '%s'", len(projects), query) - - // Return the search results as a partial HTML update - data := ProjectListData{Projects: projects} - return io.Render( - data, - "project.list", // Template name for the list - "templates/project/_list.go.html", // Partial template file - ) - }) -} - -// Helper Functions - -// parseObjectID extracts and validates a MongoDB ObjectID from HTTP request parameters -// This function ensures that URL parameters containing IDs are valid MongoDB ObjectIDs -func parseObjectID(r *http.Request, param string) (primitive.ObjectID, error) { - // Extract the parameter value from the URL - idStr := web.URLParam(r, param) - if idStr == "" { - return primitive.NilObjectID, errors.New("missing id parameter") - } - - // Convert the string to a valid MongoDB ObjectID - return primitive.ObjectIDFromHex(idStr) -} - -// renderProjectForm is a helper function to render project creation/edit forms -// This function handles both success messages and validation errors -func renderProjectForm(io web.IO, data *ProjectFormData, success []string, errs []error) error { - log.Printf("DEBUG: Rendering project form (edit=%t, errors=%d)", data.IsEditForm, len(errs)) - - // Wrap the form data with success messages and errors for template rendering - err := io.Render( - web.NewFormData(data, success, errs...), - "project.form.page", // Main template name - "project/form-page.go.html", // Page wrapper template - "project/_form.go.html", // Form partial template - ) - - if err != nil { - log.Printf("ERROR: Failed to render project form: %v", err) - } - - return err -} - -// renderProjectEditForm is a specialized helper for rendering edit forms -// This function prepares edit-specific form data and renders the appropriate templates -func renderProjectEditForm(io web.IO, toUpdate *project.ProjectToUpdate, success []string, errs []error) error { - // Create form data structure specifically for editing - formData := &ProjectFormData{ - Project: toUpdate, - IsEditForm: true, // This flag helps the template render edit-specific elements - } - - log.Printf("DEBUG: Rendering project edit form for project %s (errors=%d)", toUpdate.ID.Hex(), len(errs)) - - // Render the edit form with the current project data - err := io.Render( - web.NewFormData(formData, success, errs...), - "project.edit.form", // Template name for edit forms - "project/_form.go.html", // Shared form partial template - ) - - if err != nil { - log.Printf("ERROR: Failed to render project edit form: %v", err) - } - - return err -}