Merge branch 'master' into 369-bootstrap
Conflicts: app/models/build_list.rb app/models/build_list/package.rb app/models/build_script.rb app/models/product_build_list.rb app/models/project_tag.rb app/views/projects/build_lists/_filter.html.haml
This commit is contained in:
commit
ab0a57d6b2
|
@ -194,15 +194,15 @@ RosaABF.controller('BuildListsController',
|
|||
page: params.page || 1,
|
||||
per_page: params.per_page || 25,
|
||||
filter: {
|
||||
ownership: params['filter[ownership]'] || 'owned',
|
||||
status: params['filter[status]'],
|
||||
platform_id: params['filter[platform_id]'],
|
||||
arch_id: params['filter[arch_id]'],
|
||||
mass_build_id: params['filter[mass_build_id]'],
|
||||
updated_at_start: params['filter[updated_at_start]'],
|
||||
updated_at_end: params['filter[updated_at_end]'],
|
||||
project_name: params['filter[project_name]'],
|
||||
id: params['filter[id]']
|
||||
ownership: params['filter[ownership]'] || 'owned',
|
||||
status: params['filter[status]'],
|
||||
save_to_platform_id: params['filter[save_to_platform_id]'],
|
||||
arch_id: params['filter[arch_id]'],
|
||||
mass_build_id: params['filter[mass_build_id]'],
|
||||
updated_at_start: params['filter[updated_at_start]'],
|
||||
updated_at_end: params['filter[updated_at_end]'],
|
||||
project_name: params['filter[project_name]'],
|
||||
id: params['filter[id]']
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,6 @@ class Api::V1::BuildListsController < Api::V1::BaseController
|
|||
before_filter :authenticate_user!
|
||||
skip_before_filter :authenticate_user!, only: [:show, :index] if APP_CONFIG['anonymous_access']
|
||||
|
||||
load_resource :project, only: :index, parent: false
|
||||
load_and_authorize_resource :build_list, only: [:show, :create, :cancel, :publish, :reject_publish, :create_container, :publish_into_testing, :rerun_tests]
|
||||
|
||||
def show
|
||||
|
@ -10,9 +9,16 @@ class Api::V1::BuildListsController < Api::V1::BaseController
|
|||
end
|
||||
|
||||
def index
|
||||
@project = Project.find(params[:project_id]) if params[:project_id].present?
|
||||
authorize!(:show, @project) if @project
|
||||
filter = BuildList::Filter.new(@project, current_user, current_ability, params[:filter] || {})
|
||||
@build_lists = filter.find.includes(:save_to_platform, :project, :user, :arch)
|
||||
@build_lists = filter.find.includes(:build_for_platform,
|
||||
:save_to_repository,
|
||||
:save_to_platform,
|
||||
:project,
|
||||
:user,
|
||||
:arch)
|
||||
|
||||
@build_lists = @build_lists.recent.paginate(paginate_params)
|
||||
respond_to :json
|
||||
end
|
||||
|
|
|
@ -22,7 +22,7 @@ class Api::V1::JobsController < Api::V1::BaseController
|
|||
else
|
||||
@build_list = build_lists.external_nodes(:owned).for_user(current_user).first
|
||||
@build_list ||= build_lists.external_nodes(:everything).
|
||||
accessible_by(current_ability, :everything).readonly(false).first
|
||||
accessible_by(current_ability, :related).readonly(false).first
|
||||
end
|
||||
set_builder
|
||||
end
|
||||
|
|
|
@ -23,6 +23,13 @@ module AbfWorker
|
|||
item = find_or_create_item
|
||||
fill_container_data if status != STARTED
|
||||
|
||||
unless subject.valid?
|
||||
item.update_attributes({status: BuildList::BUILD_ERROR})
|
||||
subject.build_error(false)
|
||||
subject.save(validate: false)
|
||||
return
|
||||
end
|
||||
|
||||
rerunning_tests = subject.rerunning_tests?
|
||||
|
||||
case status
|
||||
|
|
|
@ -40,14 +40,15 @@ class BuildList < ActiveRecord::Base
|
|||
AUTO_PUBLISH_STATUS_TESTING = 'testing'
|
||||
]
|
||||
|
||||
validates :project,
|
||||
validates :project, :project_id,
|
||||
:project_version,
|
||||
:arch,
|
||||
:arch, :arch_id,
|
||||
:include_repos,
|
||||
:build_for_platform,
|
||||
:save_to_platform,
|
||||
:save_to_repository,
|
||||
:build_for_platform, :build_for_platform_id,
|
||||
:save_to_platform, :save_to_platform_id,
|
||||
:save_to_repository, :save_to_repository_id,
|
||||
presence: true
|
||||
|
||||
validates_numericality_of :priority, greater_than_or_equal_to: 0
|
||||
validates :external_nodes, inclusion: { in: EXTERNAL_NODES }, allow_blank: true
|
||||
validates :auto_publish_status, inclusion: { in: AUTO_PUBLISH_STATUSES }
|
||||
|
@ -133,10 +134,12 @@ class BuildList < ActiveRecord::Base
|
|||
scope :for_platform, ->(platform) { where(build_for_platform_id: platform) if platform.present? }
|
||||
scope :by_mass_build, ->(mass_build) { where(mass_build_id: mass_build) }
|
||||
scope :scoped_to_arch, ->(arch) { where(arch_id: arch) if arch.present? }
|
||||
scope :scoped_to_save_platform, ->(pl_id) { where(save_to_platform_id: pl_id) if pl_id.present? }
|
||||
scope :scoped_to_project_version, ->(pr_version) { where(project_version: pr_version) if pr_version.present? }
|
||||
scope :scoped_to_is_circle, ->(is_circle) { where(is_circle: is_circle) }
|
||||
scope :for_creation_date_period, ->(start_date, end_date) {
|
||||
scope :scoped_to_save_platform, ->(pl_id) { where(save_to_platform_id: pl_id) if pl_id.present? }
|
||||
scope :scoped_to_build_for_platform, ->(pl_id) { where(build_for_platform_id: pl_id) if pl_id.present? }
|
||||
scope :scoped_to_save_to_repository, ->(repo_id) { where(save_to_repository_id: repo_id) if repo_id.present? }
|
||||
scope :scoped_to_project_version, ->(pr_version) { where(project_version: pr_version) if pr_version.present? }
|
||||
scope :scoped_to_is_circle, ->(is_circle) { where(is_circle: is_circle) }
|
||||
scope :for_creation_date_period, ->(start_date, end_date) {
|
||||
s = all
|
||||
s = s.where(["#{table_name}.created_at >= ?", start_date]) if start_date
|
||||
s = s.where(["#{table_name}.created_at <= ?", end_date]) if end_date
|
||||
|
@ -598,35 +601,41 @@ class BuildList < ActiveRecord::Base
|
|||
end
|
||||
|
||||
def self.next_build(arch_ids, platform_ids)
|
||||
kind_id = Redis.current.spop(USER_BUILDS_SET)
|
||||
key = "user_build_#{kind_id}_rpm_worker_default" if kind_id
|
||||
build_list = next_build_from_queue(kind_id, key, arch_ids, platform_ids) if key
|
||||
Redis.current.sadd(USER_BUILDS_SET, kind_id) if build_list
|
||||
build_list = next_build_from_queue(USER_BUILDS_SET, arch_ids, platform_ids)
|
||||
build_list ||= next_build_from_queue(MASS_BUILDS_SET, arch_ids, platform_ids)
|
||||
|
||||
kind_id ||= Redis.current.spop(MASS_BUILDS_SET)
|
||||
key ||= "mass_build_#{kind_id}_rpm_worker" if kind_id
|
||||
build_list ||= next_build_from_queue(kind_id, key, arch_ids, platform_ids, true) if key
|
||||
Redis.current.sadd(MASS_BUILDS_SET, kind_id) if build_list && key =~ /^mass_build/
|
||||
|
||||
build_list.delayed_add_job_to_abf_worker_queue if build_list.present?
|
||||
build_list.delayed_add_job_to_abf_worker_queue if build_list
|
||||
build_list
|
||||
end
|
||||
|
||||
def self.next_build_from_queue(kind_id, key, arch_ids, platform_ids, mass_build = false)
|
||||
if kind_id && (arch_ids.present? || platform_ids.present?)
|
||||
build_list = BuildList.where(user_id: kind_id).
|
||||
scoped_to_arch(arch_ids).
|
||||
for_status([BuildList::BUILD_PENDING, BuildList::RERUN_TESTS]).
|
||||
for_platform(platform_ids)
|
||||
build_list = build_list.where.not(mass_build_id: nil) if mass_build
|
||||
build_list = build_list.oldest.order(:created_at).first
|
||||
def self.next_build_from_queue(set, arch_ids, platform_ids)
|
||||
kind_id = Redis.current.spop(set)
|
||||
key =
|
||||
case set
|
||||
when USER_BUILDS_SET
|
||||
"user_build_#{kind_id}_rpm_worker_default"
|
||||
when MASS_BUILDS_SET
|
||||
"mass_build_#{kind_id}_rpm_worker"
|
||||
end if kind_id
|
||||
|
||||
build_list = nil if build_list && build_list.destroy_from_resque_queue != 1
|
||||
elsif key
|
||||
task = Resque.pop(key)
|
||||
task = Resque.pop(key) if key
|
||||
|
||||
if task || Redis.current.llen("resque:queue:#{key}") > 0
|
||||
Redis.current.sadd(set, kind_id)
|
||||
end
|
||||
|
||||
build_list = BuildList.where(id: task['args'][0]['id']).first if task
|
||||
return unless build_list
|
||||
|
||||
if platform_ids.present? && platform_ids.exclude?(build_list.build_for_platform_id)
|
||||
build_list.restart_job
|
||||
return
|
||||
end
|
||||
if arch_ids.present? && arch_ids.exclude?(build_list.arch_id)
|
||||
build_list.restart_job
|
||||
return
|
||||
end
|
||||
|
||||
build_list ||= BuildList.where(id: task['args'][0]['id']).first if task
|
||||
build_list
|
||||
end
|
||||
|
||||
|
|
|
@ -22,7 +22,9 @@ class BuildList::Filter
|
|||
|
||||
build_lists = build_lists.for_status(@options[:status])
|
||||
.scoped_to_arch(@options[:arch_id])
|
||||
.scoped_to_save_platform(@options[:platform_id])
|
||||
.scoped_to_save_platform(@options[:save_to_platform_id])
|
||||
.scoped_to_build_for_platform(@options[:build_for_platform_id])
|
||||
.scoped_to_save_to_repository(@options[:save_to_repository_id])
|
||||
.scoped_to_project_version(@options[:project_version])
|
||||
.scoped_to_project_name(@options[:project_name])
|
||||
.for_notified_date_period(@options[:updated_at_start], @options[:updated_at_end])
|
||||
|
@ -44,34 +46,38 @@ class BuildList::Filter
|
|||
|
||||
def set_options(options)
|
||||
@options = HashWithIndifferentAccess.new(options.reverse_merge({
|
||||
ownership: nil,
|
||||
status: nil,
|
||||
updated_at_start: nil,
|
||||
updated_at_end: nil,
|
||||
arch_id: nil,
|
||||
platform_id: nil,
|
||||
is_circle: nil,
|
||||
project_version: nil,
|
||||
id: nil,
|
||||
project_name: nil,
|
||||
mass_build_id: nil,
|
||||
new_core: nil
|
||||
ownership: nil,
|
||||
status: nil,
|
||||
updated_at_start: nil,
|
||||
updated_at_end: nil,
|
||||
arch_id: nil,
|
||||
save_to_platform_id: nil,
|
||||
build_for_platform_id: nil,
|
||||
save_to_repository_id: nil,
|
||||
is_circle: nil,
|
||||
project_version: nil,
|
||||
id: nil,
|
||||
project_name: nil,
|
||||
mass_build_id: nil,
|
||||
new_core: nil
|
||||
}))
|
||||
|
||||
@options[:ownership] = @options[:ownership].presence || (@project || !@user ? 'everything' : 'owned')
|
||||
@options[:status] = @options[:status].present? ? @options[:status].to_i : nil
|
||||
@options[:created_at_start] = build_date_from_params(:created_at_start, @options)
|
||||
@options[:created_at_end] = build_date_from_params(:created_at_end, @options)
|
||||
@options[:updated_at_start] = build_date_from_params(:updated_at_start, @options)
|
||||
@options[:updated_at_end] = build_date_from_params(:updated_at_end, @options)
|
||||
@options[:project_version] = @options[:project_version].presence
|
||||
@options[:arch_id] = @options[:arch_id].try(:to_i)
|
||||
@options[:platform_id] = @options[:platform_id].try(:to_i)
|
||||
@options[:is_circle] = @options[:is_circle].present? ? @options[:is_circle] == "1" : nil
|
||||
@options[:id] = @options[:id].presence
|
||||
@options[:project_name] = @options[:project_name].presence
|
||||
@options[:mass_build_id] = @options[:mass_build_id].presence
|
||||
@options[:new_core] = @options[:new_core].presence
|
||||
@options[:status] = @options[:status].present? ? @options[:status].to_i : nil
|
||||
@options[:created_at_start] = build_date_from_params(:created_at_start, @options)
|
||||
@options[:created_at_end] = build_date_from_params(:created_at_end, @options)
|
||||
@options[:updated_at_start] = build_date_from_params(:updated_at_start, @options)
|
||||
@options[:updated_at_end] = build_date_from_params(:updated_at_end, @options)
|
||||
@options[:project_version] = @options[:project_version].presence
|
||||
@options[:arch_id] = @options[:arch_id].try(:to_i)
|
||||
@options[:save_to_platform_id] = @options[:save_to_platform_id].try(:to_i)
|
||||
@options[:build_for_platform_id] = @options[:build_for_platform_id].try(:to_i)
|
||||
@options[:save_to_repository_id] = @options[:save_to_repository_id].try(:to_i)
|
||||
@options[:is_circle] = @options[:is_circle].present? ? @options[:is_circle] == "1" : nil
|
||||
@options[:id] = @options[:id].presence
|
||||
@options[:project_name] = @options[:project_name].presence
|
||||
@options[:mass_build_id] = @options[:mass_build_id].presence
|
||||
@options[:new_core] = @options[:new_core].presence
|
||||
end
|
||||
|
||||
def build_date_from_params(field_name, params)
|
||||
|
|
|
@ -9,7 +9,8 @@ class BuildList::Package < ActiveRecord::Base
|
|||
|
||||
attr_accessible :fullname, :name, :release, :version, :sha1, :epoch, :dependent_packages
|
||||
|
||||
validates :build_list, :project, :platform, :fullname,
|
||||
validates :build_list, :build_list_id, :project, :project_id,
|
||||
:platform, :platform_id, :fullname,
|
||||
:package_type, :name, :release, :version,
|
||||
presence: true
|
||||
validates :package_type, inclusion: PACKAGE_TYPES
|
||||
|
|
|
@ -10,7 +10,8 @@ class BuildScript < ActiveRecord::Base
|
|||
belongs_to :project
|
||||
|
||||
validates :treeish, presence: true
|
||||
validates :project, presence: true, uniqueness: { scope: :treeish }
|
||||
validates :project, presence: true
|
||||
validates :project_id, uniqueness: { scope: :treeish }
|
||||
|
||||
scope :by_active, -> { where(status: ACTIVE) }
|
||||
scope :by_treeish, -> treeish { where(treeish: treeish) }
|
||||
|
|
|
@ -94,7 +94,8 @@ class MassBuild < ActiveRecord::Base
|
|||
def build_all
|
||||
return unless start
|
||||
# later with resque
|
||||
arches_list = arch_names ? Arch.where(name: arch_names.split(', ')) : Arch.all
|
||||
arches_list = arch_names ? Arch.where(name: arch_names.split(', ')) : Arch.all
|
||||
current_ability = Ability.new(user)
|
||||
|
||||
projects_list.lines.each do |name|
|
||||
next if name.blank?
|
||||
|
@ -103,6 +104,8 @@ class MassBuild < ActiveRecord::Base
|
|||
if project = Project.joins(:repositories).where('repositories.id in (?)', save_to_platform.repository_ids).find_by(name: name)
|
||||
begin
|
||||
return if self.reload.stop_build
|
||||
# Ensures that user has rights to create a build_list
|
||||
next unless current_ability.can?(:write, project)
|
||||
increase_rt = increase_release_tag?
|
||||
arches_list.each do |arch|
|
||||
rep_id = (project.repository_ids & save_to_platform.repository_ids).first
|
||||
|
|
|
@ -45,12 +45,13 @@ class ProductBuildList < ActiveRecord::Base
|
|||
before_validation -> { self.arch_id = Arch.find_by(name: 'x86_64').id }, on: :create
|
||||
# field "not_delete" can be changed only if build has been completed
|
||||
before_validation -> { self.not_delete = false unless build_completed?; true }
|
||||
validates :product,
|
||||
|
||||
validates :product, :product_id,
|
||||
:status,
|
||||
:project,
|
||||
:project, :project_id,
|
||||
:main_script,
|
||||
:project_version,
|
||||
:arch, presence: true
|
||||
:arch, :arch_id,
|
||||
presence: true
|
||||
validates :status, inclusion: { in: STATUSES }
|
||||
validates :main_script, :params, length: { maximum: 255 }
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ class ProjectTag < ActiveRecord::Base
|
|||
|
||||
belongs_to :project
|
||||
|
||||
validates :project, :commit, :sha1, :tag_name, :format_id, presence: true
|
||||
validates :project, :commit_id, :sha1, :tag_name, :format_id, presence: true
|
||||
validates :project_id, uniqueness: { scope: [:tag_name, :format_id] }
|
||||
|
||||
attr_accessible :project_id, :commit_id, :sha1, :tag_name, :format_id
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
json.build_lists @build_lists do |build_list|
|
||||
json.(build_list, :id, :status)
|
||||
json.(build_list, :id, :status, :project_id)
|
||||
json.url api_v1_build_list_path(build_list, format: :json)
|
||||
end
|
||||
|
||||
|
|
|
@ -2,3 +2,8 @@ json.(project, :id, :name)
|
|||
json.fullname project.name_with_owner
|
||||
json.url api_v1_project_path(project.id, format: :json)
|
||||
json.git_url git_repo_url(project.name_with_owner)
|
||||
json.maintainer do
|
||||
if project.maintainer
|
||||
json.partial! 'api/v1/maintainers/maintainer', maintainer: project.maintainer
|
||||
end
|
||||
end
|
|
@ -5,7 +5,9 @@ json.project do
|
|||
json.updated_at @project.updated_at.to_i
|
||||
json.partial! 'api/v1/shared/owner', owner: @project.owner
|
||||
json.maintainer do
|
||||
json.partial! 'api/v1/shared/member', member: @project.maintainer
|
||||
if @project.maintainer
|
||||
json.partial! 'api/v1/maintainers/maintainer', maintainer: @project.maintainer
|
||||
end
|
||||
end
|
||||
|
||||
json.project_statistics @project.project_statistics do |statistic|
|
||||
|
|
|
@ -24,9 +24,9 @@
|
|||
= t "layout.build_lists.ownership.#{ownership}"
|
||||
.col-md-4.col-sm-6
|
||||
%h4= t 'activerecord.models.platform'
|
||||
= f.select :platform_id, availables_main_platforms.collect{ |pl| [pl.name, pl.id] },
|
||||
= f.select :save_to_platform_id, availables_main_platforms.collect{ |pl| [pl.name, pl.id] },
|
||||
{ include_blank: true },
|
||||
html_options.merge(id: 'platform', 'ng-model' => 'params.filter.platform_id')
|
||||
html_options.merge(id: 'platform', 'ng-model' => 'params.filter.save_to_platform_id')
|
||||
-[:updated_at_start, :updated_at_end].each do |attr|
|
||||
.col-md-2.col-sm-4
|
||||
%h4= t attr == :updated_at_start ? '_on' : 'until'
|
||||
|
|
|
@ -9,7 +9,7 @@ class ApiDefender < Rack::Throttle::Hourly
|
|||
options = {
|
||||
cache: Redis.new(thread_safe: true),
|
||||
key_prefix: :throttle,
|
||||
max: 2000 # only 2000 request per hour
|
||||
max: 3000 # only 3000 request per hour
|
||||
}
|
||||
@app, @options = app, options
|
||||
end
|
||||
|
|
|
@ -51,6 +51,20 @@ shared_examples_for 'create build list via api' do
|
|||
it 'should not create without existing commit hash in project' do
|
||||
lambda{ post :create, @create_params.deep_merge(build_list: {commit_hash: 'wrong'})}.should change{@project.build_lists.count}.by(0)
|
||||
end
|
||||
|
||||
it 'should not create without existing arch' do
|
||||
lambda{ post :create, @create_params.deep_merge(build_list: {arch_id: -1})}.should change{@project.build_lists.count}.by(0)
|
||||
end
|
||||
|
||||
it 'should not create without existing save_to_platform' do
|
||||
lambda{
|
||||
post :create, @create_params.deep_merge(build_list: {save_to_platform_id: -1, save_to_repository_id: -1})
|
||||
}.should change{@project.build_lists.count}.by(0)
|
||||
end
|
||||
|
||||
it 'should not create without existing save_to_repository' do
|
||||
lambda{ post :create, @create_params.deep_merge(build_list: {save_to_repository_id: -1})}.should change{@project.build_lists.count}.by(0)
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples_for 'not create build list via api' do
|
||||
|
|
|
@ -120,6 +120,10 @@ shared_examples_for 'api projects user with admin rights' do
|
|||
get :members, id: @project.id, format: :json
|
||||
response.should be_success
|
||||
end
|
||||
it 'should not set a wrong maintainer_id' do
|
||||
put :update, project: { maintainer_id: -1 }, id: @project.id, format: :json
|
||||
response.should_not be_success
|
||||
end
|
||||
|
||||
context 'api project user with update rights' do
|
||||
before do
|
||||
|
@ -129,7 +133,7 @@ shared_examples_for 'api projects user with admin rights' do
|
|||
it 'should be able to perform update action' do
|
||||
response.should be_success
|
||||
end
|
||||
it 'ensures that group has been updated' do
|
||||
it 'ensures that description has been updated' do
|
||||
@project.reload
|
||||
@project.description.should == 'new description'
|
||||
end
|
||||
|
|
|
@ -29,12 +29,8 @@ describe BuildList do
|
|||
expect(BuildList.next_build([], [build_list.build_for_platform_id])).to eq build_list
|
||||
end
|
||||
|
||||
it 'returns nothing if build list does not in queue' do
|
||||
expect_any_instance_of(BuildList).to receive(:destroy_from_resque_queue).and_return(0)
|
||||
expect(BuildList.next_build([], [build_list.build_for_platform_id])).to be_nil
|
||||
end
|
||||
|
||||
it 'returns nothing for wrong platform' do
|
||||
expect_any_instance_of(BuildList).to receive(:restart_job)
|
||||
expect(BuildList.next_build([], [-1])).to be_nil
|
||||
end
|
||||
end
|
||||
|
@ -45,12 +41,8 @@ describe BuildList do
|
|||
expect(BuildList.next_build([build_list.arch_id], [])).to eq build_list
|
||||
end
|
||||
|
||||
it 'returns nothing if build list does not in queue' do
|
||||
expect_any_instance_of(BuildList).to receive(:destroy_from_resque_queue).and_return(0)
|
||||
expect(BuildList.next_build([build_list.arch_id], [])).to be_nil
|
||||
end
|
||||
|
||||
it 'returns nothing for wrong arch' do
|
||||
expect_any_instance_of(BuildList).to receive(:restart_job)
|
||||
expect(BuildList.next_build([-1], [])).to be_nil
|
||||
end
|
||||
end
|
||||
|
|
|
@ -12,7 +12,7 @@ describe BuildScript do
|
|||
context 'ensures that validations and associations exist' do
|
||||
it { should belong_to(:project) }
|
||||
|
||||
it { should validate_presence_of(:project_id) }
|
||||
it { should validate_presence_of(:project) }
|
||||
it { should validate_presence_of(:treeish) }
|
||||
|
||||
context 'uniqueness' do
|
||||
|
|
|
@ -6,8 +6,8 @@ describe ProjectStatistic do
|
|||
it { should belong_to(:project) }
|
||||
it { should belong_to(:arch) }
|
||||
|
||||
it { should validate_presence_of(:project_id) }
|
||||
it { should validate_presence_of(:arch_id) }
|
||||
it { should validate_presence_of(:project) }
|
||||
it { should validate_presence_of(:arch) }
|
||||
it { should validate_presence_of(:average_build_time) }
|
||||
it { should validate_presence_of(:build_count) }
|
||||
|
||||
|
|
Loading…
Reference in New Issue