diff --git a/app/assets/stylesheets/3-atoms/_badges.scss b/app/assets/stylesheets/3-atoms/_badges.scss
index 2b7f1ba2..2e170082 100644
--- a/app/assets/stylesheets/3-atoms/_badges.scss
+++ b/app/assets/stylesheets/3-atoms/_badges.scss
@@ -10,7 +10,7 @@
font-size: 11px;
&.archived {
- background-color: rgba(29,78,216,0.2);
+ background-color: rgba(29, 78, 216, 0.2);
border-color: #1d4ed8;
}
@@ -19,4 +19,7 @@
border-color: #d77e72;
}
+ &.version-jump {
+ background-color: #57ce81;
+ }
}
diff --git a/app/assets/stylesheets/3-atoms/_forms.scss b/app/assets/stylesheets/3-atoms/_forms.scss
index e6528415..ebd25152 100644
--- a/app/assets/stylesheets/3-atoms/_forms.scss
+++ b/app/assets/stylesheets/3-atoms/_forms.scss
@@ -70,6 +70,7 @@ input[type="checkbox"] {
}
.field,
+.select,
.actions {
margin-bottom: 10px;
}
diff --git a/app/controllers/madmin/application_controller.rb b/app/controllers/madmin/application_controller.rb
index 85dbf73d..12ace7d4 100644
--- a/app/controllers/madmin/application_controller.rb
+++ b/app/controllers/madmin/application_controller.rb
@@ -1,5 +1,6 @@
module Madmin
class ApplicationController < Madmin::BaseController
+ include Pundit::Authorization
before_action :ensure_admin
def ensure_admin
diff --git a/app/controllers/madmin/version_jumps_controller.rb b/app/controllers/madmin/version_jumps_controller.rb
new file mode 100644
index 00000000..986de88f
--- /dev/null
+++ b/app/controllers/madmin/version_jumps_controller.rb
@@ -0,0 +1,4 @@
+module Madmin
+ class VersionJumpsController < Madmin::ResourceController
+ end
+end
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index ec933f54..84537228 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -43,7 +43,7 @@ def toggle_locked
end
def new_clone
- @original = Project.includes(:projects, stories: :estimates).find(params[:id])
+ @original = Project.includes(:projects, :version_jump, stories: :estimates).find(params[:id])
end
def clone
@@ -111,11 +111,11 @@ def find_project
end
def projects_params
- params.require(:project).permit(:title, :status, :parent_id)
+ params.require(:project).permit(:title, :status, :parent_id, :version_jump_id)
end
def clone_params
- params.require(:project).permit(:title, :parent_id)
+ params.require(:project).permit(:title, :parent_id, :version_jump_id)
end
def parent_id
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index fae02738..7d60ec97 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -17,6 +17,13 @@ def link_unless_archived(project, text, url, classes: nil, method: :get, remote:
end
end
+ def version_jump_badge(project)
+ return "" unless project.version_jump
+
+ jump = project.version_jump
+ content_tag(:span, jump.to_label, class: "status-badge version-jump")
+ end
+
private
def text_content(icon, text)
diff --git a/app/madmin/resources/project_resource.rb b/app/madmin/resources/project_resource.rb
index 966c2b9f..750bb2ce 100644
--- a/app/madmin/resources/project_resource.rb
+++ b/app/madmin/resources/project_resource.rb
@@ -10,6 +10,7 @@ class ProjectResource < Madmin::Resource
attribute :users
attribute :parent
attribute :projects
+ attribute :version_jump
def self.display_name(record)
record.title.truncate(20)
diff --git a/app/madmin/resources/version_jump_resource.rb b/app/madmin/resources/version_jump_resource.rb
new file mode 100644
index 00000000..6bc0f68f
--- /dev/null
+++ b/app/madmin/resources/version_jump_resource.rb
@@ -0,0 +1,24 @@
+class VersionJumpResource < Madmin::Resource
+ # Attributes
+ attribute :id, form: false
+ attribute :technology
+ attribute :initial_version
+ attribute :target_version
+
+ # Associations
+ attribute :projects, form: false
+
+ # Uncomment this to customize the display name of records in the admin area.
+ def self.display_name(record)
+ record.to_label
+ end
+
+ # Uncomment this to customize the default sort column and direction.
+ # def self.default_sort_column
+ # "created_at"
+ # end
+ #
+ # def self.default_sort_direction
+ # "desc"
+ # end
+end
diff --git a/app/models/project.rb b/app/models/project.rb
index 68429561..f2f2293e 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -4,6 +4,7 @@ class Project < ApplicationRecord
has_many :stories
has_many :estimates, through: :stories
has_many :users, through: :estimates
+ belongs_to :version_jump, optional: true
belongs_to :parent, class_name: "Project", required: false
has_many :projects, class_name: "Project", foreign_key: :parent_id, dependent: :destroy
@@ -52,6 +53,10 @@ def archived?
status == "archived"
end
+ def locked?
+ locked_at.present?
+ end
+
def breadcrumb
parent.present? ? "#{parent.breadcrumb} ยป #{title}" : title
end
diff --git a/app/models/version_jump.rb b/app/models/version_jump.rb
new file mode 100644
index 00000000..37f0fbac
--- /dev/null
+++ b/app/models/version_jump.rb
@@ -0,0 +1,10 @@
+class VersionJump < ApplicationRecord
+ has_many :projects
+
+ validates :technology, :initial_version, :target_version, presence: true
+ validates :technology, uniqueness: {scope: [:initial_version, :target_version]}
+
+ def to_label
+ "#{technology} / #{initial_version} - #{target_version}"
+ end
+end
diff --git a/app/policies/estimate_policy.rb b/app/policies/estimate_policy.rb
new file mode 100644
index 00000000..6c9496d3
--- /dev/null
+++ b/app/policies/estimate_policy.rb
@@ -0,0 +1,2 @@
+class EstimatePolicy < ApplicationPolicy
+end
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index c9286f43..c0ac8a41 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -7,6 +7,8 @@ def initialize(user, project)
end
def update?
+ return false unless project.is_a?(Project)
+
return !project.locked_at? if project.parent_id.nil?
!project.parent.locked_at?
diff --git a/app/policies/story_policy.rb b/app/policies/story_policy.rb
new file mode 100644
index 00000000..9c6c7db0
--- /dev/null
+++ b/app/policies/story_policy.rb
@@ -0,0 +1,2 @@
+class StoryPolicy < ApplicationPolicy
+end
diff --git a/app/policies/user_policy.rb b/app/policies/user_policy.rb
new file mode 100644
index 00000000..8b6187ab
--- /dev/null
+++ b/app/policies/user_policy.rb
@@ -0,0 +1,2 @@
+class UserPolicy < ApplicationPolicy
+end
diff --git a/app/policies/version_jump_policy.rb b/app/policies/version_jump_policy.rb
new file mode 100644
index 00000000..47af0fa7
--- /dev/null
+++ b/app/policies/version_jump_policy.rb
@@ -0,0 +1,9 @@
+class VersionJumpPolicy < ApplicationPolicy
+ def update?
+ true
+ end
+
+ def create?
+ true
+ end
+end
diff --git a/app/views/madmin/application/index.html.erb b/app/views/madmin/application/index.html.erb
index 0e17c2a7..ea987e0d 100644
--- a/app/views/madmin/application/index.html.erb
+++ b/app/views/madmin/application/index.html.erb
@@ -11,6 +11,12 @@
<% end %>
+
+ <% if policy(resource).create? %>
+ <%= link_to resource.new_path, class: "bg-white hover:bg-gray-100 text-gray-800 font-semibold py-2 px-4 border border-gray-400 rounded shadow" do %>
+ New <%= resource.friendly_name %>
+ <% end %>
+ <% end %>
@@ -48,7 +54,9 @@
<%= link_to "View", resource.show_path(record), class: "text-indigo-500" %>
- <%# = link_to "Edit", resource.edit_path(record), class: "text-indigo-500" %>
+ <% if policy(resource).update? %>
+ <%= link_to "Edit", resource.edit_path(record), class: "text-indigo-500" %>
+ <% end %>
|
<% end %>
diff --git a/app/views/madmin/application/show.html.erb b/app/views/madmin/application/show.html.erb
index d83dd3a2..27c35bae 100644
--- a/app/views/madmin/application/show.html.erb
+++ b/app/views/madmin/application/show.html.erb
@@ -7,9 +7,13 @@
- <%# = link_to "Edit", resource.edit_path(@record), class: "block bg-white hover:bg-gray-100 text-gray-800 font-semibold py-2 px-4 border border-gray-400 rounded shadow" %>
+ <% if policy(resource).update? %>
+ <%= link_to "Edit", resource.edit_path(@record), class: "block bg-white hover:bg-gray-100 text-gray-800 font-semibold py-2 px-4 border border-gray-400 rounded shadow" %>
+ <% end %>
- <%# = button_to "Delete", resource.show_path(@record), method: :delete, data: { turbo_confirm: "Are you sure?" }, class: "bg-white hover:bg-gray-100 text-red-500 font-semibold py-2 px-4 border border-red-500 rounded shadow pointer-cursor" %>
+ <% if policy(resource).destroy? %>
+ <%= button_to "Delete", resource.show_path(@record), method: :delete, data: { turbo_confirm: "Are you sure?" }, class: "bg-white hover:bg-gray-100 text-red-500 font-semibold py-2 px-4 border border-red-500 rounded shadow pointer-cursor" %>
+ <% end %>
diff --git a/app/views/projects/_form.html.erb b/app/views/projects/_form.html.erb
index 33fb158d..68d293c1 100644
--- a/app/views/projects/_form.html.erb
+++ b/app/views/projects/_form.html.erb
@@ -16,6 +16,11 @@
<%= f.text_field :title, placeholder: "Project Title", class: "project-story-title", autofocus: true %>
+
+ <%= f.label :version_jump_id %>
+ <%= f.select :version_jump_id, options_from_collection_for_select(VersionJump.all, :id, :to_label) %>
+
+
<%= f.submit yield(:button_text), class: "button green", id: "edit" %>
diff --git a/app/views/projects/_sub_project_form.html.erb b/app/views/projects/_sub_project_form.html.erb
index 6bd8820c..9e544af7 100644
--- a/app/views/projects/_sub_project_form.html.erb
+++ b/app/views/projects/_sub_project_form.html.erb
@@ -17,6 +17,11 @@
<%= f.hidden_field :parent_id, value: parent.id %>
+
+ <%= f.label :version_jump_id %>
+ <%= f.select :version_jump_id, options_from_collection_for_select(VersionJump.all, :id, :to_label) %>
+
+
<%= f.submit yield(:button_text), class: "button green", id: "edit" %>
diff --git a/app/views/projects/index.html.erb b/app/views/projects/index.html.erb
index 76c6a564..afa08173 100644
--- a/app/views/projects/index.html.erb
+++ b/app/views/projects/index.html.erb
@@ -9,8 +9,13 @@
<%= link_to project_path(project.id) do %>
<%= project.title %>
- ">Locked
- <%= project.status %>
+ <% if project.locked? %>
+ Locked
+ <% end %>
+ <% if project.status.present? %>
+ <%= project.status %>
+ <% end %>
+ <%= version_jump_badge(project) %>
<% end %>
diff --git a/app/views/projects/new_clone.html.erb b/app/views/projects/new_clone.html.erb
index ed7e0cf3..0a5e70eb 100644
--- a/app/views/projects/new_clone.html.erb
+++ b/app/views/projects/new_clone.html.erb
@@ -12,6 +12,11 @@
<%= f.select :parent_id, options_from_collection_for_select(Project.active.parents, :id, :title, selected: @original.parent_id), include_blank: "None" %>
+
+ <%= f.label :version_jump_id %>
+ <%= f.select :version_jump_id, options_from_collection_for_select(VersionJump.all, :id, :to_label) %>
+
+
<% if @original.projects.any? %>
Sub-projects to clone
diff --git a/app/views/projects/show.html.erb b/app/views/projects/show.html.erb
index a569ec3d..c6555974 100644
--- a/app/views/projects/show.html.erb
+++ b/app/views/projects/show.html.erb
@@ -108,6 +108,7 @@
<% end %>
<%= link_to 'Clone Project', new_clone_project_path(@project.id), class: "button green" %>
+ <%= link_to 'Edit Project', edit_project_path(@project.id), class: "button green" %>
<% if is_unlocked?(@project) %>
<%= link_unless_archived(@project, "Add Sub-Project", project_new_sub_project_path(@project), classes: :green) unless @project.parent_id.present? %>
diff --git a/app/views/shared/_project_title.html.erb b/app/views/shared/_project_title.html.erb
index 5a806332..2c3b1cfe 100644
--- a/app/views/shared/_project_title.html.erb
+++ b/app/views/shared/_project_title.html.erb
@@ -20,6 +20,7 @@
<% end %>
+ <%= version_jump_badge(project) %>
">Locked
"><%= project.status %>
diff --git a/config/routes/madmin.rb b/config/routes/madmin.rb
index 9f95e715..85982206 100644
--- a/config/routes/madmin.rb
+++ b/config/routes/madmin.rb
@@ -4,5 +4,6 @@
resources :stories
resources :estimates, except: [:update, :edit, :create]
resources :users, except: [:update, :edit, :create]
+ resources :version_jumps
root to: "dashboard#show"
end
diff --git a/db/migrate/20230830143908_create_version_jumps.rb b/db/migrate/20230830143908_create_version_jumps.rb
new file mode 100644
index 00000000..0861eb3b
--- /dev/null
+++ b/db/migrate/20230830143908_create_version_jumps.rb
@@ -0,0 +1,11 @@
+class CreateVersionJumps < ActiveRecord::Migration[7.0]
+ def change
+ create_table :version_jumps do |t|
+ t.string :technology
+ t.string :initial_version
+ t.string :target_version
+
+ t.timestamps
+ end
+ end
+end
diff --git a/db/migrate/20230831175732_add_version_jump_to_projects.rb b/db/migrate/20230831175732_add_version_jump_to_projects.rb
new file mode 100644
index 00000000..18c0a8f1
--- /dev/null
+++ b/db/migrate/20230831175732_add_version_jump_to_projects.rb
@@ -0,0 +1,5 @@
+class AddVersionJumpToProjects < ActiveRecord::Migration[7.0]
+ def change
+ add_reference :projects, :version_jump
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index cff8cb41..22f3da62 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema[7.0].define(version: 2022_06_21_141342) do
+ActiveRecord::Schema[7.0].define(version: 2023_08_31_175732) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -31,6 +31,8 @@
t.integer "parent_id"
t.integer "position"
t.datetime "locked_at", precision: nil
+ t.bigint "version_jump_id"
+ t.index ["version_jump_id"], name: "index_projects_on_version_jump_id"
end
create_table "stories", force: :cascade do |t|
@@ -65,4 +67,12 @@
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
end
+ create_table "version_jumps", force: :cascade do |t|
+ t.string "technology"
+ t.string "initial_version"
+ t.string "target_version"
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ end
+
end
diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb
index c27828e0..701f5e6e 100644
--- a/spec/factories/projects.rb
+++ b/spec/factories/projects.rb
@@ -1,6 +1,7 @@
FactoryBot.define do
factory :project do
title { Faker::Company.name }
+ version_jump
trait :locked do
locked_at { Time.current }
end
diff --git a/spec/factories/version_jump.rb b/spec/factories/version_jump.rb
new file mode 100644
index 00000000..2753b74a
--- /dev/null
+++ b/spec/factories/version_jump.rb
@@ -0,0 +1,7 @@
+FactoryBot.define do
+ factory :version_jump do
+ sequence(:technology) { |n| ["Rails", "Ruby", "Node", "React"].sample + n.to_s }
+ initial_version { ["2.3", "3.0", "3.1", "3.2", "4.0", "4.1", "4.2"].sample }
+ target_version { ["5.0", "5.1", "5.2", "6.0", "6.1", "7.0"].sample }
+ end
+end
diff --git a/spec/features/projects_manage_spec.rb b/spec/features/projects_manage_spec.rb
index fe0f2e9a..ac4bb97f 100644
--- a/spec/features/projects_manage_spec.rb
+++ b/spec/features/projects_manage_spec.rb
@@ -3,6 +3,7 @@
RSpec.describe "managing projects", js: true do
let(:user) { FactoryBot.create(:user) }
let(:project) { FactoryBot.create(:project) }
+ let!(:version_jump) { VersionJump.create(technology: "Rails", initial_version: "4.2", target_version: "5.0") }
before do
login_as(user, scope: :user)
@@ -16,9 +17,16 @@
it "allows me to add a project" do
visit root_path
click_link "Add a Project"
- fill_in "project[title]", with: "Super Project"
- click_button "Create"
- expect(Project.count).to eq 1
+ fill_in "Title", with: "Super Project"
+ select "Rails / 4.2 - 5.0", from: "Version jump"
+
+ expect {
+ click_button "Create"
+ }.to change(Project, :count).by 1
+
+ project = Project.last
+ expect(project.title).to eq "Super Project"
+ expect(project.version_jump.to_label).to eq "Rails / 4.2 - 5.0"
end
context "when the project is archived" do
@@ -84,10 +92,14 @@
it "allows me to add sub projects" do
visit project_path(id: project.id)
click_link "Add Sub-Project"
- fill_in "project[title]", with: "Super Sub Project"
- click_button "Create"
- expect(page).to have_content "Project created!"
+ fill_in "Title", with: "Super Sub Project"
+ expect {
+ click_button "Create"
+ }.to change(Project, :count).by 1
expect(current_path).to eq project_path(id: project.id)
+
+ sub = project.projects.last
+ expect(sub.title).to eq "Super Sub Project"
end
it "lists available sub projects with a link" do
@@ -203,7 +215,7 @@
expect(page).to have_text("Clone project #{project.title}")
- fill_in :project_title, with: "Cloned Project"
+ fill_in "Title", with: "Cloned Project"
expect {
click_button "Clone"
diff --git a/spec/models/version_jump_spec.rb b/spec/models/version_jump_spec.rb
new file mode 100644
index 00000000..461ff6ce
--- /dev/null
+++ b/spec/models/version_jump_spec.rb
@@ -0,0 +1,31 @@
+require "rails_helper"
+
+RSpec.describe VersionJump, type: :model do
+ subject { FactoryBot.create(:version_jump) }
+
+ it "should be uniq by technology, initial version, and target version" do
+ attrs = {
+ technology: "Rails",
+ initial_version: "4.2",
+ target_version: "5.0"
+ }
+
+ jump1 = VersionJump.create(attrs)
+ expect(jump1).to be_persisted
+
+ jump2 = VersionJump.new(attrs)
+ expect(jump2).to be_invalid
+ expect(jump2.errors[:technology]).to be_present
+
+ jump2.technology = "Ruby"
+ expect(jump2).to be_valid
+
+ jump2.technology = attrs[:technology]
+ jump2.initial_version = "4.1"
+ expect(jump2).to be_valid
+
+ jump2.initial_version = attrs[:initial_version]
+ jump2.target_version = "5.1"
+ expect(jump2).to be_valid
+ end
+end