Add template assignments and settings page

This commit is contained in:
Oliver Günther
2025-11-24 14:31:23 +01:00
parent 63849341a3
commit a9497b4d36
12 changed files with 515 additions and 0 deletions
@@ -0,0 +1,59 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
class Projects::Settings::SubitemsController < Projects::SettingsController
menu_item :settings_subitems
def show; end
def update
workspace_type = params[:workspace_type]
template_id = params[:template_id]
assignment = @project.subproject_template_assignments.find_or_initialize_by(workspace_type:)
if template_id.blank?
if assignment.persisted?
assignment.destroy
flash[:notice] = I18n.t(:notice_successful_update)
end
else
assignment.template_id = template_id
if assignment.save
flash[:notice] = I18n.t(:notice_successful_update)
else
flash[:error] = assignment.errors.full_messages.join(", ")
end
end
redirect_to project_settings_subitems_path(@project)
end
end
@@ -0,0 +1,105 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
module Projects
module Settings
class SubitemsTemplateForm < ApplicationForm
attr_reader :project, :assignments
def initialize(project:)
super()
@project = project
@assignments = @project.subproject_template_assignments
end
form do |f|
if project.portfolio?
program_template = assignments.detect(&:program?)
f.select_list(
name: :program_template,
scope_name_to_model: false,
label: I18n.t("projects.settings.subitems.program_template_label"),
caption: I18n.t("projects.settings.subitems.program_template_caption"),
input_width: :large,
include_blank: I18n.t("projects.settings.subitems.no_template")
) do |list|
available_templates
.workspace_type(:program)
.find_each do |template|
list.option(
value: template.id,
label: template.name,
selected: template.id == program_template&.template_id
)
end
end
end
project_template = assignments.detect(&:project?)
f.select_list(
name: :project_template,
scope_name_to_model: false,
label: I18n.t("projects.settings.subitems.project_template_label"),
caption: I18n.t("projects.settings.subitems.project_template_caption"),
input_width: :large,
include_blank: I18n.t("projects.settings.subitems.no_template")
) do |list|
available_templates
.workspace_type(:project)
.find_each do |template|
list.option(
value: template.id,
label: template.name,
selected: template.id == project_template&.template_id
)
end
end
f.submit(
name: :submit,
label: I18n.t(:button_save),
scheme: :primary
)
end
private
def available_templates
Project
.visible(User.current)
.active
.templated
.order(name: :asc)
end
end
end
end
+7
View File
@@ -111,12 +111,19 @@ class Project < ApplicationRecord
has_many :recurring_meetings, dependent: :destroy
belongs_to :template, class_name: "Project", optional: true
has_many :templated_projects,
class_name: "Project",
foreign_key: "template_id",
inverse_of: :template,
dependent: nil
has_many :subproject_template_assignments,
class_name: "SubprojectTemplateAssignment",
foreign_key: "template_id",
inverse_of: :template,
dependent: :delete_all
accepts_nested_attributes_for :available_phases
validates_associated :available_phases, on: :saving_phases
@@ -0,0 +1,56 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
class SubprojectTemplateAssignment < ApplicationRecord
belongs_to :project, inverse_of: :subproject_template_assignments
belongs_to :template, class_name: "Project", inverse_of: :template_assignments
enum :workspace_type, {
project: "project",
program: "program"
}, validate: true
validates :project_id, presence: true
validates :template_id, presence: true
validates :workspace_type, presence: true
validates :project_id, uniqueness: { scope: :workspace_type }
validate :template_must_be_templated
private
def template_must_be_templated
return if template.blank?
unless template.templated?
errors.add(:template, :must_be_template)
end
end
end
@@ -0,0 +1,65 @@
<%#-- copyright
OpenProject is an open source project management software.
Copyright (C) the OpenProject GmbH
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License version 3.
OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
Copyright (C) 2006-2013 Jean-Philippe Lang
Copyright (C) 2010-2013 the ChiliProject Team
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
See COPYRIGHT and LICENSE files for more details.
++#%>
<%=
render Primer::OpenProject::PageHeader.new do |header|
header.with_title { I18n.t(:label_subitems) }
header.with_breadcrumbs(
[{ href: project_overview_path(@project.id), text: @project.name },
{ href: project_settings_general_path(@project.id), text: I18n.t("label_project_settings") },
I18n.t(:label_subitems)]
)
header.with_tab_nav(label: nil) do |tab_nav|
tab_nav.with_tab(
selected: true,
href: project_settings_subitems_path(@project)
) do |t|
t.with_text { I18n.t(:label_templates) }
end
end
end
%>
<%= error_messages_for(@project) %>
<%= render Primer::Beta::Text.new { I18n.t("projects.settings.subitems.template_section") } %>
<%=
settings_primer_form_with(url: project_settings_subitems_path(@project)) do |f|
render(
Primer::Forms::FormList.new(
Projects::Settings::SubitemsTemplateForm.new(
f,
project: @project,
)
)
)
end
%>
+1
View File
@@ -731,6 +731,7 @@ Redmine::MenuManager.map :project_menu do |menu|
if: ->(_) { OpenProject::FeatureDecisions.project_initiation_active? }
},
modules: { caption: :label_module_plural },
subitems: { caption: :label_subitems },
work_packages: {
caption: :label_work_package_plural,
if: ->(project) {
+1
View File
@@ -153,6 +153,7 @@ Rails.application.reloader.to_prepare do
update_artifact_export_settings
toggle_project_custom_field
disable_all_of_section enable_all_of_section],
"projects/settings/subitems": %i[show update],
"projects/templated": %i[create destroy],
"projects/identifier": %i[show update],
"projects/status": %i[update destroy]
+11
View File
@@ -559,6 +559,14 @@ en:
The project will only be visible to project members depending on their role and associated permissions.
Sub-projects are not affected and have their own settings.
change_identifier: Change identifier
subitems:
template_section: >
Select templates to be used when creating new subitems.
project_template_label: "Template for projects"
project_template_caption: "Select a template project to be used as the default for new subitems of this type."
program_template_label: "Template for programs"
program_template_caption: "Select a template program to be used as the default for new subitems of this type."
no_template: "No predefined template"
actions:
label_enable_all: "Enable all"
label_disable_all: "Disable all"
@@ -3610,6 +3618,7 @@ en:
label_subproject: "Subproject"
label_subproject_new: "New subproject"
label_subproject_plural: "Subprojects"
label_subitems: "Subitems"
label_subtask_plural: "Subtasks"
label_summary: "Summary"
label_system: "System"
@@ -3617,6 +3626,8 @@ en:
label_table_of_contents: "Table of contents"
label_tag: "Tag"
label_team_planner: "Team Planner"
label_template: "Template"
label_templates: "Templates"
label_text: "Long text"
label_this_month: "this month"
label_this_week: "this week"
+1
View File
@@ -285,6 +285,7 @@ Rails.application.routes.draw do
post :toggle_public
end
resource :modules, only: %i[show update]
resource :subitems, only: %i[show update]
resource :creation_wizard, controller: "creation_wizard", only: %i[show] do
get :disable_dialog
post :toggle
@@ -0,0 +1,43 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
class CreateSubprojectTemplateAssignments < ActiveRecord::Migration[8.0]
def change
create_table :subproject_template_assignments do |t|
t.references :project, null: false, foreign_key: { on_delete: :cascade }, index: true
t.references :template, null: false, foreign_key: { to_table: :projects, on_delete: :cascade }, index: true
t.string :workspace_type, null: false, index: true
t.timestamps
t.index %i[project_id workspace_type], unique: true, name: "idx_subproj_tmpl_assignments_on_project_workspace"
end
end
end
+1
View File
@@ -424,6 +424,7 @@
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"dev": true,
"peer": true,
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
@@ -0,0 +1,165 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
require "spec_helper"
RSpec.describe SubprojectTemplateAssignment do
let(:project) { create(:project) }
let(:template) { create(:project, templated: true) }
describe "associations" do
it { is_expected.to belong_to(:project).inverse_of(:subproject_template_assignments) }
it { is_expected.to belong_to(:template).class_name("Project").inverse_of(:template_assignments) }
it "allows accessing assignments from project" do
assignment = create(:subproject_template_assignment, project:, template:)
expect(project.subproject_template_assignments).to include(assignment)
end
end
describe "enums" do
it {
expect(described_class).to define_enum_for(:workspace_type)
.with_values(project: "project", program: "program")
.backed_by_column_of_type(:string)
}
end
describe "validations" do
subject(:assignment) do
build(:subproject_template_assignment,
project:,
template:,
workspace_type: "project")
end
it { is_expected.to validate_presence_of(:project_id) }
it { is_expected.to validate_presence_of(:template_id) }
it { is_expected.to validate_presence_of(:workspace_type) }
describe "uniqueness" do
before do
create(:subproject_template_assignment,
project:,
template:,
workspace_type: "project")
end
it "allows only one assignment per project and workspace_type" do
duplicate = build(:subproject_template_assignment,
project:,
template:,
workspace_type: "project")
expect(duplicate).not_to be_valid
expect(duplicate.errors[:project_id]).to include("has already been taken")
end
it "allows different workspace_types for the same project" do
different_type = build(:subproject_template_assignment,
project:,
template:,
workspace_type: "program")
expect(different_type).to be_valid
end
it "allows same workspace_type for different projects" do
other_project = create(:project)
different_project = build(:subproject_template_assignment,
project: other_project,
template:,
workspace_type: "project")
expect(different_project).to be_valid
end
end
describe "template validation" do
context "when template is marked as templated" do
let(:template) { create(:project, templated: true) }
it "is valid" do
expect(assignment).to be_valid
end
end
context "when template is not marked as templated" do
let(:template) { create(:project, templated: false) }
it "is invalid" do
expect(assignment).not_to be_valid
expect(assignment.errors[:template]).to include("must be template")
end
end
end
end
describe "cascading deletes" do
let!(:assignment) do
create(:subproject_template_assignment,
project:,
template:)
end
context "when project is deleted" do
it "deletes the assignment via cascade" do
expect { project.destroy }.to change(described_class, :count).by(-1)
end
end
context "when template is deleted" do
it "deletes the assignment via cascade" do
expect { template.destroy }.to change(described_class, :count).by(-1)
end
end
end
describe "factory" do
it "creates a valid assignment with default attributes" do
assignment = build(:subproject_template_assignment)
expect(assignment).to be_valid
end
it "creates a valid assignment with :for_project trait" do
assignment = build(:subproject_template_assignment, :for_project)
expect(assignment).to be_valid
expect(assignment.workspace_type).to eq("project")
expect(assignment.template.workspace_type).to eq("project")
end
it "creates a valid assignment with :for_program trait" do
assignment = build(:subproject_template_assignment, :for_program)
expect(assignment).to be_valid
expect(assignment.workspace_type).to eq("program")
expect(assignment.template.workspace_type).to eq("program")
end
end
end