Move github source to private server
This commit is contained in:
14
.dockerignore
Normal file
14
.dockerignore
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
log/*
|
||||||
|
tmp/*
|
||||||
|
db/*.sqlite3
|
||||||
|
db/*.sqlite3-journal
|
||||||
|
public/assets
|
||||||
|
node_modules
|
||||||
|
.bundle
|
||||||
|
Dockerfile
|
||||||
|
docker-compose.yml
|
||||||
|
README.md
|
||||||
|
test/*
|
||||||
|
spec/*
|
||||||
41
Dockerfile
Normal file
41
Dockerfile
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# Use Ruby 2.6.5 as the base image
|
||||||
|
FROM ruby:2.6.5
|
||||||
|
|
||||||
|
# Install system dependencies
|
||||||
|
RUN apt-get update -qq && apt-get install -y \
|
||||||
|
build-essential \
|
||||||
|
libpq-dev \
|
||||||
|
nodejs \
|
||||||
|
libsqlite3-dev \
|
||||||
|
yarn
|
||||||
|
|
||||||
|
# Set the working directory
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install bundler
|
||||||
|
RUN gem install bundler -v 2.1.4
|
||||||
|
|
||||||
|
# Copy Gemfile and Gemfile.lock
|
||||||
|
COPY Gemfile Gemfile.lock ./
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
RUN bundle install
|
||||||
|
|
||||||
|
# Copy the rest of the application code
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Set environment variables
|
||||||
|
ENV RAILS_ENV=production
|
||||||
|
ENV RAILS_SERVE_STATIC_FILES=true
|
||||||
|
ENV RAILS_LOG_TO_STDOUT=true
|
||||||
|
|
||||||
|
# Precompile assets
|
||||||
|
# Note: SECRET_KEY_BASE is required for asset precompilation in some Rails versions
|
||||||
|
# You can provide a dummy value here if it's not strictly checked during precompile
|
||||||
|
RUN bundle exec rake assets:precompile SECRET_KEY_BASE=dummy_key
|
||||||
|
|
||||||
|
# Expose port 8080 (Cloud Run default)
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
# Start the application
|
||||||
|
CMD ["bundle", "exec", "puma", "-C", "config/puma.rb"]
|
||||||
4
Gemfile
4
Gemfile
@@ -8,8 +8,8 @@ end
|
|||||||
|
|
||||||
# Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
|
# Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
|
||||||
gem 'rails', '~> 5.1.0'
|
gem 'rails', '~> 5.1.0'
|
||||||
# Use sqlite3 as the database for Active Record
|
# Use PostgreSQL as the database for Active Record
|
||||||
gem 'sqlite3'
|
gem 'pg'
|
||||||
# Use Puma as the app server
|
# Use Puma as the app server
|
||||||
gem 'puma', '~> 3.7'
|
gem 'puma', '~> 3.7'
|
||||||
# Use SCSS for stylesheets
|
# Use SCSS for stylesheets
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -3,6 +3,7 @@ class ApplicationController < ActionController::Base
|
|||||||
|
|
||||||
include AuthorizationConcern
|
include AuthorizationConcern
|
||||||
|
|
||||||
|
before_action :authenticate_user!
|
||||||
before_action :configure_permitted_parameters, if: :devise_controller?
|
before_action :configure_permitted_parameters, if: :devise_controller?
|
||||||
before_action :set_current_user_department
|
before_action :set_current_user_department
|
||||||
|
|
||||||
|
|||||||
31
app/controllers/comments_controller.rb
Normal file
31
app/controllers/comments_controller.rb
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
class CommentsController < ApplicationController
|
||||||
|
before_action :set_task
|
||||||
|
|
||||||
|
def create
|
||||||
|
authorize_task!
|
||||||
|
@comment = @task.comments.build(comment_params)
|
||||||
|
@comment.user = current_user
|
||||||
|
|
||||||
|
if @comment.save
|
||||||
|
respond_to do |format|
|
||||||
|
format.html { redirect_to @task, notice: 'Comment added.' }
|
||||||
|
format.js
|
||||||
|
end
|
||||||
|
else
|
||||||
|
respond_to do |format|
|
||||||
|
format.html { redirect_to @task, alert: 'Comment cannot be blank.' }
|
||||||
|
format.js
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_task
|
||||||
|
@task = Task.find(params[:task_id])
|
||||||
|
end
|
||||||
|
|
||||||
|
def comment_params
|
||||||
|
params.require(:comment).permit(:content)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -53,7 +53,6 @@ class TasksController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def edit
|
def edit
|
||||||
authorize_task_update!(@task)
|
|
||||||
@departments = accessible_departments.ordered
|
@departments = accessible_departments.ordered
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -72,7 +71,14 @@ class TasksController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
if @task.update(task_params)
|
updated_params = task_params.to_h
|
||||||
|
if updated_params[:status] == "1"
|
||||||
|
updated_params[:status] = "completed"
|
||||||
|
elsif updated_params[:status] == "0"
|
||||||
|
updated_params[:status] = "open"
|
||||||
|
end
|
||||||
|
|
||||||
|
if @task.update(updated_params)
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
format.html { redirect_to @task, notice: 'Task was successfully updated.' }
|
format.html { redirect_to @task, notice: 'Task was successfully updated.' }
|
||||||
format.js
|
format.js
|
||||||
|
|||||||
6
app/models/comment.rb
Normal file
6
app/models/comment.rb
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
class Comment < ApplicationRecord
|
||||||
|
belongs_to :user
|
||||||
|
belongs_to :task
|
||||||
|
|
||||||
|
validates :content, presence: true
|
||||||
|
end
|
||||||
@@ -4,6 +4,7 @@ class Task < ApplicationRecord
|
|||||||
belongs_to :creator, class_name: 'User'
|
belongs_to :creator, class_name: 'User'
|
||||||
|
|
||||||
has_many :task_activities, dependent: :destroy
|
has_many :task_activities, dependent: :destroy
|
||||||
|
has_many :comments, dependent: :destroy
|
||||||
|
|
||||||
enum priority: {
|
enum priority: {
|
||||||
planning: 'planning',
|
planning: 'planning',
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ class User < ApplicationRecord
|
|||||||
belongs_to :department, optional: true
|
belongs_to :department, optional: true
|
||||||
has_many :assigned_tasks, class_name: 'Task', foreign_key: 'assignee_id'
|
has_many :assigned_tasks, class_name: 'Task', foreign_key: 'assignee_id'
|
||||||
has_many :created_tasks, class_name: 'Task', foreign_key: 'creator_id'
|
has_many :created_tasks, class_name: 'Task', foreign_key: 'creator_id'
|
||||||
|
has_many :comments, dependent: :destroy
|
||||||
|
|
||||||
enum role: { admin: 'admin', manager: 'manager', employee: 'employee' }
|
enum role: { admin: 'admin', manager: 'manager', employee: 'employee' }
|
||||||
|
|
||||||
|
|||||||
9
app/views/comments/_comment.html.erb
Normal file
9
app/views/comments/_comment.html.erb
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<div class="comment" id="comment_<%= comment.id %>">
|
||||||
|
<div class="comment-header">
|
||||||
|
<span class="comment-user"><%= comment.user.name %></span>
|
||||||
|
<span class="comment-date"><%= time_ago_in_words(comment.created_at) %> ago</span>
|
||||||
|
</div>
|
||||||
|
<div class="comment-content">
|
||||||
|
<%= simple_format(comment.content) %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
8
app/views/comments/_form.html.erb
Normal file
8
app/views/comments/_form.html.erb
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<%= form_with(model: [task, comment], local: false, class: "comment-form") do |f| %>
|
||||||
|
<div class="field">
|
||||||
|
<%= f.text_area :content, placeholder: "Add a comment...", class: "comment-textarea", required: true %>
|
||||||
|
</div>
|
||||||
|
<div class="actions">
|
||||||
|
<%= f.submit "Post Comment", class: "submit-btn" %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
16
app/views/comments/create.js.erb
Normal file
16
app/views/comments/create.js.erb
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
var commentsList = document.getElementById('comments-list');
|
||||||
|
var commentForm = document.querySelector('.comment-form');
|
||||||
|
|
||||||
|
if (commentsList && <%= @comment.persisted? %>) {
|
||||||
|
var newComment = '<%= j render partial: "comments/comment", locals: { comment: @comment } %>';
|
||||||
|
commentsList.insertAdjacentHTML('afterbegin', newComment);
|
||||||
|
commentForm.reset();
|
||||||
|
|
||||||
|
// Optional: add a small animation
|
||||||
|
var commentElement = document.getElementById('comment_<%= @comment.id %>');
|
||||||
|
commentElement.style.opacity = '0';
|
||||||
|
commentElement.style.transition = 'opacity 0.5s';
|
||||||
|
setTimeout(function() {
|
||||||
|
commentElement.style.opacity = '1';
|
||||||
|
}, 10);
|
||||||
|
}
|
||||||
@@ -1,36 +1,50 @@
|
|||||||
<h1>Departments</h1>
|
<div class="dashboard-header">
|
||||||
|
<h1>Departments</h1>
|
||||||
|
<% if current_user&.admin? %>
|
||||||
|
<%= link_to 'New Department', new_department_path, class: 'btn btn-success' %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
|
||||||
<% if flash[:notice] %>
|
<% if flash[:notice] %>
|
||||||
<div class="notice"><%= flash[:notice] %></div>
|
<div class="notice"><%= flash[:notice] %></div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<div class="departments-grid">
|
<div class="content-container">
|
||||||
<% if @departments.empty? %>
|
<div class="departments-grid">
|
||||||
<p class="empty-state">No departments found.</p>
|
<% if @departments.empty? %>
|
||||||
<% else %>
|
<div class="empty-state-card">
|
||||||
<% @departments.each do |department| %>
|
<p class="empty-state">No departments found.</p>
|
||||||
<div class="department-card">
|
</div>
|
||||||
<h3><%= link_to department.name, department_path(department) %></h3>
|
<% else %>
|
||||||
<p><%= department.description %></p>
|
<% @departments.each do |department| %>
|
||||||
|
<div class="department-card-modern">
|
||||||
|
<div class="department-card-header">
|
||||||
|
<h3><%= link_to department.name, department_path(department) %></h3>
|
||||||
|
<div class="department-actions-mini">
|
||||||
|
<% if current_user&.admin? %>
|
||||||
|
<%= link_to 'Edit', edit_department_path(department), class: 'btn-mini' %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="department-stats">
|
<p class="department-desc"><%= department.description&.truncate(100) || "No description provided." %></p>
|
||||||
<div class="stat">
|
|
||||||
<strong>Users:</strong> <%= department.user_count %>
|
<div class="department-stats-grid">
|
||||||
</div>
|
<div class="dept-stat-box">
|
||||||
<div class="stat">
|
<span class="dept-stat-label">Users</span>
|
||||||
<strong>Tasks:</strong> <%= department.task_count %>
|
<span class="dept-stat-value"><%= department.user_count %></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat">
|
<div class="dept-stat-box">
|
||||||
<strong>Open:</strong> <%= department.incomplete_task_count %>
|
<span class="dept-stat-label">Tasks</span>
|
||||||
|
<span class="dept-stat-value"><%= department.task_count %></span>
|
||||||
|
</div>
|
||||||
|
<div class="dept-stat-box">
|
||||||
|
<span class="dept-stat-label">Open</span>
|
||||||
|
<span class="dept-stat-value text-primary"><%= department.incomplete_task_count %></span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="actions-section">
|
|
||||||
<% if current_user&.admin? %>
|
|
||||||
<%= link_to 'New Department', new_department_path, class: 'action-btn primary' %>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>Project Management System</title>
|
<title>Project Management System</title>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
<%= csrf_meta_tags %>
|
<%= csrf_meta_tags %>
|
||||||
|
|
||||||
<%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
|
<%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
|
||||||
|
|||||||
@@ -1,58 +1,53 @@
|
|||||||
<div id="task_<%= task.id %>" class="task <%= 'completed' if task.status == 'completed' %>">
|
<div id="task_<%= task.id %>" class="task-card <%= task.status %>" data-priority="<%= task.priority %>">
|
||||||
<div class="task-header">
|
<div class="task-card-header">
|
||||||
<div class="task-priority" style="color: <%= task.priority_color %>">
|
<div class="task-card-title-section">
|
||||||
<%= task.priority_icon %> <%= task.priority.humanize %>
|
<div class="task-checkbox-wrapper">
|
||||||
</div>
|
<%= form_with(model: task, local: false, method: :patch) do |f| %>
|
||||||
<div class="task-status-badge" style="background-color: <%= task.status_badge[:color] %>">
|
<%= f.check_box :status, { checked: task.status == 'completed', onchange: "this.form.submit();" }, "completed", "open" %>
|
||||||
<%= task.status_badge[:text] %>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<%= form_with(model: task, local: false, method: :patch, class: "toggle-form") do |form| %>
|
|
||||||
<div class="task-content">
|
|
||||||
<%= form.check_box :status, { checked: task.status == 'completed', onchange: "this.form.submit();" }, { class: "task-checkbox" } %>
|
|
||||||
|
|
||||||
<div class="task-info">
|
|
||||||
<span class="task-title"
|
|
||||||
title="<%= task.description&.truncate(100) || 'No description' %>"
|
|
||||||
data-description="<%= task.description || '' %>">
|
|
||||||
<%= task.title %>
|
|
||||||
</span>
|
|
||||||
<% if task.description.present? %>
|
|
||||||
<span class="description-indicator">📝</span>
|
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
<h3 class="task-card-title"><%= task.title %></h3>
|
||||||
|
<% if task.description.present? %>
|
||||||
|
<span class="task-desc-indicator" title="Has description">📝</span>
|
||||||
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
|
||||||
|
|
||||||
<div class="task-meta">
|
<div class="task-card-badges">
|
||||||
<% if task.department %>
|
<span class="badge-priority" style="color: <%= task.priority_color %>; border-color: <%= task.priority_color %>22; background-color: <%= task.priority_color %>11;">
|
||||||
<span class="department-tag">
|
<%= task.priority_icon %> <%= task.priority.humanize %>
|
||||||
<%= task.department.name %>
|
|
||||||
</span>
|
</span>
|
||||||
<% end %>
|
<span class="badge-status" style="background-color: <%= task.status_badge[:color] %>">
|
||||||
|
<%= task.status_badge[:text] %>
|
||||||
<% if task.assignee %>
|
|
||||||
<span class="assignee-tag">
|
|
||||||
Assigned to: <%= task.assignee.name %>
|
|
||||||
</span>
|
</span>
|
||||||
<% end %>
|
</div>
|
||||||
|
|
||||||
<span class="created-tag">
|
|
||||||
Created: <%= task.created_at.strftime('%m/%d/%Y') %>
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="task-actions">
|
<div class="task-card-body">
|
||||||
<%= link_to 'View', task, class: 'details-btn' %>
|
<div class="task-card-info-grid">
|
||||||
<% if task.updateable_by?(current_user) %>
|
<div class="info-item">
|
||||||
<%= link_to 'Edit', edit_task_path(task), class: 'edit-btn' %>
|
<span class="info-label">Dept</span>
|
||||||
<% end %>
|
<span class="info-value"><%= task.department&.name || 'Personal' %></span>
|
||||||
<% if task.assign?(current_user) && !task.assignee %>
|
</div>
|
||||||
<%= link_to 'Assign', assign_task_path(task), method: :patch, class: 'assign-btn' %>
|
<div class="info-item">
|
||||||
<% end %>
|
<span class="info-label">Assignee</span>
|
||||||
<% if task.updateable_by?(current_user) || current_user.admin? %>
|
<span class="info-value"><%= task.assignee&.name || 'Unassigned' %></span>
|
||||||
<%= link_to 'Delete', task, method: :delete, data: { confirm: 'Are you sure?' }, class: 'delete-btn' %>
|
</div>
|
||||||
<% end %>
|
<div class="info-item">
|
||||||
|
<span class="info-label">Created</span>
|
||||||
|
<span class="info-value"><%= task.created_at.strftime('%b %d, %Y') %></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="task-card-footer">
|
||||||
|
<div class="task-card-actions">
|
||||||
|
<%= link_to 'View', task, class: 'btn-task-action' %>
|
||||||
|
<% if task.updateable_by?(current_user) %>
|
||||||
|
<%= link_to 'Edit', edit_task_path(task), class: 'btn-task-action' %>
|
||||||
|
<% end %>
|
||||||
|
<% if task.updateable_by?(current_user) || current_user.admin? %>
|
||||||
|
<%= link_to 'Delete', task, method: :delete, data: { confirm: 'Are you sure?' }, class: 'btn-task-action action-delete' %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -119,10 +119,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="dashboard-actions">
|
<div class="dashboard-actions">
|
||||||
<%= link_to 'View All Tasks', tasks_path, class: 'action-btn primary' %>
|
<%= link_to 'View All Tasks', tasks_path, class: 'btn btn-primary' %>
|
||||||
<%= link_to 'Create New Task', new_task_path, class: 'action-btn success' %>
|
<%= link_to 'Create New Task', new_task_path, class: 'btn btn-success' %>
|
||||||
<% if current_user.admin? || current_user.manager? %>
|
<% if current_user.admin? || current_user.manager? %>
|
||||||
<%= link_to 'Manage Departments', departments_path, class: 'action-btn secondary' %>
|
<%= link_to 'Manage Departments', departments_path, class: 'btn btn-secondary' %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -21,52 +21,38 @@
|
|||||||
<div class="sidebar">
|
<div class="sidebar">
|
||||||
<div class="filter-section">
|
<div class="filter-section">
|
||||||
<h3>Filters</h3>
|
<h3>Filters</h3>
|
||||||
|
<%= form_with(url: tasks_path, method: :get, local: true, class: "combined-filter-form") do |f| %>
|
||||||
<% if accessible_departments.count > 1 %>
|
<% if accessible_departments.count > 1 %>
|
||||||
<div class="filter-group">
|
<div class="filter-group">
|
||||||
<label>Department</label>
|
<label>Department</label>
|
||||||
<%= form_with(url: tasks_path, method: :get, local: true, class: "filter-form") do |f| %>
|
|
||||||
<%= f.select :department_id,
|
<%= f.select :department_id,
|
||||||
options_for_select([[ "All Departments", "" ]] + accessible_departments.map { |d| [d.name, d.id] },
|
options_for_select([[ "All Departments", "" ]] + accessible_departments.map { |d| [d.name, d.id] },
|
||||||
params[:department_id]),
|
params[:department_id]),
|
||||||
{ onchange: "this.form.submit();" },
|
{},
|
||||||
{ class: "filter-select" } %>
|
{ class: "filter-select", onchange: "this.form.submit();" } %>
|
||||||
<% end %>
|
</div>
|
||||||
</div>
|
<% end %>
|
||||||
<% end %>
|
|
||||||
|
|
||||||
<div class="filter-group">
|
<div class="filter-group">
|
||||||
<label>Priority</label>
|
<label>Priority</label>
|
||||||
<%= form_with(url: tasks_path, method: :get, local: true, class: "filter-form") do |f| %>
|
|
||||||
<%= f.select :priority,
|
<%= f.select :priority,
|
||||||
options_for_select([[ "All Priorities", "" ]] + Task.priorities.keys.map { |p| [p.humanize, p] },
|
options_for_select([[ "All Priorities", "" ]] + Task.priorities.keys.map { |p| [p.humanize, p] },
|
||||||
params[:priority]),
|
params[:priority]),
|
||||||
{ onchange: "this.form.submit();" },
|
{},
|
||||||
{ class: "filter-select" } %>
|
{ class: "filter-select", onchange: "this.form.submit();" } %>
|
||||||
<% end %>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="filter-group">
|
<div class="filter-group">
|
||||||
<label>Status</label>
|
<label>Status</label>
|
||||||
<%= form_with(url: tasks_path, method: :get, local: true, class: "filter-form") do |f| %>
|
|
||||||
<%= f.select :status,
|
<%= f.select :status,
|
||||||
options_for_select([[ "All Statuses", "" ]] + Task.statuses.keys.map { |s| [s.humanize, s] },
|
options_for_select([[ "All Statuses", "" ]] + Task.statuses.keys.map { |s| [s.humanize, s] },
|
||||||
params[:status]),
|
params[:status]),
|
||||||
{ onchange: "this.form.submit();" },
|
{},
|
||||||
{ class: "filter-select" } %>
|
{ class: "filter-select", onchange: "this.form.submit();" } %>
|
||||||
<% end %>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="actions-section">
|
|
||||||
<h3>Actions</h3>
|
|
||||||
<%= link_to 'Dashboard', dashboard_path, class: 'action-btn' %>
|
|
||||||
<%= link_to 'My Tasks', my_tasks_path, class: 'action-btn' if current_user.employee? %>
|
|
||||||
<%= link_to 'Departments', departments_path, class: 'action-btn' if current_user.admin? || current_user.manager? %>
|
|
||||||
<% if current_user.admin? %>
|
|
||||||
<%= link_to 'New User', new_user_registration_path, class: 'action-btn' %>
|
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="main-content">
|
<div class="main-content">
|
||||||
|
|||||||
@@ -24,6 +24,15 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="comments-section">
|
||||||
|
<h2>Conversations</h2>
|
||||||
|
<%= render 'comments/form', task: @task, comment: Comment.new %>
|
||||||
|
|
||||||
|
<div id="comments-list">
|
||||||
|
<%= render partial: 'comments/comment', collection: @task.comments.order(created_at: :desc) %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="task-actions">
|
<div class="task-actions">
|
||||||
<%= form_with(model: @task, local: false, method: :patch, class: "toggle-status-form") do |form| %>
|
<%= form_with(model: @task, local: false, method: :patch, class: "toggle-status-form") do |form| %>
|
||||||
<%= form.hidden_field :completed, value: !@task.completed %>
|
<%= form.hidden_field :completed, value: !@task.completed %>
|
||||||
|
|||||||
26
cloudbuild.yaml
Normal file
26
cloudbuild.yaml
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
steps:
|
||||||
|
# Build the container image
|
||||||
|
- name: 'gcr.io/cloud-builders/docker'
|
||||||
|
args: ['build', '-t', 'gcr.io/$PROJECT_ID/todo-app', '.']
|
||||||
|
|
||||||
|
# Push the container image to Container Registry
|
||||||
|
- name: 'gcr.io/cloud-builders/docker'
|
||||||
|
args: ['push', 'gcr.io/$PROJECT_ID/todo-app']
|
||||||
|
|
||||||
|
# Deploy container image to Cloud Run
|
||||||
|
- name: 'gcr.io/google.com/cloudsdktool/cloud-sdk'
|
||||||
|
entrypoint: gcloud
|
||||||
|
args:
|
||||||
|
- 'run'
|
||||||
|
- 'deploy'
|
||||||
|
- 'todo-app'
|
||||||
|
- '--image'
|
||||||
|
- 'gcr.io/$PROJECT_ID/todo-app'
|
||||||
|
- '--region'
|
||||||
|
- 'us-central1'
|
||||||
|
- '--platform'
|
||||||
|
- 'managed'
|
||||||
|
- '--allow-unauthenticated'
|
||||||
|
|
||||||
|
images:
|
||||||
|
- 'gcr.io/$PROJECT_ID/todo-app'
|
||||||
@@ -1,25 +1,22 @@
|
|||||||
# SQLite version 3.x
|
|
||||||
# gem install sqlite3
|
|
||||||
#
|
|
||||||
# Ensure the SQLite 3 gem is defined in your Gemfile
|
|
||||||
# gem 'sqlite3'
|
|
||||||
#
|
|
||||||
default: &default
|
default: &default
|
||||||
adapter: sqlite3
|
adapter: postgresql
|
||||||
|
encoding: unicode
|
||||||
|
# For details on connection pooling, see Rails configuration guide
|
||||||
|
# http://guides.rubyonrails.org/configuring.html#database-pooling
|
||||||
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
|
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
|
||||||
timeout: 5000
|
|
||||||
|
|
||||||
development:
|
development:
|
||||||
<<: *default
|
<<: *default
|
||||||
database: db/development.sqlite3
|
database: todo_development
|
||||||
|
|
||||||
# Warning: The database defined as "test" will be erased and
|
|
||||||
# re-generated from your development database when you run "rake".
|
|
||||||
# Do not set this db to the same as development or production.
|
|
||||||
test:
|
test:
|
||||||
<<: *default
|
<<: *default
|
||||||
database: db/test.sqlite3
|
database: todo_test
|
||||||
|
|
||||||
production:
|
production:
|
||||||
<<: *default
|
<<: *default
|
||||||
database: db/production.sqlite3
|
database: <%= ENV['DB_NAME'] || 'todo_production' %>
|
||||||
|
username: <%= ENV['DB_USER'] %>
|
||||||
|
password: <%= ENV['DB_PASSWORD'] %>
|
||||||
|
host: <%= ENV['DB_HOST'] %>
|
||||||
|
# When using Cloud SQL with Cloud Run, the host is often /cloudsql/CONNECTION_NAME
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ Rails.application.routes.draw do
|
|||||||
member do
|
member do
|
||||||
patch :assign
|
patch :assign
|
||||||
end
|
end
|
||||||
|
resources :comments, only: [:create]
|
||||||
end
|
end
|
||||||
|
|
||||||
root 'tasks#index'
|
root 'tasks#index'
|
||||||
|
|||||||
11
db/migrate/20260128050859_create_comments.rb
Normal file
11
db/migrate/20260128050859_create_comments.rb
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
class CreateComments < ActiveRecord::Migration[5.1]
|
||||||
|
def change
|
||||||
|
create_table :comments do |t|
|
||||||
|
t.text :content
|
||||||
|
t.references :user, foreign_key: true
|
||||||
|
t.references :task, foreign_key: true
|
||||||
|
|
||||||
|
t.timestamps
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
12
db/schema.rb
12
db/schema.rb
@@ -10,7 +10,17 @@
|
|||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema.define(version: 20260122043706) do
|
ActiveRecord::Schema.define(version: 20260128050859) do
|
||||||
|
|
||||||
|
create_table "comments", force: :cascade do |t|
|
||||||
|
t.text "content"
|
||||||
|
t.integer "user_id"
|
||||||
|
t.integer "task_id"
|
||||||
|
t.datetime "created_at", null: false
|
||||||
|
t.datetime "updated_at", null: false
|
||||||
|
t.index ["task_id"], name: "index_comments_on_task_id"
|
||||||
|
t.index ["user_id"], name: "index_comments_on_user_id"
|
||||||
|
end
|
||||||
|
|
||||||
create_table "departments", force: :cascade do |t|
|
create_table "departments", force: :cascade do |t|
|
||||||
t.string "name", null: false
|
t.string "name", null: false
|
||||||
|
|||||||
11
test/fixtures/comments.yml
vendored
Normal file
11
test/fixtures/comments.yml
vendored
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
|
||||||
|
|
||||||
|
one:
|
||||||
|
content: "This is a test comment"
|
||||||
|
user: employee
|
||||||
|
task: one
|
||||||
|
|
||||||
|
two:
|
||||||
|
content: "Another test comment"
|
||||||
|
user: manager
|
||||||
|
task: two
|
||||||
7
test/models/comment_test.rb
Normal file
7
test/models/comment_test.rb
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
require 'test_helper'
|
||||||
|
|
||||||
|
class CommentTest < ActiveSupport::TestCase
|
||||||
|
# test "the truth" do
|
||||||
|
# assert true
|
||||||
|
# end
|
||||||
|
end
|
||||||
33
test/system/RECOVERY_ARTIFACT.md
Normal file
33
test/system/RECOVERY_ARTIFACT.md
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
This summary outlines the evolution of our work on the Rails Todo Application, moving from an initial audit to a modernized, stabilized system with AJAX features and robust role-based access.
|
||||||
|
|
||||||
|
1. Initial Audit & Modernization
|
||||||
|
Audit Findings: Identified a legacy Rails 5.1 stack with broken tests and a static UI that required full page reloads for every action.
|
||||||
|
AJAX Implementation:
|
||||||
|
Transformed the task completion toggle into an asynchronous action.
|
||||||
|
Implemented dynamic partial updates via
|
||||||
|
|
||||||
|
update.js.erb
|
||||||
|
, allowing the UI to reflect changes instantly without flickering.
|
||||||
|
Test Suite Foundation: Repaired the existing (broken) tests and added new model/controller tests to ensure reliable feature verification.
|
||||||
|
2. Dynamic Task Management
|
||||||
|
Sorting Logic: Implemented an "Active First" sorting system. Pending tasks are kept at the top, while completed tasks are automatically moved to the bottom.
|
||||||
|
Real-time Reordering: Updated the AJAX logic so that toggling a task's status triggers a re-render of the entire list, ensuring the correct sorting order is maintained dynamically.
|
||||||
|
3. System Stabilization (RBAC & Schema)
|
||||||
|
Architecture Update: Following your expansion of the codebase to include Users, Departments, and Priorities, I stabilized the logic across all layers.
|
||||||
|
Authorization: Refactored the AuthorizationConcern into a proper Rails module to handle role-based permissions (Admin, Manager, Employee).
|
||||||
|
Database & Reliability:
|
||||||
|
Generated the missing TaskActivity model to support action logging.
|
||||||
|
Updated all fixtures and test cases to account for new required fields like
|
||||||
|
|
||||||
|
status
|
||||||
|
,
|
||||||
|
|
||||||
|
priority
|
||||||
|
, and creator.
|
||||||
|
Fixed critical CSS syntax errors that were breaking asset compilation and testing.
|
||||||
|
4. Verification & Devise Support
|
||||||
|
Browser-Based Verification: Confirmed via automated agents that the AJAX toggle and reordering worked perfectly in the browser without full-page refreshes.
|
||||||
|
Devise Expertise: Guided you through handling the Confirmable module in development, including using the Rails console for manual confirmation and checking server logs for confirmation links.
|
||||||
|
Final Code State
|
||||||
|
Tests: 22/22 Passing (model, controller, and AJAX).
|
||||||
|
Features: AJAX completion, dynamic sorting, role-based access, and department-level filtering.
|
||||||
Reference in New Issue
Block a user