Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5b298a1e59 | ||
|
|
4c30a133d3 | ||
|
|
6716129d4a | ||
|
|
85937a3fcd | ||
|
|
a2e1b7f3d2 | ||
|
|
7f6123a6e6 | ||
|
|
8a94b07955 | ||
|
|
7c9aef4324 | ||
|
|
8a7d1716e2 | ||
|
|
e3da79930c | ||
|
|
7c9e164a9a | ||
|
|
0f5d105be3 | ||
|
|
910c1912e0 | ||
|
|
fbb09e4015 | ||
|
|
cfca2bec27 | ||
|
|
142e14743a | ||
|
|
ec595da889 | ||
|
|
6c3180f203 | ||
|
|
fd0a6fb408 | ||
|
|
f72af5d45f | ||
|
|
6bbbc67a3c | ||
|
|
754a76fd5d | ||
|
|
e0ac034974 | ||
|
|
de19e675c8 |
12
.github/dependabot.yml
vendored
12
.github/dependabot.yml
vendored
@@ -1,12 +0,0 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: bundler
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: weekly
|
||||
open-pull-requests-limit: 10
|
||||
- package-ecosystem: github-actions
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: weekly
|
||||
open-pull-requests-limit: 10
|
||||
124
.github/workflows/ci.yml
vendored
124
.github/workflows/ci.yml
vendored
@@ -1,124 +0,0 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches: [ main ]
|
||||
|
||||
jobs:
|
||||
scan_ruby:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
bundler-cache: true
|
||||
|
||||
- name: Scan for common Rails security vulnerabilities using static analysis
|
||||
run: bin/brakeman --no-pager
|
||||
|
||||
- name: Scan for known security vulnerabilities in gems used
|
||||
run: bin/bundler-audit
|
||||
|
||||
scan_js:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
bundler-cache: true
|
||||
|
||||
- name: Scan for security vulnerabilities in JavaScript dependencies
|
||||
run: bin/importmap audit
|
||||
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
RUBOCOP_CACHE_ROOT: tmp/rubocop
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
bundler-cache: true
|
||||
|
||||
- name: Prepare RuboCop cache
|
||||
uses: actions/cache@v4
|
||||
env:
|
||||
DEPENDENCIES_HASH: ${{ hashFiles('.ruby-version', '**/.rubocop.yml', '**/.rubocop_todo.yml', 'Gemfile.lock') }}
|
||||
with:
|
||||
path: ${{ env.RUBOCOP_CACHE_ROOT }}
|
||||
key: rubocop-${{ runner.os }}-${{ env.DEPENDENCIES_HASH }}-${{ github.ref_name == github.event.repository.default_branch && github.run_id || 'default' }}
|
||||
restore-keys: |
|
||||
rubocop-${{ runner.os }}-${{ env.DEPENDENCIES_HASH }}-
|
||||
|
||||
- name: Lint code for consistent style
|
||||
run: bin/rubocop -f github
|
||||
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
# services:
|
||||
# redis:
|
||||
# image: valkey/valkey:8
|
||||
# ports:
|
||||
# - 6379:6379
|
||||
# options: --health-cmd "redis-cli ping" --health-interval 10s --health-timeout 5s --health-retries 5
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
bundler-cache: true
|
||||
|
||||
- name: Run tests
|
||||
env:
|
||||
RAILS_ENV: test
|
||||
# RAILS_MASTER_KEY: ${{ secrets.RAILS_MASTER_KEY }}
|
||||
# REDIS_URL: redis://localhost:6379/0
|
||||
run: bin/rails db:test:prepare test
|
||||
|
||||
system-test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
# services:
|
||||
# redis:
|
||||
# image: valkey/valkey:8
|
||||
# ports:
|
||||
# - 6379:6379
|
||||
# options: --health-cmd "redis-cli ping" --health-interval 10s --health-timeout 5s --health-retries 5
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
bundler-cache: true
|
||||
|
||||
- name: Run System Tests
|
||||
env:
|
||||
RAILS_ENV: test
|
||||
# RAILS_MASTER_KEY: ${{ secrets.RAILS_MASTER_KEY }}
|
||||
# REDIS_URL: redis://localhost:6379/0
|
||||
run: bin/rails db:test:prepare test:system
|
||||
|
||||
- name: Keep screenshots from failed system tests
|
||||
uses: actions/upload-artifact@v4
|
||||
if: failure()
|
||||
with:
|
||||
name: screenshots
|
||||
path: ${{ github.workspace }}/tmp/screenshots
|
||||
if-no-files-found: ignore
|
||||
@@ -1,3 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
echo "Docker set up on $KAMAL_HOSTS..."
|
||||
@@ -1,3 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
echo "Booted app version $KAMAL_VERSION on $KAMAL_HOSTS..."
|
||||
@@ -1,14 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
# A sample post-deploy hook
|
||||
#
|
||||
# These environment variables are available:
|
||||
# KAMAL_RECORDED_AT
|
||||
# KAMAL_PERFORMER
|
||||
# KAMAL_VERSION
|
||||
# KAMAL_HOSTS
|
||||
# KAMAL_ROLES (if set)
|
||||
# KAMAL_DESTINATION (if set)
|
||||
# KAMAL_RUNTIME
|
||||
|
||||
echo "$KAMAL_PERFORMER deployed $KAMAL_VERSION to $KAMAL_DESTINATION in $KAMAL_RUNTIME seconds"
|
||||
@@ -1,3 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
echo "Rebooted kamal-proxy on $KAMAL_HOSTS"
|
||||
@@ -1,3 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
echo "Booting app version $KAMAL_VERSION on $KAMAL_HOSTS..."
|
||||
@@ -1,51 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
# A sample pre-build hook
|
||||
#
|
||||
# Checks:
|
||||
# 1. We have a clean checkout
|
||||
# 2. A remote is configured
|
||||
# 3. The branch has been pushed to the remote
|
||||
# 4. The version we are deploying matches the remote
|
||||
#
|
||||
# These environment variables are available:
|
||||
# KAMAL_RECORDED_AT
|
||||
# KAMAL_PERFORMER
|
||||
# KAMAL_VERSION
|
||||
# KAMAL_HOSTS
|
||||
# KAMAL_ROLES (if set)
|
||||
# KAMAL_DESTINATION (if set)
|
||||
|
||||
if [ -n "$(git status --porcelain)" ]; then
|
||||
echo "Git checkout is not clean, aborting..." >&2
|
||||
git status --porcelain >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
first_remote=$(git remote)
|
||||
|
||||
if [ -z "$first_remote" ]; then
|
||||
echo "No git remote set, aborting..." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
current_branch=$(git branch --show-current)
|
||||
|
||||
if [ -z "$current_branch" ]; then
|
||||
echo "Not on a git branch, aborting..." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
remote_head=$(git ls-remote $first_remote --tags $current_branch | cut -f1)
|
||||
|
||||
if [ -z "$remote_head" ]; then
|
||||
echo "Branch not pushed to remote, aborting..." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$KAMAL_VERSION" != "$remote_head" ]; then
|
||||
echo "Version ($KAMAL_VERSION) does not match remote HEAD ($remote_head), aborting..." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
exit 0
|
||||
@@ -1,47 +0,0 @@
|
||||
#!/usr/bin/env ruby
|
||||
|
||||
# A sample pre-connect check
|
||||
#
|
||||
# Warms DNS before connecting to hosts in parallel
|
||||
#
|
||||
# These environment variables are available:
|
||||
# KAMAL_RECORDED_AT
|
||||
# KAMAL_PERFORMER
|
||||
# KAMAL_VERSION
|
||||
# KAMAL_HOSTS
|
||||
# KAMAL_ROLES (if set)
|
||||
# KAMAL_DESTINATION (if set)
|
||||
# KAMAL_RUNTIME
|
||||
|
||||
hosts = ENV["KAMAL_HOSTS"].split(",")
|
||||
results = nil
|
||||
max = 3
|
||||
|
||||
elapsed = Benchmark.realtime do
|
||||
results = hosts.map do |host|
|
||||
Thread.new do
|
||||
tries = 1
|
||||
|
||||
begin
|
||||
Socket.getaddrinfo(host, 0, Socket::AF_UNSPEC, Socket::SOCK_STREAM, nil, Socket::AI_CANONNAME)
|
||||
rescue SocketError
|
||||
if tries < max
|
||||
puts "Retrying DNS warmup: #{host}"
|
||||
tries += 1
|
||||
sleep rand
|
||||
retry
|
||||
else
|
||||
puts "DNS warmup failed: #{host}"
|
||||
host
|
||||
end
|
||||
end
|
||||
|
||||
tries
|
||||
end
|
||||
end.map(&:value)
|
||||
end
|
||||
|
||||
retries = results.sum - hosts.size
|
||||
nopes = results.count { |r| r == max }
|
||||
|
||||
puts "Prewarmed %d DNS lookups in %.2f sec: %d retries, %d failures" % [ hosts.size, elapsed, retries, nopes ]
|
||||
@@ -1,122 +0,0 @@
|
||||
#!/usr/bin/env ruby
|
||||
|
||||
# A sample pre-deploy hook
|
||||
#
|
||||
# Checks the Github status of the build, waiting for a pending build to complete for up to 720 seconds.
|
||||
#
|
||||
# Fails unless the combined status is "success"
|
||||
#
|
||||
# These environment variables are available:
|
||||
# KAMAL_RECORDED_AT
|
||||
# KAMAL_PERFORMER
|
||||
# KAMAL_VERSION
|
||||
# KAMAL_HOSTS
|
||||
# KAMAL_COMMAND
|
||||
# KAMAL_SUBCOMMAND
|
||||
# KAMAL_ROLES (if set)
|
||||
# KAMAL_DESTINATION (if set)
|
||||
|
||||
# Only check the build status for production deployments
|
||||
if ENV["KAMAL_COMMAND"] == "rollback" || ENV["KAMAL_DESTINATION"] != "production"
|
||||
exit 0
|
||||
end
|
||||
|
||||
require "bundler/inline"
|
||||
|
||||
# true = install gems so this is fast on repeat invocations
|
||||
gemfile(true, quiet: true) do
|
||||
source "https://rubygems.org"
|
||||
|
||||
gem "octokit"
|
||||
gem "faraday-retry"
|
||||
end
|
||||
|
||||
MAX_ATTEMPTS = 72
|
||||
ATTEMPTS_GAP = 10
|
||||
|
||||
def exit_with_error(message)
|
||||
$stderr.puts message
|
||||
exit 1
|
||||
end
|
||||
|
||||
class GithubStatusChecks
|
||||
attr_reader :remote_url, :git_sha, :github_client, :combined_status
|
||||
|
||||
def initialize
|
||||
@remote_url = github_repo_from_remote_url
|
||||
@git_sha = `git rev-parse HEAD`.strip
|
||||
@github_client = Octokit::Client.new(access_token: ENV["GITHUB_TOKEN"])
|
||||
refresh!
|
||||
end
|
||||
|
||||
def refresh!
|
||||
@combined_status = github_client.combined_status(remote_url, git_sha)
|
||||
end
|
||||
|
||||
def state
|
||||
combined_status[:state]
|
||||
end
|
||||
|
||||
def first_status_url
|
||||
first_status = combined_status[:statuses].find { |status| status[:state] == state }
|
||||
first_status && first_status[:target_url]
|
||||
end
|
||||
|
||||
def complete_count
|
||||
combined_status[:statuses].count { |status| status[:state] != "pending"}
|
||||
end
|
||||
|
||||
def total_count
|
||||
combined_status[:statuses].count
|
||||
end
|
||||
|
||||
def current_status
|
||||
if total_count > 0
|
||||
"Completed #{complete_count}/#{total_count} checks, see #{first_status_url} ..."
|
||||
else
|
||||
"Build not started..."
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def github_repo_from_remote_url
|
||||
url = `git config --get remote.origin.url`.strip.delete_suffix(".git")
|
||||
if url.start_with?("https://github.com/")
|
||||
url.delete_prefix("https://github.com/")
|
||||
elsif url.start_with?("git@github.com:")
|
||||
url.delete_prefix("git@github.com:")
|
||||
else
|
||||
url
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
$stdout.sync = true
|
||||
|
||||
begin
|
||||
puts "Checking build status..."
|
||||
|
||||
attempts = 0
|
||||
checks = GithubStatusChecks.new
|
||||
|
||||
loop do
|
||||
case checks.state
|
||||
when "success"
|
||||
puts "Checks passed, see #{checks.first_status_url}"
|
||||
exit 0
|
||||
when "failure"
|
||||
exit_with_error "Checks failed, see #{checks.first_status_url}"
|
||||
when "pending"
|
||||
attempts += 1
|
||||
end
|
||||
|
||||
exit_with_error "Checks are still pending, gave up after #{MAX_ATTEMPTS * ATTEMPTS_GAP} seconds" if attempts == MAX_ATTEMPTS
|
||||
|
||||
puts checks.current_status
|
||||
sleep(ATTEMPTS_GAP)
|
||||
checks.refresh!
|
||||
end
|
||||
rescue Octokit::NotFound
|
||||
exit_with_error "Build status could not be found"
|
||||
end
|
||||
@@ -1,3 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
echo "Rebooting kamal-proxy on $KAMAL_HOSTS..."
|
||||
@@ -1,20 +0,0 @@
|
||||
# Secrets defined here are available for reference under registry/password, env/secret, builder/secrets,
|
||||
# and accessories/*/env/secret in config/deploy.yml. All secrets should be pulled from either
|
||||
# password manager, ENV, or a file. DO NOT ENTER RAW CREDENTIALS HERE! This file needs to be safe for git.
|
||||
|
||||
# Example of extracting secrets from 1password (or another compatible pw manager)
|
||||
# SECRETS=$(kamal secrets fetch --adapter 1password --account your-account --from Vault/Item KAMAL_REGISTRY_PASSWORD RAILS_MASTER_KEY)
|
||||
# KAMAL_REGISTRY_PASSWORD=$(kamal secrets extract KAMAL_REGISTRY_PASSWORD ${SECRETS})
|
||||
# RAILS_MASTER_KEY=$(kamal secrets extract RAILS_MASTER_KEY ${SECRETS})
|
||||
|
||||
# Example of extracting secrets from Rails credentials
|
||||
# KAMAL_REGISTRY_PASSWORD=$(rails credentials:fetch kamal.registry_password)
|
||||
|
||||
# Use a GITHUB_TOKEN if private repositories are needed for the image
|
||||
# GITHUB_TOKEN=$(gh config get -h github.com oauth_token)
|
||||
|
||||
# Grab the registry password from ENV
|
||||
# KAMAL_REGISTRY_PASSWORD=$KAMAL_REGISTRY_PASSWORD
|
||||
|
||||
# Improve security by using a password manager. Never check config/master.key into git!
|
||||
RAILS_MASTER_KEY=$(cat config/master.key)
|
||||
2
Gemfile
2
Gemfile
@@ -64,3 +64,5 @@ group :test do
|
||||
gem "capybara"
|
||||
gem "selenium-webdriver"
|
||||
end
|
||||
|
||||
gem "tailwindcss", "~> 0.1.1"
|
||||
|
||||
@@ -351,6 +351,7 @@ GEM
|
||||
stimulus-rails (1.3.4)
|
||||
railties (>= 6.0.0)
|
||||
stringio (3.2.0)
|
||||
tailwindcss (0.1.1)
|
||||
thor (1.5.0)
|
||||
thruster (0.1.17)
|
||||
thruster (0.1.17-aarch64-linux)
|
||||
@@ -415,6 +416,7 @@ DEPENDENCIES
|
||||
solid_queue
|
||||
sqlite3 (>= 2.1)
|
||||
stimulus-rails
|
||||
tailwindcss (~> 0.1.1)
|
||||
thruster
|
||||
turbo-rails
|
||||
tzinfo-data
|
||||
@@ -552,6 +554,7 @@ CHECKSUMS
|
||||
sshkit (1.25.0) sha256=c8c6543cdb60f91f1d277306d585dd11b6a064cb44eab0972827e4311ff96744
|
||||
stimulus-rails (1.3.4) sha256=765676ffa1f33af64ce026d26b48e8ffb2e0b94e0f50e9119e11d6107d67cb06
|
||||
stringio (3.2.0) sha256=c37cb2e58b4ffbd33fe5cd948c05934af997b36e0b6ca6fdf43afa234cf222e1
|
||||
tailwindcss (0.1.1) sha256=cdd453b13267d6441cba86cc6bf2b91e06bf01171983d90701b8d9b634d3be75
|
||||
thor (1.5.0) sha256=e3a9e55fe857e44859ce104a84675ab6e8cd59c650a49106a05f55f136425e73
|
||||
thruster (0.1.17) sha256=6f3f1de43e22f0162d81cbc363f45ee42a1b8460213856c1a899cbf0d3297235
|
||||
thruster (0.1.17-aarch64-linux) sha256=1b3a34b2814185c2aeaf835b5ecff5348cdcf8e77809f7a092d46e4b962a16ba
|
||||
|
||||
@@ -1,10 +1,32 @@
|
||||
/*
|
||||
* This is a manifest file that'll be compiled into application.css.
|
||||
*
|
||||
* With Propshaft, assets are served efficiently without preprocessing steps. You can still include
|
||||
* application-wide styles in this file, but keep in mind that CSS precedence will follow the standard
|
||||
* cascading order, meaning styles declared later in the document or manifest will override earlier ones,
|
||||
* depending on specificity.
|
||||
*
|
||||
* Consider organizing styles into separate files for maintainability.
|
||||
*/
|
||||
/* application.css */
|
||||
|
||||
body {
|
||||
font-family: sans-serif;
|
||||
background-color: #f9f9f9;
|
||||
margin: 18px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
li {
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
margin-bottom: 10px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.task-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.done-task {
|
||||
background-color: #f0f0f0;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.done-task .task-title {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
48
app/controllers/projects_controller.rb
Normal file
48
app/controllers/projects_controller.rb
Normal file
@@ -0,0 +1,48 @@
|
||||
class ProjectsController < ApplicationController
|
||||
def index
|
||||
@projects = Project.all
|
||||
end
|
||||
|
||||
def show
|
||||
@project = Project.find(params[:id])
|
||||
end
|
||||
|
||||
def new
|
||||
@project = Project.new
|
||||
end
|
||||
|
||||
def create
|
||||
@project = Project.new(project_params)
|
||||
if @project.save
|
||||
redirect_to @project
|
||||
else
|
||||
render :new
|
||||
end
|
||||
end
|
||||
|
||||
def edit
|
||||
@project = Project.find(params[:id])
|
||||
end
|
||||
|
||||
def update
|
||||
@project = Project.find(params[:id])
|
||||
if @project.update(project_params)
|
||||
redirect_to @project
|
||||
else
|
||||
render :edit
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@project = Project.find(params[:id])
|
||||
@project.destroy
|
||||
redirect_to projects_path
|
||||
end
|
||||
|
||||
|
||||
private
|
||||
|
||||
def project_params
|
||||
params.require(:project).permit(:project_name, :description)
|
||||
end
|
||||
end
|
||||
53
app/controllers/tasks_controller.rb
Normal file
53
app/controllers/tasks_controller.rb
Normal file
@@ -0,0 +1,53 @@
|
||||
class TasksController < ApplicationController
|
||||
before_action :set_project
|
||||
|
||||
def create
|
||||
@task = @project.tasks.build(task_params)
|
||||
|
||||
if @task.save
|
||||
respond_to do |format|
|
||||
format.turbo_stream
|
||||
format.html { redirect_to @project, notice: "Task created!" }
|
||||
end
|
||||
else
|
||||
respond_to do |format|
|
||||
format.turbo_stream { render turbo_stream: turbo_stream.replace("new_task", partial: "tasks/form", locals: { project: @project, task: @task }) }
|
||||
format.html { render "projects/show", status: :unprocessable_entity }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@task = @project.tasks.find_by(id: params[:id])
|
||||
return unless @task
|
||||
|
||||
task_dom_id = "task_#{@task.id}"
|
||||
@task.destroy
|
||||
|
||||
respond_to do |format|
|
||||
format.turbo_stream { render turbo_stream: turbo_stream.remove(task_dom_id) }
|
||||
format.html { redirect_to @project }
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
def complete
|
||||
@task = @project.tasks.find(params[:id])
|
||||
@task.update(status: "done")
|
||||
|
||||
respond_to do |format|
|
||||
format.turbo_stream
|
||||
format.html { redirect_to @project }
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_project
|
||||
@project = Project.find(params[:project_id])
|
||||
end
|
||||
|
||||
def task_params
|
||||
params.require(:task).permit(:title, :status)
|
||||
end
|
||||
end
|
||||
2
app/helpers/articles_helper.rb
Normal file
2
app/helpers/articles_helper.rb
Normal file
@@ -0,0 +1,2 @@
|
||||
module ArticlesHelper
|
||||
end
|
||||
2
app/helpers/projects_helper.rb
Normal file
2
app/helpers/projects_helper.rb
Normal file
@@ -0,0 +1,2 @@
|
||||
module ProjectsHelper
|
||||
end
|
||||
2
app/helpers/tasks_helper.rb
Normal file
2
app/helpers/tasks_helper.rb
Normal file
@@ -0,0 +1,2 @@
|
||||
module TasksHelper
|
||||
end
|
||||
5
app/models/project.rb
Normal file
5
app/models/project.rb
Normal file
@@ -0,0 +1,5 @@
|
||||
class Project < ApplicationRecord
|
||||
has_many :tasks, dependent: :destroy
|
||||
|
||||
validates :project_name, :description, presence: true
|
||||
end
|
||||
7
app/models/task.rb
Normal file
7
app/models/task.rb
Normal file
@@ -0,0 +1,7 @@
|
||||
class Task < ApplicationRecord
|
||||
belongs_to :project
|
||||
|
||||
enum :status, { to_do: 0, in_progress: 1, done: 2 }
|
||||
|
||||
validates :title, presence: true
|
||||
end
|
||||
2
app/views/articles/index.html.erb
Normal file
2
app/views/articles/index.html.erb
Normal file
@@ -0,0 +1,2 @@
|
||||
<h1>Articles#index</h1>
|
||||
<p>Find me in app/views/articles/index.html.erb</p>
|
||||
35
app/views/projects/index.html.erb
Normal file
35
app/views/projects/index.html.erb
Normal file
@@ -0,0 +1,35 @@
|
||||
<h1>Projects</h1>
|
||||
|
||||
<div class="projects-list">
|
||||
<ul>
|
||||
<% @projects.each do |project| %>
|
||||
<% total_tasks = project.tasks.count %>
|
||||
<% done_tasks_count = project.tasks.where(status: "done").count %>
|
||||
|
||||
<li>
|
||||
<div class="project-item">
|
||||
<%= link_to project.project_name, project_path(project) %>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="project-actions">
|
||||
<%= link_to "Destroy", project_path(project), data: {
|
||||
turbo_method: :delete,
|
||||
turbo_confirm: "Are you sure?"
|
||||
} %>
|
||||
</div>
|
||||
|
||||
<div class="project-done">
|
||||
<% if total_tasks.zero? %>
|
||||
No tasks yet
|
||||
<% else %>
|
||||
Done tasks: <%= ((done_tasks_count.to_f / total_tasks) * 100).round %>%
|
||||
<% end %>
|
||||
</div>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
<h2><%= link_to "Add Project", new_project_path %></h2>
|
||||
23
app/views/projects/new.html.erb
Normal file
23
app/views/projects/new.html.erb
Normal file
@@ -0,0 +1,23 @@
|
||||
<h1>New Project</h1>
|
||||
|
||||
<%= form_with model: @project do |form| %>
|
||||
<div>
|
||||
<%= form.label :project_name %><br>
|
||||
<%= form.text_field :project_name %>
|
||||
<% @project.errors.full_messages_for(:project_name).each do |message| %>
|
||||
<p><%= message %></p>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<%= form.label :description %><br>
|
||||
<%= form.text_area :description %>
|
||||
<% @project.errors.full_messages_for(:description).each do |message| %>
|
||||
<p><%= message %></p>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<%= form.submit %>
|
||||
</div>
|
||||
<% end %>
|
||||
22
app/views/projects/show.html.erb
Normal file
22
app/views/projects/show.html.erb
Normal file
@@ -0,0 +1,22 @@
|
||||
<h1><%= @project.project_name %></h1>
|
||||
<p><%= @project.description %></p>
|
||||
|
||||
<%= turbo_frame_tag "tasks" do %>
|
||||
|
||||
<ul id="active_tasks">
|
||||
<% @project.tasks.where(status: %w[to_do in_progress]).each do |task| %>
|
||||
<%= render partial: "tasks/task", locals: { task: task } %>
|
||||
<% end %>
|
||||
</ul>
|
||||
|
||||
<ul id="done_tasks">
|
||||
<% @project.tasks.where(status: "done").each do |task| %>
|
||||
<%= render partial: "tasks/task", locals: { task: task } %>
|
||||
<% end %>
|
||||
</ul>
|
||||
|
||||
<% end %>
|
||||
|
||||
<%= turbo_frame_tag "new_task" do %>
|
||||
<%= render "tasks/form", project: @project, task: Task.new %>
|
||||
<% end %>
|
||||
15
app/views/tasks/_form.html.erb
Normal file
15
app/views/tasks/_form.html.erb
Normal file
@@ -0,0 +1,15 @@
|
||||
<h2>Add New Task</h2>
|
||||
|
||||
<%= form_with model: [project, task], local: false do |f| %>
|
||||
<p>
|
||||
<%= f.label :title %><br>
|
||||
<%= f.text_field :title %>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<%= f.label :status %><br>
|
||||
<%= f.select :status, Task.statuses.keys.reject { |s| s == "done" } %>
|
||||
</p>
|
||||
|
||||
<%= f.submit "Create Task" %>
|
||||
<% end %>
|
||||
18
app/views/tasks/_task.html.erb
Normal file
18
app/views/tasks/_task.html.erb
Normal file
@@ -0,0 +1,18 @@
|
||||
<li id="<%= dom_id(task) %>" class="task-item <%= 'done-task' if task.status == 'done' %>">
|
||||
<div class="task-title"><%= task.title %></div>
|
||||
<div class="task-status"><%= task.status.titleize %></div>
|
||||
<div class="task-actions flex gap-2">
|
||||
<% unless task.status == "done" %>
|
||||
<%= button_to "Complete",
|
||||
complete_project_task_path(task.project, task),
|
||||
method: :patch,
|
||||
data: { turbo_frame: dom_id(task) },
|
||||
class: "task-btn complete-btn" %>
|
||||
<% end %>
|
||||
|
||||
<%= link_to "Destroy",
|
||||
project_task_path(task.project, task),
|
||||
data: { turbo_method: :delete, turbo_confirm: "Are you sure?" },
|
||||
class: "task-btn destroy-btn" %>
|
||||
</div>
|
||||
</li>
|
||||
3
app/views/tasks/complete.turbo_stream.erb
Normal file
3
app/views/tasks/complete.turbo_stream.erb
Normal file
@@ -0,0 +1,3 @@
|
||||
<%= turbo_stream.replace dom_id(@task) do %>
|
||||
<%= render partial: "tasks/task", locals: { task: @task } %>
|
||||
<% end %>
|
||||
7
app/views/tasks/create.turbo_stream.erb
Normal file
7
app/views/tasks/create.turbo_stream.erb
Normal file
@@ -0,0 +1,7 @@
|
||||
<%= turbo_stream.prepend "active_tasks" do %>
|
||||
<%= render partial: "tasks/task", locals: { task: @task } %>
|
||||
<% end %>
|
||||
|
||||
<%= turbo_stream.replace "new_task" do %>
|
||||
<%= render "tasks/form", project: @project, task: Task.new %>
|
||||
<% end %>
|
||||
1
app/views/tasks/destroy.turbo_stream.erb
Normal file
1
app/views/tasks/destroy.turbo_stream.erb
Normal file
@@ -0,0 +1 @@
|
||||
<%= turbo_stream.remove dom_id(@task) %>
|
||||
@@ -1,4 +1,17 @@
|
||||
Rails.application.routes.draw do
|
||||
|
||||
root "projects#index"
|
||||
|
||||
resources :projects do
|
||||
resources :tasks do
|
||||
member do
|
||||
patch :complete
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
# Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html
|
||||
|
||||
# Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500.
|
||||
|
||||
9
db/migrate/20260126072711_create_projects.rb
Normal file
9
db/migrate/20260126072711_create_projects.rb
Normal file
@@ -0,0 +1,9 @@
|
||||
class CreateProjects < ActiveRecord::Migration[8.1]
|
||||
def change
|
||||
create_table :projects do |t|
|
||||
t.string :project_name
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
end
|
||||
end
|
||||
10
db/migrate/20260126072814_create_tasks.rb
Normal file
10
db/migrate/20260126072814_create_tasks.rb
Normal file
@@ -0,0 +1,10 @@
|
||||
class CreateTasks < ActiveRecord::Migration[8.1]
|
||||
def change
|
||||
create_table :tasks do |t|
|
||||
t.string :title
|
||||
t.string :status
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
end
|
||||
end
|
||||
5
db/migrate/20260126082816_add_description_to_projects.rb
Normal file
5
db/migrate/20260126082816_add_description_to_projects.rb
Normal file
@@ -0,0 +1,5 @@
|
||||
class AddDescriptionToProjects < ActiveRecord::Migration[8.1]
|
||||
def change
|
||||
add_column :projects, :description, :string
|
||||
end
|
||||
end
|
||||
6
db/migrate/20260126091632_change_status_type_in_tasks.rb
Normal file
6
db/migrate/20260126091632_change_status_type_in_tasks.rb
Normal file
@@ -0,0 +1,6 @@
|
||||
class ChangeStatusTypeInTasks < ActiveRecord::Migration[8.1]
|
||||
def change
|
||||
remove_column :tasks, :status, :string
|
||||
add_column :tasks, :status, :integer, default: 0, null: false
|
||||
end
|
||||
end
|
||||
5
db/migrate/20260126094649_add_project_to_tasks.rb
Normal file
5
db/migrate/20260126094649_add_project_to_tasks.rb
Normal file
@@ -0,0 +1,5 @@
|
||||
class AddProjectToTasks < ActiveRecord::Migration[8.1]
|
||||
def change
|
||||
add_reference :tasks, :project, null: false, foreign_key: true
|
||||
end
|
||||
end
|
||||
31
db/schema.rb
generated
Normal file
31
db/schema.rb
generated
Normal file
@@ -0,0 +1,31 @@
|
||||
# This file is auto-generated from the current state of the database. Instead
|
||||
# of editing this file, please use the migrations feature of Active Record to
|
||||
# incrementally modify your database, and then regenerate this schema definition.
|
||||
#
|
||||
# This file is the source Rails uses to define your schema when running `bin/rails
|
||||
# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to
|
||||
# be faster and is potentially less error prone than running all of your
|
||||
# migrations from scratch. Old migrations may fail to apply correctly if those
|
||||
# migrations use external dependencies or application code.
|
||||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[8.1].define(version: 2026_01_26_094649) do
|
||||
create_table "projects", force: :cascade do |t|
|
||||
t.datetime "created_at", null: false
|
||||
t.string "description"
|
||||
t.string "project_name"
|
||||
t.datetime "updated_at", null: false
|
||||
end
|
||||
|
||||
create_table "tasks", force: :cascade do |t|
|
||||
t.datetime "created_at", null: false
|
||||
t.integer "project_id", null: false
|
||||
t.integer "status", default: 0, null: false
|
||||
t.string "title"
|
||||
t.datetime "updated_at", null: false
|
||||
t.index ["project_id"], name: "index_tasks_on_project_id"
|
||||
end
|
||||
|
||||
add_foreign_key "tasks", "projects"
|
||||
end
|
||||
40
db/seeds.rb
40
db/seeds.rb
@@ -7,3 +7,43 @@
|
||||
# ["Action", "Comedy", "Drama", "Horror"].each do |genre_name|
|
||||
# MovieGenre.find_or_create_by!(name: genre_name)
|
||||
# end
|
||||
# db/seeds.rb
|
||||
|
||||
Task.destroy_all
|
||||
Project.destroy_all
|
||||
|
||||
project1 = Project.create!(
|
||||
project_name: "project one",
|
||||
description: "This is project one"
|
||||
)
|
||||
|
||||
project2 = Project.create!(
|
||||
project_name: "project two",
|
||||
description: "This is project two"
|
||||
)
|
||||
|
||||
Task.create!(
|
||||
title: "first task of project one",
|
||||
status: :to_do,
|
||||
project: project1
|
||||
)
|
||||
|
||||
Task.create!(
|
||||
title: "second task of project one",
|
||||
status: :in_progress,
|
||||
project: project1
|
||||
)
|
||||
|
||||
Task.create!(
|
||||
title: "third task of project one",
|
||||
status: :done,
|
||||
project: project1
|
||||
)
|
||||
|
||||
Task.create!(
|
||||
title: "first task of project two",
|
||||
status: :to_do,
|
||||
project: project2
|
||||
)
|
||||
|
||||
puts "Seeding complete"
|
||||
8
test/controllers/projects_controller_test.rb
Normal file
8
test/controllers/projects_controller_test.rb
Normal file
@@ -0,0 +1,8 @@
|
||||
require "test_helper"
|
||||
|
||||
class ProjectsControllerTest < ActionDispatch::IntegrationTest
|
||||
test "should get index" do
|
||||
get projects_index_url
|
||||
assert_response :success
|
||||
end
|
||||
end
|
||||
7
test/controllers/tasks_controller_test.rb
Normal file
7
test/controllers/tasks_controller_test.rb
Normal file
@@ -0,0 +1,7 @@
|
||||
require "test_helper"
|
||||
|
||||
class TasksControllerTest < ActionDispatch::IntegrationTest
|
||||
# test "the truth" do
|
||||
# assert true
|
||||
# end
|
||||
end
|
||||
7
test/fixtures/projects.yml
vendored
Normal file
7
test/fixtures/projects.yml
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
|
||||
|
||||
one:
|
||||
project_name: MyString
|
||||
|
||||
two:
|
||||
project_name: MyString
|
||||
9
test/fixtures/tasks.yml
vendored
Normal file
9
test/fixtures/tasks.yml
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
|
||||
|
||||
one:
|
||||
title: MyString
|
||||
status: MyString
|
||||
|
||||
two:
|
||||
title: MyString
|
||||
status: MyString
|
||||
7
test/models/project_test.rb
Normal file
7
test/models/project_test.rb
Normal file
@@ -0,0 +1,7 @@
|
||||
require "test_helper"
|
||||
|
||||
class ProjectTest < ActiveSupport::TestCase
|
||||
# test "the truth" do
|
||||
# assert true
|
||||
# end
|
||||
end
|
||||
7
test/models/task_test.rb
Normal file
7
test/models/task_test.rb
Normal file
@@ -0,0 +1,7 @@
|
||||
require "test_helper"
|
||||
|
||||
class TaskTest < ActiveSupport::TestCase
|
||||
# test "the truth" do
|
||||
# assert true
|
||||
# end
|
||||
end
|
||||
Reference in New Issue
Block a user