Move github source to private server

This commit is contained in:
Zin Bo Thit
2026-01-29 12:00:13 +06:30
parent 417600b23e
commit 87cc77e0fb
27 changed files with 916 additions and 381 deletions

14
.dockerignore Normal file
View 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
View 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"]

View File

@@ -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

View File

@@ -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

View 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

View File

@@ -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
View File

@@ -0,0 +1,6 @@
class Comment < ApplicationRecord
belongs_to :user
belongs_to :task
validates :content, presence: true
end

View File

@@ -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',

View File

@@ -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' }

View 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>

View 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 %>

View 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);
}

View File

@@ -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>

View File

@@ -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' %>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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">

View File

@@ -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
View 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'

View File

@@ -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

View File

@@ -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'

View 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

View File

@@ -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
View 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

View File

@@ -0,0 +1,7 @@
require 'test_helper'
class CommentTest < ActiveSupport::TestCase
# test "the truth" do
# assert true
# end
end

View 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.