Commit e6aa1c52 authored by Bryce Johnson's avatar Bryce Johnson

Merge branch 'master' into repository-page-ui-issues

parents db5244ab 433d8a10
See the general Documentation guidelines http://docs.gitlab.com/ce/development/doc_styleguide.html.
## What does this MR do?
(briefly describe what this MR is about)
## Moving docs to a new location?
See the guidelines: http://docs.gitlab.com/ce/development/doc_styleguide.html#changing-document-location
- [ ] Make sure the old link is not removed and has its contents replaced with a link to the new location.
- [ ] Make sure internal links pointing to the document in question are not broken.
- [ ] Search and replace any links referring to old docs in GitLab Rails app, specifically under the `app/views/` directory.
- [ ] If working on CE, submit an MR to EE with the changes as well.
Please view this file on the master branch, on stable branches it's out of date. Please view this file on the master branch, on stable branches it's out of date.
v 8.12.0 (unreleased) v 8.12.0 (unreleased)
- Filter tags by name !6121
- Make push events have equal vertical spacing. - Make push events have equal vertical spacing.
- Add two-factor recovery endpoint to internal API !5510 - Add two-factor recovery endpoint to internal API !5510
- Remove vendor prefixes for linear-gradient CSS (ClemMakesApps) - Remove vendor prefixes for linear-gradient CSS (ClemMakesApps)
...@@ -11,15 +12,18 @@ v 8.12.0 (unreleased) ...@@ -11,15 +12,18 @@ v 8.12.0 (unreleased)
- Reduce contributions calendar data payload (ClemMakesApps) - Reduce contributions calendar data payload (ClemMakesApps)
- Add `web_url` field to issue, merge request, and snippet API objects (Ben Boeckel) - Add `web_url` field to issue, merge request, and snippet API objects (Ben Boeckel)
- Set path for all JavaScript cookies to honor GitLab's subdirectory setting !5627 (Mike Greiling) - Set path for all JavaScript cookies to honor GitLab's subdirectory setting !5627 (Mike Greiling)
- Fix bug where pagination is still displayed despite all todos marked as done (ClemMakesApps)
- Shorten task status phrase (ClemMakesApps) - Shorten task status phrase (ClemMakesApps)
- Add hover color to emoji icon (ClemMakesApps) - Add hover color to emoji icon (ClemMakesApps)
- Fix branches page dropdown sort alignment (ClemMakesApps) - Fix branches page dropdown sort alignment (ClemMakesApps)
- Add white background for no readme container (ClemMakesApps) - Add white background for no readme container (ClemMakesApps)
- API: Expose issue confidentiality flag. (Robert Schilling)
- Optimistic locking for Issues and Merge Requests (title and description overriding prevention) - Optimistic locking for Issues and Merge Requests (title and description overriding prevention)
- Add `wiki_page_events` to project hook APIs (Ben Boeckel) - Add `wiki_page_events` to project hook APIs (Ben Boeckel)
- Remove Gitorious import - Remove Gitorious import
- Fix inconsistent background color for filter input field (ClemMakesApps) - Fix inconsistent background color for filter input field (ClemMakesApps)
- Add Sentry logging to API calls - Add Sentry logging to API calls
- Add BroadcastMessage API
- Automatically expand hidden discussions when accessed by a permalink !5585 (Mike Greiling) - Automatically expand hidden discussions when accessed by a permalink !5585 (Mike Greiling)
- Remove unused mixins (ClemMakesApps) - Remove unused mixins (ClemMakesApps)
- Add search to all issue board lists - Add search to all issue board lists
...@@ -29,11 +33,14 @@ v 8.12.0 (unreleased) ...@@ -29,11 +33,14 @@ v 8.12.0 (unreleased)
- Add last commit time to repo view (ClemMakesApps) - Add last commit time to repo view (ClemMakesApps)
- Added project specific enable/disable setting for LFS !5997 - Added project specific enable/disable setting for LFS !5997
- Don't expose a user's token in the `/api/v3/user` API (!6047) - Don't expose a user's token in the `/api/v3/user` API (!6047)
- Remove redundant js-timeago-pending from user activity log (ClemMakesApps)
- Added tests for diff notes - Added tests for diff notes
- Add a button to download latest successful artifacts for branches and tags !5142 - Add a button to download latest successful artifacts for branches and tags !5142
- Remove redundant pipeline tooltips (ClemMakesApps) - Remove redundant pipeline tooltips (ClemMakesApps)
- Expire commit info views after one day, instead of two weeks, to allow for user email updates
- Add delimiter to project stars and forks count (ClemMakesApps) - Add delimiter to project stars and forks count (ClemMakesApps)
- Fix badge count alignment (ClemMakesApps) - Fix badge count alignment (ClemMakesApps)
- Fix repo title alignment (ClemMakesApps)
- Fix branch title trailing space on hover (ClemMakesApps) - Fix branch title trailing space on hover (ClemMakesApps)
- Award emoji tooltips containing more than 10 usernames are now truncated !4780 (jlogandavison) - Award emoji tooltips containing more than 10 usernames are now truncated !4780 (jlogandavison)
- Fix duplicate "me" in award emoji tooltip !5218 (jlogandavison) - Fix duplicate "me" in award emoji tooltip !5218 (jlogandavison)
...@@ -53,17 +60,18 @@ v 8.12.0 (unreleased) ...@@ -53,17 +60,18 @@ v 8.12.0 (unreleased)
- Fix hover leading space bug in pipeline graph !5980 - Fix hover leading space bug in pipeline graph !5980
- User can edit closed MR with deleted fork (Katarzyna Kobierska Ula Budziszewska) !5496 - User can edit closed MR with deleted fork (Katarzyna Kobierska Ula Budziszewska) !5496
- Fix repository page ui issues - Fix repository page ui issues
- Fixed invisible scroll controls on build page on iPhone
v 8.11.4 (unreleased) v 8.11.4 (unreleased)
- Fix broken gitlab:backup:restore because of bad permissions on repo storage !6098 (Dirk Hörner) - Fix broken gitlab:backup:restore because of bad permissions on repo storage !6098 (Dirk Hörner)
- Fix sorting issues by "last updated" doesn't work after import from GitHub
- GitHub importer use default project visibility for non-private projects
- Creating an issue through our API now emails label subscribers !5720 - Creating an issue through our API now emails label subscribers !5720
- Block concurrent updates for Pipeline
- Fix resolving conflicts on forks - Fix resolving conflicts on forks
- Fix diff commenting on merge requests created prior to 8.10 - Fix diff commenting on merge requests created prior to 8.10
v 8.11.4 (unreleased)
- Fix issue boards leak private label names and descriptions - Fix issue boards leak private label names and descriptions
v 8.11.3 (unreleased)
v 8.11.3 v 8.11.3
- Do not enforce using hash with hidden key in CI configuration. !6079 - Do not enforce using hash with hidden key in CI configuration. !6079
- Allow system info page to handle case where info is unavailable - Allow system info page to handle case where info is unavailable
......
...@@ -97,9 +97,6 @@ gem 'fog-rackspace', '~> 0.1.1' ...@@ -97,9 +97,6 @@ gem 'fog-rackspace', '~> 0.1.1'
# for aws storage # for aws storage
gem 'unf', '~> 0.1.4' gem 'unf', '~> 0.1.4'
# Authorization
gem 'six', '~> 0.2.0'
# Seed data # Seed data
gem 'seed-fu', '~> 2.3.5' gem 'seed-fu', '~> 2.3.5'
......
...@@ -683,7 +683,6 @@ GEM ...@@ -683,7 +683,6 @@ GEM
rack (~> 1.5) rack (~> 1.5)
rack-protection (~> 1.4) rack-protection (~> 1.4)
tilt (>= 1.3, < 3) tilt (>= 1.3, < 3)
six (0.2.0)
slack-notifier (1.2.1) slack-notifier (1.2.1)
slop (3.6.0) slop (3.6.0)
spinach (0.8.10) spinach (0.8.10)
...@@ -954,7 +953,6 @@ DEPENDENCIES ...@@ -954,7 +953,6 @@ DEPENDENCIES
sidekiq-cron (~> 0.4.0) sidekiq-cron (~> 0.4.0)
simplecov (= 0.12.0) simplecov (= 0.12.0)
sinatra (~> 1.4.4) sinatra (~> 1.4.4)
six (~> 0.2.0)
slack-notifier (~> 1.2.0) slack-notifier (~> 1.2.0)
spinach-rails (~> 0.2.1) spinach-rails (~> 0.2.1)
spinach-rerun-reporter (~> 0.0.2) spinach-rerun-reporter (~> 0.0.2)
......
...@@ -50,7 +50,7 @@ etc.). ...@@ -50,7 +50,7 @@ etc.).
The most important thing is making sure valid issues receive feedback from the The most important thing is making sure valid issues receive feedback from the
development team. Therefore the priority is mentioning developers that can help development team. Therefore the priority is mentioning developers that can help
on those issue. Please select someone with relevant experience from on those issues. Please select someone with relevant experience from
[GitLab core team][core-team]. If there is nobody mentioned with that expertise [GitLab core team][core-team]. If there is nobody mentioned with that expertise
look in the commit history for the affected files to find someone. Avoid look in the commit history for the affected files to find someone. Avoid
mentioning the lead developer, this is the person that is least likely to give a mentioning the lead developer, this is the person that is least likely to give a
......
...@@ -12,7 +12,7 @@ ...@@ -12,7 +12,7 @@
} }
Activities.prototype.updateTooltips = function() { Activities.prototype.updateTooltips = function() {
return gl.utils.localTimeAgo($('.js-timeago', '#activity')); return gl.utils.localTimeAgo($('.js-timeago', '.content_list'));
}; };
Activities.prototype.reloadActivities = function() { Activities.prototype.reloadActivities = function() {
......
(function(w) { (function(w) {
$(function() { $(function() {
$('.js-toggle-button').on('click', function(e) { $('body').on('click', '.js-toggle-button', function(e) {
e.preventDefault(); e.preventDefault();
$(this) $(this)
.find('.fa') .find('.fa')
......
...@@ -556,7 +556,7 @@ ...@@ -556,7 +556,7 @@
if (isInput) { if (isInput) {
field = $(this.el); field = $(this.el);
} else { } else {
field = this.dropdown.parent().find("input[name='" + fieldName + "'][value='" + value + "']"); field = this.dropdown.parent().find("input[name='" + fieldName + "'][value='" + escape(value) + "']");
} }
if (el.hasClass(ACTIVE_CLASS)) { if (el.hasClass(ACTIVE_CLASS)) {
el.removeClass(ACTIVE_CLASS); el.removeClass(ACTIVE_CLASS);
......
...@@ -164,7 +164,7 @@ ...@@ -164,7 +164,7 @@
instance.addInput(this.fieldName, label.id); instance.addInput(this.fieldName, label.id);
} }
} }
if ($form.find("input[type='hidden'][name='" + ($dropdown.data('fieldName')) + "'][value='" + (this.id(label)) + "']").length) { if ($form.find("input[type='hidden'][name='" + ($dropdown.data('fieldName')) + "'][value='" + escape(this.id(label)) + "']").length) {
selectedClass.push('is-active'); selectedClass.push('is-active');
} }
if ($dropdown.hasClass('js-multiselect') && removesAll) { if ($dropdown.hasClass('js-multiselect') && removesAll) {
......
(function() { (function() {
Turbolinks.enableProgressBar(); Turbolinks.enableProgressBar();
start = function() { $(document).on('page:fetch', function() {
$('.tanuki-logo').addClass('animate'); $('.tanuki-logo').addClass('animate');
}; });
stop = function() { $(document).on('page:change', function() {
$('.tanuki-logo').removeClass('animate'); $('.tanuki-logo').removeClass('animate');
}; });
$(document).on('page:fetch', start);
$(document).on('page:change', stop);
}).call(this); }).call(this);
...@@ -66,7 +66,7 @@ ...@@ -66,7 +66,7 @@
success: (function(_this) { success: (function(_this) {
return function(data) { return function(data) {
$this.remove(); $this.remove();
$('.js-todos-list').remove(); $('.prepend-top-default').html('<div class="nothing-here-block">You\'re all done!</div>');
return _this.updateBadges(data); return _this.updateBadges(data);
}; };
})(this) })(this)
......
...@@ -84,7 +84,7 @@ header { ...@@ -84,7 +84,7 @@ header {
.side-nav-toggle { .side-nav-toggle {
position: absolute; position: absolute;
left: -10px; left: -10px;
margin: 6px 0; margin: 7px 0;
font-size: 18px; font-size: 18px;
padding: 6px 10px; padding: 6px 10px;
border: none; border: none;
......
...@@ -36,6 +36,7 @@ ...@@ -36,6 +36,7 @@
&.affix { &.affix {
right: 30px; right: 30px;
bottom: 15px; bottom: 15px;
z-index: 1;
@media (min-width: $screen-md-min) { @media (min-width: $screen-md-min) {
right: 26%; right: 26%;
......
...@@ -24,7 +24,7 @@ class ApplicationController < ActionController::Base ...@@ -24,7 +24,7 @@ class ApplicationController < ActionController::Base
protect_from_forgery with: :exception protect_from_forgery with: :exception
helper_method :abilities, :can?, :current_application_settings helper_method :can?, :current_application_settings
helper_method :import_sources_enabled?, :github_import_enabled?, :github_import_configured?, :gitlab_import_enabled?, :gitlab_import_configured?, :bitbucket_import_enabled?, :bitbucket_import_configured?, :google_code_import_enabled?, :fogbugz_import_enabled?, :git_import_enabled?, :gitlab_project_import_enabled? helper_method :import_sources_enabled?, :github_import_enabled?, :github_import_configured?, :gitlab_import_enabled?, :gitlab_import_configured?, :bitbucket_import_enabled?, :bitbucket_import_configured?, :google_code_import_enabled?, :fogbugz_import_enabled?, :git_import_enabled?, :gitlab_project_import_enabled?
rescue_from Encoding::CompatibilityError do |exception| rescue_from Encoding::CompatibilityError do |exception|
...@@ -97,12 +97,8 @@ class ApplicationController < ActionController::Base ...@@ -97,12 +97,8 @@ class ApplicationController < ActionController::Base
current_application_settings.after_sign_out_path.presence || new_user_session_path current_application_settings.after_sign_out_path.presence || new_user_session_path
end end
def abilities
Ability.abilities
end
def can?(object, action, subject) def can?(object, action, subject)
abilities.allowed?(object, action, subject) Ability.allowed?(object, action, subject)
end end
def access_denied! def access_denied!
......
...@@ -14,7 +14,7 @@ class NamespacesController < ApplicationController ...@@ -14,7 +14,7 @@ class NamespacesController < ApplicationController
if user if user
redirect_to user_path(user) redirect_to user_path(user)
elsif group && can?(current_user, :read_group, namespace) elsif group && can?(current_user, :read_group, group)
redirect_to group_path(group) redirect_to group_path(group)
elsif current_user.nil? elsif current_user.nil?
authenticate_user! authenticate_user!
......
class Projects::TagsController < Projects::ApplicationController class Projects::TagsController < Projects::ApplicationController
include SortingHelper
# Authorize # Authorize
before_action :require_non_empty_project before_action :require_non_empty_project
before_action :authorize_download_code! before_action :authorize_download_code!
...@@ -6,8 +8,10 @@ class Projects::TagsController < Projects::ApplicationController ...@@ -6,8 +8,10 @@ class Projects::TagsController < Projects::ApplicationController
before_action :authorize_admin_project!, only: [:destroy] before_action :authorize_admin_project!, only: [:destroy]
def index def index
@sort = params[:sort] || 'name' params[:sort] = params[:sort].presence || 'name'
@tags = @repository.tags_sorted_by(@sort)
@sort = params[:sort]
@tags = TagsFinder.new(@repository, params).execute
@tags = Kaminari.paginate_array(@tags).page(params[:page]) @tags = Kaminari.paginate_array(@tags).page(params[:page])
@releases = project.releases.where(tag: @tags.map(&:name)) @releases = project.releases.where(tag: @tags.map(&:name))
......
...@@ -64,7 +64,7 @@ class IssuableFinder ...@@ -64,7 +64,7 @@ class IssuableFinder
if project? if project?
@project = Project.find(params[:project_id]) @project = Project.find(params[:project_id])
unless Ability.abilities.allowed?(current_user, :read_project, @project) unless Ability.allowed?(current_user, :read_project, @project)
@project = nil @project = nil
end end
else else
......
class TagsFinder
def initialize(repository, params)
@repository = repository
@params = params
end
def execute
tags = @repository.tags_sorted_by(sort)
filter_by_name(tags)
end
private
def sort
@params[:sort].presence
end
def search
@params[:search].presence
end
def filter_by_name(tags)
if search
tags.select { |tag| tag.name.include?(search) }
else
tags
end
end
end
...@@ -83,7 +83,7 @@ class TodosFinder ...@@ -83,7 +83,7 @@ class TodosFinder
if project? if project?
@project = Project.find(params[:project_id]) @project = Project.find(params[:project_id])
unless Ability.abilities.allowed?(current_user, :read_project, @project) unless Ability.allowed?(current_user, :read_project, @project)
@project = nil @project = nil
end end
else else
......
module SentryHelper module SentryHelper
def sentry_enabled? def sentry_enabled?
Rails.env.production? && current_application_settings.sentry_enabled? Gitlab::Sentry.enabled?
end end
def sentry_context def sentry_context
return unless sentry_enabled? Gitlab::Sentry.context(current_user)
if current_user
Raven.user_context(
id: current_user.id,
email: current_user.email,
username: current_user.username,
)
end
Raven.tags_context(program: sentry_program_context)
end
def sentry_program_context
if Sidekiq.server?
'sidekiq'
else
'rails'
end
end end
end end
...@@ -3,6 +3,16 @@ module TagsHelper ...@@ -3,6 +3,16 @@ module TagsHelper
"/tags/#{tag}" "/tags/#{tag}"
end end
def filter_tags_path(options = {})
exist_opts = {
search: params[:search],
sort: params[:sort]
}
options = exist_opts.merge(options)
namespace_project_tags_path(@project.namespace, @project, @id, options)
end
def tag_list(project) def tag_list(project)
html = '' html = ''
project.tag_list.each do |tag| project.tag_list.each do |tag|
......
...@@ -9,7 +9,7 @@ class BaseMailer < ActionMailer::Base ...@@ -9,7 +9,7 @@ class BaseMailer < ActionMailer::Base
default reply_to: Proc.new { default_reply_to_address.format } default reply_to: Proc.new { default_reply_to_address.format }
def can? def can?
Ability.abilities.allowed?(current_user, action, subject) Ability.allowed?(current_user, action, subject)
end end
private private
......
class Ability class Ability
class << self class << self
# rubocop: disable Metrics/CyclomaticComplexity
def allowed(user, subject)
return anonymous_abilities(user, subject) if user.nil?
return [] unless user.is_a?(User)
return [] if user.blocked?
abilities_by_subject_class(user: user, subject: subject)
end
def abilities_by_subject_class(user:, subject:)
case subject
when CommitStatus then commit_status_abilities(user, subject)
when Project then project_abilities(user, subject)
when Issue then issue_abilities(user, subject)
when Note then note_abilities(user, subject)
when ProjectSnippet then project_snippet_abilities(user, subject)
when PersonalSnippet then personal_snippet_abilities(user, subject)
when MergeRequest then merge_request_abilities(user, subject)
when Group then group_abilities(user, subject)
when Namespace then namespace_abilities(user, subject)
when GroupMember then group_member_abilities(user, subject)
when ProjectMember then project_member_abilities(user, subject)
when User then user_abilities
when ExternalIssue, Deployment, Environment then project_abilities(user, subject.project)
when Ci::Runner then runner_abilities(user, subject)
else []
end.concat(global_abilities(user))
end
# Given a list of users and a project this method returns the users that can # Given a list of users and a project this method returns the users that can
# read the given project. # read the given project.
def users_that_can_read_project(users, project) def users_that_can_read_project(users, project)
...@@ -61,359 +32,7 @@ class Ability ...@@ -61,359 +32,7 @@ class Ability
issues.select { |issue| issue.visible_to_user?(user) } issues.select { |issue| issue.visible_to_user?(user) }
end end
# List of possible abilities for anonymous user # TODO: make this private and use the actual abilities stuff for this
def anonymous_abilities(user, subject)
if subject.is_a?(PersonalSnippet)
anonymous_personal_snippet_abilities(subject)
elsif subject.is_a?(ProjectSnippet)
anonymous_project_snippet_abilities(subject)
elsif subject.is_a?(CommitStatus)
anonymous_commit_status_abilities(subject)
elsif subject.is_a?(Project) || subject.respond_to?(:project)
anonymous_project_abilities(subject)
elsif subject.is_a?(Group) || subject.respond_to?(:group)
anonymous_group_abilities(subject)
elsif subject.is_a?(User)
anonymous_user_abilities
else
[]
end
end
def anonymous_project_abilities(subject)
project = if subject.is_a?(Project)
subject
else
subject.project
end
if project && project.public?
rules = [
:read_project,
:read_board,
:read_list,
:read_wiki,
:read_label,
:read_milestone,
:read_project_snippet,
:read_project_member,
:read_merge_request,
:read_note,
:read_pipeline,
:read_commit_status,
:read_container_image,
:download_code
]
# Allow to read builds by anonymous user if guests are allowed
rules << :read_build if project.public_builds?
# Allow to read issues by anonymous user if issue is not confidential
rules << :read_issue unless subject.is_a?(Issue) && subject.confidential?
rules - project_disabled_features_rules(project)
else
[]
end
end
def anonymous_commit_status_abilities(subject)
rules = anonymous_project_abilities(subject.project)
# If subject is Ci::Build which inherits from CommitStatus filter the abilities
rules = filter_build_abilities(rules) if subject.is_a?(Ci::Build)
rules
end
def anonymous_group_abilities(subject)
rules = []
group = if subject.is_a?(Group)
subject
else
subject.group
end
rules << :read_group if group.public?
rules
end
def anonymous_personal_snippet_abilities(snippet)
if snippet.public?
[:read_personal_snippet]
else
[]
end
end
def anonymous_project_snippet_abilities(snippet)
if snippet.public?
[:read_project_snippet]
else
[]
end
end
def anonymous_user_abilities
[:read_user] unless restricted_public_level?
end
def global_abilities(user)
rules = []
rules << :create_group if user.can_create_group
rules << :read_users_list
rules
end
def project_abilities(user, project)
key = "/user/#{user.id}/project/#{project.id}"
if RequestStore.active?
RequestStore.store[key] ||= uncached_project_abilities(user, project)
else
uncached_project_abilities(user, project)
end
end
def uncached_project_abilities(user, project)
rules = []
# Push abilities on the users team role
rules.push(*project_team_rules(project.team, user))
owner = user.admin? ||
project.owner == user ||
(project.group && project.group.has_owner?(user))
if owner
rules.push(*project_owner_rules)
end
if project.public? || (project.internal? && !user.external?)
rules.push(*public_project_rules)
# Allow to read builds for internal projects
rules << :read_build if project.public_builds?
unless owner || project.team.member?(user) || project_group_member?(project, user)
rules << :request_access if project.request_access_enabled
end
end
if project.archived?
rules -= project_archived_rules
end
(rules - project_disabled_features_rules(project)).uniq
end
def project_team_rules(team, user)
# Rules based on role in project
if team.master?(user)
project_master_rules
elsif team.developer?(user)
project_dev_rules
elsif team.reporter?(user)
project_report_rules
elsif team.guest?(user)
project_guest_rules
else
[]
end
end
def public_project_rules
@public_project_rules ||= project_guest_rules + [
:download_code,
:fork_project,
:read_commit_status,
:read_pipeline,
:read_container_image
]
end
def project_guest_rules
@project_guest_rules ||= [
:read_project,
:read_wiki,
:read_issue,
:read_board,
:read_list,
:read_label,
:read_milestone,
:read_project_snippet,
:read_project_member,
:read_merge_request,
:read_note,
:create_project,
:create_issue,
:create_note,
:upload_file
]
end
def project_report_rules
@project_report_rules ||= project_guest_rules + [
:download_code,
:fork_project,
:create_project_snippet,
:update_issue,
:admin_issue,
:admin_label,
:admin_list,
:read_commit_status,
:read_build,
:read_container_image,
:read_pipeline,
:read_environment,
:read_deployment
]
end
def project_dev_rules
@project_dev_rules ||= project_report_rules + [
:admin_merge_request,
:update_merge_request,
:create_commit_status,
:update_commit_status,
:create_build,
:update_build,
:create_pipeline,
:update_pipeline,
:create_merge_request,
:create_wiki,
:push_code,
:resolve_note,
:create_container_image,
:update_container_image,
:create_environment,
:create_deployment
]
end
def project_archived_rules
@project_archived_rules ||= [
:create_merge_request,
:push_code,
:push_code_to_protected_branches,
:update_merge_request,
:admin_merge_request
]
end
def project_master_rules
@project_master_rules ||= project_dev_rules + [
:push_code_to_protected_branches,
:update_project_snippet,
:update_environment,
:update_deployment,
:admin_milestone,
:admin_project_snippet,
:admin_project_member,
:admin_merge_request,
:admin_note,
:admin_wiki,
:admin_project,
:admin_commit_status,
:admin_build,
:admin_container_image,
:admin_pipeline,
:admin_environment,
:admin_deployment
]
end
def project_owner_rules
@project_owner_rules ||= project_master_rules + [
:change_namespace,
:change_visibility_level,
:rename_project,
:remove_project,
:archive_project,
:remove_fork_project,
:destroy_merge_request,
:destroy_issue
]
end
def project_disabled_features_rules(project)
rules = []
unless project.issues_enabled
rules += named_abilities('issue')
end
unless project.merge_requests_enabled
rules += named_abilities('merge_request')
end
unless project.issues_enabled or project.merge_requests_enabled
rules += named_abilities('label')
rules += named_abilities('milestone')
end
unless project.snippets_enabled
rules += named_abilities('project_snippet')
end
unless project.has_wiki?
rules += named_abilities('wiki')
end
unless project.builds_enabled
rules += named_abilities('build')
rules += named_abilities('pipeline')
rules += named_abilities('environment')
rules += named_abilities('deployment')
end
unless project.container_registry_enabled
rules += named_abilities('container_image')
end
rules
end
def group_abilities(user, group)
rules = []
rules << :read_group if can_read_group?(user, group)
owner = user.admin? || group.has_owner?(user)
master = owner || group.has_master?(user)
# Only group masters and group owners can create new projects
if master
rules += [
:create_projects,
:admin_milestones
]
end
# Only group owner and administrators can admin group
if owner
rules += [
:admin_group,
:admin_namespace,
:admin_group_member,
:change_visibility_level
]
end
if group.public? || (group.internal? && !user.external?)
rules << :request_access if group.request_access_enabled && group.users.exclude?(user)
end
rules.flatten
end
def can_read_group?(user, group)
return true if user.admin?
return true if group.public?
return true if group.internal? && !user.external?
return true if group.users.include?(user)
GroupProjectsFinder.new(group).execute(user).any?
end
def can_edit_note?(user, note) def can_edit_note?(user, note)
return false if !note.editable? || !user.present? return false if !note.editable? || !user.present?
return true if note.author == user || user.admin? return true if note.author == user || user.admin?
...@@ -426,207 +45,23 @@ class Ability ...@@ -426,207 +45,23 @@ class Ability
end end
end end
def namespace_abilities(user, namespace) def allowed?(user, action, subject)
rules = [] allowed(user, subject).include?(action)
# Only namespace owner and administrators can admin it
if namespace.owner == user || user.admin?
rules += [
:create_projects,
:admin_namespace
]
end
rules.flatten
end
[:issue, :merge_request].each do |name|
define_method "#{name}_abilities" do |user, subject|
rules = []
if subject.author == user || (subject.respond_to?(:assignee) && subject.assignee == user)
rules += [
:"read_#{name}",
:"update_#{name}",
]
end
rules += project_abilities(user, subject.project)
rules = filter_confidential_issues_abilities(user, subject, rules) if subject.is_a?(Issue)
rules
end
end
def note_abilities(user, note)
rules = []
if note.author == user
rules += [
:read_note,
:update_note,
:admin_note,
:resolve_note
]
end
if note.respond_to?(:project) && note.project
rules += project_abilities(user, note.project)
end
if note.for_merge_request? && note.noteable.author == user
rules << :resolve_note
end
rules
end
def personal_snippet_abilities(user, snippet)
rules = []
if snippet.author == user
rules += [
:read_personal_snippet,
:update_personal_snippet,
:admin_personal_snippet
]
end
if snippet.public? || (snippet.internal? && !user.external?)
rules << :read_personal_snippet
end
rules
end end
def project_snippet_abilities(user, snippet) def allowed(user, subject)
rules = [] return uncached_allowed(user, subject) unless RequestStore.active?
if snippet.author == user || user.admin?
rules += [
:read_project_snippet,
:update_project_snippet,
:admin_project_snippet
]
end
if snippet.public? || (snippet.internal? && !user.external?) || (snippet.private? && snippet.project.team.member?(user))
rules << :read_project_snippet
end
rules
end
def group_member_abilities(user, subject)
rules = []
target_user = subject.user
group = subject.group
unless group.last_owner?(target_user)
can_manage = group_abilities(user, group).include?(:admin_group_member)
if can_manage
rules << :update_group_member
rules << :destroy_group_member
elsif user == target_user
rules << :destroy_group_member
end
end
rules
end
def project_member_abilities(user, subject)
rules = []
target_user = subject.user
project = subject.project
unless target_user == project.owner
can_manage = project_abilities(user, project).include?(:admin_project_member)
if can_manage
rules << :update_project_member
rules << :destroy_project_member
elsif user == target_user
rules << :destroy_project_member
end
end
rules
end
def commit_status_abilities(user, subject)
rules = project_abilities(user, subject.project)
# If subject is Ci::Build which inherits from CommitStatus filter the abilities
rules = filter_build_abilities(rules) if subject.is_a?(Ci::Build)
rules
end
def filter_build_abilities(rules)
# If we can't read build we should also not have that
# ability when looking at this in context of commit_status
%w(read create update admin).each do |rule|
rules.delete(:"#{rule}_commit_status") unless rules.include?(:"#{rule}_build")
end
rules
end
def runner_abilities(user, runner)
if user.is_admin?
[:assign_runner]
elsif runner.is_shared? || runner.locked?
[]
elsif user.ci_authorized_runners.include?(runner)
[:assign_runner]
else
[]
end
end
def user_abilities user_key = user ? user.id : 'anonymous'
[:read_user] subject_key = subject ? "#{subject.class.name}/#{subject.id}" : 'global'
end key = "/ability/#{user_key}/#{subject_key}"
RequestStore[key] ||= uncached_allowed(user, subject).freeze
def abilities
@abilities ||= begin
abilities = Six.new
abilities << self
abilities
end
end end
private private
def restricted_public_level? def uncached_allowed(user, subject)
current_application_settings.restricted_visibility_levels.include?(Gitlab::VisibilityLevel::PUBLIC) BasePolicy.class_for(subject).abilities(user, subject)
end
def named_abilities(name)
[
:"read_#{name}",
:"create_#{name}",
:"update_#{name}",
:"admin_#{name}"
]
end
def filter_confidential_issues_abilities(user, issue, rules)
return rules if user.admin? || !issue.confidential?
unless issue.author == user || issue.assignee == user || issue.project.team.member?(user, Gitlab::Access::REPORTER)
rules.delete(:admin_issue)
rules.delete(:read_issue)
rules.delete(:update_issue)
end
rules
end
def project_group_member?(project, user)
project.group &&
(
project.group.members.exists?(user_id: user.id) ||
project.group.requesters.exists?(user_id: user.id)
)
end end
end end
end end
...@@ -65,7 +65,7 @@ class Event < ActiveRecord::Base ...@@ -65,7 +65,7 @@ class Event < ActiveRecord::Base
elsif created_project? elsif created_project?
true true
elsif issue? || issue_note? elsif issue? || issue_note?
Ability.abilities.allowed?(user, :read_issue, note? ? note_target : target) Ability.allowed?(user, :read_issue, note? ? note_target : target)
else else
((merge_request? || note?) && target.present?) || milestone? ((merge_request? || note?) && target.present?) || milestone?
end end
......
...@@ -411,7 +411,7 @@ class MergeRequest < ActiveRecord::Base ...@@ -411,7 +411,7 @@ class MergeRequest < ActiveRecord::Base
def can_remove_source_branch?(current_user) def can_remove_source_branch?(current_user)
!source_project.protected_branch?(source_branch) && !source_project.protected_branch?(source_branch) &&
!source_project.root_ref?(source_branch) && !source_project.root_ref?(source_branch) &&
Ability.abilities.allowed?(current_user, :push_code, source_project) && Ability.allowed?(current_user, :push_code, source_project) &&
diff_head_commit == source_branch_head diff_head_commit == source_branch_head
end end
......
...@@ -460,16 +460,12 @@ class User < ActiveRecord::Base ...@@ -460,16 +460,12 @@ class User < ActiveRecord::Base
can?(:create_group, nil) can?(:create_group, nil)
end end
def abilities
Ability.abilities
end
def can_select_namespace? def can_select_namespace?
several_namespaces? || admin several_namespaces? || admin
end end
def can?(action, subject) def can?(action, subject)
abilities.allowed?(self, action, subject) Ability.allowed?(self, action, subject)
end end
def first_name def first_name
......
class BasePolicy
class RuleSet
attr_reader :can_set, :cannot_set
def initialize(can_set, cannot_set)
@can_set = can_set
@cannot_set = cannot_set
end
def size
to_set.size
end
def self.empty
new(Set.new, Set.new)
end
def can?(ability)
@can_set.include?(ability) && !@cannot_set.include?(ability)
end
def include?(ability)
can?(ability)
end
def to_set
@can_set - @cannot_set
end
def merge(other)
@can_set.merge(other.can_set)
@cannot_set.merge(other.cannot_set)
end
def can!(*abilities)
@can_set.merge(abilities)
end
def cannot!(*abilities)
@cannot_set.merge(abilities)
end
def freeze
@can_set.freeze
@cannot_set.freeze
super
end
end
def self.abilities(user, subject)
new(user, subject).abilities
end
def self.class_for(subject)
return GlobalPolicy if subject.nil?
subject.class.ancestors.each do |klass|
next unless klass.name
begin
policy_class = "#{klass.name}Policy".constantize
# NOTE: the < operator here tests whether policy_class
# inherits from BasePolicy
return policy_class if policy_class < BasePolicy
rescue NameError
nil
end
end
raise "no policy for #{subject.class.name}"
end
attr_reader :user, :subject
def initialize(user, subject)
@user = user
@subject = subject
end
def abilities
return RuleSet.empty if @user && @user.blocked?
return anonymous_abilities if @user.nil?
collect_rules { rules }
end
def anonymous_abilities
collect_rules { anonymous_rules }
end
def anonymous_rules
rules
end
def delegate!(new_subject)
@rule_set.merge(Ability.allowed(@user, new_subject))
end
def can?(rule)
@rule_set.can?(rule)
end
def can!(*rules)
@rule_set.can!(*rules)
end
def cannot!(*rules)
@rule_set.cannot!(*rules)
end
private
def collect_rules(&b)
@rule_set = RuleSet.empty
yield
@rule_set
end
end
module Ci
class BuildPolicy < CommitStatusPolicy
def rules
super
# If we can't read build we should also not have that
# ability when looking at this in context of commit_status
%w(read create update admin).each do |rule|
cannot! :"#{rule}_commit_status" unless can? :"#{rule}_build"
end
end
end
end
module Ci
class RunnerPolicy < BasePolicy
def rules
return unless @user
can! :assign_runner if @user.is_admin?
return if @subject.is_shared? || @subject.locked?
can! :assign_runner if @user.ci_authorized_runners.include?(@subject)
end
end
end
class CommitStatusPolicy < BasePolicy
def rules
delegate! @subject.project
end
end
class DeploymentPolicy < BasePolicy
def rules
delegate! @subject.project
end
end
class EnvironmentPolicy < BasePolicy
def rules
delegate! @subject.project
end
end
class ExternalIssuePolicy < BasePolicy
def rules
delegate! @subject.project
end
end
class GlobalPolicy < BasePolicy
def rules
return unless @user
can! :create_group if @user.can_create_group
can! :read_users_list
end
end
class GroupMemberPolicy < BasePolicy
def rules
return unless @user
target_user = @subject.user
group = @subject.group
return if group.last_owner?(target_user)
can_manage = Ability.allowed?(@user, :admin_group_member, group)
if can_manage
can! :update_group_member
can! :destroy_group_member
elsif @user == target_user
can! :destroy_group_member
end
end
end
class GroupPolicy < BasePolicy
def rules
can! :read_group if @subject.public?
return unless @user
globally_viewable = @subject.public? || (@subject.internal? && !@user.external?)
member = @subject.users.include?(@user)
owner = @user.admin? || @subject.has_owner?(@user)
master = owner || @subject.has_master?(@user)
can_read = false
can_read ||= globally_viewable
can_read ||= member
can_read ||= @user.admin?
can_read ||= GroupProjectsFinder.new(@subject).execute(@user).any?
can! :read_group if can_read
# Only group masters and group owners can create new projects
if master
can! :create_projects
can! :admin_milestones
end
# Only group owner and administrators can admin group
if owner
can! :admin_group
can! :admin_namespace
can! :admin_group_member
can! :change_visibility_level
end
if globally_viewable && @subject.request_access_enabled && !member
can! :request_access
end
end
def can_read_group?
return true if @subject.public?
return true if @user.admin?
return true if @subject.internal? && !@user.external?
return true if @subject.users.include?(@user)
GroupProjectsFinder.new(@subject).execute(@user).any?
end
end
class IssuablePolicy < BasePolicy
def action_name
@subject.class.name.underscore
end
def rules
if @user && (@subject.author == @user || @subject.assignee == @user)
can! :"read_#{action_name}"
can! :"update_#{action_name}"
end
delegate! @subject.project
end
end
class IssuePolicy < IssuablePolicy
def issue
@subject
end
def rules
super
if @subject.confidential? && !can_read_confidential?
cannot! :read_issue
cannot! :admin_issue
cannot! :update_issue
cannot! :read_issue
end
end
private
def can_read_confidential?
return false unless @user
return true if @user.admin?
return true if @subject.author == @user
return true if @subject.assignee == @user
return true if @subject.project.team.member?(@user, Gitlab::Access::REPORTER)
false
end
end
class MergeRequestPolicy < IssuablePolicy
# pass
end
class NamespacePolicy < BasePolicy
def rules
return unless @user
if @subject.owner == @user || @user.admin?
can! :create_projects
can! :admin_namespace
end
end
end
class NotePolicy < BasePolicy
def rules
delegate! @subject.project
return unless @user
if @subject.author == @user
can! :read_note
can! :update_note
can! :admin_note
can! :resolve_note
end
if @subject.for_merge_request? &&
@subject.noteable.author == @user
can! :resolve_note
end
end
end
class PersonalSnippetPolicy < BasePolicy
def rules
can! :read_personal_snippet if @subject.public?
return unless @user
if @subject.author == @user
can! :read_personal_snippet
can! :update_personal_snippet
can! :admin_personal_snippet
end
if @subject.internal? && !@user.external?
can! :read_personal_snippet
end
end
end
class ProjectMemberPolicy < BasePolicy
def rules
# anonymous users have no abilities here
return unless @user
target_user = @subject.user
project = @subject.project
return if target_user == project.owner
can_manage = Ability.allowed?(@user, :admin_project_member, project)
if can_manage
can! :update_project_member
can! :destroy_project_member
end
if @user == target_user
can! :destroy_project_member
end
end
end
class ProjectPolicy < BasePolicy
def rules
team_access!(user)
owner = user.admin? ||
project.owner == user ||
(project.group && project.group.has_owner?(user))
owner_access! if owner
if project.public? || (project.internal? && !user.external?)
guest_access!
public_access!
# Allow to read builds for internal projects
can! :read_build if project.public_builds?
if project.request_access_enabled &&
!(owner || project.team.member?(user) || project_group_member?(user))
can! :request_access
end
end
archived_access! if project.archived?
disabled_features!
end
def project
@subject
end
def guest_access!
can! :read_project
can! :read_board
can! :read_list
can! :read_wiki
can! :read_issue
can! :read_label
can! :read_milestone
can! :read_project_snippet
can! :read_project_member
can! :read_merge_request
can! :read_note
can! :create_project
can! :create_issue
can! :create_note
can! :upload_file
end
def reporter_access!
can! :download_code
can! :fork_project
can! :create_project_snippet
can! :update_issue
can! :admin_issue
can! :admin_label
can! :admin_list
can! :read_commit_status
can! :read_build
can! :read_container_image
can! :read_pipeline
can! :read_environment
can! :read_deployment
end
def developer_access!
can! :admin_merge_request
can! :update_merge_request
can! :create_commit_status
can! :update_commit_status
can! :create_build
can! :update_build
can! :create_pipeline
can! :update_pipeline
can! :create_merge_request
can! :create_wiki
can! :push_code
can! :resolve_note
can! :create_container_image
can! :update_container_image
can! :create_environment
can! :create_deployment
end
def master_access!
can! :push_code_to_protected_branches
can! :update_project_snippet
can! :update_environment
can! :update_deployment
can! :admin_milestone
can! :admin_project_snippet
can! :admin_project_member
can! :admin_merge_request
can! :admin_note
can! :admin_wiki
can! :admin_project
can! :admin_commit_status
can! :admin_build
can! :admin_container_image
can! :admin_pipeline
can! :admin_environment
can! :admin_deployment
end
def public_access!
can! :download_code
can! :fork_project
can! :read_commit_status
can! :read_pipeline
can! :read_container_image
end
def owner_access!
guest_access!
reporter_access!
developer_access!
master_access!
can! :change_namespace
can! :change_visibility_level
can! :rename_project
can! :remove_project
can! :archive_project
can! :remove_fork_project
can! :destroy_merge_request
can! :destroy_issue
end
# Push abilities on the users team role
def team_access!(user)
access = project.team.max_member_access(user.id)
guest_access! if access >= Gitlab::Access::GUEST
reporter_access! if access >= Gitlab::Access::REPORTER
developer_access! if access >= Gitlab::Access::DEVELOPER
master_access! if access >= Gitlab::Access::MASTER
end
def archived_access!
cannot! :create_merge_request
cannot! :push_code
cannot! :push_code_to_protected_branches
cannot! :update_merge_request
cannot! :admin_merge_request
end
def disabled_features!
unless project.issues_enabled
cannot!(*named_abilities(:issue))
end
unless project.merge_requests_enabled
cannot!(*named_abilities(:merge_request))
end
unless project.issues_enabled || project.merge_requests_enabled
cannot!(*named_abilities(:label))
cannot!(*named_abilities(:milestone))
end
unless project.snippets_enabled
cannot!(*named_abilities(:project_snippet))
end
unless project.has_wiki?
cannot!(*named_abilities(:wiki))
end
unless project.builds_enabled
cannot!(*named_abilities(:build))
cannot!(*named_abilities(:pipeline))
cannot!(*named_abilities(:environment))
cannot!(*named_abilities(:deployment))
end
unless project.container_registry_enabled
cannot!(*named_abilities(:container_image))
end
end
def anonymous_rules
return unless project.public?
can! :read_project
can! :read_board
can! :read_list
can! :read_wiki
can! :read_label
can! :read_milestone
can! :read_project_snippet
can! :read_project_member
can! :read_merge_request
can! :read_note
can! :read_pipeline
can! :read_commit_status
can! :read_container_image
can! :download_code
# NOTE: may be overridden by IssuePolicy
can! :read_issue
# Allow to read builds by anonymous user if guests are allowed
can! :read_build if project.public_builds?
disabled_features!
end
def project_group_member?(user)
project.group &&
(
project.group.members.exists?(user_id: user.id) ||
project.group.requesters.exists?(user_id: user.id)
)
end
def named_abilities(name)
[
:"read_#{name}",
:"create_#{name}",
:"update_#{name}",
:"admin_#{name}"
]
end
end
class ProjectSnippetPolicy < BasePolicy
def rules
can! :read_project_snippet if @subject.public?
return unless @user
if @user && @subject.author == @user || @user.admin?
can! :read_project_snippet
can! :update_project_snippet
can! :admin_project_snippet
end
if @subject.internal? && !@user.external?
can! :read_project_snippet
end
if @subject.private? && @subject.project.team.member?(@user)
can! :read_project_snippet
end
end
end
class UserPolicy < BasePolicy
include Gitlab::CurrentSettings
def rules
can! :read_user if @user || !restricted_public_level?
end
def restricted_public_level?
current_application_settings.restricted_visibility_levels.include?(Gitlab::VisibilityLevel::PUBLIC)
end
end
...@@ -7,12 +7,8 @@ class BaseService ...@@ -7,12 +7,8 @@ class BaseService
@project, @current_user, @params = project, user, params.dup @project, @current_user, @params = project, user, params.dup
end end
def abilities
Ability.abilities
end
def can?(object, action, subject) def can?(object, action, subject)
abilities.allowed?(object, action, subject) Ability.allowed?(object, action, subject)
end end
def notification_service def notification_service
......
...@@ -10,13 +10,15 @@ module Ci ...@@ -10,13 +10,15 @@ module Ci
create_builds! create_builds!
end end
new_builds = @pipeline.with_lock do
stage_indexes_of_created_builds.map do |index| new_builds =
process_stage(index) stage_indexes_of_created_builds.map do |index|
end process_stage(index)
end
# Return a flag if a when builds got enqueued # Return a flag if a when builds got enqueued
new_builds.flatten.any? new_builds.flatten.any?
end
end end
private private
......
...@@ -66,7 +66,7 @@ ...@@ -66,7 +66,7 @@
- if @todos.any? - if @todos.any?
.js-todos-options{ data: {per_page: @todos.limit_value, current_page: @todos.current_page, total_pages: @todos.total_pages} } .js-todos-options{ data: {per_page: @todos.limit_value, current_page: @todos.current_page, total_pages: @todos.total_pages} }
- @todos.group_by(&:project).each do |group| - @todos.group_by(&:project).each do |group|
.panel.panel-default.panel-small.js-todos-list .panel.panel-default.panel-small
- project = group[0] - project = group[0]
.panel-heading .panel-heading
= link_to project.name_with_namespace, namespace_project_path(project.namespace, project) = link_to project.name_with_namespace, namespace_project_path(project.namespace, project)
......
- if event.visible_to_user?(current_user) - if event.visible_to_user?(current_user)
.event-item{ class: event_row_class(event) } .event-item{ class: event_row_class(event) }
.event-item-timestamp .event-item-timestamp
#{time_ago_with_tooltip(event.created_at)} #{time_ago_with_tooltip(event.created_at, skip_js: true)}
= cache [event, current_application_settings, "v2.2"] do = cache [event, current_application_settings, "v2.2"] do
= author_avatar(event, size: 40) = author_avatar(event, size: 40)
......
= render 'layouts/nav/group_settings'
.scrolling-tabs-container{ class: nav_control_class } .scrolling-tabs-container{ class: nav_control_class }
= render 'layouts/nav/group_settings'
.fade-left .fade-left
= icon('angle-left') = icon('angle-left')
.fade-right .fade-right
......
- if current_user - if current_user
- can_admin_group = can?(current_user, :admin_group, @group)
- can_edit = can?(current_user, :admin_group, @group) - can_edit = can?(current_user, :admin_group, @group)
- member = @group.members.find_by(user_id: current_user.id) - member = @group.members.find_by(user_id: current_user.id)
- can_leave = member && can?(current_user, :destroy_group_member, member) - can_leave = member && can?(current_user, :destroy_group_member, member)
.controls - if can_admin_group || can_edit || can_leave
.dropdown.group-settings-dropdown .controls
%a.dropdown-new.btn.btn-default#group-settings-button{href: '#', 'data-toggle' => 'dropdown'} .dropdown.group-settings-dropdown
= icon('cog') %a.dropdown-new.btn.btn-default#group-settings-button{href: '#', 'data-toggle' => 'dropdown'}
= icon('caret-down') = icon('cog')
%ul.dropdown-menu.dropdown-menu-align-right = icon('caret-down')
= nav_link(path: 'groups#projects') do %ul.dropdown-menu.dropdown-menu-align-right
= link_to 'Projects', projects_group_path(@group), title: 'Projects' - if can_admin_group
%li.divider = nav_link(path: 'groups#projects') do
- if can_edit = link_to 'Projects', projects_group_path(@group), title: 'Projects'
%li - if can_edit || can_leave
= link_to 'Edit Group', edit_group_path(@group) %li.divider
- if can_leave - if can_edit
%li %li
= link_to polymorphic_path([:leave, @group, :members]), = link_to 'Edit Group', edit_group_path(@group)
data: { confirm: leave_confirmation_message(@group) }, method: :delete, title: 'Leave group' do - if can_leave
Leave Group %li
= link_to polymorphic_path([:leave, @group, :members]),
data: { confirm: leave_confirmation_message(@group) }, method: :delete, title: 'Leave group' do
Leave Group
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
- cache_key = [project.path_with_namespace, commit.id, current_application_settings, note_count] - cache_key = [project.path_with_namespace, commit.id, current_application_settings, note_count]
- cache_key.push(commit.status) if commit.status - cache_key.push(commit.status) if commit.status
= cache(cache_key) do = cache(cache_key, expires_in: 1.day) do
%li.commit.js-toggle-container{ id: "commit-#{commit.short_id}" } %li.commit.js-toggle-container{ id: "commit-#{commit.short_id}" }
= author_avatar(commit, size: 36) = author_avatar(commit, size: 36)
......
...@@ -8,21 +8,24 @@ ...@@ -8,21 +8,24 @@
Tags give the ability to mark specific points in history as being important Tags give the ability to mark specific points in history as being important
.nav-controls .nav-controls
- if can? current_user, :push_code, @project = form_tag(filter_tags_path, method: :get) do
= link_to new_namespace_project_tag_path(@project.namespace, @project), class: 'btn btn-create new-tag-btn' do = search_field_tag :search, params[:search], { placeholder: 'Filter by tag name', id: 'tag-search', class: 'form-control search-text-input input-short', spellcheck: false }
New tag
.dropdown.inline .dropdown.inline
%button.dropdown-toggle.btn{ type: 'button', data: { toggle: 'dropdown'} } %button.dropdown-toggle.btn{ type: 'button', data: { toggle: 'dropdown'} }
%span.light= @sort.humanize %span.light
= @sort.humanize
%b.caret %b.caret
%ul.dropdown-menu.dropdown-menu-align-right %ul.dropdown-menu.dropdown-menu-align-right
%li %li
= link_to namespace_project_tags_path(sort: nil) do = link_to filter_tags_path(sort: nil) do
Name Name
= link_to namespace_project_tags_path(sort: sort_value_recently_updated) do = link_to filter_tags_path(sort: sort_value_recently_updated) do
= sort_title_recently_updated = sort_title_recently_updated
= link_to namespace_project_tags_path(sort: sort_value_oldest_updated) do = link_to filter_tags_path(sort: sort_value_oldest_updated) do
= sort_title_oldest_updated = sort_title_oldest_updated
- if can?(current_user, :push_code, @project)
= link_to new_namespace_project_tag_path(@project.namespace, @project), class: 'btn btn-create new-tag-btn' do
New tag
.tags .tags
- if @tags.any? - if @tags.any?
......
...@@ -12,7 +12,7 @@ ...@@ -12,7 +12,7 @@
- if params[:label_name].present? - if params[:label_name].present?
- if params[:label_name].respond_to?('any?') - if params[:label_name].respond_to?('any?')
- params[:label_name].each do |label| - params[:label_name].each do |label|
= hidden_field_tag "label_name[]", label, id: nil = hidden_field_tag "label_name[]", u(label), id: nil
.dropdown .dropdown
%button.dropdown-menu-toggle.js-label-select.js-multiselect{class: classes.join(' '), type: "button", data: dropdown_data} %button.dropdown-menu-toggle.js-label-select.js-multiselect{class: classes.join(' '), type: "button", data: dropdown_data}
%span.dropdown-toggle-text %span.dropdown-toggle-text
......
...@@ -18,6 +18,7 @@ if Rails.env.production? ...@@ -18,6 +18,7 @@ if Rails.env.production?
# Sanitize fields based on those sanitized from Rails. # Sanitize fields based on those sanitized from Rails.
config.sanitize_fields = Rails.application.config.filter_parameters.map(&:to_s) config.sanitize_fields = Rails.application.config.filter_parameters.map(&:to_s)
config.tags = { program: Gitlab::Sentry.program_context }
end end
end end
end end
# Broadcast Messages
> **Note:** This feature was introduced in GitLab 8.12.
The broadcast message API is only accessible to administrators. All requests by
guests will respond with `401 Unauthorized`, and all requests by normal users
will respond with `403 Forbidden`.
## Get all broadcast messages
```
GET /broadcast_messages
```
```bash
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/broadcast_messages
```
Example response:
```json
[
{
"message":"Example broadcast message",
"starts_at":"2016-08-24T23:21:16.078Z",
"ends_at":"2016-08-26T23:21:16.080Z",
"color":"#E75E40",
"font":"#FFFFFF",
"id":1,
"active": false
}
]
```
## Get a specific broadcast message
```
GET /broadcast_messages/:id
```
| Attribute | Type | Required | Description |
| ----------- | -------- | -------- | ------------------------- |
| `id` | integer | yes | Broadcast message ID |
```bash
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/broadcast_messages/1
```
Example response:
```json
{
"message":"Deploy in progress",
"starts_at":"2016-08-24T23:21:16.078Z",
"ends_at":"2016-08-26T23:21:16.080Z",
"color":"#cecece",
"font":"#FFFFFF",
"id":1,
"active":false
}
```
## Create a broadcast message
Responds with `400 Bad request` when the `message` parameter is missing or the
`color` or `font` values are invalid, and `201 Created` when the broadcast
message was successfully created.
```
POST /broadcast_messages
```
| Attribute | Type | Required | Description |
| ----------- | -------- | -------- | ---------------------------------------------------- |
| `message` | string | yes | Message to display |
| `starts_at` | datetime | no | Starting time (defaults to current time) |
| `ends_at` | datetime | no | Ending time (defaults to one hour from current time) |
| `color` | string | no | Background color hex code |
| `font` | string | no | Foreground color hex code |
```bash
curl --data "message=Deploy in progress&color=#cecece" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/broadcast_messages
```
Example response:
```json
{
"message":"Deploy in progress",
"starts_at":"2016-08-26T00:41:35.060Z",
"ends_at":"2016-08-26T01:41:35.060Z",
"color":"#cecece",
"font":"#FFFFFF",
"id":1,
"active": true
}
```
## Update a broadcast message
```
PUT /broadcast_messages/:id
```
| Attribute | Type | Required | Description |
| ----------- | -------- | -------- | ------------------------- |
| `id` | integer | yes | Broadcast message ID |
| `message` | string | no | Message to display |
| `starts_at` | datetime | no | Starting time |
| `ends_at` | datetime | no | Ending time |
| `color` | string | no | Background color hex code |
| `font` | string | no | Foreground color hex code |
```bash
curl --request PUT --data "message=Update message&color=#000" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/broadcast_messages/1
```
Example response:
```json
{
"message":"Update message",
"starts_at":"2016-08-26T00:41:35.060Z",
"ends_at":"2016-08-26T01:41:35.060Z",
"color":"#000",
"font":"#FFFFFF",
"id":1,
"active": true
}
```
## Delete a broadcast message
```
DELETE /broadcast_messages/:id
```
| Attribute | Type | Required | Description |
| ----------- | -------- | -------- | ------------------------- |
| `id` | integer | yes | Broadcast message ID |
```bash
curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/broadcast_messages/1
```
Example response:
```json
{
"message":"Update message",
"starts_at":"2016-08-26T00:41:35.060Z",
"ends_at":"2016-08-26T01:41:35.060Z",
"color":"#000",
"font":"#FFFFFF",
"id":1,
"active": true
}
```
...@@ -10,7 +10,7 @@ GET /projects/:id/repository/commits ...@@ -10,7 +10,7 @@ GET /projects/:id/repository/commits
| Attribute | Type | Required | Description | | Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- | | --------- | ---- | -------- | ----------- |
| `id` | integer | yes | The ID of a project or NAMESPACE/PROJECT_NAME owned by the authenticated user | `id` | integer/string | yes | The ID of a project or NAMESPACE/PROJECT_NAME owned by the authenticated user
| `ref_name` | string | no | The name of a repository branch or tag or if not given the default branch | | `ref_name` | string | no | The name of a repository branch or tag or if not given the default branch |
| `since` | string | no | Only commits after or in this date will be returned in ISO 8601 format YYYY-MM-DDTHH:MM:SSZ | | `since` | string | no | Only commits after or in this date will be returned in ISO 8601 format YYYY-MM-DDTHH:MM:SSZ |
| `until` | string | no | Only commits before or in this date will be returned in ISO 8601 format YYYY-MM-DDTHH:MM:SSZ | | `until` | string | no | Only commits before or in this date will be returned in ISO 8601 format YYYY-MM-DDTHH:MM:SSZ |
...@@ -58,7 +58,7 @@ Parameters: ...@@ -58,7 +58,7 @@ Parameters:
| Attribute | Type | Required | Description | | Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- | | --------- | ---- | -------- | ----------- |
| `id` | integer | yes | The ID of a project or NAMESPACE/PROJECT_NAME owned by the authenticated user | `id` | integer/string | yes | The ID of a project or NAMESPACE/PROJECT_NAME owned by the authenticated user
| `sha` | string | yes | The commit hash or name of a repository branch or tag | | `sha` | string | yes | The commit hash or name of a repository branch or tag |
```bash ```bash
...@@ -102,7 +102,7 @@ Parameters: ...@@ -102,7 +102,7 @@ Parameters:
| Attribute | Type | Required | Description | | Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- | | --------- | ---- | -------- | ----------- |
| `id` | integer | yes | The ID of a project or NAMESPACE/PROJECT_NAME owned by the authenticated user | `id` | integer/string | yes | The ID of a project or NAMESPACE/PROJECT_NAME owned by the authenticated user
| `sha` | string | yes | The commit hash or name of a repository branch or tag | | `sha` | string | yes | The commit hash or name of a repository branch or tag |
```bash ```bash
...@@ -138,7 +138,7 @@ Parameters: ...@@ -138,7 +138,7 @@ Parameters:
| Attribute | Type | Required | Description | | Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- | | --------- | ---- | -------- | ----------- |
| `id` | integer | yes | The ID of a project or NAMESPACE/PROJECT_NAME owned by the authenticated user | `id` | integer/string | yes | The ID of a project or NAMESPACE/PROJECT_NAME owned by the authenticated user
| `sha` | string | yes | The commit hash or name of a repository branch or tag | | `sha` | string | yes | The commit hash or name of a repository branch or tag |
```bash ```bash
...@@ -187,7 +187,7 @@ POST /projects/:id/repository/commits/:sha/comments ...@@ -187,7 +187,7 @@ POST /projects/:id/repository/commits/:sha/comments
| Attribute | Type | Required | Description | | Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- | | --------- | ---- | -------- | ----------- |
| `id` | integer | yes | The ID of a project or NAMESPACE/PROJECT_NAME owned by the authenticated user | `id` | integer/string | yes | The ID of a project or NAMESPACE/PROJECT_NAME owned by the authenticated user
| `sha` | string | yes | The commit SHA or name of a repository branch or tag | | `sha` | string | yes | The commit SHA or name of a repository branch or tag |
| `note` | string | yes | The text of the comment | | `note` | string | yes | The text of the comment |
| `path` | string | no | The file path relative to the repository | | `path` | string | no | The file path relative to the repository |
...@@ -232,7 +232,7 @@ GET /projects/:id/repository/commits/:sha/statuses ...@@ -232,7 +232,7 @@ GET /projects/:id/repository/commits/:sha/statuses
| Attribute | Type | Required | Description | | Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- | | --------- | ---- | -------- | ----------- |
| `id` | integer | yes | The ID of a project or NAMESPACE/PROJECT_NAME owned by the authenticated user | `id` | integer/string | yes | The ID of a project or NAMESPACE/PROJECT_NAME owned by the authenticated user
| `sha` | string | yes | The commit SHA | `sha` | string | yes | The commit SHA
| `ref_name`| string | no | The name of a repository branch or tag or, if not given, the default branch | `ref_name`| string | no | The name of a repository branch or tag or, if not given, the default branch
| `stage` | string | no | Filter by [build stage](../ci/yaml/README.md#stages), e.g., `test` | `stage` | string | no | Filter by [build stage](../ci/yaml/README.md#stages), e.g., `test`
...@@ -306,7 +306,7 @@ POST /projects/:id/statuses/:sha ...@@ -306,7 +306,7 @@ POST /projects/:id/statuses/:sha
| Attribute | Type | Required | Description | | Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- | | --------- | ---- | -------- | ----------- |
| `id` | integer | yes | The ID of a project or NAMESPACE/PROJECT_NAME owned by the authenticated user | `id` | integer/string | yes | The ID of a project or NAMESPACE/PROJECT_NAME owned by the authenticated user
| `sha` | string | yes | The commit SHA | `sha` | string | yes | The commit SHA
| `state` | string | yes | The state of the status. Can be one of the following: `pending`, `running`, `success`, `failed`, `canceled` | `state` | string | yes | The state of the status. Can be one of the following: `pending`, `running`, `success`, `failed`, `canceled`
| `ref` | string | no | The `ref` (branch or tag) to which the status refers | `ref` | string | no | The `ref` (branch or tag) to which the status refers
......
...@@ -80,7 +80,8 @@ Example response: ...@@ -80,7 +80,8 @@ Example response:
"subscribed" : false, "subscribed" : false,
"user_notes_count": 1, "user_notes_count": 1,
"due_date": "2016-07-22", "due_date": "2016-07-22",
"web_url": "http://example.com/example/example/issues/6" "web_url": "http://example.com/example/example/issues/6",
"confidential": false
} }
] ]
``` ```
...@@ -158,7 +159,8 @@ Example response: ...@@ -158,7 +159,8 @@ Example response:
"subscribed" : false, "subscribed" : false,
"user_notes_count": 1, "user_notes_count": 1,
"due_date": null, "due_date": null,
"web_url": "http://example.com/example/example/issues/1" "web_url": "http://example.com/example/example/issues/1",
"confidential": false
} }
] ]
``` ```
...@@ -238,7 +240,8 @@ Example response: ...@@ -238,7 +240,8 @@ Example response:
"subscribed" : false, "subscribed" : false,
"user_notes_count": 1, "user_notes_count": 1,
"due_date": "2016-07-22", "due_date": "2016-07-22",
"web_url": "http://example.com/example/example/issues/1" "web_url": "http://example.com/example/example/issues/1",
"confidential": false
} }
] ]
``` ```
...@@ -303,7 +306,8 @@ Example response: ...@@ -303,7 +306,8 @@ Example response:
"subscribed": false, "subscribed": false,
"user_notes_count": 1, "user_notes_count": 1,
"due_date": null, "due_date": null,
"web_url": "http://example.com/example/example/issues/1" "web_url": "http://example.com/example/example/issues/1",
"confidential": false
} }
``` ```
...@@ -324,6 +328,7 @@ POST /projects/:id/issues ...@@ -324,6 +328,7 @@ POST /projects/:id/issues
| `id` | integer | yes | The ID of a project | | `id` | integer | yes | The ID of a project |
| `title` | string | yes | The title of an issue | | `title` | string | yes | The title of an issue |
| `description` | string | no | The description of an issue | | `description` | string | no | The description of an issue |
| `confidential` | boolean | no | Set an issue to be confidential. Default is `false`. |
| `assignee_id` | integer | no | The ID of a user to assign issue | | `assignee_id` | integer | no | The ID of a user to assign issue |
| `milestone_id` | integer | no | The ID of a milestone to assign issue | | `milestone_id` | integer | no | The ID of a milestone to assign issue |
| `labels` | string | no | Comma-separated label names for an issue | | `labels` | string | no | Comma-separated label names for an issue |
...@@ -362,7 +367,8 @@ Example response: ...@@ -362,7 +367,8 @@ Example response:
"subscribed" : true, "subscribed" : true,
"user_notes_count": 0, "user_notes_count": 0,
"due_date": null, "due_date": null,
"web_url": "http://example.com/example/example/issues/14" "web_url": "http://example.com/example/example/issues/14",
"confidential": false
} }
``` ```
...@@ -385,6 +391,7 @@ PUT /projects/:id/issues/:issue_id ...@@ -385,6 +391,7 @@ PUT /projects/:id/issues/:issue_id
| `issue_id` | integer | yes | The ID of a project's issue | | `issue_id` | integer | yes | The ID of a project's issue |
| `title` | string | no | The title of an issue | | `title` | string | no | The title of an issue |
| `description` | string | no | The description of an issue | | `description` | string | no | The description of an issue |
| `confidential` | boolean | no | Updates an issue to be confidential |
| `assignee_id` | integer | no | The ID of a user to assign the issue to | | `assignee_id` | integer | no | The ID of a user to assign the issue to |
| `milestone_id` | integer | no | The ID of a milestone to assign the issue to | | `milestone_id` | integer | no | The ID of a milestone to assign the issue to |
| `labels` | string | no | Comma-separated label names for an issue | | `labels` | string | no | Comma-separated label names for an issue |
...@@ -424,7 +431,8 @@ Example response: ...@@ -424,7 +431,8 @@ Example response:
"subscribed" : true, "subscribed" : true,
"user_notes_count": 0, "user_notes_count": 0,
"due_date": "2016-07-22", "due_date": "2016-07-22",
"web_url": "http://example.com/example/example/issues/15" "web_url": "http://example.com/example/example/issues/15",
"confidential": false
} }
``` ```
...@@ -503,7 +511,8 @@ Example response: ...@@ -503,7 +511,8 @@ Example response:
"web_url": "https://gitlab.example.com/u/solon.cremin" "web_url": "https://gitlab.example.com/u/solon.cremin"
}, },
"due_date": null, "due_date": null,
"web_url": "http://example.com/example/example/issues/11" "web_url": "http://example.com/example/example/issues/11",
"confidential": false
} }
``` ```
...@@ -559,7 +568,8 @@ Example response: ...@@ -559,7 +568,8 @@ Example response:
"web_url": "https://gitlab.example.com/u/solon.cremin" "web_url": "https://gitlab.example.com/u/solon.cremin"
}, },
"due_date": null, "due_date": null,
"web_url": "http://example.com/example/example/issues/11" "web_url": "http://example.com/example/example/issues/11",
"confidential": false
} }
``` ```
...@@ -616,7 +626,8 @@ Example response: ...@@ -616,7 +626,8 @@ Example response:
}, },
"subscribed": false, "subscribed": false,
"due_date": null, "due_date": null,
"web_url": "http://example.com/example/example/issues/12" "web_url": "http://example.com/example/example/issues/12",
"confidential": false
} }
``` ```
...@@ -704,7 +715,8 @@ Example response: ...@@ -704,7 +715,8 @@ Example response:
"upvotes": 0, "upvotes": 0,
"downvotes": 0, "downvotes": 0,
"due_date": null, "due_date": null,
"web_url": "http://example.com/example/example/issues/110" "web_url": "http://example.com/example/example/issues/110",
"confidential": false
}, },
"target_url": "https://gitlab.example.com/gitlab-org/gitlab-ci/issues/10", "target_url": "https://gitlab.example.com/gitlab-org/gitlab-ci/issues/10",
"body": "Vel voluptas atque dicta mollitia adipisci qui at.", "body": "Vel voluptas atque dicta mollitia adipisci qui at.",
......
...@@ -535,7 +535,7 @@ POST /projects/:id/star ...@@ -535,7 +535,7 @@ POST /projects/:id/star
| Attribute | Type | Required | Description | | Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- | | --------- | ---- | -------- | ----------- |
| `id` | integer | yes | The ID of the project or NAMESPACE/PROJECT_NAME | | `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
```bash ```bash
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/star" curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/star"
...@@ -602,7 +602,7 @@ DELETE /projects/:id/star ...@@ -602,7 +602,7 @@ DELETE /projects/:id/star
| Attribute | Type | Required | Description | | Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- | | --------- | ---- | -------- | ----------- |
| `id` | integer | yes | The ID of the project or NAMESPACE/PROJECT_NAME | | `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
```bash ```bash
curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/star" curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/star"
...@@ -673,7 +673,7 @@ POST /projects/:id/archive ...@@ -673,7 +673,7 @@ POST /projects/:id/archive
| Attribute | Type | Required | Description | | Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- | | --------- | ---- | -------- | ----------- |
| `id` | integer | yes | The ID of the project or NAMESPACE/PROJECT_NAME | | `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
```bash ```bash
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/archive" curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/archive"
...@@ -760,7 +760,7 @@ POST /projects/:id/unarchive ...@@ -760,7 +760,7 @@ POST /projects/:id/unarchive
| Attribute | Type | Required | Description | | Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- | | --------- | ---- | -------- | ----------- |
| `id` | integer | yes | The ID of the project or NAMESPACE/PROJECT_NAME | | `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
```bash ```bash
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/unarchive" curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/unarchive"
......
...@@ -54,6 +54,7 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps ...@@ -54,6 +54,7 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps
page.within('.todos-pending-count') { expect(page).to have_content '0' } page.within('.todos-pending-count') { expect(page).to have_content '0' }
expect(page).to have_content 'To do 0' expect(page).to have_content 'To do 0'
expect(page).to have_content 'Done 4' expect(page).to have_content 'Done 4'
expect(page).to have_content "You're all done!"
expect(page).not_to have_link project.name_with_namespace expect(page).not_to have_link project.name_with_namespace
should_not_see_todo "John Doe assigned you merge request #{merge_request.to_reference}" should_not_see_todo "John Doe assigned you merge request #{merge_request.to_reference}"
should_not_see_todo "John Doe mentioned you on issue #{issue.to_reference}" should_not_see_todo "John Doe mentioned you on issue #{issue.to_reference}"
......
...@@ -31,6 +31,7 @@ module API ...@@ -31,6 +31,7 @@ module API
mount ::API::AccessRequests mount ::API::AccessRequests
mount ::API::AwardEmoji mount ::API::AwardEmoji
mount ::API::Branches mount ::API::Branches
mount ::API::BroadcastMessages
mount ::API::Builds mount ::API::Builds
mount ::API::CommitStatuses mount ::API::CommitStatuses
mount ::API::Commits mount ::API::Commits
......
module API
class BroadcastMessages < Grape::API
before { authenticate! }
before { authenticated_as_admin! }
resource :broadcast_messages do
helpers do
def find_message
BroadcastMessage.find(params[:id])
end
end
desc 'Get all broadcast messages' do
detail 'This feature was introduced in GitLab 8.12.'
success Entities::BroadcastMessage
end
params do
optional :page, type: Integer, desc: 'Current page number'
optional :per_page, type: Integer, desc: 'Number of messages per page'
end
get do
messages = BroadcastMessage.all
present paginate(messages), with: Entities::BroadcastMessage
end
desc 'Create a broadcast message' do
detail 'This feature was introduced in GitLab 8.12.'
success Entities::BroadcastMessage
end
params do
requires :message, type: String, desc: 'Message to display'
optional :starts_at, type: DateTime, desc: 'Starting time', default: -> { Time.zone.now }
optional :ends_at, type: DateTime, desc: 'Ending time', default: -> { 1.hour.from_now }
optional :color, type: String, desc: 'Background color'
optional :font, type: String, desc: 'Foreground color'
end
post do
create_params = declared(params, include_missing: false).to_h
message = BroadcastMessage.create(create_params)
if message.persisted?
present message, with: Entities::BroadcastMessage
else
render_validation_error!(message)
end
end
desc 'Get a specific broadcast message' do
detail 'This feature was introduced in GitLab 8.12.'
success Entities::BroadcastMessage
end
params do
requires :id, type: Integer, desc: 'Broadcast message ID'
end
get ':id' do
message = find_message
present message, with: Entities::BroadcastMessage
end
desc 'Update a broadcast message' do
detail 'This feature was introduced in GitLab 8.12.'
success Entities::BroadcastMessage
end
params do
requires :id, type: Integer, desc: 'Broadcast message ID'
optional :message, type: String, desc: 'Message to display'
optional :starts_at, type: DateTime, desc: 'Starting time'
optional :ends_at, type: DateTime, desc: 'Ending time'
optional :color, type: String, desc: 'Background color'
optional :font, type: String, desc: 'Foreground color'
end
put ':id' do
message = find_message
update_params = declared(params, include_missing: false).to_h
if message.update(update_params)
present message, with: Entities::BroadcastMessage
else
render_validation_error!(message)
end
end
desc 'Delete a broadcast message' do
detail 'This feature was introduced in GitLab 8.12.'
success Entities::BroadcastMessage
end
params do
requires :id, type: Integer, desc: 'Broadcast message ID'
end
delete ':id' do
message = find_message
present message.destroy, with: Entities::BroadcastMessage
end
end
end
end
...@@ -211,6 +211,7 @@ module API ...@@ -211,6 +211,7 @@ module API
expose :user_notes_count expose :user_notes_count
expose :upvotes, :downvotes expose :upvotes, :downvotes
expose :due_date expose :due_date
expose :confidential
expose :web_url do |issue, options| expose :web_url do |issue, options|
Gitlab::UrlBuilder.build(issue) Gitlab::UrlBuilder.build(issue)
...@@ -574,5 +575,10 @@ module API ...@@ -574,5 +575,10 @@ module API
class Template < Grape::Entity class Template < Grape::Entity
expose :name, :content expose :name, :content
end end
class BroadcastMessage < Grape::Entity
expose :id, :message, :starts_at, :ends_at, :color, :font
expose :active?, as: :active
end
end end
end end
...@@ -30,7 +30,7 @@ module API ...@@ -30,7 +30,7 @@ module API
# Example Request: # Example Request:
# POST /groups # POST /groups
post do post do
authorize! :create_group, current_user authorize! :create_group
required_attributes! [:name, :path] required_attributes! [:name, :path]
attrs = attributes_for_keys [:name, :path, :description, :visibility_level] attrs = attributes_for_keys [:name, :path, :description, :visibility_level]
......
...@@ -129,7 +129,7 @@ module API ...@@ -129,7 +129,7 @@ module API
forbidden! unless current_user.is_admin? forbidden! unless current_user.is_admin?
end end
def authorize!(action, subject) def authorize!(action, subject = nil)
forbidden! unless can?(current_user, action, subject) forbidden! unless can?(current_user, action, subject)
end end
...@@ -148,7 +148,7 @@ module API ...@@ -148,7 +148,7 @@ module API
end end
def can?(object, action, subject) def can?(object, action, subject)
abilities.allowed?(object, action, subject) Ability.allowed?(object, action, subject)
end end
# Checks the occurrences of required attributes, each attribute must be present in the params hash # Checks the occurrences of required attributes, each attribute must be present in the params hash
...@@ -408,14 +408,6 @@ module API ...@@ -408,14 +408,6 @@ module API
links.join(', ') links.join(', ')
end end
def abilities
@abilities ||= begin
abilities = Six.new
abilities << Ability
abilities
end
end
def secret_token def secret_token
File.read(Gitlab.config.gitlab_shell.secret_file).chomp File.read(Gitlab.config.gitlab_shell.secret_file).chomp
end end
......
...@@ -140,12 +140,13 @@ module API ...@@ -140,12 +140,13 @@ module API
# labels (optional) - The labels of an issue # labels (optional) - The labels of an issue
# created_at (optional) - Date time string, ISO 8601 formatted # created_at (optional) - Date time string, ISO 8601 formatted
# due_date (optional) - Date time string in the format YEAR-MONTH-DAY # due_date (optional) - Date time string in the format YEAR-MONTH-DAY
# confidential (optional) - Boolean parameter if the issue should be confidential
# Example Request: # Example Request:
# POST /projects/:id/issues # POST /projects/:id/issues
post ':id/issues' do post ':id/issues' do
required_attributes! [:title] required_attributes! [:title]
keys = [:title, :description, :assignee_id, :milestone_id, :due_date] keys = [:title, :description, :assignee_id, :milestone_id, :due_date, :confidential]
keys << :created_at if current_user.admin? || user_project.owner == current_user keys << :created_at if current_user.admin? || user_project.owner == current_user
attrs = attributes_for_keys(keys) attrs = attributes_for_keys(keys)
...@@ -156,6 +157,10 @@ module API ...@@ -156,6 +157,10 @@ module API
attrs[:labels] = params[:labels] if params[:labels] attrs[:labels] = params[:labels] if params[:labels]
# Convert and filter out invalid confidential flags
attrs['confidential'] = to_boolean(attrs['confidential'])
attrs.delete('confidential') if attrs['confidential'].nil?
issue = ::Issues::CreateService.new(user_project, current_user, attrs.merge(request: request, api: true)).execute issue = ::Issues::CreateService.new(user_project, current_user, attrs.merge(request: request, api: true)).execute
if issue.spam? if issue.spam?
...@@ -182,12 +187,13 @@ module API ...@@ -182,12 +187,13 @@ module API
# state_event (optional) - The state event of an issue (close|reopen) # state_event (optional) - The state event of an issue (close|reopen)
# updated_at (optional) - Date time string, ISO 8601 formatted # updated_at (optional) - Date time string, ISO 8601 formatted
# due_date (optional) - Date time string in the format YEAR-MONTH-DAY # due_date (optional) - Date time string in the format YEAR-MONTH-DAY
# confidential (optional) - Boolean parameter if the issue should be confidential
# Example Request: # Example Request:
# PUT /projects/:id/issues/:issue_id # PUT /projects/:id/issues/:issue_id
put ':id/issues/:issue_id' do put ':id/issues/:issue_id' do
issue = user_project.issues.find(params[:issue_id]) issue = user_project.issues.find(params[:issue_id])
authorize! :update_issue, issue authorize! :update_issue, issue
keys = [:title, :description, :assignee_id, :milestone_id, :state_event, :due_date] keys = [:title, :description, :assignee_id, :milestone_id, :state_event, :due_date, :confidential]
keys << :updated_at if current_user.admin? || user_project.owner == current_user keys << :updated_at if current_user.admin? || user_project.owner == current_user
attrs = attributes_for_keys(keys) attrs = attributes_for_keys(keys)
...@@ -198,6 +204,10 @@ module API ...@@ -198,6 +204,10 @@ module API
attrs[:labels] = params[:labels] if params[:labels] attrs[:labels] = params[:labels] if params[:labels]
# Convert and filter out invalid confidential flags
attrs['confidential'] = to_boolean(attrs['confidential'])
attrs.delete('confidential') if attrs['confidential'].nil?
issue = ::Issues::UpdateService.new(user_project, current_user, attrs).execute(issue) issue = ::Issues::UpdateService.new(user_project, current_user, attrs).execute(issue)
if issue.valid? if issue.valid?
......
...@@ -211,7 +211,7 @@ module Banzai ...@@ -211,7 +211,7 @@ module Banzai
end end
def can?(user, permission, subject) def can?(user, permission, subject)
Ability.abilities.allowed?(user, permission, subject) Ability.allowed?(user, permission, subject)
end end
def find_projects_for_hash_keys(hash) def find_projects_for_hash_keys(hash)
......
...@@ -152,12 +152,14 @@ module Gitlab ...@@ -152,12 +152,14 @@ module Gitlab
end end
def create_comments(issuable, comments) def create_comments(issuable, comments)
comments.each do |raw| ActiveRecord::Base.no_touching do
begin comments.each do |raw|
comment = CommentFormatter.new(project, raw) begin
issuable.notes.create!(comment.attributes) comment = CommentFormatter.new(project, raw)
rescue => e issuable.notes.create!(comment.attributes)
errors << { type: :comment, url: Gitlab::UrlSanitizer.sanitize(raw.url), errors: e.message } rescue => e
errors << { type: :comment, url: Gitlab::UrlSanitizer.sanitize(raw.url), errors: e.message }
end
end end
end end
end end
......
...@@ -12,7 +12,7 @@ module Gitlab ...@@ -12,7 +12,7 @@ module Gitlab
author_id: author_id, author_id: author_id,
assignee_id: assignee_id, assignee_id: assignee_id,
created_at: raw_data.created_at, created_at: raw_data.created_at,
updated_at: updated_at updated_at: raw_data.updated_at
} }
end end
...@@ -69,10 +69,6 @@ module Gitlab ...@@ -69,10 +69,6 @@ module Gitlab
def state def state
raw_data.state == 'closed' ? 'closed' : 'opened' raw_data.state == 'closed' ? 'closed' : 'opened'
end end
def updated_at
state == 'closed' ? raw_data.closed_at : raw_data.updated_at
end
end end
end end
end end
...@@ -3,14 +3,14 @@ module Gitlab ...@@ -3,14 +3,14 @@ module Gitlab
class MilestoneFormatter < BaseFormatter class MilestoneFormatter < BaseFormatter
def attributes def attributes
{ {
iid: number, iid: raw_data.number,
project: project, project: project,
title: title, title: raw_data.title,
description: description, description: raw_data.description,
due_date: due_date, due_date: raw_data.due_on,
state: state, state: state,
created_at: created_at, created_at: raw_data.created_at,
updated_at: updated_at updated_at: raw_data.updated_at
} }
end end
...@@ -20,33 +20,9 @@ module Gitlab ...@@ -20,33 +20,9 @@ module Gitlab
private private
def number
raw_data.number
end
def title
raw_data.title
end
def description
raw_data.description
end
def due_date
raw_data.due_on
end
def state def state
raw_data.state == 'closed' ? 'closed' : 'active' raw_data.state == 'closed' ? 'closed' : 'active'
end end
def created_at
raw_data.created_at
end
def updated_at
state == 'closed' ? raw_data.closed_at : raw_data.updated_at
end
end end
end end
end end
...@@ -17,7 +17,7 @@ module Gitlab ...@@ -17,7 +17,7 @@ module Gitlab
path: repo.name, path: repo.name,
description: repo.description, description: repo.description,
namespace_id: namespace.id, namespace_id: namespace.id,
visibility_level: repo.private ? Gitlab::VisibilityLevel::PRIVATE : Gitlab::VisibilityLevel::PUBLIC, visibility_level: repo.private ? Gitlab::VisibilityLevel::PRIVATE : ApplicationSetting.current.default_project_visibility,
import_type: "github", import_type: "github",
import_source: repo.full_name, import_source: repo.full_name,
import_url: repo.clone_url.sub("https://", "https://#{@session_data[:github_access_token]}@"), import_url: repo.clone_url.sub("https://", "https://#{@session_data[:github_access_token]}@"),
......
...@@ -20,7 +20,7 @@ module Gitlab ...@@ -20,7 +20,7 @@ module Gitlab
author_id: author_id, author_id: author_id,
assignee_id: assignee_id, assignee_id: assignee_id,
created_at: raw_data.created_at, created_at: raw_data.created_at,
updated_at: updated_at updated_at: raw_data.updated_at
} }
end end
...@@ -103,15 +103,6 @@ module Gitlab ...@@ -103,15 +103,6 @@ module Gitlab
'opened' 'opened'
end end
end end
def updated_at
case state
when 'merged' then raw_data.merged_at
when 'closed' then raw_data.closed_at
else
raw_data.updated_at
end
end
end end
end end
end end
module Gitlab
module Sentry
def self.enabled?
Rails.env.production? && current_application_settings.sentry_enabled?
end
def self.context(current_user = nil)
return unless self.enabled?
if current_user
Raven.user_context(
id: current_user.id,
email: current_user.email,
username: current_user.username,
)
end
end
def self.program_context
if Sidekiq.server?
'sidekiq'
else
'rails'
end
end
end
end
...@@ -41,8 +41,8 @@ describe Projects::Boards::IssuesController do ...@@ -41,8 +41,8 @@ describe Projects::Boards::IssuesController do
context 'with unauthorized user' do context 'with unauthorized user' do
before do before do
allow(Ability.abilities).to receive(:allowed?).with(user, :read_project, project).and_return(true) allow(Ability).to receive(:allowed?).with(user, :read_project, project).and_return(true)
allow(Ability.abilities).to receive(:allowed?).with(user, :read_issue, project).and_return(false) allow(Ability).to receive(:allowed?).with(user, :read_issue, project).and_return(false)
end end
it 'returns a successful 403 response' do it 'returns a successful 403 response' do
......
...@@ -35,8 +35,8 @@ describe Projects::Boards::ListsController do ...@@ -35,8 +35,8 @@ describe Projects::Boards::ListsController do
context 'with unauthorized user' do context 'with unauthorized user' do
before do before do
allow(Ability.abilities).to receive(:allowed?).with(user, :read_project, project).and_return(true) allow(Ability).to receive(:allowed?).with(user, :read_project, project).and_return(true)
allow(Ability.abilities).to receive(:allowed?).with(user, :read_list, project).and_return(false) allow(Ability).to receive(:allowed?).with(user, :read_list, project).and_return(false)
end end
it 'returns a forbidden 403 response' do it 'returns a forbidden 403 response' do
......
...@@ -23,8 +23,8 @@ describe Projects::BoardsController do ...@@ -23,8 +23,8 @@ describe Projects::BoardsController do
context 'with unauthorized user' do context 'with unauthorized user' do
before do before do
allow(Ability.abilities).to receive(:allowed?).with(user, :read_project, project).and_return(true) allow(Ability).to receive(:allowed?).with(user, :read_project, project).and_return(true)
allow(Ability.abilities).to receive(:allowed?).with(user, :read_board, project).and_return(false) allow(Ability).to receive(:allowed?).with(user, :read_board, project).and_return(false)
end end
it 'returns a successful 404 response' do it 'returns a successful 404 response' do
......
...@@ -8,6 +8,7 @@ describe 'Filter issues', feature: true do ...@@ -8,6 +8,7 @@ describe 'Filter issues', feature: true do
let!(:milestone) { create(:milestone, project: project) } let!(:milestone) { create(:milestone, project: project) }
let!(:label) { create(:label, project: project) } let!(:label) { create(:label, project: project) }
let!(:issue1) { create(:issue, project: project) } let!(:issue1) { create(:issue, project: project) }
let!(:wontfix) { create(:label, project: project, title: "Won't fix") }
before do before do
project.team << [user, :master] project.team << [user, :master]
...@@ -107,6 +108,15 @@ describe 'Filter issues', feature: true do ...@@ -107,6 +108,15 @@ describe 'Filter issues', feature: true do
end end
expect(find('.js-label-select .dropdown-toggle-text')).to have_content(label.title) expect(find('.js-label-select .dropdown-toggle-text')).to have_content(label.title)
end end
it 'filters by wont fix labels' do
find('.dropdown-menu-labels a', text: label.title).click
page.within '.labels-filter' do
expect(page).to have_content wontfix.title
click_link wontfix.title
end
expect(find('.js-label-select .dropdown-toggle-text')).to have_content(wontfix.title)
end
end end
describe 'Filter issues for assignee and label from issues#index' do describe 'Filter issues for assignee and label from issues#index' do
......
...@@ -118,6 +118,20 @@ describe 'Dashboard Todos', feature: true do ...@@ -118,6 +118,20 @@ describe 'Dashboard Todos', feature: true do
expect(page).to have_css("#todo_#{Todo.first.id}") expect(page).to have_css("#todo_#{Todo.first.id}")
end end
end end
describe 'mark all as done', js: true do
before do
visit dashboard_todos_path
click_link('Mark all as done')
end
it 'shows "All done" message!' do
within('.todos-pending-count') { expect(page).to have_content '0' }
expect(page).to have_content 'To do 0'
expect(page).to have_content "You're all done!"
expect(page).not_to have_selector('.gl-pagination')
end
end
end end
context 'User has a Todo in a project pending deletion' do context 'User has a Todo in a project pending deletion' do
......
require 'spec_helper'
describe TagsFinder do
let(:user) { create(:user) }
let(:project) { create(:project) }
let(:repository) { project.repository }
describe '#execute' do
context 'sort only' do
it 'sorts by name' do
tags_finder = described_class.new(repository, {})
result = tags_finder.execute
expect(result.first.name).to eq("v1.0.0")
end
it 'sorts by recently_updated' do
tags_finder = described_class.new(repository, { sort: 'updated_desc' })
result = tags_finder.execute
recently_updated_tag = repository.tags.max do |a, b|
repository.commit(a.target).committed_date <=> repository.commit(b.target).committed_date
end
expect(result.first.name).to eq(recently_updated_tag.name)
end
it 'sorts by last_updated' do
tags_finder = described_class.new(repository, { sort: 'updated_asc' })
result = tags_finder.execute
expect(result.first.name).to eq('v1.0.0')
end
end
context 'filter only' do
it 'filters tags by name' do
tags_finder = described_class.new(repository, { search: '1.0.0' })
result = tags_finder.execute
expect(result.first.name).to eq('v1.0.0')
expect(result.count).to eq(1)
end
it 'does not find any tags with that name' do
tags_finder = described_class.new(repository, { search: 'hey' })
result = tags_finder.execute
expect(result.count).to eq(0)
end
end
context 'filter and sort' do
it 'filters tags by name and sorts by recently_updated' do
params = { sort: 'updated_desc', search: 'v1' }
tags_finder = described_class.new(repository, params)
result = tags_finder.execute
expect(result.first.name).to eq('v1.1.0')
expect(result.count).to eq(2)
end
it 'filters tags by name and sorts by last_updated' do
params = { sort: 'updated_asc', search: 'v1' }
tags_finder = described_class.new(repository, params)
result = tags_finder.execute
expect(result.first.name).to eq('v1.0.0')
expect(result.count).to eq(2)
end
end
end
end
...@@ -13,17 +13,21 @@ ...@@ -13,17 +13,21 @@
gl.utils.preventDisabledButtons(); gl.utils.preventDisabledButtons();
isClicked = false; isClicked = false;
$button = $('#test-button'); $button = $('#test-button');
expect($button).toExist();
$button.click(function() { $button.click(function() {
return isClicked = true; return isClicked = true;
}); });
$button.trigger('click'); $button.trigger('click');
return expect(isClicked).toBe(false); return expect(isClicked).toBe(false);
}); });
return it('should be on the same page if a disabled link clicked', function() {
var locationBeforeLinkClick; it('should be on the same page if a disabled link clicked', function() {
var locationBeforeLinkClick, $link;
locationBeforeLinkClick = window.location.href; locationBeforeLinkClick = window.location.href;
gl.utils.preventDisabledButtons(); gl.utils.preventDisabledButtons();
$('#test-link').click(); $link = $('#test-link');
expect($link).toExist();
$link.click();
return expect(window.location.href).toBe(locationBeforeLinkClick); return expect(window.location.href).toBe(locationBeforeLinkClick);
}); });
}); });
......
...@@ -30,7 +30,7 @@ describe Banzai::ReferenceParser::BaseParser, lib: true do ...@@ -30,7 +30,7 @@ describe Banzai::ReferenceParser::BaseParser, lib: true do
it 'returns the nodes if the attribute value equals the current project ID' do it 'returns the nodes if the attribute value equals the current project ID' do
link['data-project'] = project.id.to_s link['data-project'] = project.id.to_s
expect(Ability.abilities).not_to receive(:allowed?) expect(Ability).not_to receive(:allowed?)
expect(subject.nodes_visible_to_user(user, [link])).to eq([link]) expect(subject.nodes_visible_to_user(user, [link])).to eq([link])
end end
...@@ -39,7 +39,7 @@ describe Banzai::ReferenceParser::BaseParser, lib: true do ...@@ -39,7 +39,7 @@ describe Banzai::ReferenceParser::BaseParser, lib: true do
link['data-project'] = other_project.id.to_s link['data-project'] = other_project.id.to_s
expect(Ability.abilities).to receive(:allowed?). expect(Ability).to receive(:allowed?).
with(user, :read_project, other_project). with(user, :read_project, other_project).
and_return(true) and_return(true)
...@@ -57,7 +57,7 @@ describe Banzai::ReferenceParser::BaseParser, lib: true do ...@@ -57,7 +57,7 @@ describe Banzai::ReferenceParser::BaseParser, lib: true do
link['data-project'] = other_project.id.to_s link['data-project'] = other_project.id.to_s
expect(Ability.abilities).to receive(:allowed?). expect(Ability).to receive(:allowed?).
with(user, :read_project, other_project). with(user, :read_project, other_project).
and_return(false) and_return(false)
...@@ -221,7 +221,7 @@ describe Banzai::ReferenceParser::BaseParser, lib: true do ...@@ -221,7 +221,7 @@ describe Banzai::ReferenceParser::BaseParser, lib: true do
it 'delegates the permissions check to the Ability class' do it 'delegates the permissions check to the Ability class' do
user = double(:user) user = double(:user)
expect(Ability.abilities).to receive(:allowed?). expect(Ability).to receive(:allowed?).
with(user, :read_project, project) with(user, :read_project, project)
subject.can?(user, :read_project, project) subject.can?(user, :read_project, project)
......
...@@ -82,7 +82,7 @@ describe Banzai::ReferenceParser::UserParser, lib: true do ...@@ -82,7 +82,7 @@ describe Banzai::ReferenceParser::UserParser, lib: true do
end end
it 'returns the nodes if the user can read the group' do it 'returns the nodes if the user can read the group' do
expect(Ability.abilities).to receive(:allowed?). expect(Ability).to receive(:allowed?).
with(user, :read_group, group). with(user, :read_group, group).
and_return(true) and_return(true)
...@@ -90,7 +90,7 @@ describe Banzai::ReferenceParser::UserParser, lib: true do ...@@ -90,7 +90,7 @@ describe Banzai::ReferenceParser::UserParser, lib: true do
end end
it 'returns an empty Array if the user can not read the group' do it 'returns an empty Array if the user can not read the group' do
expect(Ability.abilities).to receive(:allowed?). expect(Ability).to receive(:allowed?).
with(user, :read_group, group). with(user, :read_group, group).
and_return(false) and_return(false)
...@@ -103,7 +103,7 @@ describe Banzai::ReferenceParser::UserParser, lib: true do ...@@ -103,7 +103,7 @@ describe Banzai::ReferenceParser::UserParser, lib: true do
it 'returns the nodes if the attribute value equals the current project ID' do it 'returns the nodes if the attribute value equals the current project ID' do
link['data-project'] = project.id.to_s link['data-project'] = project.id.to_s
expect(Ability.abilities).not_to receive(:allowed?) expect(Ability).not_to receive(:allowed?)
expect(subject.nodes_visible_to_user(user, [link])).to eq([link]) expect(subject.nodes_visible_to_user(user, [link])).to eq([link])
end end
...@@ -113,7 +113,7 @@ describe Banzai::ReferenceParser::UserParser, lib: true do ...@@ -113,7 +113,7 @@ describe Banzai::ReferenceParser::UserParser, lib: true do
link['data-project'] = other_project.id.to_s link['data-project'] = other_project.id.to_s
expect(Ability.abilities).to receive(:allowed?). expect(Ability).to receive(:allowed?).
with(user, :read_project, other_project). with(user, :read_project, other_project).
and_return(true) and_return(true)
...@@ -125,7 +125,7 @@ describe Banzai::ReferenceParser::UserParser, lib: true do ...@@ -125,7 +125,7 @@ describe Banzai::ReferenceParser::UserParser, lib: true do
link['data-project'] = other_project.id.to_s link['data-project'] = other_project.id.to_s
expect(Ability.abilities).to receive(:allowed?). expect(Ability).to receive(:allowed?).
with(user, :read_project, other_project). with(user, :read_project, other_project).
and_return(false) and_return(false)
......
...@@ -48,8 +48,7 @@ describe Gitlab::GithubImport::IssueFormatter, lib: true do ...@@ -48,8 +48,7 @@ describe Gitlab::GithubImport::IssueFormatter, lib: true do
end end
context 'when issue is closed' do context 'when issue is closed' do
let(:closed_at) { DateTime.strptime('2011-01-28T19:01:12Z') } let(:raw_data) { double(base_data.merge(state: 'closed')) }
let(:raw_data) { double(base_data.merge(state: 'closed', closed_at: closed_at)) }
it 'returns formatted attributes' do it 'returns formatted attributes' do
expected = { expected = {
...@@ -62,7 +61,7 @@ describe Gitlab::GithubImport::IssueFormatter, lib: true do ...@@ -62,7 +61,7 @@ describe Gitlab::GithubImport::IssueFormatter, lib: true do
author_id: project.creator_id, author_id: project.creator_id,
assignee_id: nil, assignee_id: nil,
created_at: created_at, created_at: created_at,
updated_at: closed_at updated_at: updated_at
} }
expect(issue.attributes).to eq(expected) expect(issue.attributes).to eq(expected)
......
...@@ -40,8 +40,7 @@ describe Gitlab::GithubImport::MilestoneFormatter, lib: true do ...@@ -40,8 +40,7 @@ describe Gitlab::GithubImport::MilestoneFormatter, lib: true do
end end
context 'when milestone is closed' do context 'when milestone is closed' do
let(:closed_at) { DateTime.strptime('2011-01-28T19:01:12Z') } let(:raw_data) { double(base_data.merge(state: 'closed')) }
let(:raw_data) { double(base_data.merge(state: 'closed', closed_at: closed_at)) }
it 'returns formatted attributes' do it 'returns formatted attributes' do
expected = { expected = {
...@@ -52,7 +51,7 @@ describe Gitlab::GithubImport::MilestoneFormatter, lib: true do ...@@ -52,7 +51,7 @@ describe Gitlab::GithubImport::MilestoneFormatter, lib: true do
state: 'closed', state: 'closed',
due_date: nil, due_date: nil,
created_at: created_at, created_at: created_at,
updated_at: closed_at updated_at: updated_at
} }
expect(formatter.attributes).to eq(expected) expect(formatter.attributes).to eq(expected)
......
...@@ -2,33 +2,59 @@ require 'spec_helper' ...@@ -2,33 +2,59 @@ require 'spec_helper'
describe Gitlab::GithubImport::ProjectCreator, lib: true do describe Gitlab::GithubImport::ProjectCreator, lib: true do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:namespace) { create(:group, owner: user) }
let(:repo) do let(:repo) do
OpenStruct.new( OpenStruct.new(
login: 'vim', login: 'vim',
name: 'vim', name: 'vim',
private: true,
full_name: 'asd/vim', full_name: 'asd/vim',
clone_url: "https://gitlab.com/asd/vim.git", clone_url: 'https://gitlab.com/asd/vim.git'
owner: OpenStruct.new(login: "john")
) )
end end
let(:namespace) { create(:group, owner: user) }
let(:token) { "asdffg" } subject(:service) { described_class.new(repo, namespace, user, github_access_token: 'asdffg') }
let(:access_params) { { github_access_token: token } }
before do before do
namespace.add_owner(user) namespace.add_owner(user)
allow_any_instance_of(Project).to receive(:add_import_job)
end end
it 'creates project' do describe '#execute' do
allow_any_instance_of(Project).to receive(:add_import_job) it 'creates a project' do
expect { service.execute }.to change(Project, :count).by(1)
end
it 'handle GitHub credentials' do
project = service.execute
expect(project.import_url).to eq('https://asdffg@gitlab.com/asd/vim.git')
expect(project.safe_import_url).to eq('https://*****@gitlab.com/asd/vim.git')
expect(project.import_data.credentials).to eq(user: 'asdffg', password: nil)
end
context 'when Github project is private' do
it 'sets project visibility to private' do
repo.private = true
project = service.execute
expect(project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE)
end
end
context 'when Github project is public' do
before do
allow_any_instance_of(ApplicationSetting).to receive(:default_project_visibility).and_return(Gitlab::VisibilityLevel::INTERNAL)
end
it 'sets project visibility to the default project visibility' do
repo.private = false
project_creator = Gitlab::GithubImport::ProjectCreator.new(repo, namespace, user, access_params) project = service.execute
project = project_creator.execute
expect(project.import_url).to eq("https://asdffg@gitlab.com/asd/vim.git") expect(project.visibility_level).to eq(Gitlab::VisibilityLevel::INTERNAL)
expect(project.safe_import_url).to eq("https://*****@gitlab.com/asd/vim.git") end
expect(project.import_data.credentials).to eq(user: "asdffg", password: nil) end
expect(project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE)
end end
end end
...@@ -62,8 +62,7 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do ...@@ -62,8 +62,7 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
end end
context 'when pull request is closed' do context 'when pull request is closed' do
let(:closed_at) { DateTime.strptime('2011-01-28T19:01:12Z') } let(:raw_data) { double(base_data.merge(state: 'closed')) }
let(:raw_data) { double(base_data.merge(state: 'closed', closed_at: closed_at)) }
it 'returns formatted attributes' do it 'returns formatted attributes' do
expected = { expected = {
...@@ -81,7 +80,7 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do ...@@ -81,7 +80,7 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
author_id: project.creator_id, author_id: project.creator_id,
assignee_id: nil, assignee_id: nil,
created_at: created_at, created_at: created_at,
updated_at: closed_at updated_at: updated_at
} }
expect(pull_request.attributes).to eq(expected) expect(pull_request.attributes).to eq(expected)
...@@ -108,7 +107,7 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do ...@@ -108,7 +107,7 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
author_id: project.creator_id, author_id: project.creator_id,
assignee_id: nil, assignee_id: nil,
created_at: created_at, created_at: created_at,
updated_at: merged_at updated_at: updated_at
} }
expect(pull_request.attributes).to eq(expected) expect(pull_request.attributes).to eq(expected)
......
...@@ -171,70 +171,6 @@ describe Ability, lib: true do ...@@ -171,70 +171,6 @@ describe Ability, lib: true do
end end
end end
shared_examples_for ".project_abilities" do |enable_request_store|
before do
RequestStore.begin! if enable_request_store
end
after do
if enable_request_store
RequestStore.end!
RequestStore.clear!
end
end
describe '.project_abilities' do
let!(:project) { create(:empty_project, :public) }
let!(:user) { create(:user) }
it 'returns permissions for admin user' do
admin = create(:admin)
results = described_class.project_abilities(admin, project)
expect(results.count).to eq(68)
end
it 'returns permissions for an owner' do
results = described_class.project_abilities(project.owner, project)
expect(results.count).to eq(68)
end
it 'returns permissions for a master' do
project.team << [user, :master]
results = described_class.project_abilities(user, project)
expect(results.count).to eq(60)
end
it 'returns permissions for a developer' do
project.team << [user, :developer]
results = described_class.project_abilities(user, project)
expect(results.count).to eq(44)
end
it 'returns permissions for a guest' do
project.team << [user, :guest]
results = described_class.project_abilities(user, project)
expect(results.count).to eq(21)
end
end
end
describe '.project_abilities with RequestStore' do
it_behaves_like ".project_abilities", true
end
describe '.project_abilities without RequestStore' do
it_behaves_like ".project_abilities", false
end
describe '.issues_readable_by_user' do describe '.issues_readable_by_user' do
context 'with an admin user' do context 'with an admin user' do
it 'returns all given issues' do it 'returns all given issues' do
...@@ -286,12 +222,12 @@ describe Ability, lib: true do ...@@ -286,12 +222,12 @@ describe Ability, lib: true do
describe '.project_disabled_features_rules' do describe '.project_disabled_features_rules' do
let(:project) { build(:project) } let(:project) { build(:project) }
subject { described_class.project_disabled_features_rules(project) } subject { described_class.allowed(project.owner, project) }
context 'wiki named abilities' do context 'wiki named abilities' do
it 'disables wiki abilities if the project has no wiki' do it 'disables wiki abilities if the project has no wiki' do
expect(project).to receive(:has_wiki?).and_return(false) expect(project).to receive(:has_wiki?).and_return(false)
expect(subject).to include(:read_wiki, :create_wiki, :update_wiki, :admin_wiki) expect(subject).not_to include(:read_wiki, :create_wiki, :update_wiki, :admin_wiki)
end end
end end
end end
......
...@@ -71,9 +71,6 @@ describe ProjectMember, models: true do ...@@ -71,9 +71,6 @@ describe ProjectMember, models: true do
describe :import_team do describe :import_team do
before do before do
@abilities = Six.new
@abilities << Ability
@project_1 = create :project @project_1 = create :project
@project_2 = create :project @project_2 = create :project
...@@ -92,8 +89,8 @@ describe ProjectMember, models: true do ...@@ -92,8 +89,8 @@ describe ProjectMember, models: true do
it { expect(@project_2.users).to include(@user_1) } it { expect(@project_2.users).to include(@user_1) }
it { expect(@project_2.users).to include(@user_2) } it { expect(@project_2.users).to include(@user_2) }
it { expect(@abilities.allowed?(@user_1, :create_project, @project_2)).to be_truthy } it { expect(Ability.allowed?(@user_1, :create_project, @project_2)).to be_truthy }
it { expect(@abilities.allowed?(@user_2, :read_project, @project_2)).to be_truthy } it { expect(Ability.allowed?(@user_2, :read_project, @project_2)).to be_truthy }
end end
describe 'project 1 should not be changed' do describe 'project 1 should not be changed' do
......
...@@ -85,8 +85,6 @@ describe Note, models: true do ...@@ -85,8 +85,6 @@ describe Note, models: true do
@u1 = create(:user) @u1 = create(:user)
@u2 = create(:user) @u2 = create(:user)
@u3 = create(:user) @u3 = create(:user)
@abilities = Six.new
@abilities << Ability
end end
describe 'read' do describe 'read' do
...@@ -95,9 +93,9 @@ describe Note, models: true do ...@@ -95,9 +93,9 @@ describe Note, models: true do
@p2.project_members.create(user: @u3, access_level: ProjectMember::GUEST) @p2.project_members.create(user: @u3, access_level: ProjectMember::GUEST)
end end
it { expect(@abilities.allowed?(@u1, :read_note, @p1)).to be_falsey } it { expect(Ability.allowed?(@u1, :read_note, @p1)).to be_falsey }
it { expect(@abilities.allowed?(@u2, :read_note, @p1)).to be_truthy } it { expect(Ability.allowed?(@u2, :read_note, @p1)).to be_truthy }
it { expect(@abilities.allowed?(@u3, :read_note, @p1)).to be_falsey } it { expect(Ability.allowed?(@u3, :read_note, @p1)).to be_falsey }
end end
describe 'write' do describe 'write' do
...@@ -106,9 +104,9 @@ describe Note, models: true do ...@@ -106,9 +104,9 @@ describe Note, models: true do
@p2.project_members.create(user: @u3, access_level: ProjectMember::DEVELOPER) @p2.project_members.create(user: @u3, access_level: ProjectMember::DEVELOPER)
end end
it { expect(@abilities.allowed?(@u1, :create_note, @p1)).to be_falsey } it { expect(Ability.allowed?(@u1, :create_note, @p1)).to be_falsey }
it { expect(@abilities.allowed?(@u2, :create_note, @p1)).to be_truthy } it { expect(Ability.allowed?(@u2, :create_note, @p1)).to be_truthy }
it { expect(@abilities.allowed?(@u3, :create_note, @p1)).to be_falsey } it { expect(Ability.allowed?(@u3, :create_note, @p1)).to be_falsey }
end end
describe 'admin' do describe 'admin' do
...@@ -118,9 +116,9 @@ describe Note, models: true do ...@@ -118,9 +116,9 @@ describe Note, models: true do
@p2.project_members.create(user: @u3, access_level: ProjectMember::MASTER) @p2.project_members.create(user: @u3, access_level: ProjectMember::MASTER)
end end
it { expect(@abilities.allowed?(@u1, :admin_note, @p1)).to be_falsey } it { expect(Ability.allowed?(@u1, :admin_note, @p1)).to be_falsey }
it { expect(@abilities.allowed?(@u2, :admin_note, @p1)).to be_truthy } it { expect(Ability.allowed?(@u2, :admin_note, @p1)).to be_truthy }
it { expect(@abilities.allowed?(@u3, :admin_note, @p1)).to be_falsey } it { expect(Ability.allowed?(@u3, :admin_note, @p1)).to be_falsey }
end end
end end
......
require 'spec_helper'
describe Project, models: true do
describe 'authorization' do
before do
@p1 = create(:project)
@u1 = create(:user)
@u2 = create(:user)
@u3 = create(:user)
@u4 = @p1.owner
@abilities = Six.new
@abilities << Ability
end
let(:guest_actions) { Ability.project_guest_rules }
let(:report_actions) { Ability.project_report_rules }
let(:dev_actions) { Ability.project_dev_rules }
let(:master_actions) { Ability.project_master_rules }
let(:owner_actions) { Ability.project_owner_rules }
describe "Non member rules" do
it "denies for non-project users any actions" do
owner_actions.each do |action|
expect(@abilities.allowed?(@u1, action, @p1)).to be_falsey
end
end
end
describe "Guest Rules" do
before do
@p1.project_members.create(project: @p1, user: @u2, access_level: ProjectMember::GUEST)
end
it "allows for project user any guest actions" do
guest_actions.each do |action|
expect(@abilities.allowed?(@u2, action, @p1)).to be_truthy
end
end
end
describe "Report Rules" do
before do
@p1.project_members.create(project: @p1, user: @u2, access_level: ProjectMember::REPORTER)
end
it "allows for project user any report actions" do
report_actions.each do |action|
expect(@abilities.allowed?(@u2, action, @p1)).to be_truthy
end
end
end
describe "Developer Rules" do
before do
@p1.project_members.create(project: @p1, user: @u2, access_level: ProjectMember::REPORTER)
@p1.project_members.create(project: @p1, user: @u3, access_level: ProjectMember::DEVELOPER)
end
it "denies for developer master-specific actions" do
[dev_actions - report_actions].each do |action|
expect(@abilities.allowed?(@u2, action, @p1)).to be_falsey
end
end
it "allows for project user any dev actions" do
dev_actions.each do |action|
expect(@abilities.allowed?(@u3, action, @p1)).to be_truthy
end
end
end
describe "Master Rules" do
before do
@p1.project_members.create(project: @p1, user: @u2, access_level: ProjectMember::DEVELOPER)
@p1.project_members.create(project: @p1, user: @u3, access_level: ProjectMember::MASTER)
end
it "denies for developer master-specific actions" do
[master_actions - dev_actions].each do |action|
expect(@abilities.allowed?(@u2, action, @p1)).to be_falsey
end
end
it "allows for project user any master actions" do
master_actions.each do |action|
expect(@abilities.allowed?(@u3, action, @p1)).to be_truthy
end
end
end
describe "Owner Rules" do
before do
@p1.project_members.create(project: @p1, user: @u2, access_level: ProjectMember::DEVELOPER)
@p1.project_members.create(project: @p1, user: @u3, access_level: ProjectMember::MASTER)
end
it "denies for masters admin-specific actions" do
[owner_actions - master_actions].each do |action|
expect(@abilities.allowed?(@u2, action, @p1)).to be_falsey
end
end
it "allows for project owner any admin actions" do
owner_actions.each do |action|
expect(@abilities.allowed?(@u4, action, @p1)).to be_truthy
end
end
end
end
end
require 'spec_helper'
describe ProjectPolicy, models: true do
let(:project) { create(:empty_project, :public) }
let(:guest) { create(:user) }
let(:reporter) { create(:user) }
let(:dev) { create(:user) }
let(:master) { create(:user) }
let(:owner) { create(:user) }
let(:admin) { create(:admin) }
let(:users_ordered_by_permissions) do
[nil, guest, reporter, dev, master, owner, admin]
end
let(:users_permissions) do
users_ordered_by_permissions.map { |u| Ability.allowed(u, project).size }
end
before do
project.team << [guest, :guest]
project.team << [master, :master]
project.team << [dev, :developer]
project.team << [reporter, :reporter]
group = create(:group)
project.project_group_links.create(
group: group,
group_access: Gitlab::Access::MASTER)
group.add_owner(owner)
end
it 'returns increasing permissions for each level' do
expect(users_permissions).to eq(users_permissions.sort.uniq)
end
end
require 'spec_helper'
describe API::BroadcastMessages, api: true do
include ApiHelpers
let(:user) { create(:user) }
let(:admin) { create(:admin) }
describe 'GET /broadcast_messages' do
it 'returns a 401 for anonymous users' do
get api('/broadcast_messages')
expect(response).to have_http_status(401)
end
it 'returns a 403 for users' do
get api('/broadcast_messages', user)
expect(response).to have_http_status(403)
end
it 'returns an Array of BroadcastMessages for admins' do
create(:broadcast_message)
get api('/broadcast_messages', admin)
expect(response).to have_http_status(200)
expect(json_response).to be_kind_of(Array)
expect(json_response.first.keys)
.to match_array(%w(id message starts_at ends_at color font active))
end
end
describe 'GET /broadcast_messages/:id' do
let!(:message) { create(:broadcast_message) }
it 'returns a 401 for anonymous users' do
get api("/broadcast_messages/#{message.id}")
expect(response).to have_http_status(401)
end
it 'returns a 403 for users' do
get api("/broadcast_messages/#{message.id}", user)
expect(response).to have_http_status(403)
end
it 'returns the specified message for admins' do
get api("/broadcast_messages/#{message.id}", admin)
expect(response).to have_http_status(200)
expect(json_response['id']).to eq message.id
expect(json_response.keys)
.to match_array(%w(id message starts_at ends_at color font active))
end
end
describe 'POST /broadcast_messages' do
it 'returns a 401 for anonymous users' do
post api('/broadcast_messages'), attributes_for(:broadcast_message)
expect(response).to have_http_status(401)
end
it 'returns a 403 for users' do
post api('/broadcast_messages', user), attributes_for(:broadcast_message)
expect(response).to have_http_status(403)
end
context 'as an admin' do
it 'requires the `message` parameter' do
attrs = attributes_for(:broadcast_message)
attrs.delete(:message)
post api('/broadcast_messages', admin), attrs
expect(response).to have_http_status(400)
expect(json_response['error']).to eq 'message is missing'
end
it 'defines sane default start and end times' do
time = Time.zone.parse('2016-07-02 10:11:12')
travel_to(time) do
post api('/broadcast_messages', admin), message: 'Test message'
expect(response).to have_http_status(201)
expect(json_response['starts_at']).to eq '2016-07-02T10:11:12.000Z'
expect(json_response['ends_at']).to eq '2016-07-02T11:11:12.000Z'
end
end
it 'accepts a custom background and foreground color' do
attrs = attributes_for(:broadcast_message, color: '#000000', font: '#cecece')
post api('/broadcast_messages', admin), attrs
expect(response).to have_http_status(201)
expect(json_response['color']).to eq attrs[:color]
expect(json_response['font']).to eq attrs[:font]
end
end
end
describe 'PUT /broadcast_messages/:id' do
let!(:message) { create(:broadcast_message) }
it 'returns a 401 for anonymous users' do
put api("/broadcast_messages/#{message.id}"),
attributes_for(:broadcast_message)
expect(response).to have_http_status(401)
end
it 'returns a 403 for users' do
put api("/broadcast_messages/#{message.id}", user),
attributes_for(:broadcast_message)
expect(response).to have_http_status(403)
end
context 'as an admin' do
it 'accepts new background and foreground colors' do
attrs = { color: '#000000', font: '#cecece' }
put api("/broadcast_messages/#{message.id}", admin), attrs
expect(response).to have_http_status(200)
expect(json_response['color']).to eq attrs[:color]
expect(json_response['font']).to eq attrs[:font]
end
it 'accepts new start and end times' do
time = Time.zone.parse('2016-07-02 10:11:12')
travel_to(time) do
attrs = { starts_at: Time.zone.now, ends_at: 3.hours.from_now }
put api("/broadcast_messages/#{message.id}", admin), attrs
expect(response).to have_http_status(200)
expect(json_response['starts_at']).to eq '2016-07-02T10:11:12.000Z'
expect(json_response['ends_at']).to eq '2016-07-02T13:11:12.000Z'
end
end
it 'accepts a new message' do
attrs = { message: 'new message' }
put api("/broadcast_messages/#{message.id}", admin), attrs
expect(response).to have_http_status(200)
expect { message.reload }.to change { message.message }.to('new message')
end
end
end
describe 'DELETE /broadcast_messages/:id' do
let!(:message) { create(:broadcast_message) }
it 'returns a 401 for anonymous users' do
delete api("/broadcast_messages/#{message.id}"),
attributes_for(:broadcast_message)
expect(response).to have_http_status(401)
end
it 'returns a 403 for users' do
delete api("/broadcast_messages/#{message.id}", user),
attributes_for(:broadcast_message)
expect(response).to have_http_status(403)
end
it 'deletes the broadcast message for admins' do
expect { delete api("/broadcast_messages/#{message.id}", admin) }
.to change { BroadcastMessage.count }.by(-1)
end
end
end
...@@ -405,6 +405,7 @@ describe API::API, api: true do ...@@ -405,6 +405,7 @@ describe API::API, api: true do
expect(json_response['milestone']).to be_a Hash expect(json_response['milestone']).to be_a Hash
expect(json_response['assignee']).to be_a Hash expect(json_response['assignee']).to be_a Hash
expect(json_response['author']).to be_a Hash expect(json_response['author']).to be_a Hash
expect(json_response['confidential']).to be_falsy
end end
it "returns a project issue by id" do it "returns a project issue by id" do
...@@ -470,13 +471,51 @@ describe API::API, api: true do ...@@ -470,13 +471,51 @@ describe API::API, api: true do
end end
describe "POST /projects/:id/issues" do describe "POST /projects/:id/issues" do
it "creates a new project issue" do it 'creates a new project issue' do
post api("/projects/#{project.id}/issues", user), post api("/projects/#{project.id}/issues", user),
title: 'new issue', labels: 'label, label2' title: 'new issue', labels: 'label, label2'
expect(response).to have_http_status(201) expect(response).to have_http_status(201)
expect(json_response['title']).to eq('new issue') expect(json_response['title']).to eq('new issue')
expect(json_response['description']).to be_nil expect(json_response['description']).to be_nil
expect(json_response['labels']).to eq(['label', 'label2']) expect(json_response['labels']).to eq(['label', 'label2'])
expect(json_response['confidential']).to be_falsy
end
it 'creates a new confidential project issue' do
post api("/projects/#{project.id}/issues", user),
title: 'new issue', confidential: true
expect(response).to have_http_status(201)
expect(json_response['title']).to eq('new issue')
expect(json_response['confidential']).to be_truthy
end
it 'creates a new confidential project issue with a different param' do
post api("/projects/#{project.id}/issues", user),
title: 'new issue', confidential: 'y'
expect(response).to have_http_status(201)
expect(json_response['title']).to eq('new issue')
expect(json_response['confidential']).to be_truthy
end
it 'creates a public issue when confidential param is false' do
post api("/projects/#{project.id}/issues", user),
title: 'new issue', confidential: false
expect(response).to have_http_status(201)
expect(json_response['title']).to eq('new issue')
expect(json_response['confidential']).to be_falsy
end
it 'creates a public issue when confidential param is invalid' do
post api("/projects/#{project.id}/issues", user),
title: 'new issue', confidential: 'foo'
expect(response).to have_http_status(201)
expect(json_response['title']).to eq('new issue')
expect(json_response['confidential']).to be_falsy
end end
it "sends notifications for subscribers of newly added labels" do it "sends notifications for subscribers of newly added labels" do
...@@ -632,6 +671,30 @@ describe API::API, api: true do ...@@ -632,6 +671,30 @@ describe API::API, api: true do
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
expect(json_response['title']).to eq('updated title') expect(json_response['title']).to eq('updated title')
end end
it 'sets an issue to confidential' do
put api("/projects/#{project.id}/issues/#{issue.id}", user),
confidential: true
expect(response).to have_http_status(200)
expect(json_response['confidential']).to be_truthy
end
it 'makes a confidential issue public' do
put api("/projects/#{project.id}/issues/#{confidential_issue.id}", user),
confidential: false
expect(response).to have_http_status(200)
expect(json_response['confidential']).to be_falsy
end
it 'does not update a confidential issue with wrong confidential flag' do
put api("/projects/#{project.id}/issues/#{confidential_issue.id}", user),
confidential: 'foo'
expect(response).to have_http_status(200)
expect(json_response['confidential']).to be_truthy
end
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