Merge branch 'backport-5986-license-templates' into 'master'

Core backports from the Premium license templates feature

See merge request gitlab-org/gitlab-ce!21212
This commit is contained in:
Robert Speicher 2018-08-15 16:47:34 +00:00
commit 085ed2862c
14 changed files with 277 additions and 42 deletions

View file

@ -1,6 +1,8 @@
import initSettingsPanels from '~/settings_panels';
import projectSelect from '~/project_select';
document.addEventListener('DOMContentLoaded', () => {
// Initialize expandable settings panels
initSettingsPanels();
projectSelect();
});

View file

@ -14,6 +14,7 @@ export default function projectSelect() {
this.orderBy = $(select).data('orderBy') || 'id';
this.withIssuesEnabled = $(select).data('withIssuesEnabled');
this.withMergeRequestsEnabled = $(select).data('withMergeRequestsEnabled');
this.allowClear = $(select).data('allowClear') || false;
placeholder = "Search for project";
if (this.includeGroups) {
@ -71,6 +72,13 @@ export default function projectSelect() {
text: function (project) {
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"
});
if (simpleFilter) return select;

View file

@ -0,0 +1,36 @@
# 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
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

View file

@ -182,12 +182,14 @@ module BlobHelper
def 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 = {
Popular: licenses.select(&:featured).map { |license| { name: license.name, id: license.key } },
Other: licenses.reject(&:featured).map { |license| { name: license.name, id: license.key } }
}
@licenses_for_select = categories.each_with_object({}) do |category, hash|
hash[category] = grouped_licenses[category].map do |license|
{ name: license.name, id: license.id }
end
end
end
def ref_project

View file

@ -0,0 +1,53 @@
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

View file

@ -325,6 +325,8 @@
.settings-content
= render partial: 'repository_mirrors_form'
= 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) }
.settings-header
%h4

View file

@ -1159,7 +1159,7 @@ module API
class License < Grape::Entity
expose :key, :name, :nickname
expose :featured, as: :popular
expose :popular?, as: :popular
expose :url, as: :html_url
expose(:source_url) { |license| license.meta['source'] }
expose(:description) { |license| license.meta['description'] }

View file

@ -16,31 +16,8 @@ module API
gitlab_version: 8.15
}
}.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
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)
not_found!(template_type.to_s.singularize) unless template
present template, with: Entities::Template
@ -56,11 +33,12 @@ module API
use :pagination
end
get "templates/licenses" do
options = {
featured: declared(params)[:popular].present? ? true : nil
}
licences = ::Kaminari.paginate_array(Licensee::License.all(options))
present paginate(licences), with: Entities::License
popular = declared(params)[:popular]
popular = to_boolean(popular) if popular.present?
templates = LicenseTemplateFinder.new(popular: popular).execute
present paginate(::Kaminari.paginate_array(templates)), with: ::API::Entities::License
end
desc 'Get the text for a specific license' do
@ -71,9 +49,15 @@ module API
requires :name, type: String, desc: 'The name of the template'
end
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] }
template = parsed_license_template
not_found!('License') unless template.present?
template.resolve!(
project_name: params[:project].presence,
fullname: params[:fullname].presence || current_user&.name
)
present template, with: ::API::Entities::License
end

View file

@ -21,7 +21,7 @@ module Gitlab
def category_directory(category)
return @base_dir unless category.present?
@base_dir + @categories[category]
File.join(@base_dir, @categories[category])
end
class << self

View file

@ -27,7 +27,7 @@ module Gitlab
directory = select_directory(file_name)
raise FileNotFoundError if directory.nil?
category_directory(directory) + file_name
File.join(category_directory(directory), file_name)
end
def list_files_for(dir)
@ -37,8 +37,8 @@ module Gitlab
entries = @repository.tree(:head, dir).entries
names = entries.map(&:name)
names.select { |f| f =~ self.class.filter_regex(@extension) }
paths = entries.map(&:path)
paths.select { |f| f =~ self.class.filter_regex(@extension) }
end
private
@ -47,10 +47,10 @@ module Gitlab
return [] unless @commit
# Insert root as directory
directories = ["", @categories.keys]
directories = ["", *@categories.keys]
directories.find do |category|
path = category_directory(category) + file_name
path = File.join(category_directory(category), file_name)
@repository.blob_at(@commit.id, path)
end
end

View file

@ -0,0 +1,49 @@
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

View file

@ -0,0 +1,37 @@
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

View file

@ -0,0 +1,59 @@
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

View file

@ -56,6 +56,8 @@ describe API::Templates do
end
it 'returns a license template' do
expect(response).to have_gitlab_http_status(200)
expect(json_response['key']).to eq('mit')
expect(json_response['name']).to eq('MIT License')
expect(json_response['nickname']).to be_nil
@ -181,6 +183,7 @@ describe API::Templates do
it 'replaces the copyright owner placeholder with the name of the current user' do
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}")
end
end