Skip to content
Snippets Groups Projects
Commit c3e4dadb authored by Cecile Ntsama's avatar Cecile Ntsama
Browse files

add project folder with CRUD Operation for the project and validation with...

add project folder with CRUD Operation for the project and validation with requirementsmanagement withing Project and search functions set up prroper authorization and comprehensive login througout all operations
parent a78f5098
No related branches found
No related tags found
1 merge request!4merge dev to main
package web
import (
"errors"
"fmt"
"log"
"net/http"
"time"
"github.com/org-harmony/harmony/src/app/project"
"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
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment