Commit b9d989dc056a2a2b9316ff9aa06b57c736426871

Authored by Dmitriy Zaporozhets
2 parents 211e435a 213e117a

Merge branch 'Andrew8xx8-gist'

Showing 60 changed files with 972 additions and 210 deletions   Show diff stats
app/controllers/projects/application_controller.rb
1 class Projects::ApplicationController < ApplicationController 1 class Projects::ApplicationController < ApplicationController
2 -  
3 - before_filter :authorize_admin_team_member!  
4 -  
5 - protected  
6 -  
7 - def user_team  
8 - @team ||= UserTeam.find_by_path(params[:id])  
9 - end  
10 - 2 + before_filter :project
  3 + before_filter :repository
11 end 4 end
app/controllers/projects/snippets_controller.rb 0 → 100644
@@ -0,0 +1,91 @@ @@ -0,0 +1,91 @@
  1 +class Projects::SnippetsController < Projects::ApplicationController
  2 + before_filter :module_enabled
  3 + before_filter :snippet, only: [:show, :edit, :destroy, :update, :raw]
  4 +
  5 + # Allow read any snippet
  6 + before_filter :authorize_read_project_snippet!
  7 +
  8 + # Allow write(create) snippet
  9 + before_filter :authorize_write_project_snippet!, only: [:new, :create]
  10 +
  11 + # Allow modify snippet
  12 + before_filter :authorize_modify_project_snippet!, only: [:edit, :update]
  13 +
  14 + # Allow destroy snippet
  15 + before_filter :authorize_admin_project_snippet!, only: [:destroy]
  16 +
  17 + layout 'project_resource'
  18 +
  19 + respond_to :html
  20 +
  21 + def index
  22 + @snippets = @project.snippets.fresh.non_expired
  23 + end
  24 +
  25 + def new
  26 + @snippet = @project.snippets.build
  27 + end
  28 +
  29 + def create
  30 + @snippet = @project.snippets.build(params[:project_snippet])
  31 + @snippet.author = current_user
  32 +
  33 + if @snippet.save
  34 + redirect_to project_snippet_path(@project, @snippet)
  35 + else
  36 + respond_with(@snippet)
  37 + end
  38 + end
  39 +
  40 + def edit
  41 + end
  42 +
  43 + def update
  44 + if @snippet.update_attributes(params[:project_snippet])
  45 + redirect_to project_snippet_path(@project, @snippet)
  46 + else
  47 + respond_with(@snippet)
  48 + end
  49 + end
  50 +
  51 + def show
  52 + @note = @project.notes.new(noteable: @snippet)
  53 + @target_type = :snippet
  54 + @target_id = @snippet.id
  55 + end
  56 +
  57 + def destroy
  58 + return access_denied! unless can?(current_user, :admin_project_snippet, @snippet)
  59 +
  60 + @snippet.destroy
  61 +
  62 + redirect_to project_snippets_path(@project)
  63 + end
  64 +
  65 + def raw
  66 + send_data(
  67 + @snippet.content,
  68 + type: "text/plain",
  69 + disposition: 'inline',
  70 + filename: @snippet.file_name
  71 + )
  72 + end
  73 +
  74 + protected
  75 +
  76 + def snippet
  77 + @snippet ||= @project.snippets.find(params[:id])
  78 + end
  79 +
  80 + def authorize_modify_project_snippet!
  81 + return render_404 unless can?(current_user, :modify_project_snippet, @snippet)
  82 + end
  83 +
  84 + def authorize_admin_project_snippet!
  85 + return render_404 unless can?(current_user, :admin_project_snippet, @snippet)
  86 + end
  87 +
  88 + def module_enabled
  89 + return render_404 unless @project.snippets_enabled
  90 + end
  91 +end
app/controllers/projects/teams_controller.rb
1 class Projects::TeamsController < Projects::ApplicationController 1 class Projects::TeamsController < Projects::ApplicationController
2 2
  3 + before_filter :authorize_admin_team_member!
  4 +
3 def available 5 def available
4 @teams = current_user.is_admin? ? UserTeam.scoped : current_user.user_teams 6 @teams = current_user.is_admin? ? UserTeam.scoped : current_user.user_teams
5 @teams = @teams.without_project(project) 7 @teams = @teams.without_project(project)
@@ -24,4 +26,9 @@ class Projects::TeamsController &lt; Projects::ApplicationController @@ -24,4 +26,9 @@ class Projects::TeamsController &lt; Projects::ApplicationController
24 redirect_to project_team_index_path(project) 26 redirect_to project_team_index_path(project)
25 end 27 end
26 28
  29 + protected
  30 +
  31 + def user_team
  32 + @team ||= UserTeam.find_by_path(params[:id])
  33 + end
27 end 34 end
app/controllers/snippets_controller.rb
1 -class SnippetsController < ProjectResourceController  
2 - before_filter :module_enabled 1 +class SnippetsController < ApplicationController
3 before_filter :snippet, only: [:show, :edit, :destroy, :update, :raw] 2 before_filter :snippet, only: [:show, :edit, :destroy, :update, :raw]
4 3
5 - # Allow read any snippet  
6 - before_filter :authorize_read_snippet!  
7 -  
8 - # Allow write(create) snippet  
9 - before_filter :authorize_write_snippet!, only: [:new, :create]  
10 -  
11 # Allow modify snippet 4 # Allow modify snippet
12 before_filter :authorize_modify_snippet!, only: [:edit, :update] 5 before_filter :authorize_modify_snippet!, only: [:edit, :update]
13 6
@@ -17,22 +10,38 @@ class SnippetsController &lt; ProjectResourceController @@ -17,22 +10,38 @@ class SnippetsController &lt; ProjectResourceController
17 respond_to :html 10 respond_to :html
18 11
19 def index 12 def index
20 - @snippets = @project.snippets.fresh.non_expired 13 + @snippets = Snippet.public.fresh.non_expired.page(params[:page]).per(20)
  14 + end
  15 +
  16 + def user_index
  17 + @user = User.find_by_username(params[:username])
  18 +
  19 + @snippets = @current_user.snippets.fresh.non_expired
  20 +
  21 + @snippets = case params[:scope]
  22 + when 'public' then
  23 + @snippets.public
  24 + when 'private' then
  25 + @snippets.private
  26 + else
  27 + @snippets
  28 + end
  29 +
  30 + @snippets = @snippets.page(params[:page]).per(20)
21 end 31 end
22 32
23 def new 33 def new
24 - @snippet = @project.snippets.new 34 + @snippet = PersonalSnippet.new
25 end 35 end
26 36
27 def create 37 def create
28 - @snippet = @project.snippets.new(params[:snippet]) 38 + @snippet = PersonalSnippet.new(params[:personal_snippet])
29 @snippet.author = current_user 39 @snippet.author = current_user
30 - @snippet.save  
31 40
32 - if @snippet.valid?  
33 - redirect_to [@project, @snippet] 41 + if @snippet.save
  42 + redirect_to snippet_path(@snippet)
34 else 43 else
35 - respond_with(@snippet) 44 + respond_with @snippet
36 end 45 end
37 end 46 end
38 47
@@ -40,27 +49,22 @@ class SnippetsController &lt; ProjectResourceController @@ -40,27 +49,22 @@ class SnippetsController &lt; ProjectResourceController
40 end 49 end
41 50
42 def update 51 def update
43 - @snippet.update_attributes(params[:snippet])  
44 -  
45 - if @snippet.valid?  
46 - redirect_to [@project, @snippet] 52 + if @snippet.update_attributes(params[:personal_snippet])
  53 + redirect_to snippet_path(@snippet)
47 else 54 else
48 - respond_with(@snippet) 55 + respond_with @snippet
49 end 56 end
50 end 57 end
51 58
52 def show 59 def show
53 - @note = @project.notes.new(noteable: @snippet)  
54 - @target_type = :snippet  
55 - @target_id = @snippet.id  
56 end 60 end
57 61
58 def destroy 62 def destroy
59 - return access_denied! unless can?(current_user, :admin_snippet, @snippet) 63 + return access_denied! unless can?(current_user, :admin_personal_snippet, @snippet)
60 64
61 @snippet.destroy 65 @snippet.destroy
62 66
63 - redirect_to project_snippets_path(@project) 67 + redirect_to snippets_path
64 end 68 end
65 69
66 def raw 70 def raw
@@ -75,18 +79,14 @@ class SnippetsController &lt; ProjectResourceController @@ -75,18 +79,14 @@ class SnippetsController &lt; ProjectResourceController
75 protected 79 protected
76 80
77 def snippet 81 def snippet
78 - @snippet ||= @project.snippets.find(params[:id]) 82 + @snippet ||= PersonalSnippet.find(params[:id])
79 end 83 end
80 84
81 def authorize_modify_snippet! 85 def authorize_modify_snippet!
82 - return render_404 unless can?(current_user, :modify_snippet, @snippet) 86 + return render_404 unless can?(current_user, :modify_personal_snippet, @snippet)
83 end 87 end
84 88
85 def authorize_admin_snippet! 89 def authorize_admin_snippet!
86 - return render_404 unless can?(current_user, :admin_snippet, @snippet)  
87 - end  
88 -  
89 - def module_enabled  
90 - return render_404 unless @project.snippets_enabled 90 + return render_404 unless can?(current_user, :admin_personal_snippet, @snippet)
91 end 91 end
92 end 92 end
app/helpers/tab_helper.rb
@@ -73,7 +73,7 @@ module TabHelper @@ -73,7 +73,7 @@ module TabHelper
73 end 73 end
74 74
75 def project_tab_class 75 def project_tab_class
76 - return "active" if current_page?(controller: "projects", action: :edit, id: @project) 76 + return "active" if current_page?(controller: "/projects", action: :edit, id: @project)
77 77
78 if ['services', 'hooks', 'deploy_keys', 'team_members'].include? controller.controller_name 78 if ['services', 'hooks', 'deploy_keys', 'team_members'].include? controller.controller_name
79 "active" 79 "active"
app/models/ability.rb
@@ -7,7 +7,8 @@ class Ability @@ -7,7 +7,8 @@ class Ability
7 when "Project" then project_abilities(user, subject) 7 when "Project" then project_abilities(user, subject)
8 when "Issue" then issue_abilities(user, subject) 8 when "Issue" then issue_abilities(user, subject)
9 when "Note" then note_abilities(user, subject) 9 when "Note" then note_abilities(user, subject)
10 - when "Snippet" then snippet_abilities(user, subject) 10 + when "ProjectSnippet" then project_snippet_abilities(user, subject)
  11 + when "PersonalSnippet" then personal_snippet_abilities(user, subject)
11 when "MergeRequest" then merge_request_abilities(user, subject) 12 when "MergeRequest" then merge_request_abilities(user, subject)
12 when "Group", "Namespace" then group_abilities(user, subject) 13 when "Group", "Namespace" then group_abilities(user, subject)
13 when "UserTeam" then user_team_abilities(user, subject) 14 when "UserTeam" then user_team_abilities(user, subject)
@@ -54,7 +55,7 @@ class Ability @@ -54,7 +55,7 @@ class Ability
54 :read_wiki, 55 :read_wiki,
55 :read_issue, 56 :read_issue,
56 :read_milestone, 57 :read_milestone,
57 - :read_snippet, 58 + :read_project_snippet,
58 :read_team_member, 59 :read_team_member,
59 :read_merge_request, 60 :read_merge_request,
60 :read_note, 61 :read_note,
@@ -67,8 +68,8 @@ class Ability @@ -67,8 +68,8 @@ class Ability
67 def project_report_rules 68 def project_report_rules
68 project_guest_rules + [ 69 project_guest_rules + [
69 :download_code, 70 :download_code,
70 - :write_snippet,  
71 - :fork_project 71 + :fork_project,
  72 + :write_project_snippet
72 ] 73 ]
73 end 74 end
74 75
@@ -84,11 +85,11 @@ class Ability @@ -84,11 +85,11 @@ class Ability
84 project_dev_rules + [ 85 project_dev_rules + [
85 :push_code_to_protected_branches, 86 :push_code_to_protected_branches,
86 :modify_issue, 87 :modify_issue,
87 - :modify_snippet, 88 + :modify_project_snippet,
88 :modify_merge_request, 89 :modify_merge_request,
89 :admin_issue, 90 :admin_issue,
90 :admin_milestone, 91 :admin_milestone,
91 - :admin_snippet, 92 + :admin_project_snippet,
92 :admin_team_member, 93 :admin_team_member,
93 :admin_merge_request, 94 :admin_merge_request,
94 :admin_note, 95 :admin_note,
@@ -135,8 +136,7 @@ class Ability @@ -135,8 +136,7 @@ class Ability
135 rules.flatten 136 rules.flatten
136 end 137 end
137 138
138 -  
139 - [:issue, :note, :snippet, :merge_request].each do |name| 139 + [:issue, :note, :project_snippet, :personal_snippet, :merge_request].each do |name|
140 define_method "#{name}_abilities" do |user, subject| 140 define_method "#{name}_abilities" do |user, subject|
141 if subject.author == user 141 if subject.author == user
142 [ 142 [
app/models/event.rb
@@ -241,6 +241,10 @@ class Event &lt; ActiveRecord::Base @@ -241,6 +241,10 @@ class Event &lt; ActiveRecord::Base
241 target.noteable_type == "Commit" 241 target.noteable_type == "Commit"
242 end 242 end
243 243
  244 + def note_project_snippet?
  245 + target.noteable_type == "Snippet"
  246 + end
  247 +
244 def note_target 248 def note_target
245 target.noteable 249 target.noteable
246 end 250 end
app/models/note.rb
@@ -159,4 +159,10 @@ class Note &lt; ActiveRecord::Base @@ -159,4 +159,10 @@ class Note &lt; ActiveRecord::Base
159 "wall" 159 "wall"
160 end 160 end
161 end 161 end
  162 +
  163 + # FIXME: Hack for polymorphic associations with STI
  164 + # For more information wisit http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#label-Polymorphic+Associations
  165 + def noteable_type=(sType)
  166 + super(sType.to_s.classify.constantize.base_class.to_s)
  167 + end
162 end 168 end
app/models/personal_snippet.rb 0 → 100644
@@ -0,0 +1,18 @@ @@ -0,0 +1,18 @@
  1 +# == Schema Information
  2 +#
  3 +# Table name: snippets
  4 +#
  5 +# id :integer not null, primary key
  6 +# title :string(255)
  7 +# content :text
  8 +# author_id :integer not null
  9 +# project_id :integer not null
  10 +# created_at :datetime not null
  11 +# updated_at :datetime not null
  12 +# file_name :string(255)
  13 +# expires_at :datetime
  14 +# type :string(255)
  15 +# private :boolean
  16 +
  17 +class PersonalSnippet < Snippet
  18 +end
app/models/project.rb
@@ -57,7 +57,7 @@ class Project &lt; ActiveRecord::Base @@ -57,7 +57,7 @@ class Project &lt; ActiveRecord::Base
57 has_many :milestones, dependent: :destroy 57 has_many :milestones, dependent: :destroy
58 has_many :users_projects, dependent: :destroy 58 has_many :users_projects, dependent: :destroy
59 has_many :notes, dependent: :destroy 59 has_many :notes, dependent: :destroy
60 - has_many :snippets, dependent: :destroy 60 + has_many :snippets, dependent: :destroy, class_name: "ProjectSnippet"
61 has_many :hooks, dependent: :destroy, class_name: "ProjectHook" 61 has_many :hooks, dependent: :destroy, class_name: "ProjectHook"
62 has_many :protected_branches, dependent: :destroy 62 has_many :protected_branches, dependent: :destroy
63 has_many :user_team_project_relationships, dependent: :destroy 63 has_many :user_team_project_relationships, dependent: :destroy
app/models/project_snippet.rb 0 → 100644
@@ -0,0 +1,27 @@ @@ -0,0 +1,27 @@
  1 +# == Schema Information
  2 +#
  3 +# Table name: snippets
  4 +#
  5 +# id :integer not null, primary key
  6 +# title :string(255)
  7 +# content :text
  8 +# author_id :integer not null
  9 +# project_id :integer not null
  10 +# created_at :datetime not null
  11 +# updated_at :datetime not null
  12 +# file_name :string(255)
  13 +# expires_at :datetime
  14 +# type :string(255)
  15 +# private :boolean
  16 +
  17 +class ProjectSnippet < Snippet
  18 + belongs_to :project
  19 + belongs_to :author, class_name: "User"
  20 +
  21 + validates :project, presence: true
  22 +
  23 + # Scopes
  24 + scope :fresh, -> { order("created_at DESC") }
  25 + scope :non_expired, -> { where(["expires_at IS NULL OR expires_at > ?", Time.current]) }
  26 + scope :expired, -> { where(["expires_at IS NOT NULL AND expires_at < ?", Time.current]) }
  27 +end
app/models/snippet.rb
@@ -11,29 +11,31 @@ @@ -11,29 +11,31 @@
11 # updated_at :datetime not null 11 # updated_at :datetime not null
12 # file_name :string(255) 12 # file_name :string(255)
13 # expires_at :datetime 13 # expires_at :datetime
14 -# 14 +# type :string(255)
  15 +# private :boolean
15 16
16 class Snippet < ActiveRecord::Base 17 class Snippet < ActiveRecord::Base
17 include Linguist::BlobHelper 18 include Linguist::BlobHelper
18 19
19 - attr_accessible :title, :content, :file_name, :expires_at 20 + attr_accessible :title, :content, :file_name, :expires_at, :private
20 21
21 - belongs_to :project  
22 belongs_to :author, class_name: "User" 22 belongs_to :author, class_name: "User"
  23 +
23 has_many :notes, as: :noteable, dependent: :destroy 24 has_many :notes, as: :noteable, dependent: :destroy
24 25
25 delegate :name, :email, to: :author, prefix: true, allow_nil: true 26 delegate :name, :email, to: :author, prefix: true, allow_nil: true
26 27
27 validates :author, presence: true 28 validates :author, presence: true
28 - validates :project, presence: true  
29 validates :title, presence: true, length: { within: 0..255 } 29 validates :title, presence: true, length: { within: 0..255 }
30 validates :file_name, presence: true, length: { within: 0..255 } 30 validates :file_name, presence: true, length: { within: 0..255 }
31 validates :content, presence: true 31 validates :content, presence: true
32 32
33 # Scopes 33 # Scopes
34 - scope :fresh, -> { order("created_at DESC") }  
35 - scope :non_expired, -> { where(["expires_at IS NULL OR expires_at > ?", Time.current]) } 34 + scope :public, -> { where(private: false) }
  35 + scope :private, -> { where(private: true) }
  36 + scope :fresh, -> { order("created_at DESC") }
36 scope :expired, -> { where(["expires_at IS NOT NULL AND expires_at < ?", Time.current]) } 37 scope :expired, -> { where(["expires_at IS NOT NULL AND expires_at < ?", Time.current]) }
  38 + scope :non_expired, -> { where(["expires_at IS NULL OR expires_at > ?", Time.current]) }
37 39
38 def self.content_types 40 def self.content_types
39 [ 41 [
app/models/user.rb
@@ -78,6 +78,7 @@ class User &lt; ActiveRecord::Base @@ -78,6 +78,7 @@ class User &lt; ActiveRecord::Base
78 has_many :team_projects, through: :user_team_project_relationships 78 has_many :team_projects, through: :user_team_project_relationships
79 79
80 # Projects 80 # Projects
  81 + has_many :snippets, dependent: :destroy, foreign_key: :author_id, class_name: "Snippet"
81 has_many :users_projects, dependent: :destroy 82 has_many :users_projects, dependent: :destroy
82 has_many :issues, dependent: :destroy, foreign_key: :author_id 83 has_many :issues, dependent: :destroy, foreign_key: :author_id
83 has_many :notes, dependent: :destroy, foreign_key: :author_id 84 has_many :notes, dependent: :destroy, foreign_key: :author_id
app/views/events/event/_note.html.haml
@@ -5,6 +5,10 @@ @@ -5,6 +5,10 @@
5 - if event.note_commit? 5 - if event.note_commit?
6 = event.note_target_type 6 = event.note_target_type
7 = link_to event.note_short_commit_id, project_commit_path(event.project, event.note_commit_id), class: "commit_short_id" 7 = link_to event.note_short_commit_id, project_commit_path(event.project, event.note_commit_id), class: "commit_short_id"
  8 + - if event.note_project_snippet?
  9 + = link_to project_snippet_path(event.project, event.note_target) do
  10 + %strong
  11 + #{event.note_target_type} ##{truncate event.note_target_id}
8 - else 12 - else
9 = link_to [event.project, event.note_target] do 13 = link_to [event.project, event.note_target] do
10 %strong 14 %strong
app/views/layouts/_head_panel.html.haml
@@ -18,6 +18,9 @@ @@ -18,6 +18,9 @@
18 %li 18 %li
19 = link_to public_root_path, title: "Public area", class: 'has_bottom_tooltip', 'data-original-title' => 'Public area' do 19 = link_to public_root_path, title: "Public area", class: 'has_bottom_tooltip', 'data-original-title' => 'Public area' do
20 %i.icon-globe 20 %i.icon-globe
  21 + %li
  22 + = link_to snippets_path, title: "Snippets area", class: 'has_bottom_tooltip', 'data-original-title' => 'Public area' do
  23 + %i.icon-paste
21 - if current_user.is_admin? 24 - if current_user.is_admin?
22 %li 25 %li
23 = link_to admin_root_path, title: "Admin area", class: 'has_bottom_tooltip', 'data-original-title' => 'Admin area' do 26 = link_to admin_root_path, title: "Admin area", class: 'has_bottom_tooltip', 'data-original-title' => 'Admin area' do
app/views/layouts/snippets.html.haml 0 → 100644
@@ -0,0 +1,23 @@ @@ -0,0 +1,23 @@
  1 +!!! 5
  2 +%html{ lang: "en"}
  3 + = render "layouts/head", title: "Snipepts"
  4 + %body{class: "#{app_theme} application"}
  5 + = render "layouts/head_panel", title: "Snippets"
  6 + = render "layouts/flash"
  7 + %nav.main-nav
  8 + .container
  9 + %ul
  10 + = nav_link(path: 'dashboard#show', html_options: {class: 'home'}) do
  11 + = link_to root_path, title: "Back to dashboard" do
  12 + %i.icon-home
  13 + = nav_link(path: 'snippet#new') do
  14 + = link_to new_snippet_path do
  15 + New snippet
  16 + = nav_link(path: 'snippets#user_index') do
  17 + = link_to user_snippets_path(@current_user) do
  18 + My snippets
  19 + = nav_link(path: 'snippets#index') do
  20 + = link_to snippets_path do
  21 + Discover snippets
  22 + .container
  23 + .content= yield
app/views/projects/snippets/_blob.html.haml 0 → 100644
@@ -0,0 +1,12 @@ @@ -0,0 +1,12 @@
  1 +.file_holder
  2 + .file_title
  3 + %i.icon-file
  4 + %strong= @snippet.file_name
  5 + %span.options
  6 + = link_to "raw", raw_project_snippet_path(@project, @snippet), class: "btn btn-tiny", target: "_blank"
  7 + .file_content.code
  8 + - unless @snippet.content.empty?
  9 + %div{class: user_color_scheme_class}
  10 + = raw @snippet.colorize(formatter: :gitlab)
  11 + - else
  12 + %p.nothing_here_message Empty file
app/views/projects/snippets/_form.html.haml 0 → 100644
@@ -0,0 +1,41 @@ @@ -0,0 +1,41 @@
  1 +%h3.page_title
  2 + = @snippet.new_record? ? "New Snippet" : "Edit Snippet ##{@snippet.id}"
  3 +%hr
  4 +.snippet-form-holder
  5 + = form_for [@project, @snippet], as: :project_snippet, url: url do |f|
  6 + -if @snippet.errors.any?
  7 + .alert.alert-error
  8 + %ul
  9 + - @snippet.errors.full_messages.each do |msg|
  10 + %li= msg
  11 +
  12 + .clearfix
  13 + = f.label :title
  14 + .input= f.text_field :title, placeholder: "Example Snippet", class: 'input-xlarge', required: true
  15 + .clearfix
  16 + = f.label "Lifetime"
  17 + .input= f.select :expires_at, lifetime_select_options, {}, {class: 'chosen span2'}
  18 + .clearfix
  19 + .file-editor
  20 + = f.label :file_name, "File"
  21 + .input
  22 + .file_holder.snippet
  23 + .file_title
  24 + = f.text_field :file_name, placeholder: "example.rb", class: 'snippet-file-name', required: true
  25 + .file_content.code
  26 + %pre#editor= @snippet.content
  27 + = f.hidden_field :content, class: 'snippet-file-content'
  28 +
  29 + .form-actions
  30 + = f.submit 'Save', class: "btn-save btn"
  31 + = link_to "Cancel", project_snippets_path(@project), class: " btn"
  32 + - unless @snippet.new_record?
  33 + .pull-right= link_to 'Destroy', project_snippet_path(@project, @snippet), confirm: 'Are you sure?', method: :delete, class: "btn pull-right danger delete-snippet", id: "destroy_snippet_#{@snippet.id}"
  34 +
  35 +
  36 +:javascript
  37 + var editor = ace.edit("editor");
  38 + $(".snippet-form-holder form").submit(function(){
  39 + $(".snippet-file-content").val(editor.getValue());
  40 + });
  41 +
app/views/projects/snippets/_snippet.html.haml 0 → 100644
@@ -0,0 +1,13 @@ @@ -0,0 +1,13 @@
  1 +%tr
  2 + %td
  3 + = image_tag gravatar_icon(snippet.author_email), class: "avatar s24"
  4 + %a{href: project_snippet_path(snippet.project, snippet)}
  5 + %strong= truncate(snippet.title, length: 60)
  6 + %td
  7 + = snippet.file_name
  8 + %td
  9 + %span.cgray
  10 + - if snippet.expires_at
  11 + = snippet.expires_at.to_date.to_s(:short)
  12 + - else
  13 + Never
app/views/projects/snippets/edit.html.haml 0 → 100644
@@ -0,0 +1 @@ @@ -0,0 +1 @@
  1 += render "projects/snippets/form", url: project_snippet_path(@project, @snippet)
app/views/projects/snippets/index.html.haml 0 → 100644
@@ -0,0 +1,19 @@ @@ -0,0 +1,19 @@
  1 +%h3.page_title
  2 + Snippets
  3 + %small share code pastes with others out of git repository
  4 +
  5 + - if can? current_user, :write_project_snippet, @project
  6 + = link_to new_project_snippet_path(@project), class: "btn btn-small add_new pull-right", title: "New Snippet" do
  7 + Add new snippet
  8 +%br
  9 +%table
  10 + %thead
  11 + %tr
  12 + %th Title
  13 + %th File Name
  14 + %th Expires At
  15 + = render partial: "projects/snippets/snippet", collection: @snippets
  16 + - if @snippets.empty?
  17 + %tr
  18 + %td{colspan: 3}
  19 + %h3.nothing_here_message Nothing here.
app/views/projects/snippets/new.html.haml 0 → 100644
@@ -0,0 +1 @@ @@ -0,0 +1 @@
  1 += render "projects/snippets/form", url: project_snippets_path(@project, @snippet)
app/views/projects/snippets/show.html.haml 0 → 100644
@@ -0,0 +1,9 @@ @@ -0,0 +1,9 @@
  1 +%h3.page_title
  2 + = @snippet.title
  3 + %small= @snippet.file_name
  4 + - if can?(current_user, :admin_project_snippet, @project) || @snippet.author == current_user
  5 + = link_to "Edit", edit_project_snippet_path(@project, @snippet), class: "btn btn-small pull-right", title: 'Edit Snippet'
  6 +
  7 +%br
  8 +%div= render 'projects/snippets/blob'
  9 +%div#notes= render "notes/notes_with_form"
app/views/snippets/_blob.html.haml
@@ -3,7 +3,7 @@ @@ -3,7 +3,7 @@
3 %i.icon-file 3 %i.icon-file
4 %strong= @snippet.file_name 4 %strong= @snippet.file_name
5 %span.options 5 %span.options
6 - = link_to "raw", raw_project_snippet_path(@project, @snippet), class: "btn btn-tiny", target: "_blank" 6 + = link_to "raw", raw_snippet_path(@snippet), class: "btn btn-tiny", target: "_blank"
7 .file_content.code 7 .file_content.code
8 - unless @snippet.content.empty? 8 - unless @snippet.content.empty?
9 %div{class: user_color_scheme_class} 9 %div{class: user_color_scheme_class}
app/views/snippets/_form.html.haml
@@ -2,7 +2,7 @@ @@ -2,7 +2,7 @@
2 = @snippet.new_record? ? "New Snippet" : "Edit Snippet ##{@snippet.id}" 2 = @snippet.new_record? ? "New Snippet" : "Edit Snippet ##{@snippet.id}"
3 %hr 3 %hr
4 .snippet-form-holder 4 .snippet-form-holder
5 - = form_for [@project, @snippet] do |f| 5 + = form_for @snippet, as: :personal_snippet, url: url do |f|
6 -if @snippet.errors.any? 6 -if @snippet.errors.any?
7 .alert.alert-error 7 .alert.alert-error
8 %ul 8 %ul
@@ -13,6 +13,9 @@ @@ -13,6 +13,9 @@
13 = f.label :title 13 = f.label :title
14 .input= f.text_field :title, placeholder: "Example Snippet", class: 'input-xlarge', required: true 14 .input= f.text_field :title, placeholder: "Example Snippet", class: 'input-xlarge', required: true
15 .clearfix 15 .clearfix
  16 + = f.label "Private?"
  17 + .input= f.check_box :private, {class: ''}
  18 + .clearfix
16 = f.label "Lifetime" 19 = f.label "Lifetime"
17 .input= f.select :expires_at, lifetime_select_options, {}, {class: 'chosen span2'} 20 .input= f.select :expires_at, lifetime_select_options, {}, {class: 'chosen span2'}
18 .clearfix 21 .clearfix
@@ -28,9 +31,9 @@ @@ -28,9 +31,9 @@
28 31
29 .form-actions 32 .form-actions
30 = f.submit 'Save', class: "btn-save btn" 33 = f.submit 'Save', class: "btn-save btn"
31 - = link_to "Cancel", project_snippets_path(@project), class: " btn" 34 + = link_to "Cancel", snippets_path(@project), class: " btn"
32 - unless @snippet.new_record? 35 - unless @snippet.new_record?
33 - .pull-right= link_to 'Destroy', [@project, @snippet], confirm: 'Removed snippet cannot be restored! Are you sure?', method: :delete, class: "btn pull-right danger delete-snippet", id: "destroy_snippet_#{@snippet.id}" 36 + .pull-right= link_to 'Destroy', snippet_path(@snippet), confirm: 'Removed snippet cannot be restored! Are you sure?', method: :delete, class: "btn pull-right danger delete-snippet", id: "destroy_snippet_#{@snippet.id}"
34 37
35 38
36 :javascript 39 :javascript
app/views/snippets/_snippet.html.haml
1 %tr 1 %tr
2 %td 2 %td
  3 + - if snippet.private?
  4 + %i.icon-lock
  5 + - else
  6 + %i.icon-globe
3 = image_tag gravatar_icon(snippet.author_email), class: "avatar s24" 7 = image_tag gravatar_icon(snippet.author_email), class: "avatar s24"
4 - %a{href: project_snippet_path(snippet.project, snippet)}  
5 - %strong= truncate(snippet.title, length: 60) 8 + - if snippet.project_id?
  9 + %a{href: project_snippet_path(snippet.project, snippet)}
  10 + %strong= truncate(snippet.title, length: 60)
  11 + - else
  12 + %a{href: snippet_path(snippet)}
  13 + %strong= truncate(snippet.title, length: 60)
6 %td 14 %td
7 = snippet.file_name 15 = snippet.file_name
8 %td 16 %td
@@ -11,3 +19,6 @@ @@ -11,3 +19,6 @@
11 = snippet.expires_at.to_date.to_s(:short) 19 = snippet.expires_at.to_date.to_s(:short)
12 - else 20 - else
13 Never 21 Never
  22 + %td
  23 + - if snippet.project_id?
  24 + = link_to snippet.project.name, project_path(snippet.project)
app/views/snippets/_snippets.html.haml 0 → 100644
@@ -0,0 +1,15 @@ @@ -0,0 +1,15 @@
  1 +%table
  2 + %thead
  3 + %tr
  4 + %th Title
  5 + %th File Name
  6 + %th Expires At
  7 + %th Project
  8 +
  9 + = render partial: 'snippet', collection: @snippets
  10 + - if @snippets.empty?
  11 + %tr
  12 + %td{colspan: 4}
  13 + %h3.nothing_here_message Nothing here.
  14 +
  15 += paginate @snippets
app/views/snippets/edit.html.haml
1 -= render "snippets/form" 1 += render "snippets/form", url: snippet_path(@snippet)
app/views/snippets/index.html.haml
1 %h3.page_title 1 %h3.page_title
2 - Snippets 2 + Public snippets
3 %small share code pastes with others out of git repository 3 %small share code pastes with others out of git repository
  4 + = link_to new_snippet_path, class: "btn btn-small add_new pull-right", title: "New Snippet" do
  5 + Add new snippet
  6 +
  7 +%hr
  8 +.row
  9 + .span12
  10 + = render 'snippets'
4 11
5 - - if can? current_user, :write_snippet, @project  
6 - = link_to new_project_snippet_path(@project), class: "btn btn-small add_new pull-right", title: "New Snippet" do  
7 - Add new snippet  
8 -%br  
9 -%table  
10 - %thead  
11 - %tr  
12 - %th Title  
13 - %th File Name  
14 - %th Expires At  
15 - = render @snippets  
16 - - if @snippets.empty?  
17 - %tr  
18 - %td{colspan: 3}  
19 - %h3.nothing_here_message Nothing here.  
app/views/snippets/new.html.haml
1 -= render "snippets/form" 1 += render "snippets/form", url: snippets_path(@snippet)
app/views/snippets/show.html.haml
1 %h3.page_title 1 %h3.page_title
  2 + - if @snippet.private?
  3 + %i.icon-lock
  4 + - else
  5 + %i.icon-globe
  6 +
2 = @snippet.title 7 = @snippet.title
3 %small= @snippet.file_name 8 %small= @snippet.file_name
4 - - if can?(current_user, :admin_snippet, @project) || @snippet.author == current_user  
5 - = link_to "Edit", edit_project_snippet_path(@project, @snippet), class: "btn btn-small pull-right", title: 'Edit Snippet' 9 + - if @snippet.author == current_user
  10 + = link_to "Edit", edit_snippet_path(@snippet), class: "btn btn-small pull-right", title: 'Edit Snippet'
6 11
7 %br 12 %br
8 %div= render 'blob' 13 %div= render 'blob'
9 -%div#notes= render "notes/notes_with_form"  
app/views/snippets/user_index.html.haml 0 → 100644
@@ -0,0 +1,20 @@ @@ -0,0 +1,20 @@
  1 +%h3.page_title
  2 + Snippets by
  3 + = @user.name
  4 + %small share code pastes with others out of git repository
  5 + = link_to new_snippet_path, class: "btn btn-small add_new pull-right", title: "New Snippet" do
  6 + Add new snippet
  7 +
  8 +%hr
  9 +.row
  10 + .span3
  11 + %ul.nav.nav-pills.nav-stacked
  12 + = nav_tab :scope, nil do
  13 + = link_to "All", user_snippets_path(@user)
  14 + = nav_tab :scope, 'private' do
  15 + = link_to "Private", user_snippets_path(@user, scope: 'private')
  16 + = nav_tab :scope, 'public' do
  17 + = link_to "Public", user_snippets_path(@user, scope: 'public')
  18 +
  19 + .span9
  20 + = render 'snippets'
config/routes.rb
@@ -39,6 +39,16 @@ Gitlab::Application.routes.draw do @@ -39,6 +39,16 @@ Gitlab::Application.routes.draw do
39 get 'help/workflow' => 'help#workflow' 39 get 'help/workflow' => 'help#workflow'
40 40
41 # 41 #
  42 + # Global snippets
  43 + #
  44 + resources :snippets do
  45 + member do
  46 + get "raw"
  47 + end
  48 + end
  49 + get "/s/:username" => "snippets#user_index", as: :user_snippets, constraints: { username: /.*/ }
  50 +
  51 + #
42 # Public namespace 52 # Public namespace
43 # 53 #
44 namespace :public do 54 namespace :public do
@@ -182,6 +192,14 @@ Gitlab::Application.routes.draw do @@ -182,6 +192,14 @@ Gitlab::Application.routes.draw do
182 resources :graph, only: [:show], constraints: {id: /(?:[^.]|\.(?!json$))+/, format: /json/} 192 resources :graph, only: [:show], constraints: {id: /(?:[^.]|\.(?!json$))+/, format: /json/}
183 match "/compare/:from...:to" => "compare#show", as: "compare", via: [:get, :post], constraints: {from: /.+/, to: /.+/} 193 match "/compare/:from...:to" => "compare#show", as: "compare", via: [:get, :post], constraints: {from: /.+/, to: /.+/}
184 194
  195 + scope module: :projects do
  196 + resources :snippets do
  197 + member do
  198 + get "raw"
  199 + end
  200 + end
  201 + end
  202 +
185 resources :wikis, only: [:show, :edit, :destroy, :create] do 203 resources :wikis, only: [:show, :edit, :destroy, :create] do
186 collection do 204 collection do
187 get :pages 205 get :pages
@@ -255,19 +273,12 @@ Gitlab::Application.routes.draw do @@ -255,19 +273,12 @@ Gitlab::Application.routes.draw do
255 end 273 end
256 end 274 end
257 275
258 - resources :snippets do  
259 - member do  
260 - get "raw"  
261 - end  
262 - end  
263 -  
264 resources :hooks, only: [:index, :create, :destroy] do 276 resources :hooks, only: [:index, :create, :destroy] do
265 member do 277 member do
266 get :test 278 get :test
267 end 279 end
268 end 280 end
269 281
270 -  
271 resources :team, controller: 'team_members', only: [:index] 282 resources :team, controller: 'team_members', only: [:index]
272 resources :milestones, except: [:destroy] 283 resources :milestones, except: [:destroy]
273 284
db/migrate/20130323174317_add_private_to_snippets.rb 0 → 100644
@@ -0,0 +1,5 @@ @@ -0,0 +1,5 @@
  1 +class AddPrivateToSnippets < ActiveRecord::Migration
  2 + def change
  3 + add_column :snippets, :private, :boolean, null: false, default: true
  4 + end
  5 +end
db/migrate/20130324151736_add_type_to_snippets.rb 0 → 100644
@@ -0,0 +1,5 @@ @@ -0,0 +1,5 @@
  1 +class AddTypeToSnippets < ActiveRecord::Migration
  2 + def change
  3 + add_column :snippets, :type, :string
  4 + end
  5 +end
db/migrate/20130324172327_change_project_id_to_null_in_snipepts.rb 0 → 100644
@@ -0,0 +1,9 @@ @@ -0,0 +1,9 @@
  1 +class ChangeProjectIdToNullInSnipepts < ActiveRecord::Migration
  2 + def up
  3 + change_column :snippets, :project_id, :integer, :null => true
  4 + end
  5 +
  6 + def down
  7 + change_column :snippets, :project_id, :integer, :null => false
  8 + end
  9 +end
db/migrate/20130324203535_add_type_value_for_snippets.rb 0 → 100644
@@ -0,0 +1,8 @@ @@ -0,0 +1,8 @@
  1 +class AddTypeValueForSnippets < ActiveRecord::Migration
  2 + def up
  3 + Snippet.where("project_id IS NOT NULL").update_all(type: 'ProjectSnippet')
  4 + end
  5 +
  6 + def down
  7 + end
  8 +end
@@ -203,12 +203,14 @@ ActiveRecord::Schema.define(:version =&gt; 20130522141856) do @@ -203,12 +203,14 @@ ActiveRecord::Schema.define(:version =&gt; 20130522141856) do
203 create_table "snippets", :force => true do |t| 203 create_table "snippets", :force => true do |t|
204 t.string "title" 204 t.string "title"
205 t.text "content" 205 t.text "content"
206 - t.integer "author_id", :null => false  
207 - t.integer "project_id", :null => false  
208 - t.datetime "created_at", :null => false  
209 - t.datetime "updated_at", :null => false 206 + t.integer "author_id", :null => false
  207 + t.integer "project_id"
  208 + t.datetime "created_at", :null => false
  209 + t.datetime "updated_at", :null => false
210 t.string "file_name" 210 t.string "file_name"
211 t.datetime "expires_at" 211 t.datetime "expires_at"
  212 + t.boolean "private", :default => true, :null => false
  213 + t.string "type"
212 end 214 end
213 215
214 add_index "snippets", ["created_at"], :name => "index_snippets_on_created_at" 216 add_index "snippets", ["created_at"], :name => "index_snippets_on_created_at"
features/project/snippets.feature 0 → 100644
@@ -0,0 +1,35 @@ @@ -0,0 +1,35 @@
  1 +Feature: Project Snippets
  2 + Background:
  3 + Given I sign in as a user
  4 + And I own project "Shop"
  5 + And project "Shop" have "Snippet one" snippet
  6 + And project "Shop" have no "Snippet two" snippet
  7 + And I visit project "Shop" snippets page
  8 +
  9 + Scenario: I should see snippets
  10 + Given I visit project "Shop" snippets page
  11 + Then I should see "Snippet one" in snippets
  12 + And I should not see "Snippet two" in snippets
  13 +
  14 + Scenario: I create new project snippet
  15 + Given I click link "New Snippet"
  16 + And I submit new snippet "Snippet three"
  17 + Then I should see snippet "Snippet three"
  18 +
  19 + @javascript
  20 + Scenario: I comment on a snippet "Snippet one"
  21 + Given I visit snippet page "Snippet one"
  22 + And I leave a comment like "Good snippet!"
  23 + Then I should see comment "Good snippet!"
  24 +
  25 + Scenario: I update "Snippet one"
  26 + Given I visit snippet page "Snippet one"
  27 + And I click link "Edit"
  28 + And I submit new title "Snippet new title"
  29 + Then I should see "Snippet new title"
  30 +
  31 + Scenario: I destroy "Snippet one"
  32 + Given I visit snippet page "Snippet one"
  33 + And I click link "Edit"
  34 + And I click link "Destroy"
  35 + Then I should not see "Snippet one" in snippets
features/snippets/discover_snippets.feature 0 → 100644
@@ -0,0 +1,10 @@ @@ -0,0 +1,10 @@
  1 +Feature: Discover Snippets
  2 + Background:
  3 + Given I sign in as a user
  4 + And I have public "Personal snippet one" snippet
  5 + And I have private "Personal snippet private" snippet
  6 +
  7 + Scenario: I should see snippets
  8 + Given I visit snippets page
  9 + Then I should see "Personal snippet one" in snippets
  10 + And I should not see "Personal snippet private" in snippets
features/snippets/snippets.feature 0 → 100644
@@ -0,0 +1,28 @@ @@ -0,0 +1,28 @@
  1 +Feature: Snippets Feature
  2 + Background:
  3 + Given I sign in as a user
  4 + And I have public "Personal snippet one" snippet
  5 + And I have private "Personal snippet private" snippet
  6 +
  7 + Scenario: I create new snippet
  8 + Given I visit new snippet page
  9 + And I submit new snippet "Personal snippet three"
  10 + Then I should see snippet "Personal snippet three"
  11 +
  12 + Scenario: I update "Personal snippet one"
  13 + Given I visit snippet page "Personal snippet one"
  14 + And I click link "Edit"
  15 + And I submit new title "Personal snippet new title"
  16 + Then I should see "Personal snippet new title"
  17 +
  18 + Scenario: Set "Personal snippet one" public
  19 + Given I visit snippet page "Personal snippet one"
  20 + And I click link "Edit"
  21 + And I uncheck "Private" checkbox
  22 + Then I should see "Personal snippet one" public
  23 +
  24 + Scenario: I destroy "Personal snippet one"
  25 + Given I visit snippet page "Personal snippet one"
  26 + And I click link "Edit"
  27 + And I click link "Destroy"
  28 + Then I should not see "Personal snippet one" in snippets
features/snippets/user_snippets.feature 0 → 100644
@@ -0,0 +1,22 @@ @@ -0,0 +1,22 @@
  1 +Feature: User Snippets
  2 + Background:
  3 + Given I sign in as a user
  4 + And I have public "Personal snippet one" snippet
  5 + And I have private "Personal snippet private" snippet
  6 +
  7 + Scenario: I should see all my snippets
  8 + Given I visit my snippets page
  9 + Then I should see "Personal snippet one" in snippets
  10 + And I should see "Personal snippet private" in snippets
  11 +
  12 + Scenario: I can see only my private snippets
  13 + Given I visit my snippets page
  14 + And I click "Private" filter
  15 + Then I should not see "Personal snippet one" in snippets
  16 + And I should see "Personal snippet private" in snippets
  17 +
  18 + Scenario: I can see only my public snippets
  19 + Given I visit my snippets page
  20 + And I click "Public" filter
  21 + Then I should see "Personal snippet one" in snippets
  22 + And I should not see "Personal snippet private" in snippets
features/steps/project/project_snippets.rb 0 → 100644
@@ -0,0 +1,100 @@ @@ -0,0 +1,100 @@
  1 +class ProjectSnippets < Spinach::FeatureSteps
  2 + include SharedAuthentication
  3 + include SharedProject
  4 + include SharedNote
  5 + include SharedPaths
  6 +
  7 + And 'project "Shop" have "Snippet one" snippet' do
  8 + create(:project_snippet,
  9 + title: "Snippet one",
  10 + content: "Test content",
  11 + file_name: "snippet.rb",
  12 + project: project,
  13 + author: project.users.first)
  14 + end
  15 +
  16 + And 'project "Shop" have no "Snippet two" snippet' do
  17 + create(:snippet,
  18 + title: "Snippet two",
  19 + content: "Test content",
  20 + file_name: "snippet.rb",
  21 + author: project.users.first)
  22 + end
  23 +
  24 + Given 'I click link "New Snippet"' do
  25 + click_link "Add new snippet"
  26 + end
  27 +
  28 + Given 'I click link "Snippet one"' do
  29 + click_link "Snippet one"
  30 + end
  31 +
  32 + Then 'I should see "Snippet one" in snippets' do
  33 + page.should have_content "Snippet one"
  34 + end
  35 +
  36 + And 'I should not see "Snippet two" in snippets' do
  37 + page.should_not have_content "Snippet two"
  38 + end
  39 +
  40 + And 'I should not see "Snippet one" in snippets' do
  41 + page.should_not have_content "Snippet one"
  42 + end
  43 +
  44 + And 'I click link "Edit"' do
  45 + within ".page_title" do
  46 + click_link "Edit"
  47 + end
  48 + end
  49 +
  50 + And 'I click link "Destroy"' do
  51 + click_link "Destroy"
  52 + end
  53 +
  54 + And 'I submit new snippet "Snippet three"' do
  55 + fill_in "project_snippet_title", :with => "Snippet three"
  56 + select "forever", :from => "project_snippet_expires_at"
  57 + fill_in "project_snippet_file_name", :with => "my_snippet.rb"
  58 + within('.file-editor') do
  59 + find(:xpath, "//input[@id='project_snippet_content']").set 'Content of snippet three'
  60 + end
  61 + click_button "Save"
  62 + end
  63 +
  64 + Then 'I should see snippet "Snippet three"' do
  65 + page.should have_content "Snippet three"
  66 + page.should have_content "Content of snippet three"
  67 + end
  68 +
  69 + And 'I submit new title "Snippet new title"' do
  70 + fill_in "project_snippet_title", :with => "Snippet new title"
  71 + click_button "Save"
  72 + end
  73 +
  74 + Then 'I should see "Snippet new title"' do
  75 + page.should have_content "Snippet new title"
  76 + end
  77 +
  78 + And 'I leave a comment like "Good snippet!"' do
  79 + within('.js-main-target-form') do
  80 + fill_in "note_note", with: "Good snippet!"
  81 + click_button "Add Comment"
  82 + end
  83 + end
  84 +
  85 + Then 'I should see comment "Good snippet!"' do
  86 + page.should have_content "Good snippet!"
  87 + end
  88 +
  89 + And 'I visit snippet page "Snippet one"' do
  90 + visit project_snippet_path(project, project_snippet)
  91 + end
  92 +
  93 + def project
  94 + @project ||= Project.find_by_name!("Shop")
  95 + end
  96 +
  97 + def project_snippet
  98 + @project_snippet ||= ProjectSnippet.find_by_title!("Snippet One")
  99 + end
  100 +end
features/steps/shared/paths.rb
@@ -275,6 +275,22 @@ module SharedPaths @@ -275,6 +275,22 @@ module SharedPaths
275 visit public_root_path 275 visit public_root_path
276 end 276 end
277 277
  278 + # ----------------------------------------
  279 + # Snippets
  280 + # ----------------------------------------
  281 +
  282 + Given 'I visit project "Shop" snippets page' do
  283 + visit project_snippets_path(project)
  284 + end
  285 +
  286 + Given 'I visit snippets page' do
  287 + visit snippets_path
  288 + end
  289 +
  290 + Given 'I visit new snippet page' do
  291 + visit new_snippet_path
  292 + end
  293 +
278 def root_ref 294 def root_ref
279 @project.repository.root_ref 295 @project.repository.root_ref
280 end 296 end
features/steps/shared/snippet.rb 0 → 100644
@@ -0,0 +1,21 @@ @@ -0,0 +1,21 @@
  1 +module SharedSnippet
  2 + include Spinach::DSL
  3 +
  4 + And 'I have public "Personal snippet one" snippet' do
  5 + create(:personal_snippet,
  6 + title: "Personal snippet one",
  7 + content: "Test content",
  8 + file_name: "snippet.rb",
  9 + private: false,
  10 + author: current_user)
  11 + end
  12 +
  13 + And 'I have private "Personal snippet private" snippet' do
  14 + create(:personal_snippet,
  15 + title: "Personal snippet private",
  16 + content: "Provate content",
  17 + file_name: "private_snippet.rb",
  18 + private: true,
  19 + author: current_user)
  20 + end
  21 +end
features/steps/snippets/discover_snippets.rb 0 → 100644
@@ -0,0 +1,17 @@ @@ -0,0 +1,17 @@
  1 +class DiscoverSnippets < Spinach::FeatureSteps
  2 + include SharedAuthentication
  3 + include SharedPaths
  4 + include SharedSnippet
  5 +
  6 + Then 'I should see "Personal snippet one" in snippets' do
  7 + page.should have_content "Personal snippet one"
  8 + end
  9 +
  10 + And 'I should not see "Personal snippet private" in snippets' do
  11 + page.should_not have_content "Personal snippet private"
  12 + end
  13 +
  14 + def snippet
  15 + @snippet ||= PersonalSnippet.find_by_title!("Personal snippet one")
  16 + end
  17 +end
features/steps/snippets/snippets.rb 0 → 100644
@@ -0,0 +1,65 @@ @@ -0,0 +1,65 @@
  1 +class SnippetsFeature < Spinach::FeatureSteps
  2 + include SharedAuthentication
  3 + include SharedPaths
  4 + include SharedProject
  5 + include SharedSnippet
  6 +
  7 + Given 'I click link "Personal snippet one"' do
  8 + click_link "Personal snippet one"
  9 + end
  10 +
  11 + And 'I should not see "Personal snippet one" in snippets' do
  12 + page.should_not have_content "Personal snippet one"
  13 + end
  14 +
  15 + And 'I click link "Edit"' do
  16 + within ".page_title" do
  17 + click_link "Edit"
  18 + end
  19 + end
  20 +
  21 + And 'I click link "Destroy"' do
  22 + click_link "Destroy"
  23 + end
  24 +
  25 + And 'I submit new snippet "Personal snippet three"' do
  26 + fill_in "personal_snippet_title", :with => "Personal snippet three"
  27 + select "forever", :from => "personal_snippet_expires_at"
  28 + fill_in "personal_snippet_file_name", :with => "my_snippet.rb"
  29 + within('.file-editor') do
  30 + find(:xpath, "//input[@id='personal_snippet_content']").set 'Content of snippet three'
  31 + end
  32 + click_button "Save"
  33 + end
  34 +
  35 + Then 'I should see snippet "Personal snippet three"' do
  36 + page.should have_content "Personal snippet three"
  37 + page.should have_content "Content of snippet three"
  38 + end
  39 +
  40 + And 'I submit new title "Personal snippet new title"' do
  41 + fill_in "personal_snippet_title", :with => "Personal snippet new title"
  42 + click_button "Save"
  43 + end
  44 +
  45 + Then 'I should see "Personal snippet new title"' do
  46 + page.should have_content "Personal snippet new title"
  47 + end
  48 +
  49 + And 'I uncheck "Private" checkbox' do
  50 + find(:xpath, "//input[@id='personal_snippet_private']").set true
  51 + click_button "Save"
  52 + end
  53 +
  54 + Then 'I should see "Personal snippet one" public' do
  55 + page.should have_no_xpath("//i[@class='public-snippet']")
  56 + end
  57 +
  58 + And 'I visit snippet page "Personal snippet one"' do
  59 + visit snippet_path(snippet)
  60 + end
  61 +
  62 + def snippet
  63 + @snippet ||= PersonalSnippet.find_by_title!("Personal snippet one")
  64 + end
  65 +end
features/steps/snippets/user_snippets.rb 0 → 100644
@@ -0,0 +1,41 @@ @@ -0,0 +1,41 @@
  1 +class UserSnippets < Spinach::FeatureSteps
  2 + include SharedAuthentication
  3 + include SharedPaths
  4 + include SharedSnippet
  5 +
  6 + Given 'I visit my snippets page' do
  7 + visit user_snippets_path(current_user)
  8 + end
  9 +
  10 + Then 'I should see "Personal snippet one" in snippets' do
  11 + page.should have_content "Personal snippet one"
  12 + end
  13 +
  14 + And 'I should see "Personal snippet private" in snippets' do
  15 + page.should have_content "Personal snippet private"
  16 + end
  17 +
  18 + Then 'I should not see "Personal snippet one" in snippets' do
  19 + page.should_not have_content "Personal snippet one"
  20 + end
  21 +
  22 + And 'I should not see "Personal snippet private" in snippets' do
  23 + page.should_not have_content "Personal snippet private"
  24 + end
  25 +
  26 + Given 'I click "Public" filter' do
  27 + within('.nav-stacked') do
  28 + click_link "Public"
  29 + end
  30 + end
  31 +
  32 + Given 'I click "Private" filter' do
  33 + within('.nav-stacked') do
  34 + click_link "Private"
  35 + end
  36 + end
  37 +
  38 + def snippet
  39 + @snippet ||= PersonalSnippet.find_by_title!("Personal snippet one")
  40 + end
  41 +end
lib/api/projects.rb
@@ -328,7 +328,7 @@ module API @@ -328,7 +328,7 @@ module API
328 # Example Request: 328 # Example Request:
329 # POST /projects/:id/snippets 329 # POST /projects/:id/snippets
330 post ":id/snippets" do 330 post ":id/snippets" do
331 - authorize! :write_snippet, user_project 331 + authorize! :write_project_snippet, user_project
332 required_attributes! [:title, :file_name, :code] 332 required_attributes! [:title, :file_name, :code]
333 333
334 attrs = attributes_for_keys [:title, :file_name] 334 attrs = attributes_for_keys [:title, :file_name]
@@ -357,7 +357,7 @@ module API @@ -357,7 +357,7 @@ module API
357 # PUT /projects/:id/snippets/:snippet_id 357 # PUT /projects/:id/snippets/:snippet_id
358 put ":id/snippets/:snippet_id" do 358 put ":id/snippets/:snippet_id" do
359 @snippet = user_project.snippets.find(params[:snippet_id]) 359 @snippet = user_project.snippets.find(params[:snippet_id])
360 - authorize! :modify_snippet, @snippet 360 + authorize! :modify_project_snippet, @snippet
361 361
362 attrs = attributes_for_keys [:title, :file_name] 362 attrs = attributes_for_keys [:title, :file_name]
363 attrs[:expires_at] = params[:lifetime] if params[:lifetime].present? 363 attrs[:expires_at] = params[:lifetime] if params[:lifetime].present?
@@ -380,7 +380,7 @@ module API @@ -380,7 +380,7 @@ module API
380 delete ":id/snippets/:snippet_id" do 380 delete ":id/snippets/:snippet_id" do
381 begin 381 begin
382 @snippet = user_project.snippets.find(params[:snippet_id]) 382 @snippet = user_project.snippets.find(params[:snippet_id])
383 - authorize! :modify_snippet, user_project 383 + authorize! :modify_project_snippet, @snippet
384 @snippet.destroy 384 @snippet.destroy
385 rescue 385 rescue
386 end 386 end
spec/factories.rb
@@ -197,7 +197,7 @@ FactoryGirl.define do @@ -197,7 +197,7 @@ FactoryGirl.define do
197 url 197 url
198 end 198 end
199 199
200 - factory :snippet do 200 + factory :project_snippet do
201 project 201 project
202 author 202 author
203 title 203 title
@@ -205,6 +205,20 @@ FactoryGirl.define do @@ -205,6 +205,20 @@ FactoryGirl.define do
205 file_name 205 file_name
206 end 206 end
207 207
  208 + factory :personal_snippet do
  209 + author
  210 + title
  211 + content
  212 + file_name
  213 + end
  214 +
  215 + factory :snippet do
  216 + author
  217 + title
  218 + content
  219 + file_name
  220 + end
  221 +
208 factory :protected_branch do 222 factory :protected_branch do
209 name 223 name
210 project 224 project
spec/features/snippets_spec.rb
@@ -1,99 +0,0 @@ @@ -1,99 +0,0 @@
1 -require 'spec_helper'  
2 -  
3 -describe "Snippets" do  
4 - let(:project) { create(:project) }  
5 -  
6 - before do  
7 - login_as :user  
8 - project.team << [@user, :developer]  
9 - end  
10 -  
11 - describe "GET /snippets" do  
12 - before do  
13 - @snippet = create(:snippet,  
14 - author: @user,  
15 - project: project)  
16 -  
17 - visit project_snippets_path(project)  
18 - end  
19 -  
20 - subject { page }  
21 -  
22 - it { should have_content(@snippet.title[0..10]) }  
23 - it { should have_content(@snippet.project.name) }  
24 -  
25 - describe "Destroy" do  
26 - before do  
27 - # admin access to remove snippet  
28 - @user.users_projects.destroy_all  
29 - project.team << [@user, :master]  
30 - visit edit_project_snippet_path(project, @snippet)  
31 - end  
32 -  
33 - it "should remove entry" do  
34 - expect {  
35 - click_link "destroy_snippet_#{@snippet.id}"  
36 - }.to change { Snippet.count }.by(-1)  
37 - end  
38 - end  
39 - end  
40 -  
41 - describe "New snippet" do  
42 - before do  
43 - visit project_snippets_path(project)  
44 - click_link "New Snippet"  
45 - end  
46 -  
47 - it "should open new snippet popup" do  
48 - page.current_path.should == new_project_snippet_path(project)  
49 - end  
50 -  
51 - describe "fill in", js: true do  
52 - before do  
53 - fill_in "snippet_title", with: "login function"  
54 - fill_in "snippet_file_name", with: "test.rb"  
55 - page.execute_script("editor.insert('def login; end');")  
56 - end  
57 -  
58 - it { expect { click_button "Save" }.to change {Snippet.count}.by(1) }  
59 -  
60 - it "should add new snippet to table" do  
61 - click_button "Save"  
62 - page.current_path.should == project_snippet_path(project, Snippet.last)  
63 - page.should have_content "login function"  
64 - page.should have_content "test.rb"  
65 - end  
66 - end  
67 - end  
68 -  
69 - describe "Edit snippet" do  
70 - before do  
71 - @snippet = create(:snippet,  
72 - author: @user,  
73 - project: project)  
74 - visit project_snippet_path(project, @snippet)  
75 - click_link "Edit Snippet"  
76 - end  
77 -  
78 - it "should open edit page" do  
79 - page.current_path.should == edit_project_snippet_path(project, @snippet)  
80 - end  
81 -  
82 - describe "fill in" do  
83 - before do  
84 - fill_in "snippet_title", with: "login function"  
85 - fill_in "snippet_file_name", with: "test.rb"  
86 - end  
87 -  
88 - it { expect { click_button "Save" }.to_not change {Snippet.count} }  
89 -  
90 - it "should update snippet fields" do  
91 - click_button "Save"  
92 -  
93 - page.current_path.should == project_snippet_path(project, @snippet)  
94 - page.should have_content "login function"  
95 - page.should have_content "test.rb"  
96 - end  
97 - end  
98 - end  
99 -end  
spec/helpers/gitlab_markdown_helper_spec.rb
@@ -10,7 +10,7 @@ describe GitlabMarkdownHelper do @@ -10,7 +10,7 @@ describe GitlabMarkdownHelper do
10 let(:commit) { project.repository.commit } 10 let(:commit) { project.repository.commit }
11 let(:issue) { create(:issue, project: project) } 11 let(:issue) { create(:issue, project: project) }
12 let(:merge_request) { create(:merge_request, project: project) } 12 let(:merge_request) { create(:merge_request, project: project) }
13 - let(:snippet) { create(:snippet, project: project) } 13 + let(:snippet) { create(:project_snippet, project: project) }
14 let(:member) { project.users_projects.where(user_id: user).first } 14 let(:member) { project.users_projects.where(user_id: user).first }
15 15
16 before do 16 before do
@@ -190,8 +190,43 @@ describe GitlabMarkdownHelper do @@ -190,8 +190,43 @@ describe GitlabMarkdownHelper do
190 describe "referencing a snippet" do 190 describe "referencing a snippet" do
191 let(:object) { snippet } 191 let(:object) { snippet }
192 let(:reference) { "$#{snippet.id}" } 192 let(:reference) { "$#{snippet.id}" }
  193 + let(:actual) { "Reference to #{reference}" }
  194 + let(:expected) { project_snippet_path(project, object) }
  195 +
  196 + it "should link using a valid id" do
  197 + gfm(actual).should match(expected)
  198 + end
  199 +
  200 + it "should link with adjacent text" do
  201 + # Wrap the reference in parenthesis
  202 + gfm(actual.gsub(reference, "(#{reference})")).should match(expected)
  203 +
  204 + # Append some text to the end of the reference
  205 + gfm(actual.gsub(reference, "#{reference}, right?")).should match(expected)
  206 + end
  207 +
  208 + it "should keep whitespace intact" do
  209 + actual = "Referenced #{reference} already."
  210 + expected = /Referenced <a.+>[^\s]+<\/a> already/
  211 + gfm(actual).should match(expected)
  212 + end
  213 +
  214 + it "should not link with an invalid id" do
  215 + # Modify the reference string so it's still parsed, but is invalid
  216 + reference.gsub!(/^(.)(\d+)$/, '\1' + ('\2' * 2))
  217 + gfm(actual).should == actual
  218 + end
  219 +
  220 + it "should include a title attribute" do
  221 + title = "Snippet: #{object.title}"
  222 + gfm(actual).should match(/title="#{title}"/)
  223 + end
  224 +
  225 + it "should include standard gfm classes" do
  226 + css = object.class.to_s.underscore
  227 + gfm(actual).should match(/class="\s?gfm gfm-snippet\s?"/)
  228 + end
193 229
194 - include_examples 'referenced object'  
195 end 230 end
196 231
197 describe "referencing multiple objects" do 232 describe "referencing multiple objects" do
spec/models/project_snippet_spec.rb 0 → 100644
@@ -0,0 +1,30 @@ @@ -0,0 +1,30 @@
  1 +# == Schema Information
  2 +#
  3 +# Table name: snippets
  4 +#
  5 +# id :integer not null, primary key
  6 +# title :string(255)
  7 +# content :text
  8 +# author_id :integer not null
  9 +# project_id :integer not null
  10 +# created_at :datetime not null
  11 +# updated_at :datetime not null
  12 +# file_name :string(255)
  13 +# expires_at :datetime
  14 +#
  15 +
  16 +require 'spec_helper'
  17 +
  18 +describe ProjectSnippet do
  19 + describe "Associations" do
  20 + it { should belong_to(:project) }
  21 + end
  22 +
  23 + describe "Mass assignment" do
  24 + it { should_not allow_mass_assignment_of(:project_id) }
  25 + end
  26 +
  27 + describe "Validation" do
  28 + it { should validate_presence_of(:project) }
  29 + end
  30 +end
spec/models/project_spec.rb
@@ -36,7 +36,7 @@ describe Project do @@ -36,7 +36,7 @@ describe Project do
36 it { should have_many(:milestones).dependent(:destroy) } 36 it { should have_many(:milestones).dependent(:destroy) }
37 it { should have_many(:users_projects).dependent(:destroy) } 37 it { should have_many(:users_projects).dependent(:destroy) }
38 it { should have_many(:notes).dependent(:destroy) } 38 it { should have_many(:notes).dependent(:destroy) }
39 - it { should have_many(:snippets).dependent(:destroy) } 39 + it { should have_many(:snippets).class_name('ProjectSnippet').dependent(:destroy) }
40 it { should have_many(:deploy_keys_projects).dependent(:destroy) } 40 it { should have_many(:deploy_keys_projects).dependent(:destroy) }
41 it { should have_many(:deploy_keys) } 41 it { should have_many(:deploy_keys) }
42 it { should have_many(:hooks).dependent(:destroy) } 42 it { should have_many(:hooks).dependent(:destroy) }
spec/models/snippet_spec.rb
@@ -17,19 +17,16 @@ require &#39;spec_helper&#39; @@ -17,19 +17,16 @@ require &#39;spec_helper&#39;
17 17
18 describe Snippet do 18 describe Snippet do
19 describe "Associations" do 19 describe "Associations" do
20 - it { should belong_to(:project) }  
21 it { should belong_to(:author).class_name('User') } 20 it { should belong_to(:author).class_name('User') }
22 it { should have_many(:notes).dependent(:destroy) } 21 it { should have_many(:notes).dependent(:destroy) }
23 end 22 end
24 23
25 describe "Mass assignment" do 24 describe "Mass assignment" do
26 it { should_not allow_mass_assignment_of(:author_id) } 25 it { should_not allow_mass_assignment_of(:author_id) }
27 - it { should_not allow_mass_assignment_of(:project_id) }  
28 end 26 end
29 27
30 describe "Validation" do 28 describe "Validation" do
31 it { should validate_presence_of(:author) } 29 it { should validate_presence_of(:author) }
32 - it { should validate_presence_of(:project) }  
33 30
34 it { should validate_presence_of(:title) } 31 it { should validate_presence_of(:title) }
35 it { should ensure_length_of(:title).is_within(0..255) } 32 it { should ensure_length_of(:title).is_within(0..255) }
spec/models/user_spec.rb
@@ -41,6 +41,7 @@ require &#39;spec_helper&#39; @@ -41,6 +41,7 @@ require &#39;spec_helper&#39;
41 describe User do 41 describe User do
42 describe "Associations" do 42 describe "Associations" do
43 it { should have_one(:namespace) } 43 it { should have_one(:namespace) }
  44 + it { should have_many(:snippets).class_name('Snippet').dependent(:destroy) }
44 it { should have_many(:users_projects).dependent(:destroy) } 45 it { should have_many(:users_projects).dependent(:destroy) }
45 it { should have_many(:groups) } 46 it { should have_many(:groups) }
46 it { should have_many(:keys).dependent(:destroy) } 47 it { should have_many(:keys).dependent(:destroy) }
spec/requests/api/notes_spec.rb
@@ -7,7 +7,7 @@ describe API::API do @@ -7,7 +7,7 @@ describe API::API do
7 let!(:project) { create(:project, namespace: user.namespace ) } 7 let!(:project) { create(:project, namespace: user.namespace ) }
8 let!(:issue) { create(:issue, project: project, author: user) } 8 let!(:issue) { create(:issue, project: project, author: user) }
9 let!(:merge_request) { create(:merge_request, project: project, author: user) } 9 let!(:merge_request) { create(:merge_request, project: project, author: user) }
10 - let!(:snippet) { create(:snippet, project: project, author: user) } 10 + let!(:snippet) { create(:project_snippet, project: project, author: user) }
11 let!(:issue_note) { create(:note, noteable: issue, project: project, author: user) } 11 let!(:issue_note) { create(:note, noteable: issue, project: project, author: user) }
12 let!(:merge_request_note) { create(:note, noteable: merge_request, project: project, author: user) } 12 let!(:merge_request_note) { create(:note, noteable: merge_request, project: project, author: user) }
13 let!(:snippet_note) { create(:note, noteable: snippet, project: project, author: user) } 13 let!(:snippet_note) { create(:note, noteable: snippet, project: project, author: user) }
spec/requests/api/projects_spec.rb
@@ -10,7 +10,7 @@ describe API::API do @@ -10,7 +10,7 @@ describe API::API do
10 let(:admin) { create(:admin) } 10 let(:admin) { create(:admin) }
11 let!(:project) { create(:project_with_code, creator_id: user.id) } 11 let!(:project) { create(:project_with_code, creator_id: user.id) }
12 let!(:hook) { create(:project_hook, project: project, url: "http://example.com") } 12 let!(:hook) { create(:project_hook, project: project, url: "http://example.com") }
13 - let!(:snippet) { create(:snippet, author: user, project: project, title: 'example') } 13 + let!(:snippet) { create(:project_snippet, author: user, project: project, title: 'example') }
14 let!(:users_project) { create(:users_project, user: user, project: project, project_access: UsersProject::MASTER) } 14 let!(:users_project) { create(:users_project, user: user, project: project, project_access: UsersProject::MASTER) }
15 let!(:users_project2) { create(:users_project, user: user3, project: project, project_access: UsersProject::DEVELOPER) } 15 let!(:users_project2) { create(:users_project, user: user3, project: project, project_access: UsersProject::DEVELOPER) }
16 16
spec/routing/project_routing_spec.rb
@@ -258,13 +258,37 @@ end @@ -258,13 +258,37 @@ end
258 # project_snippet GET /:project_id/snippets/:id(.:format) snippets#show 258 # project_snippet GET /:project_id/snippets/:id(.:format) snippets#show
259 # PUT /:project_id/snippets/:id(.:format) snippets#update 259 # PUT /:project_id/snippets/:id(.:format) snippets#update
260 # DELETE /:project_id/snippets/:id(.:format) snippets#destroy 260 # DELETE /:project_id/snippets/:id(.:format) snippets#destroy
261 -describe SnippetsController, "routing" do 261 +describe Project::SnippetsController, "routing" do
262 it "to #raw" do 262 it "to #raw" do
263 - get("/gitlabhq/snippets/1/raw").should route_to('snippets#raw', project_id: 'gitlabhq', id: '1') 263 + get("/gitlabhq/snippets/1/raw").should route_to('projects/snippets#raw', project_id: 'gitlabhq', id: '1')
264 end 264 end
265 265
266 - it_behaves_like "RESTful project resources" do  
267 - let(:controller) { 'snippets' } 266 + it "to #index" do
  267 + get("/gitlabhq/snippets").should route_to("projects/snippets#index", project_id: 'gitlabhq')
  268 + end
  269 +
  270 + it "to #create" do
  271 + post("/gitlabhq/snippets").should route_to("projects/snippets#create", project_id: 'gitlabhq')
  272 + end
  273 +
  274 + it "to #new" do
  275 + get("/gitlabhq/snippets/new").should route_to("projects/snippets#new", project_id: 'gitlabhq')
  276 + end
  277 +
  278 + it "to #edit" do
  279 + get("/gitlabhq/snippets/1/edit").should route_to("projects/snippets#edit", project_id: 'gitlabhq', id: '1')
  280 + end
  281 +
  282 + it "to #show" do
  283 + get("/gitlabhq/snippets/1").should route_to("projects/snippets#show", project_id: 'gitlabhq', id: '1')
  284 + end
  285 +
  286 + it "to #update" do
  287 + put("/gitlabhq/snippets/1").should route_to("projects/snippets#update", project_id: 'gitlabhq', id: '1')
  288 + end
  289 +
  290 + it "to #destroy" do
  291 + delete("/gitlabhq/snippets/1").should route_to("projects/snippets#destroy", project_id: 'gitlabhq', id: '1')
268 end 292 end
269 end 293 end
270 294
spec/routing/routing_spec.rb
@@ -19,6 +19,51 @@ describe &quot;Mounted Apps&quot;, &quot;routing&quot; do @@ -19,6 +19,51 @@ describe &quot;Mounted Apps&quot;, &quot;routing&quot; do
19 end 19 end
20 end 20 end
21 21
  22 +# snippets GET /snippets(.:format) snippets#index
  23 +# POST /snippets(.:format) snippets#create
  24 +# new_snippet GET /snippets/new(.:format) snippets#new
  25 +# edit_snippet GET /snippets/:id/edit(.:format) snippets#edit
  26 +# snippet GET /snippets/:id(.:format) snippets#show
  27 +# PUT /snippets/:id(.:format) snippets#update
  28 +# DELETE /snippets/:id(.:format) snippets#destroy
  29 +describe SnippetsController, "routing" do
  30 + it "to #user_index" do
  31 + get("/s/User").should route_to('snippets#user_index', username: 'User')
  32 + end
  33 +
  34 + it "to #raw" do
  35 + get("/snippets/1/raw").should route_to('snippets#raw', id: '1')
  36 + end
  37 +
  38 + it "to #index" do
  39 + get("/snippets").should route_to('snippets#index')
  40 + end
  41 +
  42 + it "to #create" do
  43 + post("/snippets").should route_to('snippets#create')
  44 + end
  45 +
  46 + it "to #new" do
  47 + get("/snippets/new").should route_to('snippets#new')
  48 + end
  49 +
  50 + it "to #edit" do
  51 + get("/snippets/1/edit").should route_to('snippets#edit', id: '1')
  52 + end
  53 +
  54 + it "to #show" do
  55 + get("/snippets/1").should route_to('snippets#show', id: '1')
  56 + end
  57 +
  58 + it "to #update" do
  59 + put("/snippets/1").should route_to('snippets#update', id: '1')
  60 + end
  61 +
  62 + it "to #destroy" do
  63 + delete("/snippets/1").should route_to('snippets#destroy', id: '1')
  64 + end
  65 +end
  66 +
22 # help GET /help(.:format) help#index 67 # help GET /help(.:format) help#index
23 # help_permissions GET /help/permissions(.:format) help#permissions 68 # help_permissions GET /help/permissions(.:format) help#permissions
24 # help_workflow GET /help/workflow(.:format) help#workflow 69 # help_workflow GET /help/workflow(.:format) help#workflow