Commit 9265de3d25715aeafd38a4ef41596dca058dc18c
1 parent
526ad466
Exists in
master
and in
4 other branches
snippets are ready
Showing
28 changed files
with
366 additions
and
8 deletions
Show diff stats
app/assets/stylesheets/highlight.css.scss
... | ... | @@ -22,8 +22,8 @@ td.linenos{ |
22 | 22 | |
23 | 23 | .highlight{ |
24 | 24 | background:none; |
25 | - padding:10px 0px 0px 0; | |
26 | - margin-left:10px; | |
25 | + padding:10px 0px 0px 10px; | |
26 | + margin-left:0px; | |
27 | 27 | } |
28 | 28 | .highlight pre{ |
29 | 29 | } |
... | ... | @@ -43,7 +43,7 @@ td.linenos { |
43 | 43 | } |
44 | 44 | |
45 | 45 | td.code .highlight { |
46 | - overflow-x: scroll; | |
46 | + overflow: auto; | |
47 | 47 | } |
48 | 48 | table.highlighttable pre{ |
49 | 49 | padding:0; | ... | ... |
app/assets/stylesheets/projects.css.scss
... | ... | @@ -310,6 +310,7 @@ input.ssh_project_url { |
310 | 310 | } |
311 | 311 | |
312 | 312 | #projects-list .project, |
313 | +#snippets-table .snippet, | |
313 | 314 | #issues-table .issue{ |
314 | 315 | cursor:pointer; |
315 | 316 | |
... | ... | @@ -360,6 +361,8 @@ input.ssh_project_url { |
360 | 361 | .user_new, |
361 | 362 | .edit_user, |
362 | 363 | .new_project, |
364 | +.new_snippet, | |
365 | +.edit_snippet, | |
363 | 366 | .edit_project { |
364 | 367 | input[type='text'], |
365 | 368 | input[type='email'], | ... | ... |
app/controllers/notes_controller.rb
... | ... | @@ -41,6 +41,8 @@ class NotesController < ApplicationController |
41 | 41 | Notify.note_commit_email(u, @note).deliver |
42 | 42 | when "Issue" then |
43 | 43 | Notify.note_issue_email(u, @note).deliver |
44 | + when "Snippet" | |
45 | + true | |
44 | 46 | else |
45 | 47 | Notify.note_wall_email(u, @note).deliver |
46 | 48 | end | ... | ... |
... | ... | @@ -0,0 +1,63 @@ |
1 | +class SnippetsController < ApplicationController | |
2 | + before_filter :authenticate_user! | |
3 | + before_filter :project | |
4 | + | |
5 | + # Authorize | |
6 | + before_filter :add_project_abilities | |
7 | + before_filter :authorize_read_snippet! | |
8 | + before_filter :authorize_write_snippet!, :only => [:new, :create, :close, :edit, :update, :sort] | |
9 | + | |
10 | + respond_to :html | |
11 | + | |
12 | + def index | |
13 | + @snippets = @project.snippets | |
14 | + end | |
15 | + | |
16 | + def new | |
17 | + @snippet = @project.snippets.new | |
18 | + end | |
19 | + | |
20 | + def create | |
21 | + @snippet = @project.snippets.new(params[:snippet]) | |
22 | + @snippet.author = current_user | |
23 | + @snippet.save | |
24 | + | |
25 | + if @snippet.valid? | |
26 | + redirect_to [@project, @snippet] | |
27 | + else | |
28 | + respond_with(@snippet) | |
29 | + end | |
30 | + end | |
31 | + | |
32 | + def edit | |
33 | + @snippet = @project.snippets.find(params[:id]) | |
34 | + end | |
35 | + | |
36 | + def update | |
37 | + @snippet = @project.snippets.find(params[:id]) | |
38 | + @snippet.update_attributes(params[:snippet]) | |
39 | + | |
40 | + if @snippet.valid? | |
41 | + redirect_to [@project, @snippet] | |
42 | + else | |
43 | + respond_with(@snippet) | |
44 | + end | |
45 | + end | |
46 | + | |
47 | + def show | |
48 | + @snippet = @project.snippets.find(params[:id]) | |
49 | + @notes = @snippet.notes | |
50 | + @note = @project.notes.new(:noteable => @snippet) | |
51 | + end | |
52 | + | |
53 | + def destroy | |
54 | + @snippet = @project.snippets.find(params[:id]) | |
55 | + authorize_admin_snippet! unless @snippet.author == current_user | |
56 | + | |
57 | + @snippet.destroy | |
58 | + | |
59 | + respond_to do |format| | |
60 | + format.js { render :nothing => true } | |
61 | + end | |
62 | + end | |
63 | +end | ... | ... |
app/helpers/application_helper.rb
... | ... | @@ -53,7 +53,7 @@ module ApplicationHelper |
53 | 53 | [projects, default_nav, project_nav].flatten.to_json |
54 | 54 | end |
55 | 55 | |
56 | - def handle_file_type(file_name, mime_type) | |
56 | + def handle_file_type(file_name, mime_type = nil) | |
57 | 57 | if file_name =~ /(\.rb|\.ru|\.rake|Rakefile|\.gemspec|\.rbx|Gemfile)$/ |
58 | 58 | :ruby |
59 | 59 | elsif file_name =~ /\.py$/ | ... | ... |
app/models/ability.rb
... | ... | @@ -12,6 +12,7 @@ class Ability |
12 | 12 | rules << [ |
13 | 13 | :read_project, |
14 | 14 | :read_issue, |
15 | + :read_snippet, | |
15 | 16 | :read_team_member, |
16 | 17 | :read_note |
17 | 18 | ] if project.readers.include?(user) |
... | ... | @@ -19,12 +20,14 @@ class Ability |
19 | 20 | rules << [ |
20 | 21 | :write_project, |
21 | 22 | :write_issue, |
23 | + :write_snippet, | |
22 | 24 | :write_note |
23 | 25 | ] if project.writers.include?(user) |
24 | 26 | |
25 | 27 | rules << [ |
26 | 28 | :admin_project, |
27 | 29 | :admin_issue, |
30 | + :admin_snippet, | |
28 | 31 | :admin_team_member, |
29 | 32 | :admin_note |
30 | 33 | ] if project.admins.include?(user) | ... | ... |
app/models/project.rb
... | ... | @@ -7,6 +7,7 @@ class Project < ActiveRecord::Base |
7 | 7 | has_many :users_projects, :dependent => :destroy |
8 | 8 | has_many :users, :through => :users_projects |
9 | 9 | has_many :notes, :dependent => :destroy |
10 | + has_many :snippets, :dependent => :destroy | |
10 | 11 | |
11 | 12 | validates :name, |
12 | 13 | :uniqueness => true, | ... | ... |
... | ... | @@ -0,0 +1,31 @@ |
1 | +class Snippet < ActiveRecord::Base | |
2 | + belongs_to :project | |
3 | + belongs_to :author, :class_name => "User" | |
4 | + has_many :notes, :as => :noteable | |
5 | + | |
6 | + attr_protected :author, :author_id, :project, :project_id | |
7 | + | |
8 | + validates_presence_of :project_id | |
9 | + validates_presence_of :author_id | |
10 | + | |
11 | + validates :title, | |
12 | + :presence => true, | |
13 | + :length => { :within => 0..255 } | |
14 | + | |
15 | + validates :file_name, | |
16 | + :presence => true, | |
17 | + :length => { :within => 0..255 } | |
18 | + | |
19 | + validates :content, | |
20 | + :presence => true, | |
21 | + :length => { :within => 0..10000 } | |
22 | + | |
23 | + | |
24 | + def self.content_types | |
25 | + [ | |
26 | + ".rb", ".py", ".pl", ".scala", ".c", ".cpp", ".java", | |
27 | + ".haml", ".html", ".sass", ".scss", ".xml", ".php", ".erb", | |
28 | + ".js", ".sh", ".coffee", ".yml", ".md" | |
29 | + ] | |
30 | + end | |
31 | +end | ... | ... |
app/views/projects/_form.html.haml
app/views/projects/_top_menu.html.haml
... | ... | @@ -18,6 +18,11 @@ |
18 | 18 | Wall |
19 | 19 | - if @project.common_notes.count > 0 |
20 | 20 | %span{ :class => "top_menu_count" }= @project.common_notes.count |
21 | + %span | |
22 | + = link_to project_snippets_path(@project), :class => (controller.controller_name == "snippets") ? "current" : nil do | |
23 | + Snippets | |
24 | + - if @project.snippets.count > 0 | |
25 | + %span{ :class => "top_menu_count" }= @project.snippets.count | |
21 | 26 | |
22 | 27 | - if @commit |
23 | 28 | %span= link_to truncate(commit_name(@project,@commit), :length => 15), project_commit_path(@project, :id => @commit.id), :class => current_page?(:controller => "commits", :action => "show", :project_id => @project, :id => @commit.id) ? "current" : nil | ... | ... |
... | ... | @@ -0,0 +1,22 @@ |
1 | +%div | |
2 | + = form_for [@project, @snippet] do |f| | |
3 | + -if @snippet.errors.any? | |
4 | + %ul | |
5 | + - @snippet.errors.full_messages.each do |msg| | |
6 | + %li= msg | |
7 | + | |
8 | + %table.round-borders | |
9 | + %tr | |
10 | + %td= f.label :title | |
11 | + %td= f.text_field :title, :placeholder => "Example Snippet" | |
12 | + %tr | |
13 | + %td= f.label :file_name | |
14 | + %td= f.text_field :file_name, :placeholder => "example.rb" | |
15 | + %tr | |
16 | + %td{:colspan => 2} | |
17 | + = f.label :content, "Code" | |
18 | + %br | |
19 | + = f.text_area :content, :style => "height:240px;width:932px;" | |
20 | + | |
21 | + .actions.prepend-top | |
22 | + = f.submit 'Save', :class => "lbutton vm" | ... | ... |
... | ... | @@ -0,0 +1,11 @@ |
1 | +%tr{ :id => dom_id(snippet), :class => "snippet", :url => project_snippet_path(@project, snippet) } | |
2 | + %td | |
3 | + = image_tag gravatar_icon(snippet.author.email), :class => "left", :width => 40, :style => "padding:0 5px;" | |
4 | + = truncate snippet.author.name, :lenght => 20 | |
5 | + %td= html_escape snippet.title | |
6 | + %td= html_escape snippet.file_name | |
7 | + %td | |
8 | + - if can?(current_user, :admin_snippet, @project) || snippet.author == current_user | |
9 | + = link_to 'Edit', edit_project_snippet_path(@project, snippet), :class => "lbutton positive" | |
10 | + - if can?(current_user, :admin_snippet, @project) || snippet.author == current_user | |
11 | + = link_to 'Destroy', [@project, snippet], :confirm => 'Are you sure?', :method => :delete, :remote => true, :class => "lbutton delete-snippet negative", :id => "destroy_snippet_#{snippet.id}" | ... | ... |
... | ... | @@ -0,0 +1 @@ |
1 | += render "snippets/form" | ... | ... |
... | ... | @@ -0,0 +1,14 @@ |
1 | +%div | |
2 | + - if can? current_user, :write_snippet, @project | |
3 | + .left= link_to 'New Snippet', new_project_snippet_path(@project), :class => "lbutton vm" | |
4 | + | |
5 | + %table.round-borders#snippets-table | |
6 | + %tr | |
7 | + %th Author | |
8 | + %th Title | |
9 | + %th File name | |
10 | + %th | |
11 | + = render @snippets | |
12 | +:javascript | |
13 | + $('.delete-snippet').live('ajax:success', function() { | |
14 | + $(this).closest('tr').fadeOut(); }); | ... | ... |
... | ... | @@ -0,0 +1 @@ |
1 | += render "snippets/form" | ... | ... |
... | ... | @@ -0,0 +1,22 @@ |
1 | +%h2 | |
2 | + = "Snippet ##{@snippet.id} - #{@snippet.title}" | |
3 | + | |
4 | +.view_file | |
5 | + .view_file_header | |
6 | + %strong | |
7 | + = @snippet.file_name | |
8 | + %br/ | |
9 | + .view_file_content | |
10 | + - ft = handle_file_type(@snippet.file_name) | |
11 | + :erb | |
12 | + <%= raw Albino.colorize(@snippet.content, ft, :html, 'utf-8', "linenos=True") %> | |
13 | + | |
14 | +- if can?(current_user, :admin_snippet, @project) || @snippet.author == current_user | |
15 | + = link_to 'Edit', edit_project_snippet_path(@project, @snippet), :class => "lbutton positive" | |
16 | +- if can?(current_user, :admin_snippet, @project) || @snippet.author == current_user | |
17 | + = link_to 'Destroy', [@project, @snippet], :confirm => 'Are you sure?', :method => :delete, :class => "lbutton delete-snippet negative", :id => "destroy_snippet_#{@snippet.id}" | |
18 | +%br | |
19 | +.snippet_notes= render "notes/notes" | |
20 | + | |
21 | +.clear | |
22 | + | ... | ... |
config/routes.rb
... | ... | @@ -0,0 +1,12 @@ |
1 | +class CreateSnippets < ActiveRecord::Migration | |
2 | + def change | |
3 | + create_table :snippets do |t| | |
4 | + t.string :title | |
5 | + t.text :content | |
6 | + t.integer :author_id, :null => false | |
7 | + t.integer :project_id, :null => false | |
8 | + | |
9 | + t.timestamps | |
10 | + end | |
11 | + end | |
12 | +end | ... | ... |
db/migrate/20111016193417_add_content_type_to_snippets.rb
0 → 100644
db/schema.rb
... | ... | @@ -11,7 +11,7 @@ |
11 | 11 | # |
12 | 12 | # It's strongly recommended to check this file into your version control system. |
13 | 13 | |
14 | -ActiveRecord::Schema.define(:version => 20111015154310) do | |
14 | +ActiveRecord::Schema.define(:version => 20111016195506) do | |
15 | 15 | |
16 | 16 | create_table "issues", :force => true do |t| |
17 | 17 | t.string "title" |
... | ... | @@ -56,6 +56,16 @@ ActiveRecord::Schema.define(:version => 20111015154310) do |
56 | 56 | t.integer "owner_id" |
57 | 57 | end |
58 | 58 | |
59 | + create_table "snippets", :force => true do |t| | |
60 | + t.string "title" | |
61 | + t.text "content" | |
62 | + t.integer "author_id", :null => false | |
63 | + t.integer "project_id", :null => false | |
64 | + t.datetime "created_at" | |
65 | + t.datetime "updated_at" | |
66 | + t.string "file_name" | |
67 | + end | |
68 | + | |
59 | 69 | create_table "users", :force => true do |t| |
60 | 70 | t.string "email", :default => "", :null => false |
61 | 71 | t.string "encrypted_password", :limit => 128, :default => "", :null => false | ... | ... |
spec/factories.rb
... | ... | @@ -35,6 +35,12 @@ Factory.add(:issue, Issue) do |obj| |
35 | 35 | obj.content = Faker::Lorem.sentences |
36 | 36 | end |
37 | 37 | |
38 | +Factory.add(:snippet, Snippet) do |obj| | |
39 | + obj.title = Faker::Lorem.sentence | |
40 | + obj.file_name = Faker::Lorem.sentence | |
41 | + obj.content = Faker::Lorem.sentences | |
42 | +end | |
43 | + | |
38 | 44 | Factory.add(:note, Note) do |obj| |
39 | 45 | obj.note = Faker::Lorem.sentence |
40 | 46 | end | ... | ... |
... | ... | @@ -0,0 +1,16 @@ |
1 | +require 'spec_helper' | |
2 | + | |
3 | +describe Snippet do | |
4 | + describe "Associations" do | |
5 | + it { should belong_to(:project) } | |
6 | + it { should belong_to(:author) } | |
7 | + end | |
8 | + | |
9 | + describe "Validation" do | |
10 | + it { should validate_presence_of(:title) } | |
11 | + it { should validate_presence_of(:author_id) } | |
12 | + it { should validate_presence_of(:project_id) } | |
13 | + it { should validate_presence_of(:file_name) } | |
14 | + it { should validate_presence_of(:content) } | |
15 | + end | |
16 | +end | ... | ... |
spec/requests/projects_security_spec.rb
... | ... | @@ -107,5 +107,14 @@ describe "Projects" do |
107 | 107 | it { project_issues_path(@project).should be_denied_for :user } |
108 | 108 | it { project_issues_path(@project).should be_denied_for :visitor } |
109 | 109 | end |
110 | + | |
111 | + describe "GET /project_code/snippets" do | |
112 | + it { project_snippets_path(@project).should be_allowed_for @u1 } | |
113 | + it { project_snippets_path(@project).should be_allowed_for @u3 } | |
114 | + it { project_snippets_path(@project).should be_denied_for :admin } | |
115 | + it { project_snippets_path(@project).should be_denied_for @u2 } | |
116 | + it { project_snippets_path(@project).should be_denied_for :user } | |
117 | + it { project_snippets_path(@project).should be_denied_for :visitor } | |
118 | + end | |
110 | 119 | end |
111 | 120 | end | ... | ... |
... | ... | @@ -0,0 +1,101 @@ |
1 | +require 'spec_helper' | |
2 | + | |
3 | +describe "Snippets" do | |
4 | + let(:project) { Factory :project } | |
5 | + | |
6 | + before do | |
7 | + login_as :user | |
8 | + project.add_access(@user, :read, :write) | |
9 | + end | |
10 | + | |
11 | + describe "GET /snippets" do | |
12 | + before do | |
13 | + @snippet = Factory :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) } | |
23 | + it { should have_content(@snippet.project.name) } | |
24 | + it { should have_content(@snippet.author.name) } | |
25 | + | |
26 | + describe "Destroy" do | |
27 | + before do | |
28 | + # admin access to remove snippet | |
29 | + @user.users_projects.destroy_all | |
30 | + project.add_access(@user, :read, :write, :admin) | |
31 | + visit project_snippets_path(project) | |
32 | + end | |
33 | + | |
34 | + it "should remove entry" do | |
35 | + expect { | |
36 | + click_link "destroy_snippet_#{@snippet.id}" | |
37 | + }.to change { Snippet.count }.by(-1) | |
38 | + end | |
39 | + end | |
40 | + end | |
41 | + | |
42 | + describe "New snippet" do | |
43 | + before do | |
44 | + visit project_snippets_path(project) | |
45 | + click_link "New Snippet" | |
46 | + end | |
47 | + | |
48 | + it "should open new snippet popup" do | |
49 | + page.current_path.should == new_project_snippet_path(project) | |
50 | + end | |
51 | + | |
52 | + describe "fill in" do | |
53 | + before do | |
54 | + fill_in "snippet_title", :with => "login function" | |
55 | + fill_in "snippet_file_name", :with => "test.rb" | |
56 | + fill_in "snippet_content", :with => "def login; end" | |
57 | + end | |
58 | + | |
59 | + it { expect { click_button "Save" }.to change {Snippet.count}.by(1) } | |
60 | + | |
61 | + it "should add new snippet to table" do | |
62 | + click_button "Save" | |
63 | + page.current_path.should == project_snippet_path(project, Snippet.last) | |
64 | + page.should have_content "login function" | |
65 | + page.should have_content "test.rb" | |
66 | + end | |
67 | + end | |
68 | + end | |
69 | + | |
70 | + describe "Edit snippet" do | |
71 | + before do | |
72 | + @snippet = Factory :snippet, | |
73 | + :author => @user, | |
74 | + :project => project | |
75 | + visit project_snippets_path(project) | |
76 | + click_link "Edit" | |
77 | + end | |
78 | + | |
79 | + it "should open edit page" do | |
80 | + page.current_path.should == edit_project_snippet_path(project, @snippet) | |
81 | + end | |
82 | + | |
83 | + describe "fill in" do | |
84 | + before do | |
85 | + fill_in "snippet_title", :with => "login function" | |
86 | + fill_in "snippet_file_name", :with => "test.rb" | |
87 | + fill_in "snippet_content", :with => "def login; end" | |
88 | + end | |
89 | + | |
90 | + it { expect { click_button "Save" }.to_not change {Snippet.count} } | |
91 | + | |
92 | + it "should update snippet fields" do | |
93 | + click_button "Save" | |
94 | + | |
95 | + page.current_path.should == project_snippet_path(project, @snippet) | |
96 | + page.should have_content "login function" | |
97 | + page.should have_content "test.rb" | |
98 | + end | |
99 | + end | |
100 | + end | |
101 | +end | ... | ... |