Commit 7649497f authored by Stan Hu's avatar Stan Hu

Merge branch 'master' of gitlab.com:gitlab-org/gitlab-ce

parents 341541d3 4d042afe
......@@ -21,10 +21,12 @@ v 8.12.0 (unreleased)
- Add Sentry logging to API calls
- Automatically expand hidden discussions when accessed by a permalink !5585 (Mike Greiling)
- Remove unused mixins (ClemMakesApps)
- Add search to all issue board lists
- Fix groups sort dropdown alignment (ClemMakesApps)
- Add horizontal scrolling to all sub-navs on mobile viewports (ClemMakesApps)
- Fix markdown help references (ClemMakesApps)
- Add last commit time to repo view (ClemMakesApps)
- Added project specific enable/disable setting for LFS !5997
- Added tests for diff notes
- Add a button to download latest successful artifacts for branches and tags !5142
- Add delimiter to project stars and forks count (ClemMakesApps)
......@@ -78,6 +80,8 @@ v 8.11.2
v 8.11.1
- Pulled due to packaging error.
v 8.11.0 (unreleased)
- Fix pipelines tab layout regression (brycepj)
v 8.11.0
- Use test coverage value from the latest successful pipeline in badge. !5862
- Add test coverage report badge. !5708
......
......@@ -54,4 +54,11 @@ $(() => {
});
}
});
gl.IssueBoardsSearch = new Vue({
el: '#js-boards-seach',
data: {
filters: Store.state.filters
}
});
});
......@@ -21,15 +21,10 @@
},
data () {
return {
query: '',
filters: Store.state.filters
};
},
watch: {
query () {
this.list.filters = this.getFilterData();
this.list.getIssues(true);
},
filters: {
handler () {
this.list.page = 1;
......@@ -38,16 +33,6 @@
deep: true
}
},
methods: {
getFilterData () {
const filters = this.filters;
let queryData = { search: this.query };
Object.keys(filters).forEach((key) => { queryData[key] = filters[key]; });
return queryData;
}
},
ready () {
const options = gl.issueBoards.getBoardSortableDefaultOptions({
disabled: this.disabled,
......
......@@ -58,10 +58,6 @@ class List {
}
}
canSearch () {
return this.type === 'backlog';
}
getIssues (emptyIssues = true) {
const filters = this.filters;
let data = { page: this.page };
......
......@@ -15,7 +15,8 @@
author_id: gl.utils.getParameterValues('author_id')[0],
assignee_id: gl.utils.getParameterValues('assignee_id')[0],
milestone_title: gl.utils.getParameterValues('milestone_title')[0],
label_name: gl.utils.getParameterValues('label_name[]')
label_name: gl.utils.getParameterValues('label_name[]'),
search: ''
};
},
addList (listObj) {
......
......@@ -160,40 +160,6 @@
border-bottom: 1px solid $border-color;
}
.board-search-container {
position: relative;
background-color: #fff;
.form-control {
padding-right: 30px;
}
}
.board-search-icon,
.board-search-clear-btn {
position: absolute;
right: $gl-padding + 10px;
top: 50%;
margin-top: -7px;
font-size: 14px;
}
.board-search-icon {
color: $gl-placeholder-color;
}
.board-search-clear-btn {
padding: 0;
line-height: 1;
background: transparent;
border: 0;
outline: 0;
&:hover {
color: $gl-link-color;
}
}
.board-delete {
margin-right: 10px;
padding: 0;
......@@ -304,3 +270,12 @@
margin-right: 8px;
font-weight: 500;
}
.issue-boards-search {
width: 335px;
.form-control {
display: inline-block;
width: 210px;
}
}
......@@ -477,3 +477,10 @@
width: 60px;
}
}
.ci-status-icon-created {
svg {
fill: $table-text-gray;
}
}
......@@ -311,6 +311,14 @@ a.deploy-project-label {
color: $gl-success;
}
.lfs-enabled {
color: $gl-success;
}
.lfs-disabled {
color: $gl-warning;
}
.breadcrumb.repo-breadcrumb {
padding: 0;
background: transparent;
......
......@@ -8,10 +8,14 @@ module ToggleAwardEmoji
def toggle_award_emoji
name = params.require(:name)
if awardable.user_can_award?(current_user, name)
awardable.toggle_award_emoji(name, current_user)
TodoService.new.new_award_emoji(to_todoable(awardable), current_user)
render json: { ok: true }
else
render json: { ok: false }
end
end
private
......
......@@ -309,7 +309,7 @@ class ProjectsController < Projects::ApplicationController
:issues_tracker_id, :default_branch,
:wiki_enabled, :visibility_level, :import_url, :last_activity_at, :namespace_id, :avatar,
:builds_enabled, :build_allow_git_fetch, :build_timeout_in_minutes, :build_coverage_regex,
:public_builds, :only_allow_merge_if_build_succeeds, :request_access_enabled
:public_builds, :only_allow_merge_if_build_succeeds, :request_access_enabled, :lfs_enabled
)
end
......
......@@ -23,10 +23,14 @@ module LfsHelper
end
def lfs_download_access?
return false unless project.lfs_enabled?
project.public? || ci? || (user && user.can?(:download_code, project))
end
def lfs_upload_access?
return false unless project.lfs_enabled?
user && user.can?(:push_code, project)
end
......
......@@ -25,6 +25,8 @@ module NavHelper
current_path?('merge_requests#commits') ||
current_path?('merge_requests#builds') ||
current_path?('merge_requests#conflicts') ||
current_path?('merge_requests#pipelines') ||
current_path?('issues#show')
if cookies[:collapsed_gutter] == 'true'
"page-gutter right-sidebar-collapsed"
......
......@@ -187,6 +187,18 @@ module ProjectsHelper
nav_tabs.flatten
end
def project_lfs_status(project)
if project.lfs_enabled?
content_tag(:span, class: 'lfs-enabled') do
'Enabled'
end
else
content_tag(:span, class: 'lfs-disabled') do
'Disabled'
end
end
end
def git_user_name
if current_user
current_user.name
......
......@@ -59,6 +59,18 @@ module Awardable
true
end
def awardable_votes?(name)
AwardEmoji::UPVOTE_NAME == name || AwardEmoji::DOWNVOTE_NAME == name
end
def user_can_award?(current_user, name)
if user_authored?(current_user)
!awardable_votes?(normalize_name(name))
else
true
end
end
def awarded_emoji?(emoji_name, current_user)
award_emoji.where(name: emoji_name, user: current_user).exists?
end
......
......@@ -196,6 +196,10 @@ module Issuable
end
end
def user_authored?(user)
user == author
end
def subscribed_without_subscriptions?(user)
participants(user).include?(user)
end
......
......@@ -223,6 +223,10 @@ class Note < ActiveRecord::Base
end
end
def user_authored?(user)
user == author
end
def award_emoji?
can_be_award_emoji? && contains_emoji_only?
end
......
......@@ -390,6 +390,13 @@ class Project < ActiveRecord::Base
end
end
def lfs_enabled?
return false unless Gitlab.config.lfs.enabled
return Gitlab.config.lfs.enabled if self[:lfs_enabled].nil?
self[:lfs_enabled]
end
def repository_storage_path
Gitlab.config.repositories.storages[repository_storage]
end
......
......@@ -73,6 +73,12 @@
%span.light last commit:
%strong
= last_commit(@project)
%li
%span.light Git LFS status:
%strong
= project_lfs_status(@project)
= link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs')
- else
%li
%span.light repository:
......
......@@ -21,11 +21,6 @@
%button.board-delete.has-tooltip.pull-right{ type: "button", title: "Delete list", "aria-label" => "Delete list", data: { placement: "bottom" }, "@click.stop" => "deleteBoard" }
= icon("trash")
= icon("spinner spin", class: "board-header-loading-spinner pull-right", "v-show" => "list.loadingMore")
.board-inner-container.board-search-container{ "v-if" => "list.canSearch()" }
%input.form-control{ type: "text", placeholder: "Search issues", "v-model" => "query", "debounce" => "250" }
= icon("search", class: "board-search-icon", "v-show" => "!query")
%button.board-search-clear-btn{ type: "button", role: "button", "aria-label" => "Clear search", "@click" => "query = ''", "v-show" => "query" }
= icon("times", class: "board-search-clear")
%board-list{ "inline-template" => true,
"v-if" => "list.type !== 'blank'",
":list" => "list",
......
......@@ -80,6 +80,16 @@
%strong Snippets
%br
%span.descr Share code pastes with others out of git repository
- if Gitlab.config.lfs.enabled && current_user.admin?
.form-group
.checkbox
= f.label :lfs_enabled do
= f.check_box :lfs_enabled, checked: @project.lfs_enabled?
%strong LFS
%br
%span.descr
Git Large File Storage
= link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs')
- if Gitlab.config.registry.enabled
.form-group
.checkbox
......
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 14 14" enable-background="new 0 0 14 14"><path d="M12.5,7 C12.5,4 10,1.5 7,1.5 C4,1.5 1.5,4 1.5,7 C1.5,10 4,12.5 7,12.5 C10,12.5 12.5,10 12.5,7 L12.5,7 Z M0,7 C0,3.1 3.1,0 7,0 C10.9,0 14,3.1 14,7 C14,10.9 10.9,14 7,14 C3.1,14 0,10.9 0,7 L0,7 Z" /><circle cx="7" cy="7" r="3.25"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14" enable-background="new 0 0 14 14"><path d="M12.5,7 C12.5,4 10,1.5 7,1.5 C4,1.5 1.5,4 1.5,7 C1.5,10 4,12.5 7,12.5 C10,12.5 12.5,10 12.5,7 L12.5,7 Z M0,7 C0,3.1 3.1,0 7,0 C10.9,0 14,3.1 14,7 C14,10.9 10.9,14 7,14 C3.1,14 0,10.9 0,7 L0,7 Z" /><circle cx="7" cy="7" r="3.25"/></svg>
......@@ -27,8 +27,11 @@
= render "shared/issuable/label_dropdown"
.pull-right
- if controller.controller_name == 'boards' && can?(current_user, :admin_list, @project)
.dropdown
- if controller.controller_name == 'boards'
#js-boards-seach.issue-boards-search
%input.pull-left.form-control{ type: "search", placeholder: "Filter by name...", "v-model" => "filters.search", "debounce" => "250" }
- if can?(current_user, :admin_list, @project)
.dropdown.pull-right
%button.btn.btn-create.js-new-board-list{ type: "button", data: { toggle: "dropdown", labels: labels_filter_path, project_id: @project.try(:id) } }
Create new list
.dropdown-menu.dropdown-menu-paging.dropdown-menu-align-right.dropdown-menu-issues-board-new.dropdown-menu-selectable
......
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class AddLfsEnabledToProjects < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
# When a migration requires downtime you **must** uncomment the following
# constant and define a short and easy to understand explanation as to why the
# migration requires downtime.
# DOWNTIME_REASON = ''
# When using the methods "add_concurrent_index" or "add_column_with_default"
# you must disable the use of transactions as these methods can not run in an
# existing transaction. When using "add_concurrent_index" make sure that this
# method is the _only_ method called in the migration, any other changes
# should go in a separate migration. This ensures that upon failure _only_ the
# index creation fails and can be retried or reverted easily.
#
# To disable transactions uncomment the following line and remove these
# comments:
# disable_ddl_transaction!
def change
add_column :projects, :lfs_enabled, :boolean
end
end
class ChangeLockVersionNotNull < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def up
change_column_null :issues, :lock_version, true
change_column_null :merge_requests, :lock_version, true
end
def down
end
end
......@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20160827011312) do
ActiveRecord::Schema.define(version: 20160830232601) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
......@@ -825,6 +825,7 @@ ActiveRecord::Schema.define(version: 20160827011312) do
t.string "repository_storage", default: "default", null: false
t.boolean "request_access_enabled", default: true, null: false
t.boolean "has_external_wiki"
t.boolean "lfs_enabled"
end
add_index "projects", ["ci_id"], name: "index_projects_on_ci_id", using: :btree
......
......@@ -452,6 +452,7 @@ Parameters:
- `import_url` (optional)
- `public_builds` (optional)
- `only_allow_merge_if_build_succeeds` (optional)
- `lfs_enabled` (optional)
### Create project for user
......@@ -478,6 +479,7 @@ Parameters:
- `import_url` (optional)
- `public_builds` (optional)
- `only_allow_merge_if_build_succeeds` (optional)
- `lfs_enabled` (optional)
### Edit project
......@@ -505,6 +507,7 @@ Parameters:
- `visibility_level` (optional)
- `public_builds` (optional)
- `only_allow_merge_if_build_succeeds` (optional)
- `lfs_enabled` (optional)
On success, method returns 200 with the updated project. If parameters are
invalid, 400 is returned.
......
......@@ -32,4 +32,12 @@ Snippets are little bits of code or text.
This is a nice place to put code or text that is used semi-regularly within the project, but does not belong in source control.
For example, a specific config file that is used by > the team that is only valid for the people that work on the code.
For example, a specific config file that is used by the team that is only valid for the people that work on the code.
## Git LFS
>**Note:** Project-specific LFS setting was added on 8.12 and is available only to admins.
Git Large File Storage allows you to easily manage large binary files with Git.
With this setting admins can better control which projects are allowed to use
LFS.
......@@ -54,7 +54,7 @@ module API
post endpoint do
required_attributes! [:name]
not_found!('Award Emoji') unless can_read_awardable?
not_found!('Award Emoji') unless can_read_awardable? && can_award_awardable?
award = awardable.create_award_emoji(params[:name], current_user)
......@@ -92,6 +92,10 @@ module API
can?(current_user, ability, awardable)
end
def can_award_awardable?
awardable.user_can_award?(current_user, params[:name])
end
def awardable
@awardable ||=
begin
......
......@@ -78,7 +78,7 @@ module API
expose :path, :path_with_namespace
expose :issues_enabled, :merge_requests_enabled, :wiki_enabled, :builds_enabled, :snippets_enabled, :container_registry_enabled
expose :created_at, :last_activity_at
expose :shared_runners_enabled
expose :shared_runners_enabled, :lfs_enabled
expose :creator_id
expose :namespace
expose :forked_from_project, using: Entities::BasicProjectDetails, if: lambda{ |project, options| project.forked? }
......
......@@ -105,6 +105,7 @@ module API
# visibility_level (optional) - 0 by default
# import_url (optional)
# public_builds (optional)
# lfs_enabled (optional)
# Example Request
# POST /projects
post do
......@@ -124,7 +125,8 @@ module API
:visibility_level,
:import_url,
:public_builds,
:only_allow_merge_if_build_succeeds]
:only_allow_merge_if_build_succeeds,
:lfs_enabled]
attrs = map_public_to_visibility_level(attrs)
@project = ::Projects::CreateService.new(current_user, attrs).execute
if @project.saved?
......@@ -156,6 +158,7 @@ module API
# visibility_level (optional)
# import_url (optional)
# public_builds (optional)
# lfs_enabled (optional)
# Example Request
# POST /projects/user/:user_id
post "user/:user_id" do
......@@ -174,7 +177,8 @@ module API
:visibility_level,
:import_url,
:public_builds,
:only_allow_merge_if_build_succeeds]
:only_allow_merge_if_build_succeeds,
:lfs_enabled]
attrs = map_public_to_visibility_level(attrs)
@project = ::Projects::CreateService.new(user, attrs).execute
if @project.saved?
......@@ -220,6 +224,7 @@ module API
# public (optional) - if true same as setting visibility_level = 20
# visibility_level (optional) - visibility level of a project
# public_builds (optional)
# lfs_enabled (optional)
# Example Request
# PUT /projects/:id
put ':id' do
......@@ -237,7 +242,8 @@ module API
:public,
:visibility_level,
:public_builds,
:only_allow_merge_if_build_succeeds]
:only_allow_merge_if_build_succeeds,
:lfs_enabled]
attrs = map_public_to_visibility_level(attrs)
authorize_admin_project
authorize! :rename_project, user_project if attrs[:name].present?
......
......@@ -110,6 +110,45 @@ describe 'Issue Boards', feature: true, js: true do
end
end
it 'search backlog list' do
page.within('#js-boards-seach') do
find('.form-control').set(issue1.title)
end
wait_for_vue_resource
expect(find('.board:nth-child(1)')).to have_selector('.card', count: 1)
expect(find('.board:nth-child(2)')).to have_selector('.card', count: 0)
expect(find('.board:nth-child(3)')).to have_selector('.card', count: 0)
expect(find('.board:nth-child(4)')).to have_selector('.card', count: 0)
end
it 'search done list' do
page.within('#js-boards-seach') do
find('.form-control').set(issue8.title)
end
wait_for_vue_resource
expect(find('.board:nth-child(1)')).to have_selector('.card', count: 0)
expect(find('.board:nth-child(2)')).to have_selector('.card', count: 0)
expect(find('.board:nth-child(3)')).to have_selector('.card', count: 0)
expect(find('.board:nth-child(4)')).to have_selector('.card', count: 1)
end
it 'search list' do
page.within('#js-boards-seach') do
find('.form-control').set(issue5.title)
end
wait_for_vue_resource
expect(find('.board:nth-child(1)')).to have_selector('.card', count: 0)
expect(find('.board:nth-child(2)')).to have_selector('.card', count: 1)
expect(find('.board:nth-child(3)')).to have_selector('.card', count: 0)
expect(find('.board:nth-child(4)')).to have_selector('.card', count: 0)
end
it 'allows user to delete board' do
page.within(find('.board:nth-child(2)')) do
find('.board-delete').click
......@@ -162,32 +201,6 @@ describe 'Issue Boards', feature: true, js: true do
end
end
it 'is searchable' do
page.within(find('.board', match: :first)) do
find('.form-control').set issue1.title
wait_for_vue_resource(spinner: false)
expect(page).to have_selector('.card', count: 1)
end
end
it 'clears search' do
page.within(find('.board', match: :first)) do
find('.form-control').set issue1.title
expect(page).to have_selector('.card', count: 1)
find('.board-search-clear-btn').click
end
wait_for_vue_resource
page.within(find('.board', match: :first)) do
expect(page).to have_selector('.card', count: 6)
end
end
it 'moves issue from backlog into list' do
drag_to(list_to_index: 1)
......
......@@ -12,7 +12,6 @@ describe 'Awards Emoji', feature: true do
describe 'Click award emoji from issue#show' do
let!(:issue) do
create(:issue,
author: @user,
assignee: @user,
project: project)
end
......
......@@ -60,15 +60,6 @@ describe('List model', () => {
}, 0);
});
it('can\'t search when not backlog', () => {
expect(list.canSearch()).toBe(false);
});
it('can search when backlog', () => {
list.type = 'backlog';
expect(list.canSearch()).toBe(true);
});
it('gets issue from list', (done) => {
setTimeout(() => {
const issue = list.findIssue(1);
......
......@@ -4,7 +4,7 @@ describe API::API, api: true do
include ApiHelpers
let(:user) { create(:user) }
let!(:project) { create(:project) }
let(:issue) { create(:issue, project: project, author: user) }
let(:issue) { create(:issue, project: project) }
let!(:award_emoji) { create(:award_emoji, awardable: issue, user: user) }
let!(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
let!(:downvote) { create(:award_emoji, :downvote, awardable: merge_request, user: user) }
......@@ -115,6 +115,8 @@ describe API::API, api: true do
end
describe "POST /projects/:id/awardable/:awardable_id/award_emoji" do
let(:issue2) { create(:issue, project: project, author: user) }
context "on an issue" do
it "creates a new award emoji" do
post api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user), name: 'blowfish'
......@@ -136,6 +138,12 @@ describe API::API, api: true do
expect(response).to have_http_status(401)
end
it "returns a 404 error if the user authored issue" do
post api("/projects/#{project.id}/issues/#{issue2.id}/award_emoji", user), name: 'thumbsup'
expect(response).to have_http_status(404)
end
it "normalizes +1 as thumbsup award" do
post api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user), name: '+1'
......@@ -155,6 +163,8 @@ describe API::API, api: true do
end
describe "POST /projects/:id/awardable/:awardable_id/notes/:note_id/award_emoji" do
let(:note2) { create(:note, project: project, noteable: issue, author: user) }
it 'creates a new award emoji' do
expect do
post api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji", user), name: 'rocket'
......@@ -164,6 +174,12 @@ describe API::API, api: true do
expect(json_response['user']['username']).to eq(user.username)
end
it "it returns 404 error when user authored note" do
post api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note2.id}/award_emoji", user), name: 'thumbsup'
expect(response).to have_http_status(404)
end
it "normalizes +1 as thumbsup award" do
post api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji", user), name: '+1'
......
......@@ -44,6 +44,113 @@ describe 'Git LFS API and storage' do
end
end
context 'project specific LFS settings' do
let(:project) { create(:empty_project) }
let(:body) do
{
'objects' => [
{ 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
'size' => 1575078
},
{ 'oid' => sample_oid,
'size' => sample_size
}
],
'operation' => 'upload'
}
end
let(:authorization) { authorize_user }
context 'with LFS disabled globally' do
before do
project.team << [user, :master]
allow(Gitlab.config.lfs).to receive(:enabled).and_return(false)
end
describe 'LFS disabled in project' do
before do
project.update_attribute(:lfs_enabled, false)
end
it 'responds with a 501 message on upload' do
post_lfs_json "#{project.http_url_to_repo}/info/lfs/objects/batch", body, headers
expect(response).to have_http_status(501)
end
it 'responds with a 501 message on download' do
get "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}", nil, headers
expect(response).to have_http_status(501)
end
end
describe 'LFS enabled in project' do
before do
project.update_attribute(:lfs_enabled, true)
end
it 'responds with a 501 message on upload' do
post_lfs_json "#{project.http_url_to_repo}/info/lfs/objects/batch", body, headers
expect(response).to have_http_status(501)
end
it 'responds with a 501 message on download' do
get "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}", nil, headers
expect(response).to have_http_status(501)
end
end
end
context 'with LFS enabled globally' do
before do
project.team << [user, :master]
enable_lfs
end
describe 'LFS disabled in project' do
before do
project.update_attribute(:lfs_enabled, false)
end
it 'responds with a 403 message on upload' do
post_lfs_json "#{project.http_url_to_repo}/info/lfs/objects/batch", body, headers
expect(response).to have_http_status(403)
expect(json_response).to include('message' => 'Access forbidden. Check your access level.')
end
it 'responds with a 403 message on download' do
get "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}", nil, headers
expect(response).to have_http_status(403)
expect(json_response).to include('message' => 'Access forbidden. Check your access level.')
end
end
describe 'LFS enabled in project' do
before do
project.update_attribute(:lfs_enabled, true)
end
it 'responds with a 200 message on upload' do
post_lfs_json "#{project.http_url_to_repo}/info/lfs/objects/batch", body, headers
expect(response).to have_http_status(200)
expect(json_response['objects'].first['size']).to eq(1575078)
end
it 'responds with a 200 message on download' do
get "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}", nil, headers
expect(response).to have_http_status(200)
end
end
end
end
describe 'deprecated API' do
let(:project) { create(:empty_project) }
......
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