Commit a6bddf15 authored by Daniel Barker's avatar Daniel Barker Committed by Robert Speicher

Add instance-level license template

parent 087673ac
import initSettingsPanels from '~/settings_panels'; import initSettingsPanels from '~/settings_panels';
import projectSelect from '~/project_select';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
// Initialize expandable settings panels // Initialize expandable settings panels
initSettingsPanels(); initSettingsPanels();
projectSelect();
}); });
...@@ -14,6 +14,7 @@ export default function projectSelect() { ...@@ -14,6 +14,7 @@ export default function projectSelect() {
this.orderBy = $(select).data('orderBy') || 'id'; this.orderBy = $(select).data('orderBy') || 'id';
this.withIssuesEnabled = $(select).data('withIssuesEnabled'); this.withIssuesEnabled = $(select).data('withIssuesEnabled');
this.withMergeRequestsEnabled = $(select).data('withMergeRequestsEnabled'); this.withMergeRequestsEnabled = $(select).data('withMergeRequestsEnabled');
this.allowClear = $(select).data('allowClear') || false;
placeholder = "Search for project"; placeholder = "Search for project";
if (this.includeGroups) { if (this.includeGroups) {
...@@ -71,6 +72,13 @@ export default function projectSelect() { ...@@ -71,6 +72,13 @@ export default function projectSelect() {
text: function (project) { text: function (project) {
return project.name_with_namespace || project.name; return project.name_with_namespace || project.name;
}, },
initSelection: function(el, callback) {
return Api.project(el.val()).then(({ data }) => callback(data));
},
allowClear: this.allowClear,
dropdownCssClass: "ajax-project-dropdown" dropdownCssClass: "ajax-project-dropdown"
}); });
if (simpleFilter) return select; if (simpleFilter) return select;
......
# LicenseTemplateFinder
#
# Used to find license templates, which may come from a variety of external
# sources
#
# Arguments:
# popular: boolean. When set to true, only "popular" licenses are shown. When
# false, all licenses except popular ones are shown. When nil (the
# default), *all* licenses will be shown.
class LicenseTemplateFinder
prepend ::EE::LicenseTemplateFinder
attr_reader :params
def initialize(params = {})
@params = params
end
def execute
Licensee::License.all(featured: popular_only?).map do |license|
LicenseTemplate.new(
id: license.key,
name: license.name,
nickname: license.nickname,
category: (license.featured? ? :Popular : :Other),
content: license.content,
url: license.url,
meta: license.meta
)
end
end
private
def popular_only?
params.fetch(:popular, nil)
end
end
...@@ -182,12 +182,14 @@ module BlobHelper ...@@ -182,12 +182,14 @@ module BlobHelper
def licenses_for_select def licenses_for_select
return @licenses_for_select if defined?(@licenses_for_select) return @licenses_for_select if defined?(@licenses_for_select)
licenses = Licensee::License.all grouped_licenses = LicenseTemplateFinder.new.execute.group_by(&:category)
categories = grouped_licenses.keys
@licenses_for_select = { @licenses_for_select = categories.each_with_object({}) do |category, hash|
Popular: licenses.select(&:featured).map { |license| { name: license.name, id: license.key } }, hash[category] = grouped_licenses[category].map do |license|
Other: licenses.reject(&:featured).map { |license| { name: license.name, id: license.key } } { name: license.name, id: license.id }
} end
end
end end
def ref_project def ref_project
......
class LicenseTemplate
PROJECT_TEMPLATE_REGEX =
%r{[\<\{\[]
(project|description|
one\sline\s.+\swhat\sit\sdoes\.) # matching the start and end is enough here
[\>\}\]]}xi.freeze
YEAR_TEMPLATE_REGEX = /[<{\[](year|yyyy)[>}\]]/i.freeze
FULLNAME_TEMPLATE_REGEX =
%r{[\<\{\[]
(fullname|name\sof\s(author|copyright\sowner))
[\>\}\]]}xi.freeze
attr_reader :id, :name, :category, :nickname, :url, :meta
alias_method :key, :id
def initialize(id:, name:, category:, content:, nickname: nil, url: nil, meta: {})
@id = id
@name = name
@category = category
@content = content
@nickname = nickname
@url = url
@meta = meta
end
def popular?
category == :Popular
end
alias_method :featured?, :popular?
# Returns the text of the license
def content
if @content.respond_to?(:call)
@content = @content.call
else
@content
end
end
# Populate placeholders in the LicenseTemplate content
def resolve!(project_name: nil, fullname: nil, year: Time.now.year.to_s)
# Ensure the string isn't shared with any other instance of LicenseTemplate
new_content = content.dup
new_content.gsub!(YEAR_TEMPLATE_REGEX, year) if year.present?
new_content.gsub!(PROJECT_TEMPLATE_REGEX, project_name) if project_name.present?
new_content.gsub!(FULLNAME_TEMPLATE_REGEX, fullname) if fullname.present?
@content = new_content
self
end
end
...@@ -385,6 +385,8 @@ ...@@ -385,6 +385,8 @@
.settings-content .settings-content
= render partial: 'slack' = render partial: 'slack'
= render_if_exists 'admin/application_settings/templates', expanded: expanded
%section.settings.as-third-party-offers.no-animate#js-third-party-offers-settings{ class: ('expanded' if expanded) } %section.settings.as-third-party-offers.no-animate#js-third-party-offers-settings{ class: ('expanded' if expanded) }
.settings-header .settings-header
%h4 %h4
......
...@@ -206,6 +206,7 @@ ActiveRecord::Schema.define(version: 20180807153545) do ...@@ -206,6 +206,7 @@ ActiveRecord::Schema.define(version: 20180807153545) do
t.string "encrypted_external_auth_client_key_pass_iv" t.string "encrypted_external_auth_client_key_pass_iv"
t.string "email_additional_text" t.string "email_additional_text"
t.boolean "enforce_terms", default: false t.boolean "enforce_terms", default: false
t.integer "file_template_project_id"
t.boolean "pseudonymizer_enabled", default: false, null: false t.boolean "pseudonymizer_enabled", default: false, null: false
t.boolean "hide_third_party_offers", default: false, null: false t.boolean "hide_third_party_offers", default: false, null: false
t.boolean "snowplow_enabled", default: false, null: false t.boolean "snowplow_enabled", default: false, null: false
...@@ -2962,6 +2963,7 @@ ActiveRecord::Schema.define(version: 20180807153545) do ...@@ -2962,6 +2963,7 @@ ActiveRecord::Schema.define(version: 20180807153545) do
add_index "web_hooks", ["type"], name: "index_web_hooks_on_type", using: :btree add_index "web_hooks", ["type"], name: "index_web_hooks_on_type", using: :btree
add_foreign_key "application_settings", "namespaces", column: "custom_project_templates_group_id", on_delete: :nullify add_foreign_key "application_settings", "namespaces", column: "custom_project_templates_group_id", on_delete: :nullify
add_foreign_key "application_settings", "projects", column: "file_template_project_id", name: "fk_ec757bd087", on_delete: :nullify
add_foreign_key "approvals", "merge_requests", name: "fk_310d714958", on_delete: :cascade add_foreign_key "approvals", "merge_requests", name: "fk_310d714958", on_delete: :cascade
add_foreign_key "approver_groups", "namespaces", column: "group_id", on_delete: :cascade add_foreign_key "approver_groups", "namespaces", column: "group_id", on_delete: :cascade
add_foreign_key "badges", "namespaces", column: "group_id", on_delete: :cascade add_foreign_key "badges", "namespaces", column: "group_id", on_delete: :cascade
......
...@@ -56,7 +56,8 @@ Example response: ...@@ -56,7 +56,8 @@ Example response:
"enforce_terms": true, "enforce_terms": true,
"terms": "Hello world!", "terms": "Hello world!",
"performance_bar_allowed_group_id": 42, "performance_bar_allowed_group_id": 42,
"instance_statistics_visibility_private": false "instance_statistics_visibility_private": false,
"file_template_project_id": 1
} }
``` ```
...@@ -107,6 +108,7 @@ PUT /application/settings ...@@ -107,6 +108,7 @@ PUT /application/settings
| `email_author_in_body` | boolean | no | Some email servers do not support overriding the email sender name. Enable this option to include the name of the author of the issue, merge request or comment in the email body instead. | | `email_author_in_body` | boolean | no | Some email servers do not support overriding the email sender name. Enable this option to include the name of the author of the issue, merge request or comment in the email body instead. |
| `email_additional_text` | string | no | **(Premium)** Additional text added to the bottom of every email for legal/auditing/compliance reasons reasons | | `email_additional_text` | string | no | **(Premium)** Additional text added to the bottom of every email for legal/auditing/compliance reasons reasons |
| `enabled_git_access_protocol` | string | no | Enabled protocols for Git access. Allowed values are: `ssh`, `http`, and `nil` to allow both protocols. | | `enabled_git_access_protocol` | string | no | Enabled protocols for Git access. Allowed values are: `ssh`, `http`, and `nil` to allow both protocols. |
| `file_template_project_id | integer | no | **(Premium)** The ID of a project to load custom file templates from |
| `geo_status_timeout` | integer | no | The amount of seconds after which a request to get a secondary node status will time out. | | `geo_status_timeout` | integer | no | The amount of seconds after which a request to get a secondary node status will time out. |
| `gravatar_enabled` | boolean | no | Enable Gravatar | | `gravatar_enabled` | boolean | no | Enable Gravatar |
| `help_page_hide_commercial_content` | boolean | no | Hide marketing-related entries from help | | `help_page_hide_commercial_content` | boolean | no | Hide marketing-related entries from help |
...@@ -236,6 +238,7 @@ Example response: ...@@ -236,6 +238,7 @@ Example response:
"enforce_terms": true, "enforce_terms": true,
"terms": "Hello world!", "terms": "Hello world!",
"performance_bar_allowed_group_id": 42, "performance_bar_allowed_group_id": 42,
"instance_statistics_visibility_private": false "instance_statistics_visibility_private": false,
"file_template_project_id": 1
} }
``` ```
# Instance-level Template Repository
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/5986) in
> [GitLab Premium](https://about.gitlab.com/pricing) 11.3.
## Overview
In hosted systems, enterprises often have a need to share their own templates
across teams. This feature allows an administrator to pick a project to be the
instance-wide collection of templates. These templates are then exposed to all
users while the project remains secure. Currently supported templates: Licenses.
## Configuration
An administrator can choose any project to be the template repository. This is
done through the `Settings` page in the `Admin Area` or through the API. On the
`Settings` page, there is a `Templates` section with a selection box for
choosing a project:
![](img/file_template_admin_area.png)
Once a project has been selected you can add custom templates to the repository,
and they will appear in the appropriate places in the frontend and API.
Templates must be added to a specific subdirectory in the repository,
corresponding to the kind of template. They must also have the correct extension
for the template type.
Currently, only custom license templates are supported. This must go in the
`LICENSE/` subdirectory, and must have `.txt` file extensions. So, the hierarchy
should look like this:
```text
|-- README.md
|-- LICENSE
|-- custom_license.txt
|-- another_license.txt
```
Once this is established, the list of `Custom` licenses will be included when
creating a new file and the file type is `License`. These will appear at the
bottom of the list:
![](img/file_template_user_dropdown.png)
If this feature has been disabled or no licenses are present, then there will be
no `Custom` section in the selection dropdown.
...@@ -24,6 +24,10 @@ module EE ...@@ -24,6 +24,10 @@ module EE
attrs << :email_additional_text attrs << :email_additional_text
end end
if License.feature_available?(:custom_file_templates)
attrs << :file_template_project_id
end
if License.feature_available?(:pseudonymizer) if License.feature_available?(:pseudonymizer)
attrs << :pseudonymizer_enabled attrs << :pseudonymizer_enabled
end end
......
module EE
module LicenseTemplateFinder
include ::Gitlab::Utils::StrongMemoize
extend ::Gitlab::Utils::Override
override :execute
def execute
return super unless custom_templates?
extra = custom_licenses.map do |template|
LicenseTemplate.new(
id: template.name,
name: template.name,
nickname: template.name,
category: :Custom,
content: -> { template.content }
)
end
super + extra
end
private
def custom_templates?
!popular_only? &&
::License.feature_available?(:custom_file_templates) &&
template_project.present?
end
def custom_licenses
::Gitlab::Template::LicenseTemplate.all(template_project)
end
def template_project
strong_memoize(:template_project) { ::Gitlab::CurrentSettings.file_template_project }
end
end
end
...@@ -97,7 +97,10 @@ module EE ...@@ -97,7 +97,10 @@ module EE
end end
def self.possible_licensed_attributes def self.possible_licensed_attributes
repository_mirror_attributes + external_authorization_service_attributes + [:email_additional_text] repository_mirror_attributes + external_authorization_service_attributes + %i[
email_additional_text
file_template_project_id
]
end end
end end
end end
...@@ -11,6 +11,8 @@ module EE ...@@ -11,6 +11,8 @@ module EE
EMAIL_ADDITIONAL_TEXT_CHARACTER_LIMIT = 10_000 EMAIL_ADDITIONAL_TEXT_CHARACTER_LIMIT = 10_000
belongs_to :file_template_project, class_name: "Project"
ignore_column :minimum_mirror_sync_time ignore_column :minimum_mirror_sync_time
validates :shared_runners_minutes, validates :shared_runners_minutes,
......
...@@ -40,6 +40,7 @@ class License < ActiveRecord::Base ...@@ -40,6 +40,7 @@ class License < ActiveRecord::Base
board_assignee_lists board_assignee_lists
board_milestone_lists board_milestone_lists
cross_project_pipelines cross_project_pipelines
custom_file_templates
email_additional_text email_additional_text
db_load_balancing db_load_balancing
deploy_board deploy_board
...@@ -150,6 +151,7 @@ class License < ActiveRecord::Base ...@@ -150,6 +151,7 @@ class License < ActiveRecord::Base
GLOBAL_FEATURES = %i[ GLOBAL_FEATURES = %i[
admin_audit_log admin_audit_log
auditor_user auditor_user
custom_file_templates
db_load_balancing db_load_balancing
elastic_search elastic_search
extended_audit_events extended_audit_events
......
- if License.feature_available?(:custom_file_templates)
%section.settings.as-visibility-access.no-animate#js-visibility-settings{ class: ('expanded' if expanded) }
.settings-header
%h4
= _('Templates')
%button.btn.js-settings-toggle{ type: 'button' }
= expanded ? _('Collapse') : _('Expand')
%p
= _('Set instance-wide template repository')
.settings-content
= form_for @application_setting, url: admin_application_settings_path, html: { class: 'fieldset-form' } do |f|
= form_errors(@application_setting)
%fieldset
.form-group
= f.label :file_template_project_id, class: 'label-light' do
.form-text.text-muted
Select a
= link_to 'template repository', help_page_path("user/admin_area/settings/instance_template_repository", anchor: "version-check")
= project_select_tag('application_setting[file_template_project_id]', class: 'project-item-select hidden-filter-value', toggle_class: 'js-project-search js-project-filter js-filter-submit', dropdown_class: 'dropdown-menu-selectable dropdown-menu-project js-filter-submit',
placeholder: admin_project_dropdown_label('Search projects'), idAttribute: 'id', data: { order_by: 'last_activity_at', idattribute: 'id', all_projects: 'true', simple_filter: true, allow_clear: true}, value: @application_setting.file_template_project_id)
= f.submit 'Save changes', class: "btn btn-success"
---
title: Added an instance-level license template project
merge_request: 6631
author: Dan Barker
type: added
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class AddProjectToApplicationSettings < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
add_column :application_settings, :file_template_project_id, :integer
add_concurrent_foreign_key :application_settings, :projects, column: :file_template_project_id, on_delete: :nullify
end
def down
remove_foreign_key :application_settings, column: :file_template_project_id
remove_column :application_settings, :file_template_project_id, :integer
end
end
...@@ -120,6 +120,7 @@ module EE ...@@ -120,6 +120,7 @@ module EE
::License.feature_available?(:external_authorization_service) ::License.feature_available?(:external_authorization_service)
end) end)
expose :email_additional_text, if: ->(_instance, _opts) { ::License.feature_available?(:email_additional_text) } expose :email_additional_text, if: ->(_instance, _opts) { ::License.feature_available?(:email_additional_text) }
expose :file_template_project_id, if: ->(_instance, _opts) { ::License.feature_available?(:custom_file_templates) }
end end
end end
......
module Gitlab
module Template
class LicenseTemplate < BaseTemplate
class << self
def extension
'.txt'
end
def base_dir
'LICENSE/'
end
def finder(project)
Gitlab::Template::Finders::RepoTemplateFinder.new(project, self.base_dir, self.extension, self.categories)
end
end
end
end
end
require 'spec_helper'
describe LicenseTemplateFinder do
describe '#execute' do
subject(:result) { described_class.new(params).execute }
let(:params) { {} }
let(:project) { create(:project) }
let(:custom) { result.select { |template| template.category == :Custom } }
before do
stub_ee_application_setting(file_template_project: project)
allow(Gitlab::Template::LicenseTemplate)
.to receive(:all)
.with(project)
.and_return([OpenStruct.new(name: "custom template")])
end
context 'custom file templates feature enabled' do
before do
stub_licensed_features(custom_file_templates: true)
end
it 'includes custom file templates' do
expect(custom.map(&:name)).to contain_exactly("custom template")
end
it 'skips custom file templates when only "popular" templates are requested' do
params[:popular] = true
expect(custom).to be_empty
end
end
context 'custom file templates feature disabled' do
it 'does not include custom file templates' do
stub_licensed_features(custom_file_templates: false)
expect(custom).to be_empty
end
end
end
end
require 'spec_helper'
describe BlobHelper do
include TreeHelper
describe '#licenses_for_select' do
subject(:result) { helper.licenses_for_select }
let(:categories) { result.keys }
let(:custom) { result[:Custom] }
let(:popular) { result[:Popular] }
let(:other) { result[:Other] }
let(:project) { create(:project) }
it 'returns Custom licenses when enabled' do
stub_licensed_features(custom_file_templates: true)
stub_ee_application_setting(file_template_project: project)
expect(Gitlab::Template::LicenseTemplate)
.to receive(:all)
.with(project)
.and_return([OpenStruct.new(name: "name")])
expect(categories).to contain_exactly(:Popular, :Other, :Custom)
expect(custom).to contain_exactly({ name: "name", id: "name" })
expect(popular).to be_present
expect(other).to be_present
end
it 'returns no Custom licenses when disabled' do
stub_licensed_features(custom_file_templates: false)
expect(categories).to contain_exactly(:Popular, :Other)
expect(custom).to be_nil
expect(popular).to be_present
expect(other).to be_present
end
end
end
...@@ -5,6 +5,7 @@ describe API::Settings, 'EE Settings' do ...@@ -5,6 +5,7 @@ describe API::Settings, 'EE Settings' do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:admin) { create(:admin) } let(:admin) { create(:admin) }
let(:project) { create(:project) }
before do before do
stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false') stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
...@@ -12,12 +13,15 @@ describe API::Settings, 'EE Settings' do ...@@ -12,12 +13,15 @@ describe API::Settings, 'EE Settings' do
describe "PUT /application/settings" do describe "PUT /application/settings" do
it 'sets EE specific settings' do it 'sets EE specific settings' do
stub_licensed_features(custom_file_templates: true)
put api("/application/settings", admin), put api("/application/settings", admin),
help_text: 'Help text', help_text: 'Help text',
snowplow_collector_uri: 'https://snowplow.example.com', snowplow_collector_uri: 'https://snowplow.example.com',
snowplow_cookie_domain: '.example.com', snowplow_cookie_domain: '.example.com',
snowplow_enabled: true, snowplow_enabled: true,
snowplow_site_id: 'site_id' snowplow_site_id: 'site_id',
file_template_project_id: project.id
expect(response).to have_gitlab_http_status(200) expect(response).to have_gitlab_http_status(200)
expect(json_response['help_text']).to eq('Help text') expect(json_response['help_text']).to eq('Help text')
...@@ -25,6 +29,7 @@ describe API::Settings, 'EE Settings' do ...@@ -25,6 +29,7 @@ describe API::Settings, 'EE Settings' do
expect(json_response['snowplow_cookie_domain']).to eq('.example.com') expect(json_response['snowplow_cookie_domain']).to eq('.example.com')
expect(json_response['snowplow_enabled']).to be_truthy expect(json_response['snowplow_enabled']).to be_truthy
expect(json_response['snowplow_site_id']).to eq('site_id') expect(json_response['snowplow_site_id']).to eq('site_id')
expect(json_response['file_template_project_id']).to eq(project.id)
end end
end end
...@@ -72,6 +77,7 @@ describe API::Settings, 'EE Settings' do ...@@ -72,6 +77,7 @@ describe API::Settings, 'EE Settings' do
it 'allows updating the settings' do it 'allows updating the settings' do
put api("/application/settings", admin), settings put api("/application/settings", admin), settings
expect(response).to have_gitlab_http_status(200)
settings.each do |attribute, value| settings.each do |attribute, value|
expect(ApplicationSetting.current.public_send(attribute)).to eq(value) expect(ApplicationSetting.current.public_send(attribute)).to eq(value)
...@@ -111,6 +117,13 @@ describe API::Settings, 'EE Settings' do ...@@ -111,6 +117,13 @@ describe API::Settings, 'EE Settings' do
it_behaves_like 'settings for licensed features' it_behaves_like 'settings for licensed features'
end end
context 'custom file template project' do
let(:settings) { { file_template_project_id: project.id } }
let(:feature) { :custom_file_templates }
it_behaves_like 'settings for licensed features'
end
context "missing snowplow_collector_uri value when snowplow_enabled is true" do context "missing snowplow_collector_uri value when snowplow_enabled is true" do
it "returns a blank parameter error message" do it "returns a blank parameter error message" do
put api("/application/settings", admin), snowplow_enabled: true put api("/application/settings", admin), snowplow_enabled: true
......
...@@ -1187,7 +1187,7 @@ module API ...@@ -1187,7 +1187,7 @@ module API
class License < Grape::Entity class License < Grape::Entity
expose :key, :name, :nickname expose :key, :name, :nickname
expose :featured, as: :popular expose :popular?, as: :popular
expose :url, as: :html_url expose :url, as: :html_url
expose(:source_url) { |license| license.meta['source'] } expose(:source_url) { |license| license.meta['source'] }
expose(:description) { |license| license.meta['description'] } expose(:description) { |license| license.meta['description'] }
......
...@@ -151,6 +151,7 @@ module API ...@@ -151,6 +151,7 @@ module API
optional :email_additional_text, type: String, desc: 'Additional text added to the bottom of every email for legal/auditing/compliance reasons' optional :email_additional_text, type: String, desc: 'Additional text added to the bottom of every email for legal/auditing/compliance reasons'
optional :help_text, type: String, desc: 'GitLab server administrator information' optional :help_text, type: String, desc: 'GitLab server administrator information'
optional :repository_size_limit, type: Integer, desc: 'Size limit per repository (MB)' optional :repository_size_limit, type: Integer, desc: 'Size limit per repository (MB)'
optional :file_template_project_id, type: Integer, desc: 'ID of project where instance-level file templates are stored.'
optional :repository_storages, type: Array[String], desc: 'A list of names of enabled storage paths, taken from `gitlab.yml`. New projects will be created in one of these stores, chosen at random.' optional :repository_storages, type: Array[String], desc: 'A list of names of enabled storage paths, taken from `gitlab.yml`. New projects will be created in one of these stores, chosen at random.'
optional :snowplow_enabled, type: Boolean, desc: 'Enable Snowplow' optional :snowplow_enabled, type: Boolean, desc: 'Enable Snowplow'
given snowplow_enabled: ->(val) { val } do given snowplow_enabled: ->(val) { val } do
...@@ -203,6 +204,10 @@ module API ...@@ -203,6 +204,10 @@ module API
unless ::License.feature_available?(:email_additional_text) unless ::License.feature_available?(:email_additional_text)
attrs = attrs.except(:email_additional_text) attrs = attrs.except(:email_additional_text)
end end
unless ::License.feature_available?(:custom_file_templates)
attrs = attrs.except(:file_template_project_id)
end
## EE-only END: Remove unlicensed attributes ## EE-only END: Remove unlicensed attributes
if ApplicationSettings::UpdateService.new(current_settings, current_user, attrs).execute if ApplicationSettings::UpdateService.new(current_settings, current_user, attrs).execute
......
...@@ -16,31 +16,8 @@ module API ...@@ -16,31 +16,8 @@ module API
gitlab_version: 8.15 gitlab_version: 8.15
} }
}.freeze }.freeze
PROJECT_TEMPLATE_REGEX =
%r{[\<\{\[]
(project|description|
one\sline\s.+\swhat\sit\sdoes\.) # matching the start and end is enough here
[\>\}\]]}xi.freeze
YEAR_TEMPLATE_REGEX = /[<{\[](year|yyyy)[>}\]]/i.freeze
FULLNAME_TEMPLATE_REGEX =
%r{[\<\{\[]
(fullname|name\sof\s(author|copyright\sowner))
[\>\}\]]}xi.freeze
helpers do helpers do
def parsed_license_template
# We create a fresh Licensee::License object since we'll modify its
# content in place below.
template = Licensee::License.new(params[:name])
template.content.gsub!(YEAR_TEMPLATE_REGEX, Time.now.year.to_s)
template.content.gsub!(PROJECT_TEMPLATE_REGEX, params[:project]) if params[:project].present?
fullname = params[:fullname].presence || current_user.try(:name)
template.content.gsub!(FULLNAME_TEMPLATE_REGEX, fullname) if fullname
template
end
def render_response(template_type, template) def render_response(template_type, template)
not_found!(template_type.to_s.singularize) unless template not_found!(template_type.to_s.singularize) unless template
present template, with: Entities::Template present template, with: Entities::Template
...@@ -56,11 +33,12 @@ module API ...@@ -56,11 +33,12 @@ module API
use :pagination use :pagination
end end
get "templates/licenses" do get "templates/licenses" do
options = { popular = declared(params)[:popular]
featured: declared(params)[:popular].present? ? true : nil popular = to_boolean(popular) if popular.present?
}
licences = ::Kaminari.paginate_array(Licensee::License.all(options)) templates = LicenseTemplateFinder.new(popular: popular).execute
present paginate(licences), with: Entities::License
present paginate(::Kaminari.paginate_array(templates)), with: ::API::Entities::License
end end
desc 'Get the text for a specific license' do desc 'Get the text for a specific license' do
...@@ -71,9 +49,15 @@ module API ...@@ -71,9 +49,15 @@ module API
requires :name, type: String, desc: 'The name of the template' requires :name, type: String, desc: 'The name of the template'
end end
get "templates/licenses/:name", requirements: { name: /[\w\.-]+/ } do get "templates/licenses/:name", requirements: { name: /[\w\.-]+/ } do
not_found!('License') unless Licensee::License.find(declared(params)[:name]) templates = LicenseTemplateFinder.new.execute
template = templates.find { |template| template.key == params[:name] }
not_found!('License') unless template.present?
template = parsed_license_template template.resolve!(
project_name: params[:project].presence,
fullname: params[:fullname].presence || current_user&.name
)
present template, with: ::API::Entities::License present template, with: ::API::Entities::License
end end
......
...@@ -21,7 +21,7 @@ module Gitlab ...@@ -21,7 +21,7 @@ module Gitlab
def category_directory(category) def category_directory(category)
return @base_dir unless category.present? return @base_dir unless category.present?
@base_dir + @categories[category] File.join(@base_dir, @categories[category])
end end
class << self class << self
......
...@@ -27,7 +27,7 @@ module Gitlab ...@@ -27,7 +27,7 @@ module Gitlab
directory = select_directory(file_name) directory = select_directory(file_name)
raise FileNotFoundError if directory.nil? raise FileNotFoundError if directory.nil?
category_directory(directory) + file_name File.join(category_directory(directory), file_name)
end end
def list_files_for(dir) def list_files_for(dir)
...@@ -37,8 +37,8 @@ module Gitlab ...@@ -37,8 +37,8 @@ module Gitlab
entries = @repository.tree(:head, dir).entries entries = @repository.tree(:head, dir).entries
names = entries.map(&:name) paths = entries.map(&:path)
names.select { |f| f =~ self.class.filter_regex(@extension) } paths.select { |f| f =~ self.class.filter_regex(@extension) }
end end
private private
...@@ -47,10 +47,10 @@ module Gitlab ...@@ -47,10 +47,10 @@ module Gitlab
return [] unless @commit return [] unless @commit
# Insert root as directory # Insert root as directory
directories = ["", @categories.keys] directories = ["", *@categories.keys]
directories.find do |category| directories.find do |category|
path = category_directory(category) + file_name path = File.join(category_directory(category), file_name)
@repository.blob_at(@commit.id, path) @repository.blob_at(@commit.id, path)
end end
end end
......
...@@ -6325,6 +6325,9 @@ msgstr "" ...@@ -6325,6 +6325,9 @@ msgstr ""
msgid "Set default and restrict visibility levels. Configure import sources and git access protocol." msgid "Set default and restrict visibility levels. Configure import sources and git access protocol."
msgstr "" msgstr ""
msgid "Set instance-wide template repository"
msgstr ""
msgid "Set max session time for web terminal." msgid "Set max session time for web terminal."
msgstr "" msgstr ""
...@@ -6818,6 +6821,9 @@ msgstr "" ...@@ -6818,6 +6821,9 @@ msgstr ""
msgid "Template" msgid "Template"
msgstr "" msgstr ""
msgid "Templates"
msgstr ""
msgid "Terms of Service Agreement and Privacy Policy" msgid "Terms of Service Agreement and Privacy Policy"
msgstr "" msgstr ""
......
require 'spec_helper'
describe LicenseTemplateFinder do
describe '#execute' do
subject(:result) { described_class.new(params).execute }
let(:categories) { categorised_licenses.keys }
let(:categorised_licenses) { result.group_by(&:category) }
context 'popular: true' do
let(:params) { { popular: true } }
it 'only returns popular licenses' do
expect(categories).to contain_exactly(:Popular)
expect(categorised_licenses[:Popular]).to be_present
end
end
context 'popular: false' do
let(:params) { { popular: false } }
it 'only returns unpopular licenses' do
expect(categories).to contain_exactly(:Other)
expect(categorised_licenses[:Other]).to be_present
end
end
context 'popular: nil' do
let(:params) { { popular: nil } }
it 'returns all licenses known by the Licensee gem' do
from_licensee = Licensee::License.all.map { |l| l.key }
expect(result.map(&:id)).to match_array(from_licensee)
end
it 'correctly copies all attributes' do
licensee = Licensee::License.all.first
found = result.find { |r| r.key == licensee.key }
aggregate_failures do
%i[key name content nickname url meta featured?].each do |k|
expect(found.public_send(k)).to eq(licensee.public_send(k))
end
end
end
end
end
end
require 'spec_helper'
describe Gitlab::Template::Finders::RepoTemplateFinder do
set(:project) { create(:project, :repository) }
let(:categories) { { 'HTML' => 'html' } }
subject(:finder) { described_class.new(project, 'files/', '.html', categories) }
describe '#read' do
it 'returns the content of the given path' do
result = finder.read('files/html/500.html')
expect(result).to be_present
end
it 'raises an error if the path does not exist' do
expect { finder.read('does/not/exist') }.to raise_error(described_class::FileNotFoundError)
end
end
describe '#find' do
it 'returns the full path of the found template' do
result = finder.find('500')
expect(result).to eq('files/html/500.html')
end
end
describe '#list_files_for' do
it 'returns the full path of the found files' do
result = finder.list_files_for('files/html')
expect(result).to contain_exactly('files/html/500.html')
end
end
end
require 'spec_helper'
describe LicenseTemplate do
describe '#content' do
it 'calls a proc exactly once if provided' do
lazy = build_template(-> { 'bar' })
content = lazy.content
expect(content).to eq('bar')
expect(content.object_id).to eq(lazy.content.object_id)
content.replace('foo')
expect(lazy.content).to eq('foo')
end
it 'returns a string if provided' do
lazy = build_template('bar')
expect(lazy.content).to eq('bar')
end
end
describe '#resolve!' do
let(:content) do
<<~TEXT
Pretend License
[project]
Copyright (c) [year] [fullname]
TEXT
end
let(:expected) do
<<~TEXT
Pretend License
Foo Project
Copyright (c) 1985 Nick Thomas
TEXT
end
let(:template) { build_template(content) }
it 'updates placeholders in a copy of the template content' do
expect(template.content.object_id).to eq(content.object_id)
template.resolve!(project_name: "Foo Project", fullname: "Nick Thomas", year: "1985")
expect(template.content).to eq(expected)
expect(template.content.object_id).not_to eq(content.object_id)
end
end
def build_template(content)
described_class.new(id: 'foo', name: 'foo', category: :Other, content: content)
end
end
...@@ -56,6 +56,8 @@ describe API::Templates do ...@@ -56,6 +56,8 @@ describe API::Templates do
end end
it 'returns a license template' do it 'returns a license template' do
expect(response).to have_gitlab_http_status(200)
expect(json_response['key']).to eq('mit') expect(json_response['key']).to eq('mit')
expect(json_response['name']).to eq('MIT License') expect(json_response['name']).to eq('MIT License')
expect(json_response['nickname']).to be_nil expect(json_response['nickname']).to be_nil
...@@ -181,6 +183,7 @@ describe API::Templates do ...@@ -181,6 +183,7 @@ describe API::Templates do
it 'replaces the copyright owner placeholder with the name of the current user' do it 'replaces the copyright owner placeholder with the name of the current user' do
get api('/templates/licenses/mit', user) get api('/templates/licenses/mit', user)
expect(response).to have_gitlab_http_status(200)
expect(json_response['content']).to include("Copyright (c) #{Time.now.year} #{user.name}") expect(json_response['content']).to include("Copyright (c) #{Time.now.year} #{user.name}")
end end
end end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment