Commit 7763705702f45acb40fcbc846cff81dde6d462b2
Exists in
master
and in
1 other branch
Merge branch 'master' of https://github.com/Undev/errbit into Undev-master
Showing
36 changed files
with
401 additions
and
154 deletions
Show diff stats
.gitignore
Gemfile
1 | 1 | source 'http://rubygems.org' |
2 | 2 | |
3 | -gem 'rails', '3.0.0.rc' | |
3 | +gem 'rails', '3.0.3' | |
4 | 4 | gem 'libxml-ruby' |
5 | 5 | gem 'bson_ext', :require => nil |
6 | -gem 'mongoid', '2.0.0.beta.15' | |
6 | +gem 'mongoid', '~> 2.0.0.beta.20' | |
7 | 7 | gem 'haml' |
8 | 8 | gem 'will_paginate' |
9 | -gem 'devise', '1.1.1' | |
9 | +gem 'devise', '~> 1.1.3' | |
10 | 10 | |
11 | 11 | group :development, :test do |
12 | - gem 'rspec-rails', '>= 2.0.0.beta.19' | |
12 | + gem 'rspec-rails', '~> 2.1' | |
13 | 13 | end |
14 | 14 | |
15 | 15 | group :test do |
16 | - gem 'rspec', '>= 2.0.0.beta.19' | |
17 | - gem 'database_cleaner', '0.5.2' | |
16 | + gem 'rspec', '~> 2.1' | |
17 | + gem 'database_cleaner', '~> 0.6.0' | |
18 | 18 | gem 'factory_girl_rails' |
19 | 19 | end | ... | ... |
Gemfile.lock
... | ... | @@ -2,40 +2,39 @@ GEM |
2 | 2 | remote: http://rubygems.org/ |
3 | 3 | specs: |
4 | 4 | abstract (1.0.0) |
5 | - actionmailer (3.0.0.rc) | |
6 | - actionpack (= 3.0.0.rc) | |
7 | - mail (~> 2.2.5) | |
8 | - actionpack (3.0.0.rc) | |
9 | - activemodel (= 3.0.0.rc) | |
10 | - activesupport (= 3.0.0.rc) | |
5 | + actionmailer (3.0.3) | |
6 | + actionpack (= 3.0.3) | |
7 | + mail (~> 2.2.9) | |
8 | + actionpack (3.0.3) | |
9 | + activemodel (= 3.0.3) | |
10 | + activesupport (= 3.0.3) | |
11 | 11 | builder (~> 2.1.2) |
12 | 12 | erubis (~> 2.6.6) |
13 | - i18n (~> 0.4.1) | |
13 | + i18n (~> 0.4) | |
14 | 14 | rack (~> 1.2.1) |
15 | - rack-mount (~> 0.6.9) | |
16 | - rack-test (~> 0.5.4) | |
17 | - tzinfo (~> 0.3.22) | |
18 | - activemodel (3.0.0.rc) | |
19 | - activesupport (= 3.0.0.rc) | |
15 | + rack-mount (~> 0.6.13) | |
16 | + rack-test (~> 0.5.6) | |
17 | + tzinfo (~> 0.3.23) | |
18 | + activemodel (3.0.3) | |
19 | + activesupport (= 3.0.3) | |
20 | 20 | builder (~> 2.1.2) |
21 | - i18n (~> 0.4.1) | |
22 | - activerecord (3.0.0.rc) | |
23 | - activemodel (= 3.0.0.rc) | |
24 | - activesupport (= 3.0.0.rc) | |
25 | - arel (~> 0.4.0) | |
26 | - tzinfo (~> 0.3.22) | |
27 | - activeresource (3.0.0.rc) | |
28 | - activemodel (= 3.0.0.rc) | |
29 | - activesupport (= 3.0.0.rc) | |
30 | - activesupport (3.0.0.rc) | |
31 | - arel (0.4.0) | |
32 | - activesupport (>= 3.0.0.beta) | |
21 | + i18n (~> 0.4) | |
22 | + activerecord (3.0.3) | |
23 | + activemodel (= 3.0.3) | |
24 | + activesupport (= 3.0.3) | |
25 | + arel (~> 2.0.2) | |
26 | + tzinfo (~> 0.3.23) | |
27 | + activeresource (3.0.3) | |
28 | + activemodel (= 3.0.3) | |
29 | + activesupport (= 3.0.3) | |
30 | + activesupport (3.0.3) | |
31 | + arel (2.0.4) | |
33 | 32 | bcrypt-ruby (2.1.2) |
34 | - bson (1.0.4) | |
35 | - bson_ext (1.0.4) | |
33 | + bson (1.1.2) | |
34 | + bson_ext (1.1.2) | |
36 | 35 | builder (2.1.2) |
37 | - database_cleaner (0.5.2) | |
38 | - devise (1.1.1) | |
36 | + database_cleaner (0.6.0) | |
37 | + devise (1.1.3) | |
39 | 38 | bcrypt-ruby (~> 2.1.2) |
40 | 39 | warden (~> 0.10.7) |
41 | 40 | diff-lcs (1.1.2) |
... | ... | @@ -45,64 +44,58 @@ GEM |
45 | 44 | factory_girl_rails (1.0) |
46 | 45 | factory_girl (~> 1.3) |
47 | 46 | rails (>= 3.0.0.beta4) |
48 | - haml (3.0.16) | |
49 | - i18n (0.4.1) | |
47 | + haml (3.0.24) | |
48 | + i18n (0.4.2) | |
50 | 49 | libxml-ruby (1.1.4) |
51 | - mail (2.2.5) | |
50 | + mail (2.2.10) | |
52 | 51 | activesupport (>= 2.3.6) |
53 | - mime-types | |
54 | - treetop (>= 1.4.5) | |
52 | + i18n (~> 0.4.1) | |
53 | + mime-types (~> 1.16) | |
54 | + treetop (~> 1.4.8) | |
55 | 55 | mime-types (1.16) |
56 | - mongo (1.0.6) | |
57 | - bson (>= 1.0.4) | |
58 | - mongoid (2.0.0.beta.15) | |
59 | - activemodel (= 3.0.0.rc) | |
60 | - bson (= 1.0.4) | |
61 | - mongo (= 1.0.6) | |
62 | - tzinfo (= 0.3.22) | |
56 | + mongo (1.1.2) | |
57 | + bson (>= 1.1.1) | |
58 | + mongoid (2.0.0.beta.20) | |
59 | + activemodel (~> 3.0) | |
60 | + mongo (~> 1.1) | |
61 | + tzinfo (~> 0.3.22) | |
63 | 62 | will_paginate (~> 3.0.pre) |
64 | - nokogiri (1.4.3.1) | |
65 | 63 | polyglot (0.3.1) |
66 | 64 | rack (1.2.1) |
67 | - rack-mount (0.6.9) | |
65 | + rack-mount (0.6.13) | |
68 | 66 | rack (>= 1.0.0) |
69 | - rack-test (0.5.4) | |
67 | + rack-test (0.5.6) | |
70 | 68 | rack (>= 1.0) |
71 | - rails (3.0.0.rc) | |
72 | - actionmailer (= 3.0.0.rc) | |
73 | - actionpack (= 3.0.0.rc) | |
74 | - activerecord (= 3.0.0.rc) | |
75 | - activeresource (= 3.0.0.rc) | |
76 | - activesupport (= 3.0.0.rc) | |
77 | - bundler (>= 1.0.0.rc.1) | |
78 | - railties (= 3.0.0.rc) | |
79 | - railties (3.0.0.rc) | |
80 | - actionpack (= 3.0.0.rc) | |
81 | - activesupport (= 3.0.0.rc) | |
82 | - rake (>= 0.8.3) | |
83 | - thor (~> 0.14.0) | |
69 | + rails (3.0.3) | |
70 | + actionmailer (= 3.0.3) | |
71 | + actionpack (= 3.0.3) | |
72 | + activerecord (= 3.0.3) | |
73 | + activeresource (= 3.0.3) | |
74 | + activesupport (= 3.0.3) | |
75 | + bundler (~> 1.0) | |
76 | + railties (= 3.0.3) | |
77 | + railties (3.0.3) | |
78 | + actionpack (= 3.0.3) | |
79 | + activesupport (= 3.0.3) | |
80 | + rake (>= 0.8.7) | |
81 | + thor (~> 0.14.4) | |
84 | 82 | rake (0.8.7) |
85 | - rspec (2.0.0.beta.19) | |
86 | - rspec-core (= 2.0.0.beta.19) | |
87 | - rspec-expectations (= 2.0.0.beta.19) | |
88 | - rspec-mocks (= 2.0.0.beta.19) | |
89 | - rspec-core (2.0.0.beta.19) | |
90 | - rspec-expectations (2.0.0.beta.19) | |
91 | - diff-lcs (>= 1.1.2) | |
92 | - rspec-mocks (2.0.0.beta.19) | |
93 | - rspec-rails (2.0.0.beta.19) | |
94 | - rspec (= 2.0.0.beta.19) | |
95 | - webrat (>= 0.7.2.beta.1) | |
96 | - thor (0.14.0) | |
97 | - treetop (1.4.8) | |
83 | + rspec (2.1.0) | |
84 | + rspec-core (~> 2.1.0) | |
85 | + rspec-expectations (~> 2.1.0) | |
86 | + rspec-mocks (~> 2.1.0) | |
87 | + rspec-core (2.1.0) | |
88 | + rspec-expectations (2.1.0) | |
89 | + diff-lcs (~> 1.1.2) | |
90 | + rspec-mocks (2.1.0) | |
91 | + rspec-rails (2.1.0) | |
92 | + rspec (~> 2.1.0) | |
93 | + thor (0.14.6) | |
94 | + treetop (1.4.9) | |
98 | 95 | polyglot (>= 0.3.1) |
99 | - tzinfo (0.3.22) | |
96 | + tzinfo (0.3.23) | |
100 | 97 | warden (0.10.7) |
101 | 98 | rack (>= 1.0.0) |
102 | - webrat (0.7.2.beta.1) | |
103 | - nokogiri (>= 1.2.0) | |
104 | - rack (>= 1.0) | |
105 | - rack-test (>= 0.5.3) | |
106 | 99 | will_paginate (3.0.pre2) |
107 | 100 | |
108 | 101 | PLATFORMS |
... | ... | @@ -110,13 +103,13 @@ PLATFORMS |
110 | 103 | |
111 | 104 | DEPENDENCIES |
112 | 105 | bson_ext |
113 | - database_cleaner (= 0.5.2) | |
114 | - devise (= 1.1.1) | |
106 | + database_cleaner (~> 0.6.0) | |
107 | + devise (~> 1.1.3) | |
115 | 108 | factory_girl_rails |
116 | 109 | haml |
117 | 110 | libxml-ruby |
118 | - mongoid (= 2.0.0.beta.15) | |
119 | - rails (= 3.0.0.rc) | |
120 | - rspec (>= 2.0.0.beta.19) | |
121 | - rspec-rails (>= 2.0.0.beta.19) | |
111 | + mongoid (~> 2.0.0.beta.20) | |
112 | + rails (= 3.0.3) | |
113 | + rspec (~> 2.1) | |
114 | + rspec-rails (~> 2.1) | |
122 | 115 | will_paginate | ... | ... |
README.md
... | ... | @@ -39,7 +39,7 @@ for you. Checkout [Hoptoad](http://hoptoadapp.com) from the guys over at |
39 | 39 | |
40 | 40 | 3. Install Bundler |
41 | 41 | |
42 | - gem install bundler --pre | |
42 | + gem install bundler | |
43 | 43 | |
44 | 44 | **Running Locally:** |
45 | 45 | |
... | ... | @@ -70,7 +70,6 @@ for you. Checkout [Hoptoad](http://hoptoadapp.com) from the guys over at |
70 | 70 | TODO |
71 | 71 | ---- |
72 | 72 | |
73 | -* Add a deployment view | |
74 | 73 | * Add ability for watchers to be configured for types of notifications they should receive |
75 | 74 | |
76 | 75 | Special Thanks | ... | ... |
Rakefile
... | ... | @@ -8,4 +8,18 @@ require 'bundler' |
8 | 8 | Errbit::Application.load_tasks |
9 | 9 | |
10 | 10 | Rake::Task[:default].clear |
11 | + | |
12 | +namespace :spec do | |
13 | + desc "Preparing test env" | |
14 | + task :prepare do | |
15 | + tmp_env = Rails.env | |
16 | + Rails.env = "test" | |
17 | + %w( errbit:bootstrap ).each do |task| | |
18 | + Rake::Task[task].invoke | |
19 | + end | |
20 | + Rails.env = tmp_env | |
21 | + end | |
22 | +end | |
23 | + | |
24 | +Rake::Task["spec"].prerequisites.push("spec:prepare") | |
11 | 25 | task :default => ['spec'] |
12 | 26 | \ No newline at end of file | ... | ... |
app/controllers/apps_controller.rb
... | ... | @@ -8,7 +8,8 @@ class AppsController < ApplicationController |
8 | 8 | end |
9 | 9 | |
10 | 10 | def show |
11 | - @errs = @app.errs.paginate | |
11 | + @errs = @app.errs.ordered.paginate(:page => params[:page], :per_page => Err.per_page) | |
12 | + @deploys = @app.deploys.order_by(:created_at.desc).limit(5) | |
12 | 13 | end |
13 | 14 | |
14 | 15 | def new | ... | ... |
app/controllers/deploys_controller.rb
1 | 1 | class DeploysController < ApplicationController |
2 | 2 | |
3 | + skip_before_filter :verify_authenticity_token, :only => :create | |
3 | 4 | skip_before_filter :authenticate_user!, :only => :create |
4 | 5 | |
5 | 6 | def create |
... | ... | @@ -12,5 +13,13 @@ class DeploysController < ApplicationController |
12 | 13 | }) |
13 | 14 | render :xml => @deploy |
14 | 15 | end |
16 | + | |
17 | + def index | |
18 | + # See AppsController#find_app for the reasoning behind this code. | |
19 | + app = App.find(params[:app_id]) | |
20 | + raise(Mongoid::Errors::DocumentNotFound.new(App,app.id)) unless current_user.admin? || current_user.watching?(app) | |
21 | + | |
22 | + @deploys = app.deploys.order_by(:created_at.desc).paginate(:page => params[:page], :per_page => 10) | |
23 | + end | |
15 | 24 | |
16 | -end | |
17 | 25 | \ No newline at end of file |
26 | +end | ... | ... |
app/controllers/errs_controller.rb
... | ... | @@ -4,12 +4,12 @@ class ErrsController < ApplicationController |
4 | 4 | |
5 | 5 | def index |
6 | 6 | app_scope = current_user.admin? ? App.all : current_user.apps |
7 | - @errs = Err.for_apps(app_scope).unresolved.ordered.paginate(:page => params[:page]) | |
7 | + @errs = Err.for_apps(app_scope).unresolved.ordered.paginate(:page => params[:page], :per_page => Err.per_page) | |
8 | 8 | end |
9 | 9 | |
10 | 10 | def all |
11 | 11 | app_scope = current_user.admin? ? App.all : current_user.apps |
12 | - @errs = Err.for_apps(app_scope).ordered.paginate(:page => params[:page]) | |
12 | + @errs = Err.for_apps(app_scope).ordered.paginate(:page => params[:page], :per_page => Err.per_page) | |
13 | 13 | end |
14 | 14 | |
15 | 15 | def show | ... | ... |
app/controllers/users_controller.rb
... | ... | @@ -6,7 +6,7 @@ class UsersController < ApplicationController |
6 | 6 | before_filter :require_user_edit_priviledges, :only => [:edit, :update] |
7 | 7 | |
8 | 8 | def index |
9 | - @users = User.paginate(:page => params[:page]) | |
9 | + @users = User.paginate(:page => params[:page], :per_page => User.per_page) | |
10 | 10 | end |
11 | 11 | |
12 | 12 | def show |
... | ... | @@ -23,6 +23,9 @@ class UsersController < ApplicationController |
23 | 23 | def create |
24 | 24 | @user = User.new(params[:user]) |
25 | 25 | |
26 | + # Set protected attributes | |
27 | + @user.admin = params[:user].try(:[], :admin) if current_user.admin? | |
28 | + | |
26 | 29 | if @user.save |
27 | 30 | flash[:success] = "#{@user.name} is now part of the team. Be sure to add them as a project watcher." |
28 | 31 | redirect_to user_path(@user) | ... | ... |
app/models/deploy.rb
app/models/err.rb
1 | 1 | class Err |
2 | + cattr_reader :per_page | |
3 | + @@per_page = 30 | |
2 | 4 | include Mongoid::Document |
3 | 5 | include Mongoid::Timestamps |
4 | 6 | |
... | ... | @@ -9,7 +11,9 @@ class Err |
9 | 11 | field :fingerprint |
10 | 12 | field :last_notice_at, :type => DateTime |
11 | 13 | field :resolved, :type => Boolean, :default => false |
12 | - | |
14 | + | |
15 | + index :last_notice_at | |
16 | + | |
13 | 17 | referenced_in :app |
14 | 18 | embeds_many :notices |
15 | 19 | |
... | ... | @@ -41,7 +45,7 @@ class Err |
41 | 45 | end |
42 | 46 | |
43 | 47 | def message |
44 | - notices.first.message || klass | |
48 | + notices.first.try(:message) || klass | |
45 | 49 | end |
46 | 50 | |
47 | 51 | end |
48 | 52 | \ No newline at end of file | ... | ... |
app/models/notice.rb
... | ... | @@ -23,6 +23,7 @@ class Notice |
23 | 23 | hoptoad_notice = Hoptoad::V2.parse_xml(hoptoad_xml) |
24 | 24 | app = App.find_by_api_key!(hoptoad_notice['api-key']) |
25 | 25 | |
26 | + hoptoad_notice['request'] ||= {} | |
26 | 27 | hoptoad_notice['request']['component'] = 'unknown' if hoptoad_notice['request']['component'].blank? |
27 | 28 | hoptoad_notice['request']['action'] = nil if hoptoad_notice['request']['action'].blank? |
28 | 29 | ... | ... |
app/models/user.rb
app/views/apps/_configuration_instructions.html.haml
... | ... | @@ -16,7 +16,7 @@ |
16 | 16 | HoptoadNotifier.configure do |config| |
17 | 17 | config.api_key = '#{app.api_key}' |
18 | 18 | config.host = '#{request.host}' |
19 | - config.port = #{request.port} # Note: Deployment notifications only work on port 80 | |
19 | + config.port = #{request.port} | |
20 | 20 | end |
21 | 21 | # |
22 | 22 | # Testing | ... | ... |
app/views/apps/index.html.haml
... | ... | @@ -12,7 +12,7 @@ |
12 | 12 | - @apps.each do |app| |
13 | 13 | %tr |
14 | 14 | %td.name= link_to app.name, app_path(app) |
15 | - %td.deploy= app.last_deploy_at ? app.last_deploy_at.to_s(:micro) : 'n/a' | |
15 | + %td.deploy= app.last_deploy_at ? link_to( app.last_deploy_at.to_s(:micro), app_deploys_path(app)) : 'n/a' | |
16 | 16 | %td.count |
17 | 17 | - if app.errs.any? |
18 | 18 | = link_to app.errs.unresolved.count, app_errs_path(app) |
... | ... | @@ -23,4 +23,4 @@ |
23 | 23 | %td{:colspan => 3} |
24 | 24 | %em |
25 | 25 | No apps here. |
26 | - = link_to 'Click here to create your first one', new_app_path | |
27 | 26 | \ No newline at end of file |
27 | + = link_to 'Click here to create your first one', new_app_path | ... | ... |
app/views/apps/show.html.haml
... | ... | @@ -2,6 +2,8 @@ |
2 | 2 | - content_for :meta do |
3 | 3 | %strong Errs Caught: |
4 | 4 | = @app.errs.count |
5 | + %strong Deploy Count: | |
6 | + = @app.deploys.count | |
5 | 7 | %strong API Key: |
6 | 8 | = @app.api_key |
7 | 9 | - content_for :action_bar do |
... | ... | @@ -23,9 +25,30 @@ |
23 | 25 | %td |
24 | 26 | %em Sadly, no one is watching this app |
25 | 27 | |
28 | +%h3 Latest Deploys | |
29 | +- if @deploys.any? | |
30 | + %table.deploys | |
31 | + %thead | |
32 | + %tr | |
33 | + %th When | |
34 | + %th Who | |
35 | + %th Repository | |
36 | + %th Revision | |
37 | + | |
38 | + %tbody | |
39 | + - @deploys.each do |deploy| | |
40 | + %tr | |
41 | + %td.when #{deploy.created_at.to_s(:micro)} | |
42 | + %td.who #{deploy.username} | |
43 | + %td.repository #{deploy.repository} | |
44 | + %td.revision #{deploy.revision} | |
45 | + = link_to "All Deploys (#{@app.deploys.count})", app_deploys_path(@app), :class => 'button' | |
46 | +- else | |
47 | + %h3 No deploys | |
48 | + | |
26 | 49 | - if @app.errs.any? |
27 | - %h3 Errs | |
50 | + %h3.clear Errs | |
28 | 51 | = render 'errs/table', :errs => @errs |
29 | 52 | - else |
30 | - %h3 No errs have been caught yet, make sure you setup your app | |
31 | - = render 'configuration_instructions', :app => @app | |
32 | 53 | \ No newline at end of file |
54 | + %h3.clear No errs have been caught yet, make sure you setup your app | |
55 | + = render 'configuration_instructions', :app => @app | ... | ... |
... | ... | @@ -0,0 +1,18 @@ |
1 | +%table.errs | |
2 | + %thead | |
3 | + %tr | |
4 | + %th App | |
5 | + %th When | |
6 | + %th Who | |
7 | + %th Repository | |
8 | + %th Revision | |
9 | + %tbody | |
10 | + - deploys.each do |deploy| | |
11 | + %tr | |
12 | + %td.app | |
13 | + = deploy.app.name | |
14 | + %span.environment= deploy.environment | |
15 | + %td.latest #{time_ago_in_words(deploy.created_at)} ago | |
16 | + %td.who #{deploy.username} | |
17 | + %td.repository #{deploy.repository} | |
18 | + %td.revision #{deploy.revision} | ... | ... |
app/views/errs/_table.html.haml
... | ... | @@ -15,11 +15,11 @@ |
15 | 15 | %td.message |
16 | 16 | = link_to err.message, app_err_path(err.app, err) |
17 | 17 | %em= err.where |
18 | - %td.latest #{time_ago_in_words(err.last_notice_at)} ago | |
18 | + %td.latest #{time_ago_in_words(last_notice_at err)} ago | |
19 | 19 | %td.deploy= err.app.last_deploy_at ? err.app.last_deploy_at.to_s(:micro) : 'n/a' |
20 | 20 | %td.count= link_to err.notices.count, app_err_path(err.app, err) |
21 | 21 | - if errs.none? |
22 | 22 | %tr |
23 | 23 | %td{:colspan => (@app ? 5 : 6)} |
24 | 24 | %em No errs here |
25 | -= will_paginate @errs, :previous_label => '« Previous', :next_label => 'Next »' | |
26 | 25 | \ No newline at end of file |
26 | += will_paginate @errs, :previous_label => '« Previous', :next_label => 'Next »' | ... | ... |
app/views/errs/show.html.haml
... | ... | @@ -6,11 +6,11 @@ |
6 | 6 | %strong Environment: |
7 | 7 | = @err.environment |
8 | 8 | %strong Last Notice: |
9 | - = @err.last_notice_at.to_s(:micro) | |
9 | + = last_notice_at(@err).to_s(:micro) | |
10 | 10 | - content_for :action_bar do |
11 | 11 | %span= link_to 'resolve', resolve_app_err_path(@app, @err), :method => :put, :confirm => 'Seriously?', :class => 'resolve' if @err.unresolved? |
12 | 12 | |
13 | -%h4= @notice.message | |
13 | +%h4= @notice.try(:message) | |
14 | 14 | |
15 | 15 | = will_paginate @notices, :param_name => :notice, :page_links => false, :class => 'notice-pagination' |
16 | 16 | viewing occurrence #{@notices.current_page} of #{@notices.total_pages} |
... | ... | @@ -23,22 +23,23 @@ viewing occurrence #{@notices.current_page} of #{@notices.total_pages} |
23 | 23 | %li= link_to 'Parameters', '#params', :rel => 'params', :class => 'button' |
24 | 24 | %li= link_to 'Session', '#session', :rel => 'session', :class => 'button' |
25 | 25 | |
26 | -#summary | |
27 | - %h3 Summary | |
28 | - = render 'notices/summary', :notice => @notice | |
29 | - | |
30 | -#backtrace | |
31 | - %h3 Backtrace | |
32 | - = render 'notices/backtrace', :lines => @notice.backtrace | |
26 | +- if @notice | |
27 | + #summary | |
28 | + %h3 Summary | |
29 | + = render 'notices/summary', :notice => @notice | |
33 | 30 | |
34 | -#environment | |
35 | - %h3 Environment | |
36 | - = render 'notices/environment', :notice => @notice | |
37 | - | |
38 | -#params | |
39 | - %h3 Parameters | |
40 | - = render 'notices/params', :notice => @notice | |
41 | - | |
42 | -#session | |
43 | - %h3 Session | |
44 | - = render 'notices/session', :notice => @notice | |
45 | 31 | \ No newline at end of file |
32 | + #backtrace | |
33 | + %h3 Backtrace | |
34 | + = render 'notices/backtrace', :lines => @notice.backtrace | |
35 | + | |
36 | + #environment | |
37 | + %h3 Environment | |
38 | + = render 'notices/environment', :notice => @notice | |
39 | + | |
40 | + #params | |
41 | + %h3 Parameters | |
42 | + = render 'notices/params', :notice => @notice | |
43 | + | |
44 | + #session | |
45 | + %h3 Session | |
46 | + = render 'notices/session', :notice => @notice | ... | ... |
config/routes.rb
db/seeds.rb
... | ... | @@ -10,10 +10,12 @@ puts "-- email: #{admin_email}" |
10 | 10 | puts "-- password: #{admin_pass}" |
11 | 11 | puts "" |
12 | 12 | puts "Be sure to change these credentials ASAP!" |
13 | -User.create!({ | |
13 | +user = User.where(:email => admin_email).first || User.new({ | |
14 | 14 | :name => 'Errbit Admin', |
15 | 15 | :email => admin_email, |
16 | 16 | :password => admin_pass, |
17 | 17 | :password_confirmation => admin_pass, |
18 | - :admin => true | |
19 | -}) | |
20 | 18 | \ No newline at end of file |
19 | +}) | |
20 | + | |
21 | +user.admin = true | |
22 | +user.save! | |
21 | 23 | \ No newline at end of file | ... | ... |
lib/tasks/errbit/bootstrap.rake
public/stylesheets/application.css
... | ... | @@ -195,6 +195,12 @@ a.action { float: right; font-size: 0.9em;} |
195 | 195 | padding: 20px; border-top: 1px solid #C6C6C6; |
196 | 196 | background-color: #FFF; |
197 | 197 | } |
198 | + | |
199 | +#content a.button { | |
200 | + float: right; | |
201 | + display: block; | |
202 | + margin-bottom: 10px; | |
203 | +} | |
198 | 204 | |
199 | 205 | /* Footer */ |
200 | 206 | #footer { |
... | ... | @@ -607,4 +613,4 @@ table.backtrace li { |
607 | 613 | table.backtrace li.in-app { |
608 | 614 | color: #2adb2e; |
609 | 615 | background-color: #2f2f2f; |
610 | -} | |
611 | 616 | \ No newline at end of file |
617 | +} | ... | ... |
spec/controllers/apps_controller_spec.rb
... | ... | @@ -32,18 +32,27 @@ describe AppsController do |
32 | 32 | end |
33 | 33 | |
34 | 34 | describe "GET /apps/:id" do |
35 | + render_views | |
35 | 36 | context 'logged in as an admin' do |
36 | - it 'finds the app' do | |
37 | + before(:each) do | |
37 | 38 | sign_in Factory(:admin) |
38 | - app = Factory(:app) | |
39 | - get :show, :id => app.id | |
40 | - assigns(:app).should == app | |
39 | + @app = Factory(:app) | |
40 | + end | |
41 | + | |
42 | + it 'finds the app' do | |
43 | + get :show, :id => @app.id | |
44 | + assigns(:app).should == @app | |
45 | + end | |
46 | + | |
47 | + it "should not raise errors for app with err without notices" do | |
48 | + Factory :err, :app => @app | |
49 | + lambda { get :show, :id => @app.id }.should_not raise_error | |
41 | 50 | end |
42 | 51 | end |
43 | 52 | |
44 | 53 | context 'logged in as a user' do |
45 | 54 | it 'finds the app if the user is watching it' do |
46 | - | |
55 | + pending | |
47 | 56 | end |
48 | 57 | |
49 | 58 | it 'does not find the app if the user is not watching it' do | ... | ... |
spec/controllers/deploys_controller_spec.rb
1 | 1 | require 'spec_helper' |
2 | 2 | |
3 | 3 | describe DeploysController do |
4 | + render_views | |
4 | 5 | |
5 | 6 | context 'POST #create' do |
6 | 7 | before do |
... | ... | @@ -38,5 +39,22 @@ describe DeploysController do |
38 | 39 | end |
39 | 40 | |
40 | 41 | end |
42 | + | |
43 | + context "GET #index" do | |
44 | + before(:each) do | |
45 | + @deploy = Factory :deploy | |
46 | + sign_in Factory(:admin) | |
47 | + get :index, :app_id => @deploy.app.id | |
48 | + end | |
49 | + | |
50 | + it "should render successfully" do | |
51 | + response.should be_success | |
52 | + end | |
53 | + | |
54 | + it "should contain info about existing deploy" do | |
55 | + response.body.should match(@deploy.revision) | |
56 | + response.body.should match(@deploy.app.name) | |
57 | + end | |
58 | + end | |
41 | 59 | |
42 | 60 | end |
43 | 61 | \ No newline at end of file | ... | ... |
spec/controllers/errs_controller_spec.rb
... | ... | @@ -12,8 +12,11 @@ describe ErrsController do |
12 | 12 | |
13 | 13 | describe "GET /errs" do |
14 | 14 | context 'when logged in as an admin' do |
15 | - it "gets a paginated list of unresolved errs" do | |
15 | + before(:each) do | |
16 | 16 | sign_in Factory(:admin) |
17 | + end | |
18 | + | |
19 | + it "gets a paginated list of unresolved errs" do | |
17 | 20 | errs = WillPaginate::Collection.new(1,30) |
18 | 21 | 3.times { errs << Factory(:err) } |
19 | 22 | Err.should_receive(:unresolved).and_return( |
... | ... | @@ -22,6 +25,11 @@ describe ErrsController do |
22 | 25 | get :index |
23 | 26 | assigns(:errs).should == errs |
24 | 27 | end |
28 | + | |
29 | + it "should handle lots of errors" do | |
30 | + 1000.times { Factory :notice } | |
31 | + lambda { get :index }.should_not raise_error | |
32 | + end | |
25 | 33 | end |
26 | 34 | |
27 | 35 | context 'when logged in as a user' do |
... | ... | @@ -66,6 +74,8 @@ describe ErrsController do |
66 | 74 | end |
67 | 75 | |
68 | 76 | describe "GET /apps/:app_id/errs/:id" do |
77 | + render_views | |
78 | + | |
69 | 79 | before do |
70 | 80 | 3.times { Factory(:notice, :err => err)} |
71 | 81 | end |
... | ... | @@ -85,13 +95,9 @@ describe ErrsController do |
85 | 95 | assigns(:err).should == err |
86 | 96 | end |
87 | 97 | |
88 | - it "paginates the notices, 1 at a time" do | |
89 | - App.stub(:find).with(app.id).and_return(app) | |
90 | - app.errs.stub(:find).with(err.id).and_return(err) | |
91 | - err.notices.should_receive(:ordered).and_return(proxy = stub('proxy')) | |
92 | - proxy.should_receive(:paginate).with(:page => 3, :per_page => 1). | |
93 | - and_return(WillPaginate::Collection.new(1,1) << err.notices.first) | |
98 | + it "successfully render page" do | |
94 | 99 | get :show, :app_id => app.id, :id => err.id |
100 | + response.should be_success | |
95 | 101 | end |
96 | 102 | end |
97 | 103 | ... | ... |
spec/controllers/users_controller_spec.rb
... | ... | @@ -48,6 +48,11 @@ describe UsersController do |
48 | 48 | put :update, :id => @user.to_param, :user => {:name => 'Kermit'} |
49 | 49 | response.should redirect_to(user_path(@user)) |
50 | 50 | end |
51 | + | |
52 | + it "should not be able to become an admin" do | |
53 | + put :update, :id => @user.to_param, :user => {:admin => true} | |
54 | + @user.reload.admin.should be_false | |
55 | + end | |
51 | 56 | end |
52 | 57 | |
53 | 58 | context "when the update is unsuccessful" do |
... | ... | @@ -100,19 +105,24 @@ describe UsersController do |
100 | 105 | context "POST /users" do |
101 | 106 | context "when the create is successful" do |
102 | 107 | before do |
103 | - @user = Factory(:user) | |
104 | - User.should_receive(:new).and_return(@user) | |
105 | - @user.should_receive(:save).and_return(true) | |
108 | + @attrs = {:user => Factory.attributes_for(:user)} | |
106 | 109 | end |
107 | 110 | |
108 | 111 | it "sets a message to display" do |
109 | - post :create | |
112 | + post :create, @attrs | |
110 | 113 | request.flash[:success].should include('part of the team') |
111 | 114 | end |
112 | 115 | |
113 | 116 | it "redirects to the user's page" do |
114 | - post :create | |
115 | - response.should redirect_to(user_path(@user)) | |
117 | + post :create, @attrs | |
118 | + response.should redirect_to(user_path(assigns(:user))) | |
119 | + end | |
120 | + | |
121 | + it "should be able to create admin" do | |
122 | + @attrs[:user][:admin] = true | |
123 | + post :create, @attrs | |
124 | + response.should be_redirect | |
125 | + User.find(assigns(:user).to_param).admin.should be_true | |
116 | 126 | end |
117 | 127 | end |
118 | 128 | |
... | ... | @@ -145,6 +155,12 @@ describe UsersController do |
145 | 155 | put :update, :id => @user.to_param, :user => {:name => 'Kermit'} |
146 | 156 | response.should redirect_to(user_path(@user)) |
147 | 157 | end |
158 | + | |
159 | + it "should be able to make user an admin" do | |
160 | + put :update, :id => @user.to_param, :user => {:admin => true} | |
161 | + response.should be_redirect | |
162 | + User.find(assigns(:user).to_param).admin.should be_true | |
163 | + end | |
148 | 164 | end |
149 | 165 | |
150 | 166 | context "when the update is unsuccessful" do | ... | ... |
spec/factories/app_factories.rb
1 | -Factory.sequence(:app_name) {|n| "App ##{n}"} | |
2 | -Factory.sequence(:email) {|n| "email#{n}@example.com"} | |
3 | - | |
4 | 1 | Factory.define(:app) do |p| |
5 | 2 | p.name { Factory.next :app_name } |
6 | 3 | end |
... | ... | @@ -27,5 +24,5 @@ Factory.define(:deploy) do |d| |
27 | 24 | d.username 'clyde.frog' |
28 | 25 | d.repository 'git@github.com/jdpace/errbit.git' |
29 | 26 | d.environment 'production' |
30 | - d.revision '2e601cb575ca97f1a1097f12d0edfae241a70263' | |
27 | + d.revision ActiveSupport::SecureRandom.hex(10) | |
31 | 28 | end |
32 | 29 | \ No newline at end of file | ... | ... |
spec/factories/user_factories.rb
spec/fixtures/hoptoad_test_notice_without_request_section.xml
0 → 100644
... | ... | @@ -0,0 +1,92 @@ |
1 | +<?xml version="1.0" encoding="UTF-8"?> | |
2 | +<notice version="2.0"> | |
3 | + <api-key>APIKEY</api-key> | |
4 | + <notifier> | |
5 | + <name>Hoptoad Notifier</name> | |
6 | + <version>2.3.2</version> | |
7 | + <url>http://hoptoadapp.com</url> | |
8 | + </notifier> | |
9 | + <error> | |
10 | + <class>HoptoadTestingException</class> | |
11 | + <message>HoptoadTestingException: Testing hoptoad via "rake hoptoad:test". If you can see this, it works.</message> | |
12 | + <backtrace> | |
13 | + <line number="425" file="[GEM_ROOT]/gems/activesupport-3.0.0.rc/lib/active_support/callbacks.rb" method="_run__2115867319__process_action__262109504__callbacks"/> | |
14 | + <line number="404" file="[GEM_ROOT]/gems/activesupport-3.0.0.rc/lib/active_support/callbacks.rb" method="send"/> | |
15 | + <line number="404" file="[GEM_ROOT]/gems/activesupport-3.0.0.rc/lib/active_support/callbacks.rb" method="_run_process_action_callbacks"/> | |
16 | + <line number="93" file="[GEM_ROOT]/gems/activesupport-3.0.0.rc/lib/active_support/callbacks.rb" method="send"/> | |
17 | + <line number="93" file="[GEM_ROOT]/gems/activesupport-3.0.0.rc/lib/active_support/callbacks.rb" method="run_callbacks"/> | |
18 | + <line number="17" file="[GEM_ROOT]/gems/actionpack-3.0.0.rc/lib/abstract_controller/callbacks.rb" method="process_action"/> | |
19 | + <line number="30" file="[GEM_ROOT]/gems/actionpack-3.0.0.rc/lib/action_controller/metal/instrumentation.rb" method="process_action"/> | |
20 | + <line number="52" file="[GEM_ROOT]/gems/activesupport-3.0.0.rc/lib/active_support/notifications.rb" method="instrument"/> | |
21 | + <line number="21" file="[GEM_ROOT]/gems/activesupport-3.0.0.rc/lib/active_support/notifications/instrumenter.rb" method="instrument"/> | |
22 | + <line number="52" file="[GEM_ROOT]/gems/activesupport-3.0.0.rc/lib/active_support/notifications.rb" method="instrument"/> | |
23 | + <line number="29" file="[GEM_ROOT]/gems/actionpack-3.0.0.rc/lib/action_controller/metal/instrumentation.rb" method="process_action"/> | |
24 | + <line number="17" file="[GEM_ROOT]/gems/actionpack-3.0.0.rc/lib/action_controller/metal/rescue.rb" method="process_action"/> | |
25 | + <line number="105" file="[GEM_ROOT]/gems/actionpack-3.0.0.rc/lib/abstract_controller/base.rb" method="process"/> | |
26 | + <line number="40" file="[GEM_ROOT]/gems/actionpack-3.0.0.rc/lib/abstract_controller/rendering.rb" method="process"/> | |
27 | + <line number="133" file="[GEM_ROOT]/gems/actionpack-3.0.0.rc/lib/action_controller/metal.rb" method="dispatch"/> | |
28 | + <line number="14" file="[GEM_ROOT]/gems/actionpack-3.0.0.rc/lib/action_controller/metal/rack_delegation.rb" method="dispatch"/> | |
29 | + <line number="173" file="[GEM_ROOT]/gems/actionpack-3.0.0.rc/lib/action_controller/metal.rb" method="action"/> | |
30 | + <line number="62" file="[GEM_ROOT]/gems/actionpack-3.0.0.rc/lib/action_dispatch/routing/route_set.rb" method="call"/> | |
31 | + <line number="62" file="[GEM_ROOT]/gems/actionpack-3.0.0.rc/lib/action_dispatch/routing/route_set.rb" method="dispatch"/> | |
32 | + <line number="27" file="[GEM_ROOT]/gems/actionpack-3.0.0.rc/lib/action_dispatch/routing/route_set.rb" method="call"/> | |
33 | + <line number="148" file="[GEM_ROOT]/gems/rack-mount-0.6.9/lib/rack/mount/route_set.rb" method="call"/> | |
34 | + <line number="89" file="[GEM_ROOT]/gems/rack-mount-0.6.9/lib/rack/mount/code_generation.rb" method="recognize"/> | |
35 | + <line number="66" file="[GEM_ROOT]/gems/rack-mount-0.6.9/lib/rack/mount/code_generation.rb" method="optimized_each"/> | |
36 | + <line number="88" file="[GEM_ROOT]/gems/rack-mount-0.6.9/lib/rack/mount/code_generation.rb" method="recognize"/> | |
37 | + <line number="139" file="[GEM_ROOT]/gems/rack-mount-0.6.9/lib/rack/mount/route_set.rb" method="call"/> | |
38 | + <line number="489" file="[GEM_ROOT]/gems/actionpack-3.0.0.rc/lib/action_dispatch/routing/route_set.rb" method="call"/> | |
39 | + <line number="41" file="[GEM_ROOT]/gems/haml-3.0.15/lib/sass/plugin/rack.rb" method="call"/> | |
40 | + <line number="14" file="[GEM_ROOT]/gems/actionpack-3.0.0.rc/lib/action_dispatch/middleware/head.rb" method="call"/> | |
41 | + <line number="24" file="[GEM_ROOT]/gems/rack-1.2.1/lib/rack/methodoverride.rb" method="call"/> | |
42 | + <line number="21" file="[GEM_ROOT]/gems/actionpack-3.0.0.rc/lib/action_dispatch/middleware/params_parser.rb" method="call"/> | |
43 | + <line number="177" file="[GEM_ROOT]/gems/actionpack-3.0.0.rc/lib/action_dispatch/middleware/flash.rb" method="call"/> | |
44 | + <line number="149" file="[GEM_ROOT]/gems/actionpack-3.0.0.rc/lib/action_dispatch/middleware/session/abstract_store.rb" method="call"/> | |
45 | + <line number="268" file="[GEM_ROOT]/gems/actionpack-3.0.0.rc/lib/action_dispatch/middleware/cookies.rb" method="call"/> | |
46 | + <line number="32" file="[GEM_ROOT]/gems/activerecord-3.0.0.rc/lib/active_record/query_cache.rb" method="call"/> | |
47 | + <line number="28" file="[GEM_ROOT]/gems/activerecord-3.0.0.rc/lib/active_record/connection_adapters/abstract/query_cache.rb" method="cache"/> | |
48 | + <line number="12" file="[GEM_ROOT]/gems/activerecord-3.0.0.rc/lib/active_record/query_cache.rb" method="cache"/> | |
49 | + <line number="31" file="[GEM_ROOT]/gems/activerecord-3.0.0.rc/lib/active_record/query_cache.rb" method="call"/> | |
50 | + <line number="46" file="[GEM_ROOT]/gems/actionpack-3.0.0.rc/lib/action_dispatch/middleware/callbacks.rb" method="call"/> | |
51 | + <line number="410" file="[GEM_ROOT]/gems/activesupport-3.0.0.rc/lib/active_support/callbacks.rb" method="_run_call_callbacks"/> | |
52 | + <line number="44" file="[GEM_ROOT]/gems/actionpack-3.0.0.rc/lib/action_dispatch/middleware/callbacks.rb" method="call"/> | |
53 | + <line number="107" file="[GEM_ROOT]/gems/rack-1.2.1/lib/rack/sendfile.rb" method="call"/> | |
54 | + <line number="48" file="[GEM_ROOT]/gems/actionpack-3.0.0.rc/lib/action_dispatch/middleware/remote_ip.rb" method="call"/> | |
55 | + <line number="48" file="[GEM_ROOT]/gems/actionpack-3.0.0.rc/lib/action_dispatch/middleware/show_exceptions.rb" method="call"/> | |
56 | + <line number="13" file="[GEM_ROOT]/gems/railties-3.0.0.rc/lib/rails/rack/logger.rb" method="call"/> | |
57 | + <line number="17" file="[GEM_ROOT]/gems/rack-1.2.1/lib/rack/runtime.rb" method="call"/> | |
58 | + <line number="72" file="[GEM_ROOT]/gems/activesupport-3.0.0.rc/lib/active_support/cache/strategy/local_cache.rb" method="call"/> | |
59 | + <line number="11" file="[GEM_ROOT]/gems/rack-1.2.1/lib/rack/lock.rb" method="call"/> | |
60 | + <line number="11" file="[GEM_ROOT]/gems/rack-1.2.1/lib/rack/lock.rb" method="synchronize"/> | |
61 | + <line number="11" file="[GEM_ROOT]/gems/rack-1.2.1/lib/rack/lock.rb" method="call"/> | |
62 | + <line number="30" file="[GEM_ROOT]/gems/actionpack-3.0.0.rc/lib/action_dispatch/middleware/static.rb" method="call"/> | |
63 | + <line number="168" file="[GEM_ROOT]/gems/railties-3.0.0.rc/lib/rails/application.rb" method="call"/> | |
64 | + <line number="77" file="[GEM_ROOT]/gems/railties-3.0.0.rc/lib/rails/application.rb" method="send"/> | |
65 | + <line number="77" file="[GEM_ROOT]/gems/railties-3.0.0.rc/lib/rails/application.rb" method="method_missing"/> | |
66 | + <line number="636" file="[GEM_ROOT]/gems/rake-0.8.7/lib/rake.rb" method="call"/> | |
67 | + <line number="636" file="[GEM_ROOT]/gems/rake-0.8.7/lib/rake.rb" method="execute"/> | |
68 | + <line number="631" file="[GEM_ROOT]/gems/rake-0.8.7/lib/rake.rb" method="each"/> | |
69 | + <line number="631" file="[GEM_ROOT]/gems/rake-0.8.7/lib/rake.rb" method="execute"/> | |
70 | + <line number="597" file="[GEM_ROOT]/gems/rake-0.8.7/lib/rake.rb" method="invoke_with_call_chain"/> | |
71 | + <line number="242" file="/Users/jdpace/.rvm/rubies/ree-1.8.7-2010.02/lib/ruby/1.8/monitor.rb" method="synchronize"/> | |
72 | + <line number="590" file="[GEM_ROOT]/gems/rake-0.8.7/lib/rake.rb" method="invoke_with_call_chain"/> | |
73 | + <line number="583" file="[GEM_ROOT]/gems/rake-0.8.7/lib/rake.rb" method="invoke"/> | |
74 | + <line number="2051" file="[GEM_ROOT]/gems/rake-0.8.7/lib/rake.rb" method="invoke_task"/> | |
75 | + <line number="2029" file="[GEM_ROOT]/gems/rake-0.8.7/lib/rake.rb" method="top_level"/> | |
76 | + <line number="2029" file="[GEM_ROOT]/gems/rake-0.8.7/lib/rake.rb" method="each"/> | |
77 | + <line number="2029" file="[GEM_ROOT]/gems/rake-0.8.7/lib/rake.rb" method="top_level"/> | |
78 | + <line number="2068" file="[GEM_ROOT]/gems/rake-0.8.7/lib/rake.rb" method="standard_exception_handling"/> | |
79 | + <line number="2023" file="[GEM_ROOT]/gems/rake-0.8.7/lib/rake.rb" method="top_level"/> | |
80 | + <line number="2001" file="[GEM_ROOT]/gems/rake-0.8.7/lib/rake.rb" method="run"/> | |
81 | + <line number="2068" file="[GEM_ROOT]/gems/rake-0.8.7/lib/rake.rb" method="standard_exception_handling"/> | |
82 | + <line number="1998" file="[GEM_ROOT]/gems/rake-0.8.7/lib/rake.rb" method="run"/> | |
83 | + <line number="31" file="[GEM_ROOT]/gems/rake-0.8.7/bin/rake" method=""/> | |
84 | + <line number="19" file="[GEM_ROOT]/bin/rake" method="load"/> | |
85 | + <line number="19" file="[GEM_ROOT]/bin/rake" method=""/> | |
86 | + </backtrace> | |
87 | + </error> | |
88 | + <server-environment> | |
89 | + <project-root>/path/to/sample/project</project-root> | |
90 | + <environment-name>development</environment-name> | |
91 | + </server-environment> | |
92 | +</notice> | |
0 | 93 | \ No newline at end of file | ... | ... |
spec/models/notice_spec.rb
... | ... | @@ -98,6 +98,11 @@ describe Notice do |
98 | 98 | @notice = Notice.from_xml(@xml) |
99 | 99 | @notice.notifier['name'].should == 'Hoptoad Notifier' |
100 | 100 | end |
101 | + | |
102 | + it "should handle params withour 'request' section" do | |
103 | + @xml = Rails.root.join('spec','fixtures','hoptoad_test_notice_without_request_section.xml').read | |
104 | + lambda { Notice.from_xml(@xml) }.should_not raise_error | |
105 | + end | |
101 | 106 | end |
102 | 107 | |
103 | 108 | describe "email notifications" do | ... | ... |
spec/models/user_spec.rb
... | ... | @@ -38,5 +38,14 @@ describe User do |
38 | 38 | end |
39 | 39 | |
40 | 40 | end |
41 | + | |
42 | + context "First user" do | |
43 | + it "should be created this admin access via db:seed" do | |
44 | + require 'rake' | |
45 | + Errbit::Application.load_tasks | |
46 | + Rake::Task["db:seed"].execute | |
47 | + User.first.admin.should be_true | |
48 | + end | |
49 | + end | |
41 | 50 | |
42 | 51 | end | ... | ... |
spec/spec_helper.rb
... | ... | @@ -18,8 +18,7 @@ RSpec.configure do |config| |
18 | 18 | config.alias_example_to :fit, :focused => true |
19 | 19 | |
20 | 20 | config.before(:each) do |
21 | - DatabaseCleaner.orm = "mongoid" | |
22 | - DatabaseCleaner.strategy = :truncation | |
21 | + DatabaseCleaner[:mongoid].strategy = :truncation | |
23 | 22 | DatabaseCleaner.clean |
24 | 23 | end |
25 | 24 | end |
26 | 25 | \ No newline at end of file | ... | ... |