diff --git a/Gemfile b/Gemfile index bfc0b773..a04059bf 100644 --- a/Gemfile +++ b/Gemfile @@ -6,6 +6,7 @@ unless RUBY_PLATFORM =~ /w32/ gem 'rubyzip' gem 'zip-zip' end + gem 'seems_rateable', path: 'lib/seems_rateable' gem "rails", "3.2.13" gem "jquery-rails", "~> 2.0.2" @@ -15,6 +16,11 @@ gem "fastercsv", "~> 1.5.0", :platforms => [:mri_18, :mingw_18, :jruby] gem "builder", "3.0.0" gem 'acts-as-taggable-on' +group :development do + gem 'better_errors', path: 'lib/better_errors' + gem 'rack-mini-profiler', path: 'lib/rack-mini-profiler' +end + # Optional gem for LDAP authentication group :ldap do gem "net-ldap", "~> 0.3.1" @@ -70,16 +76,6 @@ else warn("Please configure your config/database.yml first") end -group :development do - gem "rdoc", ">= 2.4.2" - if nil - gem 'thin' - gem 'rack-mini-profiler' - end -end - - - local_gemfile = File.join(File.dirname(__FILE__), "Gemfile.local") if File.exists?(local_gemfile) puts "Loading Gemfile.local ..." if $DEBUG # `ruby -d` or `bundle -v` diff --git a/Gemfile.lock b/Gemfile.lock index 54d89f38..25cc9f48 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,3 +1,16 @@ +PATH + remote: lib/better_errors + specs: + better_errors (1.1.0) + coderay (>= 1.0.0) + erubis (>= 2.6.6) + +PATH + remote: lib/rack-mini-profiler + specs: + rack-mini-profiler (0.9.1) + rack (>= 1.1.3) + PATH remote: lib/seems_rateable specs: @@ -105,6 +118,7 @@ DEPENDENCIES activerecord-jdbc-adapter (= 1.2.5) activerecord-jdbcmysql-adapter acts-as-taggable-on + better_errors! builder (= 3.0.0) coderay (~> 1.0.6) fastercsv (~> 1.5.0) @@ -112,8 +126,8 @@ DEPENDENCIES jquery-rails (~> 2.0.2) mysql2 (~> 0.3.11) net-ldap (~> 0.3.1) + rack-mini-profiler! rack-openid rails (= 3.2.13) - rdoc (>= 2.4.2) ruby-openid (~> 2.1.4) seems_rateable! diff --git a/app/controllers/admin_controller.rb b/app/controllers/admin_controller.rb index ab3d7b12..2a3ce15d 100644 --- a/app/controllers/admin_controller.rb +++ b/app/controllers/admin_controller.rb @@ -149,7 +149,6 @@ class AdminController < ApplicationController respond_to do |format| format.html { @groups = Group.all.sort - render :layout => @user_base_tag } format.api end @@ -196,4 +195,43 @@ class AdminController < ApplicationController [:text_rmagick_available, Object.const_defined?(:Magick)] ] end + #管理功能用户列表的搜索 + def search + sort_init 'login', 'asc' + sort_update %w(login firstname lastname mail admin created_on last_login_on) + + case params[:format] + when 'xml', 'json' + @offset, @limit = api_offset_and_limit({:limit => 15}) + else + @limit = 15#per_page_option + end + + @status = params[:status] || 1 + has = { + "show_changesets" => true + } + scope = User.logged.status(@status) + scope = scope.like(params[:name]) if params[:name].present? + @user_count = scope.count + @user_pages = Paginator.new @user_count, @limit, params['page'] + @user_base_tag = params[:id] ? 'base_users':'base' + @offset ||= @user_pages.reverse_offset + unless @offset == 0 + @users = scope.offset(@offset).limit(@limit).all.reverse + else + limit = @user_count % @limit + if limit == 0 + limit = @limit + end + @users = scope.offset(@offset).limit(limit).all.reverse + end + + respond_to do |format| + format.html { + @groups = Group.all.sort + } + format.api + end + end end diff --git a/app/controllers/files_controller.rb b/app/controllers/files_controller.rb index f9236d0b..5b81b729 100644 --- a/app/controllers/files_controller.rb +++ b/app/controllers/files_controller.rb @@ -52,9 +52,14 @@ class FilesController < ApplicationController end def create - if params[:tag_name] + if params[:add_tag] + @addTag=true + #render :back tag_saveEx - render :text =>"success" + #render :text =>"success" + respond_to do |format| + format.js + end else @addTag=false container = (params[:version_id].blank? ? @project : @project.versions.find_by_id(params[:version_id])) @@ -64,7 +69,27 @@ class FilesController < ApplicationController if !attachments.empty? && !attachments[:files].blank? && Setting.notified_events.include?('file_added') Mailer.attachments_added(attachments[:files]).deliver end - redirect_to project_files_path(@project) + + # 临时用 + sort_init 'created_on', 'desc' + sort_update 'created_on' => "#{Attachment.table_name}.created_on", + 'filename' => "#{Attachment.table_name}.filename", + 'size' => "#{Attachment.table_name}.filesize", + 'downloads' => "#{Attachment.table_name}.downloads" + + @containers = [ Project.includes(:attachments).reorder("#{Attachment.table_name}.created_on DESC").find(@project.id)] #modify by Long Jun + @containers += @project.versions.includes(:attachments).reorder("#{Attachment.table_name}.created_on DESC").all.sort + + @attachtype = 0 + @contenttype = 0 + + respond_to do |format| + format.js + format.html { + redirect_to project_files_path(@project) + } + end + end end diff --git a/app/controllers/members_controller.rb b/app/controllers/members_controller.rb index d8e583bf..30c6c7e8 100644 --- a/app/controllers/members_controller.rb +++ b/app/controllers/members_controller.rb @@ -76,8 +76,7 @@ class MembersController < ApplicationController # ProjectInfo.create(:name => "test", :user_id => 123) end ## end - AppliedProject.deleteappiled(user_id, @project.id) - end + end else members << Member.new(:role_ids => params[:membership][:role_ids], :user_id => params[:membership][:user_id]) user_grades << UserGrade.new(:user_id => params[:membership][:user_id], :project_id => @project.id) @@ -95,6 +94,13 @@ class MembersController < ApplicationController end end + if members.present? && members.all? {|m| m.valid? } + members.each do |member| + AppliedProject.deleteappiled(member.user_id, @project.id) + end + + end + respond_to do |format| format.html { redirect_to_settings_in_projects } format.js { @members = members;@applied_members = applied_members; } diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index a6dd7cf3..1cf14a83 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -716,6 +716,11 @@ class ProjectsController < ApplicationController @document = @project.documents.build # @base_courses_tag = @project.project_type + #判断能否显示真名(当前用户为课程的教师时显示真名) + if @project.project_type == Project::ProjectType_course + @teachers= searchTeacherAndAssistant(@project) + @canShowRealName = isCourseTeacher(User.current.id) + end respond_to do |format| format.html{render :layout => 'base_courses' if @base_courses_tag==1} format.api diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index da9aef30..6452bcc8 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -65,13 +65,13 @@ class UsersController < ApplicationController def user_projects if User.current.admin? - @memberships = @user.memberships.all + @memberships = @user.memberships.all(conditions: "projects.project_type = #{Project::ProjectType_project}") else cond = Project.visible_condition(User.current) + " AND projects.project_type <> 1" @memberships = @user.memberships.all(:conditions => cond) end - events = Redmine::Activity::Fetcher.new(User.current, :author => @user).events(nil, nil, :limit => 20) - @events_by_day = events.group_by(&:event_date) + #events = Redmine::Activity::Fetcher.new(User.current, :author => @user).events(nil, nil, :limit => 20) + #@events_by_day = events.group_by(&:event_date) @state = 0 @@ -289,7 +289,7 @@ class UsersController < ApplicationController when '0' @offset ||= @user_pages.reverse_offset unless @offset == 0 - @users_statuses = scope.offset(@offset).limit(@limit).all.reverse + @users_statuses = scope.offset(@offset).limit(@limit).all.reverse else limit = @user_count % @limit if limit == 0 @@ -313,7 +313,7 @@ class UsersController < ApplicationController end @s_type = 1 #sort {|x,y| y.user_status.changesets_count <=> x.user_status.changesets_count} - #@users = @users[@offset, @limit] + #@users = @users[@offset, @limit] when '2' @offset ||= @user_pages.reverse_offset unless @offset == 0 @@ -326,9 +326,9 @@ class UsersController < ApplicationController @users_statuses = scope.reorder('watchers_count').offset(@offset).limit(limit).all.reverse end @s_type = 2 - #@users = @users[@offset, @limit] + #@users = @users[@offset, @limit] end - + else @offset ||= @user_pages.reverse_offset unless @offset == 0 @@ -565,7 +565,7 @@ class UsersController < ApplicationController end def watch_projects - @watch_projects = Project.joins(:watchers).where("project_type <>? and watchable_type = ? and user_id = ?", '1','Project', @user.id) + @watch_projects = Project.joins(:watchers).where("project_type <>? and watchable_type = ? and `watchers`.user_id = ?", '1','Project', @user.id) @state = 1 respond_to do |format| format.html { @@ -689,7 +689,7 @@ class UsersController < ApplicationController end def tag_saveEx - @tags = params[:tag_name][:name] + @tags = params[:tag_name] @obj_id = params[:obj_id] @obj_flag = params[:obj_flag] diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index ae6ec698..7dc4b555 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -58,9 +58,14 @@ module ApplicationHelper end # Displays a link to user's account page if active - def link_to_user(user, options={}) + def link_to_user(user, canShowRealName = false, options={}) if user.is_a?(User) - name = h(user.name(options[:format])) + if canShowRealName + name = h(user.realname(options[:format])) + else + name = h(user.name(options[:format])) + end + if user.active? || (User.current.admin? && user.logged?) link_to name, {:controller=> 'users', :action => 'show', id: user.id, host: Setting.user_domain}, :class => user.css_classes else @@ -1342,7 +1347,18 @@ module ApplicationHelper def bootstrap_head tags = stylesheet_link_tag('bootstrap/bootstrap.min', 'bootstrap/bootstrap-theme.min') - tags << javascript_include_tag('bootstrap/bootstrap.min', 'bootstrap/jquery.transition.min') + tags << javascript_include_tag('bootstrap/affix') + tags << javascript_include_tag('bootstrap/alert') + tags << javascript_include_tag('bootstrap/button') + tags << javascript_include_tag('bootstrap/carousel') + tags << javascript_include_tag('bootstrap/collapse') + tags << javascript_include_tag('bootstrap/dropdown') + tags << javascript_include_tag('bootstrap/modal') + tags << javascript_include_tag('bootstrap/popover') + tags << javascript_include_tag('bootstrap/scrollspy') + tags << javascript_include_tag('bootstrap/tab') + tags << javascript_include_tag('bootstrap/tooltip') + tags << javascript_include_tag('bootstrap/transition') tags end diff --git a/app/helpers/tags_helper.rb b/app/helpers/tags_helper.rb index ba2470fe..201a582f 100644 --- a/app/helpers/tags_helper.rb +++ b/app/helpers/tags_helper.rb @@ -49,5 +49,9 @@ module TagsHelper end return @result end - + +end + +def tagname_val + ("#tag_name_name").value end \ No newline at end of file diff --git a/app/models/mailer.rb b/app/models/mailer.rb index e5ede628..a303d92b 100644 --- a/app/models/mailer.rb +++ b/app/models/mailer.rb @@ -22,7 +22,7 @@ class Mailer < ActionMailer::Base helper :custom_fields include Redmine::I18n - + include CoursesHelper def self.default_url_options { :host => Setting.host_name, :protocol => Setting.protocol } end @@ -44,6 +44,16 @@ class Mailer < ActionMailer::Base when :Bid respond_url(journals_for_message.jour, anchor: "word_li_#{journals_for_message.id}") when :Project + if journals_for_message.jour.project_type == 1 + project = journals_for_message.jour + @teachers = searchTeacherAndAssistant journals_for_message.jour + @recipients ||= [] + @teachers.each do |teacher| + @recipients << teacher + end + mail :to => @recipients, + :subject => "您的课程#{journals_for_message.jour.name}中有了新的留言" + end project_feedback_url(journals_for_message.jour, anchor: "word_li_#{journals_for_message.id}") when :Contest show_contest_contest_url(journals_for_message.jour, anchor: "word_li_#{journals_for_message.id}") diff --git a/app/views/admin/search.html.erb b/app/views/admin/search.html.erb new file mode 100644 index 00000000..17d91e78 --- /dev/null +++ b/app/views/admin/search.html.erb @@ -0,0 +1,68 @@ +<% if User.current.admin? %> +
+ <%= link_to l(:label_user_new), new_user_path, :class => 'icon icon-add' %> +
+ +

<%= l(:label_user_plural)%>

+ + <%= form_tag(:controller => 'admin', :action => 'search', :method => :get) do %> +
+ + <%= l(:label_filter_plural) %> + + + <%= select_tag 'status', users_status_options_for_select(@status), :class => "small", :onchange => "this.form.submit(); return false;" %> + + <% if @groups.present? %> + + <%= select_tag 'group_id', content_tag('option') + options_from_collection_for_select(@groups, :id, :name, params[:group_id].to_i), :onchange => "this.form.submit(); return false;" %> + <% end %> + + + <%= text_field_tag 'name', params[:name], :size => 30 %> + <%= submit_tag l(:label_search), :class => "small", :name => nil %> +
+ <% end %> +   + +
+ + + + <%= sort_header_tag('login', :caption => l(:field_login)) %> + <%= sort_header_tag('firstname', :caption => l(:field_firstname)) %> + <%= sort_header_tag('lastname', :caption => l(:field_lastname)) %> + <%= sort_header_tag('mail', :caption => l(:field_mail)) %> + + <%= sort_header_tag('admin', :caption => l(:field_admin), :default_order => 'desc') %> + <%= sort_header_tag('created_on', :caption => l(:field_created_on), :default_order => 'desc') %> + <%= sort_header_tag('last_login_on', :caption => l(:field_last_login_on), :default_order => 'desc') %> + + + + + <% for user in @users -%> + "> + + + + + + + + + + <% end -%> + +
<%= avatar(user, :size => "14") %><%= link_to h(user.login), edit_user_path(user) %><%= h(user.firstname) %><%= h(user.lastname) %><%= checked_image user.admin? %><%= format_time(user.created_on) %> <%= change_status_link(user) %> + <%= delete_link user_path(user, :back_url => users_path(params)) unless User.current == user %>
+
+ + + <% html_title(l(:label_user_plural)) -%> + +<% end %> \ No newline at end of file diff --git a/app/views/admin/users.html.erb b/app/views/admin/users.html.erb index 77931646..4dceb5fd 100644 --- a/app/views/admin/users.html.erb +++ b/app/views/admin/users.html.erb @@ -5,7 +5,7 @@

<%= l(:label_user_plural)%>

- <%= form_tag(:controller => 'users', :action => 'search', :method => :get) do %> + <%= form_tag(:controller => 'admin', :action => 'search', :method => :get) do %>
<%= l(:label_filter_plural) %> diff --git a/app/views/attachments/upload.js.erb b/app/views/attachments/upload.js.erb index 674d73b7..63600619 100644 --- a/app/views/attachments/upload.js.erb +++ b/app/views/attachments/upload.js.erb @@ -12,6 +12,5 @@ fileSpan.find('a.remove-upload') }) .off('click'); var divattach = fileSpan.find('div.div_attachments'); -divattach.html('<%#= j(render :partial => 'tags/tagEx', :locals => {:obj => @attachment, :object_flag => "6"})%>'); - +divattach.html('<%= j(render :partial => 'tags/tagEx', :locals => {:obj => @attachment, :object_flag => "6"})%>'); <% end %> diff --git a/app/views/files/_new.html.erb b/app/views/files/_new.html.erb index a01143e0..7dcf82e1 100644 --- a/app/views/files/_new.html.erb +++ b/app/views/files/_new.html.erb @@ -2,7 +2,7 @@ <% versions = project.versions.sort %> <% attachmenttypes = project.attachmenttypes %> <%= error_messages_for 'attachment' %> -<%= form_tag(project_files_path(project), :multipart => true,:name=>"upload_form", :class => "tabular") do %> +<%= form_tag(project_files_path(project), :multipart => true,:remote => true,:method => :post,:name=>"upload_form", :class => "tabular") do %>

diff --git a/app/views/layouts/_bootstrap_base_header.html.erb b/app/views/layouts/_bootstrap_base_header.html.erb index fc7897b2..8c2d26c5 100644 --- a/app/views/layouts/_bootstrap_base_header.html.erb +++ b/app/views/layouts/_bootstrap_base_header.html.erb @@ -1,50 +1,49 @@ <% - request.headers['REQUEST_URI'] = "" if request.headers['REQUEST_URI'].nil? - realUrl = request.original_url - if (realUrl.match(/forge\.trustie\.net\/*/)) - @nav_dispaly_project_label = 1 - @nav_dispaly_forum_label = 1 - elsif (realUrl.match(/course\.trustie\.net\/*/)) - @nav_dispaly_course_all_label = 1 - @nav_dispaly_forum_label = 1 - @nav_dispaly_course_label = nil - @nav_dispaly_store_all_label = 1 - elsif (realUrl.match(/user\.trustie\.net\/*/)) - @nav_dispaly_home_path_label = 1 - @nav_dispaly_main_course_label = 1 - @nav_dispaly_main_project_label = 1 - @nav_dispaly_main_contest_label = 1 - elsif (realUrl.match(/contest\.trustie\.net\/*/)) - @nav_dispaly_contest_label = 1 - @nav_dispaly_store_all_label = 1 - else - @nav_dispaly_project_all_label = 1 - @nav_dispaly_course_all_label = 1 - @nav_dispaly_forum_label = 1 - @nav_dispaly_bid_label = 1 - @nav_dispaly_contest_label = 1 - @nav_dispaly_store_all_label = 1 - @nav_dispaly_user_label = 1 - end +request.headers['REQUEST_URI'] = "" if request.headers['REQUEST_URI'].nil? +realUrl = request.original_url +if (realUrl.match(/forge\.trustie\.net\/*/)) + @nav_dispaly_project_label = 1 + @nav_dispaly_forum_label = 1 +elsif (realUrl.match(/course\.trustie\.net\/*/)) + @nav_dispaly_course_all_label = 1 + @nav_dispaly_forum_label = 1 + @nav_dispaly_course_label = nil + @nav_dispaly_store_all_label = 1 +elsif (realUrl.match(/user\.trustie\.net\/*/)) + @nav_dispaly_home_path_label = 1 + @nav_dispaly_main_course_label = 1 + @nav_dispaly_main_project_label = 1 + @nav_dispaly_main_contest_label = 1 +elsif (realUrl.match(/contest\.trustie\.net\/*/)) + @nav_dispaly_contest_label = 1 + @nav_dispaly_store_all_label = 1 +else + @nav_dispaly_project_all_label = 1 + @nav_dispaly_course_all_label = 1 + @nav_dispaly_forum_label = 1 + @nav_dispaly_bid_label = 1 + @nav_dispaly_contest_label = 1 + @nav_dispaly_store_all_label = 1 +end %> - + diff --git a/app/views/layouts/bootstrap_base.html.erb b/app/views/layouts/bootstrap_base.html.erb index e0dc457d..040857df 100644 --- a/app/views/layouts/bootstrap_base.html.erb +++ b/app/views/layouts/bootstrap_base.html.erb @@ -16,21 +16,8 @@ <%= yield :header_tags -%> - <%= render :partial => 'layouts/bootstrap_base_header' %>
-
-
.col-xs-6 .col-sm-3
-
.col-xs-6 .col-sm-3
- - -
- -
.col-xs-6 .col-sm-3
-
.col-xs-6 .col-sm-3
-
- - <%= render_flash_messages %> <%= yield %>
diff --git a/app/views/projects/show.html.erb b/app/views/projects/show.html.erb index eb66efc7..dfe8a10b 100644 --- a/app/views/projects/show.html.erb +++ b/app/views/projects/show.html.erb @@ -18,7 +18,7 @@
<%= h(e.project) if @project.nil? || @project.id != e.project.id %> - <%= link_to_user(e.event_author) if e.respond_to?(:event_author) %> + <%= link_to_user(e.event_author,@canShowRealName) if e.respond_to?(:event_author) %> <%= l(:label_new_activity) %> <%= link_to "#{eventToLanguageCourse(e.event_type, @project)}: "<< format_activity_title(e.event_title), (e.event_type.eql?("attachment")&&e.container.kind_of?(Project)) ? project_files_path(e.container) : e.event_url %> @@ -46,6 +46,7 @@ <% if format_date(day) == format_date(@date_to - @days) %> +

Test

@@ -80,7 +81,16 @@
- + ') + html.should_not include("
:value_for_inst_d
") + end + end + + it "doesn't die if the source file is not a real filename" do + exception.stub(:backtrace).and_return([ + ":10:in `spawn_rack_application'" + ]) + response.should include("Source unavailable") + end + end +end diff --git a/lib/better_errors/spec/better_errors/middleware_spec.rb b/lib/better_errors/spec/better_errors/middleware_spec.rb new file mode 100644 index 00000000..2c638bfa --- /dev/null +++ b/lib/better_errors/spec/better_errors/middleware_spec.rb @@ -0,0 +1,146 @@ +require "spec_helper" + +module BetterErrors + describe Middleware do + let(:app) { Middleware.new(->env { ":)" }) } + let(:exception) { RuntimeError.new("oh no :(") } + + it "passes non-error responses through" do + app.call({}).should == ":)" + end + + it "calls the internal methods" do + app.should_receive :internal_call + app.call("PATH_INFO" => "/__better_errors/1/preform_awesomness") + end + + it "calls the internal methods on any subfolder path" do + app.should_receive :internal_call + app.call("PATH_INFO" => "/any_sub/folder/path/__better_errors/1/preform_awesomness") + end + + it "shows the error page" do + app.should_receive :show_error_page + app.call("PATH_INFO" => "/__better_errors/") + end + + it "shows the error page on any subfolder path" do + app.should_receive :show_error_page + app.call("PATH_INFO" => "/any_sub/folder/path/__better_errors/") + end + + it "doesn't show the error page to a non-local address" do + app.should_not_receive :better_errors_call + app.call("REMOTE_ADDR" => "1.2.3.4") + end + + it "shows to a whitelisted IP" do + BetterErrors::Middleware.allow_ip! '77.55.33.11' + app.should_receive :better_errors_call + app.call("REMOTE_ADDR" => "77.55.33.11") + end + + it "doesn't blow up when given a blank REMOTE_ADDR" do + expect { app.call("REMOTE_ADDR" => " ") }.to_not raise_error + end + + it "doesn't blow up when given an IP address with a zone index" do + expect { app.call("REMOTE_ADDR" => "0:0:0:0:0:0:0:1%0" ) }.to_not raise_error + end + + context "when requesting the /__better_errors manually" do + let(:app) { Middleware.new(->env { ":)" }) } + + it "shows that no errors have been recorded" do + status, headers, body = app.call("PATH_INFO" => "/__better_errors") + body.join.should match /No errors have been recorded yet./ + end + + it "shows that no errors have been recorded on any subfolder path" do + status, headers, body = app.call("PATH_INFO" => "/any_sub/folder/path/__better_errors") + body.join.should match /No errors have been recorded yet./ + end + end + + context "when handling an error" do + let(:app) { Middleware.new(->env { raise exception }) } + + it "returns status 500" do + status, headers, body = app.call({}) + + status.should == 500 + end + + context "original_exception" do + class OriginalExceptionException < Exception + attr_reader :original_exception + + def initialize(message, original_exception = nil) + super(message) + @original_exception = original_exception + end + end + + it "shows Original Exception if it responds_to and has an original_exception" do + app = Middleware.new(->env { + raise OriginalExceptionException.new("Other Exception", Exception.new("Original Exception")) + }) + + status, _, body = app.call({}) + + status.should == 500 + body.join.should_not match(/Other Exception/) + body.join.should match(/Original Exception/) + end + + it "won't crash if the exception responds_to but doesn't have an original_exception" do + app = Middleware.new(->env { + raise OriginalExceptionException.new("Other Exception") + }) + + status, _, body = app.call({}) + + status.should == 500 + body.join.should match(/Other Exception/) + end + end + + it "returns ExceptionWrapper's status_code" do + ad_ew = double("ActionDispatch::ExceptionWrapper") + ad_ew.stub('new').with({}, exception ){ double("ExceptionWrapper", status_code: 404) } + stub_const('ActionDispatch::ExceptionWrapper', ad_ew) + + status, headers, body = app.call({}) + + status.should == 404 + end + + it "returns UTF-8 error pages" do + status, headers, body = app.call({}) + + headers["Content-Type"].should match /charset=utf-8/ + end + + it "returns text pages by default" do + status, headers, body = app.call({}) + + headers["Content-Type"].should match /text\/plain/ + end + + it "returns HTML pages by default" do + # Chrome's 'Accept' header looks similar this. + status, headers, body = app.call("HTTP_ACCEPT" => "text/html,application/xhtml+xml;q=0.9,*/*") + + headers["Content-Type"].should match /text\/html/ + end + + it "logs the exception" do + logger = Object.new + logger.should_receive :fatal + BetterErrors.stub(:logger).and_return(logger) + + app.call({}) + end + end + end +end diff --git a/lib/better_errors/spec/better_errors/raised_exception_spec.rb b/lib/better_errors/spec/better_errors/raised_exception_spec.rb new file mode 100644 index 00000000..605ab409 --- /dev/null +++ b/lib/better_errors/spec/better_errors/raised_exception_spec.rb @@ -0,0 +1,52 @@ +require "spec_helper" + +module BetterErrors + describe RaisedException do + let(:exception) { RuntimeError.new("whoops") } + subject { RaisedException.new(exception) } + + its(:exception) { should == exception } + its(:message) { should == "whoops" } + its(:type) { should == RuntimeError } + + context "when the exception wraps another exception" do + let(:original_exception) { RuntimeError.new("something went wrong!") } + let(:exception) { double(:original_exception => original_exception) } + + its(:exception) { should == original_exception } + its(:message) { should == "something went wrong!" } + end + + context "when the exception is a syntax error" do + let(:exception) { SyntaxError.new("foo.rb:123: you made a typo!") } + + its(:message) { should == "you made a typo!" } + its(:type) { should == SyntaxError } + + it "has the right filename and line number in the backtrace" do + subject.backtrace.first.filename.should == "foo.rb" + subject.backtrace.first.line.should == 123 + end + end + + context "when the exception is a HAML syntax error" do + before do + stub_const("Haml::SyntaxError", Class.new(SyntaxError)) + end + + let(:exception) { + Haml::SyntaxError.new("you made a typo!").tap do |ex| + ex.set_backtrace(["foo.rb:123", "haml/internals/blah.rb:123456"]) + end + } + + its(:message) { should == "you made a typo!" } + its(:type) { should == Haml::SyntaxError } + + it "has the right filename and line number in the backtrace" do + subject.backtrace.first.filename.should == "foo.rb" + subject.backtrace.first.line.should == 123 + end + end + end +end diff --git a/lib/better_errors/spec/better_errors/repl/basic_spec.rb b/lib/better_errors/spec/better_errors/repl/basic_spec.rb new file mode 100644 index 00000000..c533f632 --- /dev/null +++ b/lib/better_errors/spec/better_errors/repl/basic_spec.rb @@ -0,0 +1,18 @@ +require "spec_helper" +require "better_errors/repl/basic" +require "better_errors/repl/shared_examples" + +module BetterErrors + module REPL + describe Basic do + let(:fresh_binding) { + local_a = 123 + binding + } + + let(:repl) { Basic.new fresh_binding } + + it_behaves_like "a REPL provider" + end + end +end diff --git a/lib/better_errors/spec/better_errors/repl/pry_spec.rb b/lib/better_errors/spec/better_errors/repl/pry_spec.rb new file mode 100644 index 00000000..1aa502c5 --- /dev/null +++ b/lib/better_errors/spec/better_errors/repl/pry_spec.rb @@ -0,0 +1,40 @@ +require "spec_helper" +require "pry" +require "better_errors/repl/pry" +require "better_errors/repl/shared_examples" + +module BetterErrors + module REPL + describe Pry do + let(:fresh_binding) { + local_a = 123 + binding + } + + let(:repl) { Pry.new fresh_binding } + + it "does line continuation" do + output, prompt, filled = repl.send_input "" + output.should == "=> nil\n" + prompt.should == ">>" + filled.should == "" + + output, prompt, filled = repl.send_input "def f(x)" + output.should == "" + prompt.should == ".." + filled.should == " " + + output, prompt, filled = repl.send_input "end" + if RUBY_VERSION >= "2.1.0" + output.should == "=> :f\n" + else + output.should == "=> nil\n" + end + prompt.should == ">>" + filled.should == "" + end + + it_behaves_like "a REPL provider" + end + end +end diff --git a/lib/better_errors/spec/better_errors/repl/shared_examples.rb b/lib/better_errors/spec/better_errors/repl/shared_examples.rb new file mode 100644 index 00000000..0154851d --- /dev/null +++ b/lib/better_errors/spec/better_errors/repl/shared_examples.rb @@ -0,0 +1,18 @@ +shared_examples_for "a REPL provider" do + it "evaluates ruby code in a given context" do + repl.send_input("local_a = 456") + fresh_binding.eval("local_a").should == 456 + end + + it "returns a tuple of output and the new prompt" do + output, prompt = repl.send_input("1 + 2") + output.should == "=> 3\n" + prompt.should == ">>" + end + + it "doesn't barf if the code throws an exception" do + output, prompt = repl.send_input("raise Exception") + output.should include "Exception: Exception" + prompt.should == ">>" + end +end diff --git a/lib/better_errors/spec/better_errors/stack_frame_spec.rb b/lib/better_errors/spec/better_errors/stack_frame_spec.rb new file mode 100644 index 00000000..420111ee --- /dev/null +++ b/lib/better_errors/spec/better_errors/stack_frame_spec.rb @@ -0,0 +1,157 @@ +require "spec_helper" + +module BetterErrors + describe StackFrame do + context "#application?" do + it "is true for application filenames" do + BetterErrors.stub(:application_root).and_return("/abc/xyz") + frame = StackFrame.new("/abc/xyz/app/controllers/crap_controller.rb", 123, "index") + + frame.application?.should be_true + end + + it "is false for everything else" do + BetterErrors.stub(:application_root).and_return("/abc/xyz") + frame = StackFrame.new("/abc/nope", 123, "foo") + + frame.application?.should be_false + end + + it "doesn't care if no application_root is set" do + frame = StackFrame.new("/abc/xyz/app/controllers/crap_controller.rb", 123, "index") + + frame.application?.should be_false + end + end + + context "#gem?" do + it "is true for gem filenames" do + Gem.stub(:path).and_return(["/abc/xyz"]) + frame = StackFrame.new("/abc/xyz/gems/whatever-1.2.3/lib/whatever.rb", 123, "foo") + + frame.gem?.should be_true + end + + it "is false for everything else" do + Gem.stub(:path).and_return(["/abc/xyz"]) + frame = StackFrame.new("/abc/nope", 123, "foo") + + frame.gem?.should be_false + end + end + + context "#application_path" do + it "chops off the application root" do + BetterErrors.stub(:application_root).and_return("/abc/xyz") + frame = StackFrame.new("/abc/xyz/app/controllers/crap_controller.rb", 123, "index") + + frame.application_path.should == "app/controllers/crap_controller.rb" + end + end + + context "#gem_path" do + it "chops of the gem path and stick (gem) there" do + Gem.stub(:path).and_return(["/abc/xyz"]) + frame = StackFrame.new("/abc/xyz/gems/whatever-1.2.3/lib/whatever.rb", 123, "foo") + + frame.gem_path.should == "whatever (1.2.3) lib/whatever.rb" + end + + it "prioritizes gem path over application path" do + BetterErrors.stub(:application_root).and_return("/abc/xyz") + Gem.stub(:path).and_return(["/abc/xyz/vendor"]) + frame = StackFrame.new("/abc/xyz/vendor/gems/whatever-1.2.3/lib/whatever.rb", 123, "foo") + + frame.gem_path.should == "whatever (1.2.3) lib/whatever.rb" + end + end + + context "#pretty_path" do + it "returns #application_path for application paths" do + BetterErrors.stub(:application_root).and_return("/abc/xyz") + frame = StackFrame.new("/abc/xyz/app/controllers/crap_controller.rb", 123, "index") + frame.pretty_path.should == frame.application_path + end + + it "returns #gem_path for gem paths" do + Gem.stub(:path).and_return(["/abc/xyz"]) + frame = StackFrame.new("/abc/xyz/gems/whatever-1.2.3/lib/whatever.rb", 123, "foo") + + frame.pretty_path.should == frame.gem_path + end + end + + it "special cases SyntaxErrors" do + begin + eval(%{ raise SyntaxError, "you wrote bad ruby!" }, nil, "my_file.rb", 123) + rescue SyntaxError => syntax_error + end + frames = StackFrame.from_exception(syntax_error) + frames.first.filename.should == "my_file.rb" + frames.first.line.should == 123 + end + + it "doesn't blow up if no method name is given" do + error = StandardError.allocate + + error.stub(:backtrace).and_return(["foo.rb:123"]) + frames = StackFrame.from_exception(error) + frames.first.filename.should == "foo.rb" + frames.first.line.should == 123 + + error.stub(:backtrace).and_return(["foo.rb:123: this is an error message"]) + frames = StackFrame.from_exception(error) + frames.first.filename.should == "foo.rb" + frames.first.line.should == 123 + end + + it "ignores a backtrace line if its format doesn't make any sense at all" do + error = StandardError.allocate + error.stub(:backtrace).and_return(["foo.rb:123:in `foo'", "C:in `find'", "bar.rb:123:in `bar'"]) + frames = StackFrame.from_exception(error) + frames.count.should == 2 + end + + it "doesn't blow up if a filename contains a colon" do + error = StandardError.allocate + error.stub(:backtrace).and_return(["crap:filename.rb:123"]) + frames = StackFrame.from_exception(error) + frames.first.filename.should == "crap:filename.rb" + end + + it "doesn't blow up with a BasicObject as frame binding" do + obj = BasicObject.new + def obj.my_binding + ::Kernel.binding + end + frame = StackFrame.new("/abc/xyz/app/controllers/crap_controller.rb", 123, "index", obj.my_binding) + frame.class_name.should == 'BasicObject' + end + + it "sets method names properly" do + obj = "string" + def obj.my_method + begin + raise "foo" + rescue => err + err + end + end + + frame = StackFrame.from_exception(obj.my_method).first + if BetterErrors.binding_of_caller_available? + frame.method_name.should == "#my_method" + frame.class_name.should == "String" + else + frame.method_name.should == "my_method" + frame.class_name.should == nil + end + end + + if RUBY_ENGINE == "java" + it "doesn't blow up on a native Java exception" do + expect { StackFrame.from_exception(java.lang.Exception.new) }.to_not raise_error + end + end + end +end diff --git a/lib/better_errors/spec/better_errors/support/my_source.rb b/lib/better_errors/spec/better_errors/support/my_source.rb new file mode 100644 index 00000000..6dfea0f6 --- /dev/null +++ b/lib/better_errors/spec/better_errors/support/my_source.rb @@ -0,0 +1,20 @@ +one +two +three +four +five +six +seven +eight +nine +ten +eleven +twelve +thirteen +fourteen +fifteen +sixteen +seventeen +eighteen +nineteen +twenty diff --git a/lib/better_errors/spec/better_errors_spec.rb b/lib/better_errors/spec/better_errors_spec.rb new file mode 100644 index 00000000..e9b26b67 --- /dev/null +++ b/lib/better_errors/spec/better_errors_spec.rb @@ -0,0 +1,73 @@ +require "spec_helper" + +describe BetterErrors do + context ".editor" do + it "defaults to textmate" do + subject.editor["foo.rb", 123].should == "txmt://open?url=file://foo.rb&line=123" + end + + it "url escapes the filename" do + subject.editor["&.rb", 0].should == "txmt://open?url=file://%26.rb&line=0" + end + + [:emacs, :emacsclient].each do |editor| + it "uses emacs:// scheme when set to #{editor.inspect}" do + subject.editor = editor + subject.editor[].should start_with "emacs://" + end + end + + [:macvim, :mvim].each do |editor| + it "uses mvim:// scheme when set to #{editor.inspect}" do + subject.editor = editor + subject.editor[].should start_with "mvim://" + end + end + + [:sublime, :subl, :st].each do |editor| + it "uses subl:// scheme when set to #{editor.inspect}" do + subject.editor = editor + subject.editor[].should start_with "subl://" + end + end + + [:textmate, :txmt, :tm].each do |editor| + it "uses txmt:// scheme when set to #{editor.inspect}" do + subject.editor = editor + subject.editor[].should start_with "txmt://" + end + end + + ["emacsclient", "/usr/local/bin/emacsclient"].each do |editor| + it "uses emacs:// scheme when EDITOR=#{editor}" do + ENV["EDITOR"] = editor + subject.editor = subject.default_editor + subject.editor[].should start_with "emacs://" + end + end + + ["mvim -f", "/usr/local/bin/mvim -f"].each do |editor| + it "uses mvim:// scheme when EDITOR=#{editor}" do + ENV["EDITOR"] = editor + subject.editor = subject.default_editor + subject.editor[].should start_with "mvim://" + end + end + + ["subl -w", "/Applications/Sublime Text 2.app/Contents/SharedSupport/bin/subl"].each do |editor| + it "uses mvim:// scheme when EDITOR=#{editor}" do + ENV["EDITOR"] = editor + subject.editor = subject.default_editor + subject.editor[].should start_with "subl://" + end + end + + ["mate -w", "/usr/bin/mate -w"].each do |editor| + it "uses txmt:// scheme when EDITOR=#{editor}" do + ENV["EDITOR"] = editor + subject.editor = subject.default_editor + subject.editor[].should start_with "txmt://" + end + end + end +end diff --git a/lib/better_errors/spec/spec_helper.rb b/lib/better_errors/spec/spec_helper.rb new file mode 100644 index 00000000..40d63261 --- /dev/null +++ b/lib/better_errors/spec/spec_helper.rb @@ -0,0 +1,5 @@ +$: << File.expand_path("../../lib", __FILE__) + +ENV["EDITOR"] = nil + +require "better_errors" diff --git a/lib/better_errors/spec/without_binding_of_caller.rb b/lib/better_errors/spec/without_binding_of_caller.rb new file mode 100644 index 00000000..035e2e90 --- /dev/null +++ b/lib/better_errors/spec/without_binding_of_caller.rb @@ -0,0 +1,9 @@ +module Kernel + alias_method :require_with_binding_of_caller, :require + + def require(feature) + raise LoadError if feature == "binding_of_caller" + + require_with_binding_of_caller(feature) + end +end diff --git a/lib/rack-mini-profiler/.travis.yml b/lib/rack-mini-profiler/.travis.yml new file mode 100644 index 00000000..61ab0d68 --- /dev/null +++ b/lib/rack-mini-profiler/.travis.yml @@ -0,0 +1,9 @@ +language: ruby +rvm: + - 1.9.3 + - 2.0.0 + - 2.1.1 +bundler_args: "" +services: + - redis + - memcached diff --git a/lib/rack-mini-profiler/CHANGELOG b/lib/rack-mini-profiler/CHANGELOG new file mode 100644 index 00000000..7c08a89e --- /dev/null +++ b/lib/rack-mini-profiler/CHANGELOG @@ -0,0 +1,181 @@ +28-June-2012 - Sam + + * Started change log + * Corrected profiler so it properly captures POST requests (was supressing non 200s) + * Amended Rack.MiniProfiler.config[:user_provider] to use ip addres for identity + * Fixed bug where unviewed missing ids never got cleared + * Supress all '/assets/' in the rails tie (makes debugging easier) + * record_sql was mega buggy + * added MemcacheStore + +9-July-2012 - Sam + + * Cleaned up mechanism for profiling in production, all you need to do now + is call Rack::MiniProfiler.authorize_request to get profiling working in + production + * Added option to display full backtraces pp=full-backtrace + * Cleaned up railties, got rid of the post authorize callback + * Version 0.1.3 + +12-July-2012 - Sam + + * Fixed incorrect profiling steps (was not indenting or measuring start time right + * Implemented native PG and MySql2 interceptors, this gives way more accurate times + * Refactored context so its a proper class and not a hash + * Added some more client probing built in to rails + * More tests + +18-July-2012 - Sam + + * Added First Paint time for chrome + * Bug fix to ensure non Rails installs have mini profiler + * Version 0.1.7 + +30-July-2012 - Sam + + * Made compliant with ancient versions of Rack (including Rack used by Rails2) + * Fixed broken share link + * Fixed crashes on startup (in MemoryStore and FileStore) + * Version 0.1.8 + * Unicode fix + * Version 0.1.9 + +7-August-2012 - Sam + + * Added option to disable profiler for the current session (pp=disable / pp=enable) + * yajl compatability contributed by Sven Riedel + +10-August-2012 - Sam + + * Added basic prepared statement profiling for postgres + +20-August-2012 - Sam + + * 1.12.pre + * Cap X-MiniProfiler-Ids at 10, otherwise the header can get killed + +3-September-2012 - Sam + + * 1.13.pre + * pg gem prepared statements were not being logged correctly + * added setting config.backtrace_ignores = [] - an array of regexes that match on caller lines that get ignored + * added setting config.backtrace_includes = [] - an array of regexes that get included in the trace by default + * cleaned up the way client settings are stored + * made pp=full-backtrace "sticky" + * added pp=normal-backtrace to clear the "sticky" state + * change "pp=sample" to work with "caller" no need for stack trace gem + +4-September-2012 - Sam + + * 1.15.pre + * fixed annoying bug where client settings were not sticking + * fixed long standing issue with Rack::ConditionalGet stopping MiniProfiler from working properly + +5-September-2012 - Sam + + * 1.16 + * fixed long standing problem specs (issue with memory store) + * fixed issue where profiler would be dumped when you got a 404 in production (and any time rails is bypassed) + * implemented stacktrace properly + +9-September-2012 - Sam + + * 1.17 + * pp=sample was bust unless stacktrace was installed + +10-September-2012 - Sam + + * 1.19 + * fix compat issue with 1.8.7 + +12-September-2012 - Sam + + * 1.20 + * Added pp=profile-gc , it allows you to profile the GC in Ruby 1.9.3 + +17-September-2012 + * 1.21 + * New MemchacedStore + * Rails 4 support + +17-September-2012 + * Allow rack-mini-profiler to be sourced from github + * Extracted the pp=profile-gc-time out, the object space profiler needs to disable gc + +20-September-2012 + * 1.22 + * Fix permission issue in the gem + +8-April-2013 + * 1.24 + * Flame Graph Support see: http://samsaffron.com/archive/2013/03/19/flame-graphs-in-ruby-miniprofiler + * Fix file retention leak in file_store + * New toggle_shortcut and start_hidden options + * Fix for AngularJS support and MooTools + * More robust gc profiling + * Mongoid support + * Fix for html5 implicit body tags + * script tag initialized via data-attributes + * new - Rack::MiniProfiler.counter counter_name {} + * Allow usage of existing jQuery if its already loaded + * Fix pp=enable + * 1.8.7 support ... grrr + * Net:HTTP profiling + * pre authorize to run in all non development? and production? modes + +8-April-2013 + * 1.25 + * Missed flamegraph.html from build + +11-April-2013 + * 1.26 + * (minor) allow Rack::MiniProfilerRails.initialize!(Rails.application), for post config intialization + +26-June-2013 + * 1.27 + * Disable global ajax handlers on MP requests @JP + * Add Rack::MiniProfiler.config.backtrace_threshold_ms + * jQuery 2.0 support + +18-July-2013 + * 1.28 + * diagnostics in abstract storage was raising not implemented killing + ?pp=env and others + * SOLR xml unescaped by mistake + +20-August-2013 + * 1.29 + * Bugfix: SOLR patching had an incorrect monkey patch + * Implemented exception tracing using TracePoint see pp=trace-exceptions + +30-August-2013 + + * 1.30 + * Feature: Added Rack::MiniProfiler.counter_method(klass,name) for injecting counters + * Bug: Counters were not shifting the table correctly + +3-September-2013 + + * Ripped out flamegraph so it can be isolated into a gem + * Flamegraph now has much increased fidelity + * Ripped out pp=sample it just was never really used + +17-September-2013 - Ross Wilson + * Instead of supressing all "/assets/" requests we now check the configured + config.assets.prefix path since developers can rename the path to serve Asset Pipeline + files from + +12-December-2013 - Sam Saffron + * Version 0.9.0.pre (bumped up to reflect the stability of the project) + * Improved reports for pp=profile-gc + * pp=flamegraph&flamegraph_sample_rate=1 , allow you to specify sampling rates + +13-March-2014 - Sam Saffron + * Version 0.9.1 + * Added back Ruby 1.8 support (thanks Malet) + * Corrected Rails 3.0 support (thanks Zlatko) + * Corrected fix possible XSS (admin only) + * Amend Railstie so MiniProfiler can be launched with action view or action controller (Thanks Akira) + * Corrected Sql patching to avoid setting instance vars on nil which is frozen (thanks Andy, huoxito) + + diff --git a/lib/rack-mini-profiler/Gemfile b/lib/rack-mini-profiler/Gemfile new file mode 100644 index 00000000..d65e2a66 --- /dev/null +++ b/lib/rack-mini-profiler/Gemfile @@ -0,0 +1,3 @@ +source 'http://rubygems.org' + +gemspec diff --git a/lib/rack-mini-profiler/README.md b/lib/rack-mini-profiler/README.md new file mode 100644 index 00000000..550dc1e0 --- /dev/null +++ b/lib/rack-mini-profiler/README.md @@ -0,0 +1,271 @@ +# rack-mini-profiler + +[![Code Climate](https://codeclimate.com/github/MiniProfiler/rack-mini-profiler.png)](https://codeclimate.com/github/MiniProfiler/rack-mini-profiler) [![Build Status](https://travis-ci.org/MiniProfiler/rack-mini-profiler.png)](https://travis-ci.org/MiniProfiler/rack-mini-profiler) + +Middleware that displays speed badge for every html page. Designed to work both in production and in development. + +#### Features + +* database profiling. Currently supports Mysql2, Postgres, and Mongoid3 (with fallback support to ActiveRecord) + +#### Learn more + +* [Visit our community](http://community.miniprofiler.com) +* [Watch the RailsCast](http://railscasts.com/episodes/368-miniprofiler) +* [Read about Flame graphs in rack-mini-profiler](http://samsaffron.com/archive/2013/03/19/flame-graphs-in-ruby-miniprofiler) +* [Read the announcement posts from 2012](http://samsaffron.com/archive/2012/07/12/miniprofiler-ruby-edition) + +## rack-mini-profiler needs your help + +We have decided to restructure our repository so there is a central UI repo and the various language implementation have their own. + +**WE NEED HELP.** + +- Setting up a build that reuses https://github.com/MiniProfiler/ui +- Migrating the internal data structures [per the spec](https://github.com/MiniProfiler/ui) +- Cleaning up the [horrendous class structure that is using strings as keys and crazy non-objects](https://github.com/MiniProfiler/rack-mini-profiler/blob/master/lib/mini_profiler/sql_timer_struct.rb#L36-L44) + +If you feel like taking on any of this start an issue and update us on your progress. + +## Installation + +Install/add to Gemfile + +```ruby +gem 'rack-mini-profiler' +``` + +NOTE: Be sure to require rack_mini_profiler below the `pg` and `mysql` gems in your Gemfile. rack_mini_profiler will identify these gems if they are loaded to insert instrumentation. If included too early no SQL will show up. + +#### Rails + +All you have to do is include the Gem and you're good to go in development. See notes below for use in production. + +#### Rails and manual initialization + +In case you need to make sure rack_mini_profiler initialized after all other gems. +Or you want to execute some code before rack_mini_profiler required. + +```ruby +gem 'rack-mini-profiler', require: false +``` +Note the `require: false` part - if omitted, it will cause the Railtie for the mino-profiler to +be loaded outright, and an attempt to re-initialize it manually will raise an exception. + +Then put initialize code in file like `config/initializers/rack_profiler.rb` + +```ruby +if Rails.env == 'development' + require 'rack-mini-profiler' + + # initialization is skipped so trigger it + Rack::MiniProfilerRails.initialize!(Rails.application) +end +``` + +#### Rack Builder + +```ruby +require 'rack-mini-profiler' +builder = Rack::Builder.new do + use Rack::MiniProfiler + + map('/') { run get } +end +``` + +#### Sinatra + +```ruby +require 'rack-mini-profiler' +class MyApp < Sinatra::Base + use Rack::MiniProfiler +end +``` + +### Flamegraphs + +To generate [flamegraphs](http://samsaffron.com/archive/2013/03/19/flame-graphs-in-ruby-miniprofiler): + +* add the **flamegraph** gem to your Gemfile +* visit a page in your app with `?pp=flamegraph` + +Flamegraph generation is supported in MRI 2.0 and 2.1 only. + + +## Access control in production + +rack-mini-profiler is designed with production profiling in mind. To enable that just run `Rack::MiniProfiler.authorize_request` once you know a request is allowed to profile. + +```ruby +# A hook in your ApplicationController +def authorize + if current_user.is_admin? + Rack::MiniProfiler.authorize_request + end +end +``` + +## Configuration + +Various aspects of rack-mini-profiler's behavior can be configured when your app boots. +For example in a Rails app, this should be done in an initializer: +**config/initializers/mini_profiler.rb** + +### Storage + +rack-mini-profiler stores its results so they can be shared later and aren't lost at the end of the request. + +There are 4 storage options: `MemoryStore`, `RedisStore`, `MemcacheStore`, and `FileStore`. + +`FileStore` is the default in Rails environments and will write files to `tmp/miniprofiler/*`. `MemoryStore` is the default otherwise. + +```ruby +# set MemoryStore +Rack::MiniProfiler.config.storage = Rack::MiniProfiler::MemoryStore + +# set RedisStore +if Rails.env.production? + uri = URI.parse(ENV["REDIS_SERVER_URL"]) + Rack::MiniProfiler.config.storage_options = { :host => uri.host, :port => uri.port, :password => uri.password } + Rack::MiniProfiler.config.storage = Rack::MiniProfiler::RedisStore +end +``` + +MemoryStore stores results in a processes heap - something that does not work well in a multi process environment. +FileStore stores results in the file system - something that may not work well in a multi machine environment. +RedisStore/MemcacheStore work in multi process and multi machine environments (RedisStore only saves results for up to 24 hours so it won't continue to fill up Redis). + +Additionally you may implement an AbstractStore for your own provider. + +### User result segregation + +MiniProfiler will attempt to keep all user results isolated, out-of-the-box the user provider uses the ip address: + +```ruby +Rack::MiniProfiler.config.user_provider = Proc.new{|env| Rack::Request.new(env).ip} +``` + +You can override (something that is very important in a multi-machine production setup): + +```ruby +Rack::MiniProfiler.config.user_provider = Proc.new{ |env| CurrentUser.get(env) } +``` + +The string this function returns should be unique for each user on the system (for anonymous you may need to fall back to ip address) + +### Configuration Options + +You can set configuration options using the configuration accessor on `Rack::MiniProfiler`. +For example: + +```ruby +Rack::MiniProfiler.config.position = 'right' +Rack::MiniProfiler.config.start_hidden = true +``` +The available configuration options are: + +* pre_authorize_cb - A lambda callback you can set to determine whether or not mini_profiler should be visible on a given request. Default in a Rails environment is only on in development mode. If in a Rack app, the default is always on. +* position - Can either be 'right' or 'left'. Default is 'left'. +* skip_schema_queries - Whether or not you want to log the queries about the schema of your tables. Default is 'false', 'true' in rails development. +* auto_inject (default true) - when false the miniprofiler script is not injected in the page +* backtrace_filter - a regex you can use to filter out unwanted lines from the backtraces +* toggle_shortcut (default Alt+P) - a jquery.hotkeys.js-style keyboard shortcut, used to toggle the mini_profiler's visibility. See http://code.google.com/p/js-hotkeys/ for more info. +* start_hidden (default false) - Whether or not you want the mini_profiler to be visible when loading a page +* backtrace_threshold_ms (default zero) - Minimum SQL query elapsed time before a backtrace is recorded. Backtrace recording can take a couple of milliseconds on rubies earlier than 2.0, impacting performance for very small queries. +* flamegraph_sample_rate (default 0.5ms) - How often fast_stack should get stack trace info to generate flamegraphs + +### Custom middleware ordering (required if using `Rack::Deflate` with Rails) + +If you are using `Rack::Deflate` with rails and rack-mini-profiler in its default configuration, +`Rack::MiniProfiler` will be injected (as always) at position 0 in the middleware stack. This +will result in it attempting to inject html into the already-compressed response body. To fix this, +the middleware ordering must be overriden. + +To do this, first add `, require: false` to the gemfile entry for rack-mini-profiler. +This will prevent the railtie from running. Then, customize the initialization +in the initializer like so: + +```ruby +require 'rack-mini-profiler' + +Rack::MiniProfilerRails.initialize!(Rails.application) + +Rails.application.middleware.delete(Rack::MiniProfiler) +Rails.application.middleware.insert_after(Rack::Deflater, Rack::MiniProfiler) +``` + +Deleting the middleware and then reinserting it is a bit inelegant, but +a sufficient and costless solution. It is possible that rack-mini-profiler might +support this scenario more directly if it is found that +there is significant need for this confriguration or that +the above recipe causes problems. + + +## Special query strings + +If you include the query string `pp=help` at the end of your request you will see the various options available. You can use these options to extend or contract the amount of diagnostics rack-mini-profiler gathers. + + +## Rails 2.X support + +To get MiniProfiler working with Rails 2.3.X you need to do the initialization manually as well as monkey patch away an incompatibility between activesupport and json_pure. + +Add the following code to your environment.rb (or just in a specific environment such as development.rb) for initialization and configuration of MiniProfiler. + +```ruby +# configure and initialize MiniProfiler +require 'rack-mini-profiler' +c = ::Rack::MiniProfiler.config +c.pre_authorize_cb = lambda { |env| + Rails.env.development? || Rails.env.production? +} +tmp = Rails.root.to_s + "/tmp/miniprofiler" +FileUtils.mkdir_p(tmp) unless File.exists?(tmp) +c.storage_options = {:path => tmp} +c.storage = ::Rack::MiniProfiler::FileStore +config.middleware.use(::Rack::MiniProfiler) +::Rack::MiniProfiler.profile_method(ActionController::Base, :process) {|action| "Executing action: #{action}"} +::Rack::MiniProfiler.profile_method(ActionView::Template, :render) {|x,y| "Rendering: #{path_without_format_and_extension}"} + +# monkey patch away an activesupport and json_pure incompatability +# http://pivotallabs.com/users/alex/blog/articles/1332-monkey-patch-of-the-day-activesupport-vs-json-pure-vs-ruby-1-8 +if JSON.const_defined?(:Pure) + class JSON::Pure::Generator::State + include ActiveSupport::CoreExtensions::Hash::Except + end +end +``` + +## Running the Specs + +``` +$ rake build +$ rake spec +``` + +Additionally you can also run `autotest` if you like. + +## Licence + +The MIT License (MIT) + +Copyright (c) 2013 Sam Saffron + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/lib/rack-mini-profiler/Rakefile b/lib/rack-mini-profiler/Rakefile new file mode 100644 index 00000000..a908b5b6 --- /dev/null +++ b/lib/rack-mini-profiler/Rakefile @@ -0,0 +1,46 @@ +# Rakefile +require 'rubygems' +require 'bundler' +Bundler.setup(:default, :test) + +task :default => [:spec] + +require 'rspec/core' +require 'rspec/core/rake_task' +RSpec::Core::RakeTask.new(:spec) do |spec| + spec.pattern = FileList['spec/**/*_spec.rb'] +end + +desc "builds a gem" +task :build => :update_asset_version do + `gem build rack-mini-profiler.gemspec 1>&2` +end + +desc "compile less" +task :compile_less => :copy_files do + `lessc lib/html/includes.less > lib/html/includes.css` +end + +desc "update asset version file" +task :update_asset_version => :compile_less do + require 'digest/md5' + h = [] + Dir.glob('lib/html/*.{js,html,css,tmpl}').each do |f| + h << Digest::MD5.hexdigest(::File.read(f)) + end + File.open('lib/mini_profiler/version.rb','w') do |f| + f.write \ +"module Rack + class MiniProfiler + VERSION = '#{Digest::MD5.hexdigest(h.sort.join(''))}'.freeze + end +end" + end +end + + +desc "copy files from other parts of the tree" +task :copy_files do + # TODO grab files from MiniProfiler/UI +end + diff --git a/lib/rack-mini-profiler/autotest/discover.rb b/lib/rack-mini-profiler/autotest/discover.rb new file mode 100644 index 00000000..de32a012 --- /dev/null +++ b/lib/rack-mini-profiler/autotest/discover.rb @@ -0,0 +1,2 @@ +Autotest.add_discovery { "rspec2" } + diff --git a/lib/rack-mini-profiler/lib/html/includes.css b/lib/rack-mini-profiler/lib/html/includes.css new file mode 100644 index 00000000..d96528b8 --- /dev/null +++ b/lib/rack-mini-profiler/lib/html/includes.css @@ -0,0 +1,451 @@ +.profiler-result, +.profiler-queries { + color: #555; + line-height: 1; + font-size: 12px; +} +.profiler-result pre, +.profiler-queries pre, +.profiler-result code, +.profiler-queries code, +.profiler-result label, +.profiler-queries label, +.profiler-result table, +.profiler-queries table, +.profiler-result tbody, +.profiler-queries tbody, +.profiler-result thead, +.profiler-queries thead, +.profiler-result tfoot, +.profiler-queries tfoot, +.profiler-result tr, +.profiler-queries tr, +.profiler-result th, +.profiler-queries th, +.profiler-result td, +.profiler-queries td { + margin: 0; + padding: 0; + border: 0; + font-size: 100%; + font: inherit; + vertical-align: baseline; + background-color: transparent; + overflow: visible; + max-height: none; +} +.profiler-result table, +.profiler-queries table { + border-collapse: collapse; + border-spacing: 0; +} +.profiler-result a, +.profiler-queries a, +.profiler-result a:hover, +.profiler-queries a:hover { + cursor: pointer; + color: #0077cc; +} +.profiler-result a, +.profiler-queries a { + text-decoration: none; +} +.profiler-result a:hover, +.profiler-queries a:hover { + text-decoration: underline; +} +.profiler-result { + font-family: Helvetica, Arial, sans-serif; +} +.profiler-result .profiler-toggle-duration-with-children { + float: right; +} +.profiler-result table.profiler-client-timings { + margin-top: 10px; +} +.profiler-result .profiler-label { + color: #555555; + overflow: hidden; + text-overflow: ellipsis; +} +.profiler-result .profiler-unit { + color: #aaaaaa; +} +.profiler-result .profiler-trivial { + display: none; +} +.profiler-result .profiler-trivial td, +.profiler-result .profiler-trivial td * { + color: #aaaaaa !important; +} +.profiler-result pre, +.profiler-result code, +.profiler-result .profiler-number, +.profiler-result .profiler-unit { + font-family: Consolas, monospace, serif; +} +.profiler-result .profiler-number { + color: #111111; +} +.profiler-result .profiler-info { + text-align: right; +} +.profiler-result .profiler-info .profiler-name { + float: left; +} +.profiler-result .profiler-info .profiler-server-time { + white-space: nowrap; +} +.profiler-result .profiler-timings th { + background-color: #fff; + color: #aaaaaa; + text-align: right; +} +.profiler-result .profiler-timings th, +.profiler-result .profiler-timings td { + white-space: nowrap; +} +.profiler-result .profiler-timings .profiler-duration-with-children { + display: none; +} +.profiler-result .profiler-timings .profiler-duration { + font-family: Consolas, monospace, serif; + color: #111111; + text-align: right; +} +.profiler-result .profiler-timings .profiler-indent { + letter-spacing: 4px; +} +.profiler-result .profiler-timings .profiler-queries-show .profiler-number, +.profiler-result .profiler-timings .profiler-queries-show .profiler-unit { + color: #0077cc; +} +.profiler-result .profiler-timings .profiler-queries-duration { + padding-left: 6px; +} +.profiler-result .profiler-timings .profiler-percent-in-sql { + white-space: nowrap; + text-align: right; +} +.profiler-result .profiler-timings tfoot td { + padding-top: 10px; + text-align: right; +} +.profiler-result .profiler-timings tfoot td a { + font-size: 95%; + display: inline-block; + margin-left: 12px; +} +.profiler-result .profiler-timings tfoot td a:first-child { + float: left; + margin-left: 0px; +} +.profiler-result .profiler-timings tfoot td a.profiler-custom-link { + float: left; +} +.profiler-result .profiler-queries { + font-family: Helvetica, Arial, sans-serif; +} +.profiler-result .profiler-queries .profiler-stack-trace { + margin-bottom: 15px; +} +.profiler-result .profiler-queries pre { + font-family: Consolas, monospace, serif; + white-space: pre-wrap; +} +.profiler-result .profiler-queries th { + background-color: #fff; + border-bottom: 1px solid #555; + font-weight: bold; + padding: 15px; + white-space: nowrap; +} +.profiler-result .profiler-queries td { + padding: 15px; + text-align: left; + background-color: #fff; +} +.profiler-result .profiler-queries td:last-child { + padding-right: 25px; +} +.profiler-result .profiler-queries .profiler-odd td { + background-color: #e5e5e5; +} +.profiler-result .profiler-queries .profiler-since-start, +.profiler-result .profiler-queries .profiler-duration { + text-align: right; +} +.profiler-result .profiler-queries .profiler-info div { + text-align: right; + margin-bottom: 5px; +} +.profiler-result .profiler-queries .profiler-gap-info, +.profiler-result .profiler-queries .profiler-gap-info td { + background-color: #ccc; +} +.profiler-result .profiler-queries .profiler-gap-info .profiler-unit { + color: #777; +} +.profiler-result .profiler-queries .profiler-gap-info .profiler-info { + text-align: right; +} +.profiler-result .profiler-queries .profiler-gap-info.profiler-trivial-gaps { + display: none; +} +.profiler-result .profiler-queries .profiler-trivial-gap-container { + text-align: center; +} +.profiler-result .profiler-queries .str { + color: #800000; +} +.profiler-result .profiler-queries .kwd { + color: #00008b; +} +.profiler-result .profiler-queries .com { + color: #808080; +} +.profiler-result .profiler-queries .typ { + color: #2b91af; +} +.profiler-result .profiler-queries .lit { + color: #800000; +} +.profiler-result .profiler-queries .pun { + color: #000000; +} +.profiler-result .profiler-queries .pln { + color: #000000; +} +.profiler-result .profiler-queries .tag { + color: #800000; +} +.profiler-result .profiler-queries .atn { + color: #ff0000; +} +.profiler-result .profiler-queries .atv { + color: #0000ff; +} +.profiler-result .profiler-queries .dec { + color: #800080; +} +.profiler-result .profiler-warning, +.profiler-result .profiler-warning *, +.profiler-result .profiler-warning .profiler-queries-show, +.profiler-result .profiler-warning .profiler-queries-show .profiler-unit { + color: #f00; +} +.profiler-result .profiler-warning:hover, +.profiler-result .profiler-warning *:hover, +.profiler-result .profiler-warning .profiler-queries-show:hover, +.profiler-result .profiler-warning .profiler-queries-show .profiler-unit:hover { + color: #f00; +} +.profiler-result .profiler-nuclear { + color: #f00; + font-weight: bold; + padding-right: 2px; +} +.profiler-result .profiler-nuclear:hover { + color: #f00; +} +.profiler-results { + z-index: 2147483643; + position: fixed; + top: 0px; +} +.profiler-results.profiler-left { + left: 0px; +} +.profiler-results.profiler-left.profiler-no-controls .profiler-result:last-child .profiler-button, +.profiler-results.profiler-left .profiler-controls { + -webkit-border-bottom-right-radius: 10px; + -moz-border-radius-bottomright: 10px; + border-bottom-right-radius: 10px; +} +.profiler-results.profiler-left .profiler-button, +.profiler-results.profiler-left .profiler-controls { + border-right: 1px solid #888888; +} +.profiler-results.profiler-right { + right: 0px; +} +.profiler-results.profiler-right.profiler-no-controls .profiler-result:last-child .profiler-button, +.profiler-results.profiler-right .profiler-controls { + -webkit-border-bottom-left-radius: 10px; + -moz-border-radius-bottomleft: 10px; + border-bottom-left-radius: 10px; +} +.profiler-results.profiler-right .profiler-button, +.profiler-results.profiler-right .profiler-controls { + border-left: 1px solid #888888; +} +.profiler-results .profiler-button, +.profiler-results .profiler-controls { + display: none; + z-index: 2147483640; + border-bottom: 1px solid #888888; + background-color: #fff; + padding: 4px 7px; + text-align: right; + cursor: pointer; +} +.profiler-results .profiler-button.profiler-button-active, +.profiler-results .profiler-controls.profiler-button-active { + background-color: maroon; +} +.profiler-results .profiler-button.profiler-button-active .profiler-number, +.profiler-results .profiler-controls.profiler-button-active .profiler-number, +.profiler-results .profiler-button.profiler-button-active .profiler-nuclear, +.profiler-results .profiler-controls.profiler-button-active .profiler-nuclear { + color: #fff; + font-weight: bold; +} +.profiler-results .profiler-button.profiler-button-active .profiler-unit, +.profiler-results .profiler-controls.profiler-button-active .profiler-unit { + color: #fff; + font-weight: normal; +} +.profiler-results .profiler-controls { + display: block; + font-size: 12px; + font-family: Consolas, monospace, serif; + cursor: default; + text-align: center; +} +.profiler-results .profiler-controls span { + border-right: 1px solid #aaaaaa; + padding-right: 5px; + margin-right: 5px; + cursor: pointer; +} +.profiler-results .profiler-controls span:last-child { + border-right: none; +} +.profiler-results .profiler-popup { + display: none; + z-index: 2147483641; + position: absolute; + background-color: #fff; + border: 1px solid #aaa; + padding: 5px 10px; + text-align: left; + line-height: 18px; + overflow: auto; + -moz-box-shadow: 0px 1px 15px #555555; + -webkit-box-shadow: 0px 1px 15px #555555; + box-shadow: 0px 1px 15px #555555; +} +.profiler-results .profiler-popup .profiler-info { + margin-bottom: 3px; + padding-bottom: 2px; + border-bottom: 1px solid #ddd; +} +.profiler-results .profiler-popup .profiler-info .profiler-name { + font-size: 110%; + font-weight: bold; +} +.profiler-results .profiler-popup .profiler-info .profiler-name .profiler-overall-duration { + display: none; +} +.profiler-results .profiler-popup .profiler-info .profiler-server-time { + font-size: 95%; +} +.profiler-results .profiler-popup .profiler-timings th, +.profiler-results .profiler-popup .profiler-timings td { + padding-left: 6px; + padding-right: 6px; +} +.profiler-results .profiler-popup .profiler-timings th { + font-size: 95%; + padding-bottom: 3px; +} +.profiler-results .profiler-popup .profiler-timings .profiler-label { + max-width: 275px; +} +.profiler-results .profiler-queries { + display: none; + z-index: 2147483643; + position: absolute; + overflow-y: auto; + overflow-x: auto; + background-color: #fff; +} +.profiler-results .profiler-queries th { + font-size: 17px; +} +.profiler-results.profiler-min .profiler-result { + display: none; +} +.profiler-results.profiler-min .profiler-controls span { + display: none; +} +.profiler-results.profiler-min .profiler-controls .profiler-min-max { + border-right: none; + padding: 0px; + margin: 0px; +} +.profiler-queries-bg { + z-index: 2147483642; + display: none; + background: #000; + opacity: 0.7; + position: absolute; + top: 0px; + left: 0px; + min-width: 100%; +} +.profiler-result-full .profiler-result { + width: 950px; + margin: 30px auto; +} +.profiler-result-full .profiler-result .profiler-button { + display: none; +} +.profiler-result-full .profiler-result .profiler-popup .profiler-info { + font-size: 25px; + border-bottom: 1px solid #aaaaaa; + padding-bottom: 3px; + margin-bottom: 25px; +} +.profiler-result-full .profiler-result .profiler-popup .profiler-info .profiler-overall-duration { + padding-right: 20px; + font-size: 80%; + color: #888; +} +.profiler-result-full .profiler-result .profiler-popup .profiler-timings td, +.profiler-result-full .profiler-result .profiler-popup .profiler-timings th { + padding-left: 8px; + padding-right: 8px; +} +.profiler-result-full .profiler-result .profiler-popup .profiler-timings th { + padding-bottom: 7px; +} +.profiler-result-full .profiler-result .profiler-popup .profiler-timings td { + font-size: 14px; + padding-bottom: 4px; +} +.profiler-result-full .profiler-result .profiler-popup .profiler-timings td:first-child { + padding-left: 10px; +} +.profiler-result-full .profiler-result .profiler-popup .profiler-timings .profiler-label { + max-width: 550px; +} +.profiler-result-full .profiler-result .profiler-queries { + margin: 25px 0; +} +.profiler-result-full .profiler-result .profiler-queries table { + width: 100%; +} +.profiler-result-full .profiler-result .profiler-queries th { + font-size: 16px; + color: #555; + line-height: 20px; +} +.profiler-result-full .profiler-result .profiler-queries td { + padding: 15px 10px; + text-align: left; +} +.profiler-result-full .profiler-result .profiler-queries .profiler-info div { + text-align: right; + margin-bottom: 5px; +} diff --git a/lib/rack-mini-profiler/lib/html/includes.js b/lib/rack-mini-profiler/lib/html/includes.js new file mode 100644 index 00000000..d8b4de08 --- /dev/null +++ b/lib/rack-mini-profiler/lib/html/includes.js @@ -0,0 +1,960 @@ +"use strict"; +var MiniProfiler = (function () { + var $; + + var options, + container, + controls, + fetchedIds = [], + fetchingIds = [], // so we never pull down a profiler twice + ajaxStartTime + ; + + var hasLocalStorage = function () { + try { + return 'localStorage' in window && window['localStorage'] !== null; + } catch (e) { + return false; + } + }; + + var getVersionedKey = function (keyPrefix) { + return keyPrefix + '-' + options.version; + }; + + var save = function (keyPrefix, value) { + if (!hasLocalStorage()) { return; } + + // clear old keys with this prefix, if any + for (var i = 0; i < localStorage.length; i++) { + if ((localStorage.key(i) || '').indexOf(keyPrefix) > -1) { + localStorage.removeItem(localStorage.key(i)); + } + } + + // save under this version + localStorage[getVersionedKey(keyPrefix)] = value; + }; + + var load = function (keyPrefix) { + if (!hasLocalStorage()) { return null; } + + return localStorage[getVersionedKey(keyPrefix)]; + }; + + var fetchTemplates = function (success) { + var key = 'templates', + cached = load(key); + + if (cached) { + $('body').append(cached); + success(); + } + else { + $.get(options.path + 'includes.tmpl?v=' + options.version, function (data) { + if (data) { + save(key, data); + $('body').append(data); + success(); + } + }); + } + }; + + var getClientPerformance = function() { + return window.performance == null ? null : window.performance; + }; + + var fetchResults = function (ids) { + var clientPerformance, clientProbes, i, j, p, id, idx; + + for (i = 0; i < ids.length; i++) { + id = ids[i]; + + clientPerformance = null; + clientProbes = null; + + if (window.mPt) { + clientProbes = mPt.results(); + for (j = 0; j < clientProbes.length; j++) { + clientProbes[j].d = clientProbes[j].d.getTime(); + } + mPt.flush(); + } + + if (id == options.currentId) { + + clientPerformance = getClientPerformance(); + + if (clientPerformance != null) { + // ie is buggy strip out functions + var copy = { navigation: {}, timing: {} }; + + var timing = $.extend({}, clientPerformance.timing); + + for (p in timing) { + if (timing.hasOwnProperty(p) && !$.isFunction(timing[p])) { + copy.timing[p] = timing[p]; + } + } + if (clientPerformance.navigation) { + copy.navigation.redirectCount = clientPerformance.navigation.redirectCount; + } + clientPerformance = copy; + + // hack to add chrome timings + if (window.chrome && window.chrome.loadTimes) { + var chromeTimes = window.chrome.loadTimes(); + if (chromeTimes.firstPaintTime) { + clientPerformance.timing["First Paint Time"] = Math.round(chromeTimes.firstPaintTime * 1000); + } + if (chromeTimes.firstPaintTime) { + clientPerformance.timing["First Paint After Load Time"] = Math.round(chromeTimes.firstPaintAfterLoadTime * 1000); + } + + } + } + } else if (ajaxStartTime != null && clientProbes && clientProbes.length > 0) { + clientPerformance = { timing: { navigationStart: ajaxStartTime.getTime() } }; + ajaxStartTime = null; + } + + if ($.inArray(id, fetchedIds) < 0 && $.inArray(id, fetchingIds) < 0) { + idx = fetchingIds.push(id) - 1; + + $.ajax({ + url: options.path + 'results', + data: { id: id, clientPerformance: clientPerformance, clientProbes: clientProbes, popup: 1 }, + dataType: 'json', + global: false, + type: 'POST', + success: function (json) { + fetchedIds.push(id); + if (json != "hidden") { + buttonShow(json); + } + }, + complete: function () { + fetchingIds.splice(idx, 1); + } + }); + } + } + }; + + var renderTemplate = function (json) { + return $('#profilerTemplate').tmpl(json); + }; + + var buttonShow = function (json) { + var result = renderTemplate(json); + + if (controls) + result.insertBefore(controls); + else + result.appendTo(container); + + var button = result.find('.profiler-button'), + popup = result.find('.profiler-popup'); + + // button will appear in corner with the total profiling duration - click to show details + button.click(function () { buttonClick(button, popup); }); + + // small duration steps and the column with aggregate durations are hidden by default; allow toggling + toggleHidden(popup); + + // lightbox in the queries + popup.find('.profiler-queries-show').click(function () { queriesShow($(this), result); }); + + // limit count + if (container.find('.profiler-result').length > options.maxTracesToShow) + container.find('.profiler-result').first().remove(); + button.show(); + }; + + var toggleHidden = function (popup) { + var trivial = popup.find('.profiler-toggle-trivial'); + var childrenTime = popup.find('.profiler-toggle-duration-with-children'); + var trivialGaps = popup.parent().find('.profiler-toggle-trivial-gaps'); + + var toggleIt = function (node) { + var link = $(node), + klass = "profiler-" + link.attr('class').substr('profiler-toggle-'.length), + isHidden = link.text().indexOf('show') > -1; + + popup.parent().find('.' + klass).toggle(isHidden); + link.text(link.text().replace(isHidden ? 'show' : 'hide', isHidden ? 'hide' : 'show')); + + popupPreventHorizontalScroll(popup); + }; + + childrenTime.add(trivial).add(trivialGaps).click(function () { + toggleIt(this); + }); + + // if option is set or all our timings are trivial, go ahead and show them + if (options.showTrivial || trivial.data('show-on-load')) { + toggleIt(trivial); + } + // if option is set, go ahead and show time with children + if (options.showChildrenTime) { + toggleIt(childrenTime); + } + }; + + var buttonClick = function (button, popup) { + // we're toggling this button/popup + if (popup.is(':visible')) { + popupHide(button, popup); + } + else { + var visiblePopups = container.find('.profiler-popup:visible'), + theirButtons = visiblePopups.siblings('.profiler-button'); + + // hide any other popups + popupHide(theirButtons, visiblePopups); + + // before showing the one we clicked + popupShow(button, popup); + } + }; + + var popupShow = function (button, popup) { + button.addClass('profiler-button-active'); + + popupSetDimensions(button, popup); + + popup.show(); + + popupPreventHorizontalScroll(popup); + }; + + var popupSetDimensions = function (button, popup) { + var top = button.position().top - 1, // position next to the button we clicked + windowHeight = $(window).height(), + maxHeight = windowHeight - top - 40; // make sure the popup doesn't extend below the fold + + popup + .css({ 'top': top, 'max-height': maxHeight }) + .css(options.renderPosition, button.outerWidth() - 3); // move left or right, based on config + }; + + var popupPreventHorizontalScroll = function (popup) { + var childrenHeight = 0; + + popup.children().each(function () { childrenHeight += $(this).height(); }); + + popup.css({ 'padding-right': childrenHeight > popup.height() ? 40 : 10 }); + }; + + var popupHide = function (button, popup) { + button.removeClass('profiler-button-active'); + popup.hide(); + }; + + var queriesShow = function (link, result) { + + var px = 30, + win = $(window), + width = win.width() - 2 * px, + height = win.height() - 2 * px, + queries = result.find('.profiler-queries'); + + // opaque background + $('
').appendTo('body').css({ 'height': $(document).height() }).show(); + + // center the queries and ensure long content is scrolled + queries.css({ 'top': px, 'max-height': height, 'width': width }).css(options.renderPosition, px) + .find('table').css({ 'width': width }); + + // have to show everything before we can get a position for the first query + queries.show(); + + queriesScrollIntoView(link, queries, queries); + + // syntax highlighting + prettyPrint(); + }; + + var queriesScrollIntoView = function (link, queries, whatToScroll) { + var id = link.closest('tr').attr('data-timing-id'), + cells = queries.find('tr[data-timing-id="' + id + '"] td'); + + // ensure they're in view + whatToScroll.scrollTop(whatToScroll.scrollTop() + cells.first().position().top - 100); + + // highlight and then fade back to original bg color; do it ourselves to prevent any conflicts w/ jquery.UI or other implementations of Resig's color plugin + cells.each(function () { + var cell = $(this), + highlightHex = '#FFFFBB', + highlightRgb = getRGB(highlightHex), + originalRgb = getRGB(cell.css('background-color')), + getColorDiff = function (fx, i) { + // adapted from John Resig's color plugin: http://plugins.jquery.com/project/color + return Math.max(Math.min(parseInt((fx.pos * (originalRgb[i] - highlightRgb[i])) + highlightRgb[i], 10), 255), 0); + }; + + // we need to animate some other property to piggy-back on the step function, so I choose you, opacity! + cell.css({ 'opacity': 1, 'background-color': highlightHex }) + .animate({ 'opacity': 1 }, { duration: 2000, step: function (now, fx) { + fx.elem.style.backgroundColor = "rgb(" + [getColorDiff(fx, 0), getColorDiff(fx, 1), getColorDiff(fx, 2)].join(",") + ")"; + } + }); + }); + }; + + // Color Conversion functions from highlightFade + // By Blair Mitchelmore + // http://jquery.offput.ca/highlightFade/ + // Parse strings looking for color tuples [255,255,255] + var getRGB = function (color) { + var result; + + // Check if we're already dealing with an array of colors + if (color && color.constructor == Array && color.length == 3) return color; + + // Look for rgb(num,num,num) + if (result = /rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(color)) return [parseInt(result[1]), parseInt(result[2]), parseInt(result[3])]; + + // Look for rgb(num%,num%,num%) + if (result = /rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(color)) return [parseFloat(result[1]) * 2.55, parseFloat(result[2]) * 2.55, parseFloat(result[3]) * 2.55]; + + // Look for #a0b1c2 + if (result = /#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(color)) return [parseInt(result[1], 16), parseInt(result[2], 16), parseInt(result[3], 16)]; + + // Look for #fff + if (result = /#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(color)) return [parseInt(result[1] + result[1], 16), parseInt(result[2] + result[2], 16), parseInt(result[3] + result[3], 16)]; + + // Look for rgba(0, 0, 0, 0) == transparent in Safari 3 + if (result = /rgba\(0, 0, 0, 0\)/.exec(color)) return colors['transparent']; + + return null; + }; + + var bindDocumentEvents = function () { + $(document).bind('click keyup', function (e) { + + // this happens on every keystroke, and :visible is crazy expensive in IE <9 + // and in this case, the display:none check is sufficient. + var popup = $('.profiler-popup').filter(function () { return $(this).css("display") !== "none"; }); + + if (!popup.length) { + return; + } + + var button = popup.siblings('.profiler-button'), + queries = popup.closest('.profiler-result').find('.profiler-queries'), + bg = $('.profiler-queries-bg'), + isEscPress = e.type == 'keyup' && e.which == 27, + hidePopup = false, + hideQueries = false; + + if (bg.is(':visible')) { + hideQueries = isEscPress || (e.type == 'click' && !$.contains(queries[0], e.target) && !$.contains(popup[0], e.target)); + } + else if (popup.is(':visible')) { + hidePopup = isEscPress || (e.type == 'click' && !$.contains(popup[0], e.target) && !$.contains(button[0], e.target) && button[0] != e.target); + } + + if (hideQueries) { + bg.remove(); + queries.hide(); + } + + if (hidePopup) { + popupHide(button, popup); + } + }); + $(document).bind('keydown', options.toggleShortcut, function(e) { + $('.profiler-results').toggle(); + }); + }; + + var initFullView = function () { + + // first, get jquery tmpl, then render and bind handlers + fetchTemplates(function () { + + // profiler will be defined in the full page's head + renderTemplate(profiler).appendTo(container); + + var popup = $('.profiler-popup'); + + toggleHidden(popup); + + prettyPrint(); + + // since queries are already shown, just highlight and scroll when clicking a "1 sql" link + popup.find('.profiler-queries-show').click(function () { + queriesScrollIntoView($(this), $('.profiler-queries'), $(document)); + }); + }); + }; + + var initControls = function (container) { + if (options.showControls) { + controls = $('
mc
').appendTo(container); + + $('.profiler-controls .profiler-min-max').click(function () { + container.toggleClass('profiler-min'); + }); + + container.hover(function () { + if ($(this).hasClass('profiler-min')) { + $(this).find('.profiler-min-max').show(); + } + }, + function () { + if ($(this).hasClass('profiler-min')) { + $(this).find('.profiler-min-max').hide(); + } + }); + + $('.profiler-controls .profiler-clear').click(function () { + container.find('.profiler-result').remove(); + }); + } + else { + container.addClass('profiler-no-controls'); + } + }; + + var initPopupView = function () { + + if (options.authorized) { + // all fetched profilings will go in here + container = $('
').appendTo('body'); + + // MiniProfiler.RenderIncludes() sets which corner to render in - default is upper left + container.addClass("profiler-" + options.renderPosition); + + //initialize the controls + initControls(container); + + // we'll render results json via a jquery.tmpl - after we get the templates, we'll fetch the initial json to populate it + fetchTemplates(function () { + // get master page profiler results + fetchResults(options.ids); + }); + if (options.startHidden) container.hide(); + } + else { + fetchResults(options.ids); + } + + var jQueryAjaxComplete = function (e, xhr, settings) { + if (xhr) { + // should be an array of strings, e.g. ["008c4813-9bd7-443d-9376-9441ec4d6a8c","16ff377b-8b9c-4c20-a7b5-97cd9fa7eea7"] + var stringIds = xhr.getResponseHeader('X-MiniProfiler-Ids'); + if (stringIds) { + var ids = typeof JSON != 'undefined' ? JSON.parse(stringIds) : eval(stringIds); + fetchResults(ids); + } + } + }; + + // fetch profile results for any ajax calls + // note, this does not use $ cause we want to hook into the main jQuery + if (jQuery && jQuery(document) && jQuery(document).ajaxComplete) { + jQuery(document).ajaxComplete(jQueryAjaxComplete); + } + + if (jQuery && jQuery(document).ajaxStart) + jQuery(document).ajaxStart(function () { ajaxStartTime = new Date(); }); + + // fetch results after ASP Ajax calls + if (typeof (Sys) != 'undefined' && typeof (Sys.WebForms) != 'undefined' && typeof (Sys.WebForms.PageRequestManager) != 'undefined') { + // Get the instance of PageRequestManager. + var PageRequestManager = Sys.WebForms.PageRequestManager.getInstance(); + + PageRequestManager.add_endRequest(function (sender, args) { + if (args) { + var response = args.get_response(); + if (response.get_responseAvailable() && response._xmlHttpRequest != null) { + var stringIds = args.get_response().getResponseHeader('X-MiniProfiler-Ids'); + if (stringIds) { + var ids = typeof JSON != 'undefined' ? JSON.parse(stringIds) : eval(stringIds); + fetchResults(ids); + } + } + } + }); + } + + // more Asp.Net callbacks + if (typeof (WebForm_ExecuteCallback) == "function") { + WebForm_ExecuteCallback = (function (callbackObject) { + // Store original function + var original = WebForm_ExecuteCallback; + + return function (callbackObject) { + original(callbackObject); + + var stringIds = callbackObject.xmlRequest.getResponseHeader('X-MiniProfiler-Ids'); + if (stringIds) { + var ids = typeof JSON != 'undefined' ? JSON.parse(stringIds) : eval(stringIds); + fetchResults(ids); + } + }; + + })(); + } + + // also fetch results after ExtJS requests, in case it is being used + if (typeof (Ext) != 'undefined' && typeof (Ext.Ajax) != 'undefined' && typeof (Ext.Ajax.on) != 'undefined') { + // Ext.Ajax is a singleton, so we just have to attach to its 'requestcomplete' event + Ext.Ajax.on('requestcomplete', function (e, xhr, settings) { + //iframed file uploads don't have headers + if (!xhr || !xhr.getResponseHeader) { + return; + } + + var stringIds = xhr.getResponseHeader('X-MiniProfiler-Ids'); + if (stringIds) { + var ids = typeof JSON != 'undefined' ? JSON.parse(stringIds) : eval(stringIds); + fetchResults(ids); + } + }); + } + + if (typeof (MooTools) != 'undefined' && typeof (Request) != 'undefined') { + Request.prototype.addEvents({ + onComplete: function() { + var stringIds = this.xhr.getResponseHeader('X-MiniProfiler-Ids'); + if (stringIds) { + var ids = typeof JSON != 'undefined' ? JSON.parse(stringIds) : eval(stringIds); + fetchResults(ids); + } + } + }); + } + + // add support for AngularJS, which use the basic XMLHttpRequest object. + if (window.angular && typeof (XMLHttpRequest) != 'undefined') { + var _send = XMLHttpRequest.prototype.send; + + XMLHttpRequest.prototype.send = function sendReplacement(data) { + this._onreadystatechange = this.onreadystatechange; + + this.onreadystatechange = function onReadyStateChangeReplacement() { + if (this.readyState == 4) { + var stringIds = this.getResponseHeader('X-MiniProfiler-Ids'); + if (stringIds) { + var ids = typeof JSON != 'undefined' ? JSON.parse(stringIds) : eval(stringIds); + fetchResults(ids); + } + } + + return this._onreadystatechange.apply(this, arguments); + }; + + return _send.apply(this, arguments); + }; + } + + // some elements want to be hidden on certain doc events + bindDocumentEvents(); + }; + + return { + + init: function () { + var script = document.getElementById('mini-profiler'); + if (!script || !script.getAttribute) return; + + options = (function () { + var version = script.getAttribute('data-version'); + var path = script.getAttribute('data-path'); + + var currentId = script.getAttribute('data-current-id'); + + var ids = script.getAttribute('data-ids'); + if (ids) ids = ids.split(','); + + var position = script.getAttribute('data-position'); + + var toggleShortcut = script.getAttribute('data-toggle-shortcut'); + + if (script.getAttribute('data-max-traces')) { + var maxTraces = parseInt(script.getAttribute('data-max-traces'), 10); + } + + if (script.getAttribute('data-trivial') === 'true') var trivial = true; + if (script.getAttribute('data-children') == 'true') var children = true; + if (script.getAttribute('data-controls') == 'true') var controls = true; + if (script.getAttribute('data-authorized') == 'true') var authorized = true; + if (script.getAttribute('data-start-hidden') == 'true') var startHidden = true; + + return { + ids: ids, + path: path, + version: version, + renderPosition: position, + showTrivial: trivial, + showChildrenTime: children, + maxTracesToShow: maxTraces, + showControls: controls, + currentId: currentId, + authorized: authorized, + toggleShortcut: toggleShortcut, + startHidden: startHidden + }; + })(); + + var doInit = function () { + // when rendering a shared, full page, this div will exist + container = $('.profiler-result-full'); + if (container.length) { + if (window.location.href.indexOf("&trivial=1") > 0) { + options.showTrivial = true; + } + initFullView(); + } + else { + initPopupView(); + } + }; + + // this preserves debugging + var load = function (s, f) { + var sc = document.createElement("script"); + sc.async = "async"; + sc.type = "text/javascript"; + sc.src = s; + var done = false; + sc.onload = sc.onreadystatechange = function (_, abort) { + if (!sc.readyState || /loaded|complete/.test(sc.readyState)) { + if (!abort && !done) { done = true; f(); } + } + }; + document.getElementsByTagName('head')[0].appendChild(sc); + }; + + var wait = 0; + var finish = false; + var deferInit = function() { + if (finish) return; + if (window.performance && window.performance.timing && window.performance.timing.loadEventEnd === 0 && wait < 10000) { + setTimeout(deferInit, 100); + wait += 100; + } else { + finish = true; + init(); + } + }; + + var init = function() { + if (options.authorized) { + var url = options.path + "includes.css?v=" + options.version; + if (document.createStyleSheet) { + document.createStyleSheet(url); + } else { + $('head').append($('')); + } + if (!$.tmpl) { + load(options.path + 'jquery.tmpl.js?v=' + options.version, doInit); + } else { + doInit(); + } + } else { + doInit(); + } + + // jquery.hotkeys.js + // https://github.com/jeresig/jquery.hotkeys/blob/master/jquery.hotkeys.js + + (function(d){function h(g){if("string"===typeof g.data){var h=g.handler,j=g.data.toLowerCase().split(" ");g.handler=function(b){if(!(this!==b.target&&(/textarea|select/i.test(b.target.nodeName)||"text"===b.target.type))){var c="keypress"!==b.type&&d.hotkeys.specialKeys[b.which],e=String.fromCharCode(b.which).toLowerCase(),a="",f={};b.altKey&&"alt"!==c&&(a+="alt+");b.ctrlKey&&"ctrl"!==c&&(a+="ctrl+");b.metaKey&&(!b.ctrlKey&&"meta"!==c)&&(a+="meta+");b.shiftKey&&"shift"!==c&&(a+="shift+");c?f[a+c]= + !0:(f[a+e]=!0,f[a+d.hotkeys.shiftNums[e]]=!0,"shift+"===a&&(f[d.hotkeys.shiftNums[e]]=!0));c=0;for(e=j.length;c","/":"?","\\":"|"}};d.each(["keydown","keyup","keypress"],function(){d.event.special[this]={add:h}})})(MiniProfiler.jQuery); + + }; + + var major, minor; + if (typeof(jQuery) == 'function') { + var jQueryVersion = jQuery.fn.jquery.split('.'); + major = parseInt(jQueryVersion[0], 10); + minor = parseInt(jQueryVersion[1], 10); + } + + + if (major === 2 || (major === 1 && minor >= 7)) { + MiniProfiler.jQuery = $ = jQuery; + $(deferInit); + } else { + load(options.path + "jquery.1.7.1.js?v=" + options.version, function() { + MiniProfiler.jQuery = $ = jQuery.noConflict(true); + $(deferInit); + }); + } + }, + + getClientTimingByName: function (clientTiming, name) { + + for (var i = 0; i < clientTiming.Timings.length; i++) { + if (clientTiming.Timings[i].Name == name) { + return clientTiming.Timings[i]; + } + } + return { Name: name, Duration: "", Start: "" }; + }, + + renderDate: function (jsonDate) { // JavaScriptSerializer sends dates as /Date(1308024322065)/ + if (jsonDate) { + return (typeof jsonDate === 'string') ? new Date(parseInt(jsonDate.replace("/Date(", "").replace(")/", ""), 10)).toUTCString() : jsonDate; + } + }, + + renderIndent: function (depth) { + var result = ''; + for (var i = 0; i < depth; i++) { + result += ' '; + } + return result; + }, + + renderExecuteType: function (typeId) { + // see StackExchange.Profiling.ExecuteType enum + switch (typeId) { + case 0: return 'None'; + case 1: return 'NonQuery'; + case 2: return 'Scalar'; + case 3: return 'Reader'; + } + }, + + shareUrl: function (id) { + return options.path + 'results?id=' + id; + }, + + getClientTimings: function (clientTimings) { + var list = []; + var t; + + if (!clientTimings.Timings) return []; + + for (var i = 0; i < clientTimings.Timings.length; i++) { + t = clientTimings.Timings[i]; + var trivial = t.Name != "Dom Complete" && t.Name != "Response" && t.Name != "First Paint Time"; + trivial = t.Duration < 2 ? trivial : false; + list.push( + { + isTrivial: trivial, + name: t.Name, + duration: t.Duration, + start: t.Start + }); + } + + list.sort(function (a, b) { return a.start - b.start; }); + return list; + }, + + getSqlTimings: function (root) { + var result = [], + addToResults = function (timing) { + if (timing.SqlTimings) { + for (var i = 0, sqlTiming; i < timing.SqlTimings.length; i++) { + sqlTiming = timing.SqlTimings[i]; + + // HACK: add info about the parent Timing to each SqlTiming so UI can render + sqlTiming.ParentTimingName = timing.Name; + result.push(sqlTiming); + } + } + + if (timing.Children) { + for (var i = 0; i < timing.Children.length; i++) { + addToResults(timing.Children[i]); + } + } + }; + + // start adding at the root and recurse down + addToResults(root); + + var removeDuration = function(list, duration) { + + var newList = []; + for (var i = 0; i < list.length; i++) { + + var item = list[i]; + if (duration.start > item.start) { + if (duration.start > item.finish) { + newList.push(item); + continue; + } + newList.push({ start: item.start, finish: duration.start }); + } + + if (duration.finish < item.finish) { + if (duration.finish < item.start) { + newList.push(item); + continue; + } + newList.push({ start: duration.finish, finish: item.finish }); + } + } + + return newList; + }; + + var processTimes = function (elem, parent) { + var duration = { start: elem.StartMilliseconds, finish: (elem.StartMilliseconds + elem.DurationMilliseconds) }; + elem.richTiming = [duration]; + if (parent != null) { + elem.parent = parent; + elem.parent.richTiming = removeDuration(elem.parent.richTiming, duration); + } + + if (elem.Children) { + for (var i = 0; i < elem.Children.length; i++) { + processTimes(elem.Children[i], elem); + } + } + }; + + processTimes(root, null); + + // sort results by time + result.sort(function (a, b) { return a.StartMilliseconds - b.StartMilliseconds; }); + + var determineOverlap = function(gap, node) { + var overlap = 0; + for (var i = 0; i < node.richTiming.length; i++) { + var current = node.richTiming[i]; + if (current.start > gap.finish) { + break; + } + if (current.finish < gap.start) { + continue; + } + + overlap += Math.min(gap.finish, current.finish) - Math.max(gap.start, current.start); + } + return overlap; + }; + + var determineGap = function (gap, node, match) { + var overlap = determineOverlap(gap, node); + if (match == null || overlap > match.duration) { + match = { name: node.Name, duration: overlap }; + } + else if (match.name == node.Name) { + match.duration += overlap; + } + + if (node.Children) { + for (var i = 0; i < node.Children.length; i++) { + match = determineGap(gap, node.Children[i], match); + } + } + return match; + }; + + var time = 0; + var prev = null; + $.each(result, function () { + this.prevGap = { + duration: (this.StartMilliseconds - time).toFixed(2), + start: time, + finish: this.StartMilliseconds + }; + + this.prevGap.topReason = determineGap(this.prevGap, root, null); + + time = this.StartMilliseconds + this.DurationMilliseconds; + prev = this; + }); + + + if (result.length > 0) { + var me = result[result.length - 1]; + me.nextGap = { + duration: (root.DurationMilliseconds - time).toFixed(2), + start: time, + finish: root.DurationMilliseconds + }; + me.nextGap.topReason = determineGap(me.nextGap, root, null); + } + + return result; + }, + + getSqlTimingsCount: function (root) { + var result = 0, + countSql = function (timing) { + if (timing.SqlTimings) { + result += timing.SqlTimings.length; + } + + if (timing.Children) { + for (var i = 0; i < timing.Children.length; i++) { + countSql(timing.Children[i]); + } + } + }; + countSql(root); + return result; + }, + + fetchResultsExposed: function (ids) { + return fetchResults(ids); + }, + + formatDuration: function (duration) { + return (duration || 0).toFixed(1); + } + }; +})(); + +MiniProfiler.init(); + +if (typeof prettyPrint === "undefined") { + + // prettify.js + // http://code.google.com/p/google-code-prettify/ + + window.PR_SHOULD_USE_CONTINUATION=true;window.PR_TAB_WIDTH=8;window.PR_normalizedHtml=window.PR=window.prettyPrintOne=window.prettyPrint=void 0;window._pr_isIE6=function(){var y=navigator&&navigator.userAgent&&navigator.userAgent.match(/\bMSIE ([678])\./);y=y?+y[1]:false;window._pr_isIE6=function(){return y};return y}; + (function(){function y(b){return b.replace(L,"&").replace(M,"<").replace(N,">")}function H(b,f,i){switch(b.nodeType){case 1:var o=b.tagName.toLowerCase();f.push("<",o);var l=b.attributes,n=l.length;if(n){if(i){for(var r=[],j=n;--j>=0;)r[j]=l[j];r.sort(function(q,m){return q.name"); + for(l=b.firstChild;l;l=l.nextSibling)H(l,f,i);if(b.firstChild||!/^(?:br|link|img)$/.test(o))f.push("");break;case 3:case 4:f.push(y(b.nodeValue));break}}function O(b){function f(c){if(c.charAt(0)!=="\\")return c.charCodeAt(0);switch(c.charAt(1)){case "b":return 8;case "t":return 9;case "n":return 10;case "v":return 11;case "f":return 12;case "r":return 13;case "u":case "x":return parseInt(c.substring(2),16)||c.charCodeAt(1);case "0":case "1":case "2":case "3":case "4":case "5":case "6":case "7":return parseInt(c.substring(1), + 8);default:return c.charCodeAt(1)}}function i(c){if(c<32)return(c<16?"\\x0":"\\x")+c.toString(16);c=String.fromCharCode(c);if(c==="\\"||c==="-"||c==="["||c==="]")c="\\"+c;return c}function o(c){var d=c.substring(1,c.length-1).match(RegExp("\\\\u[0-9A-Fa-f]{4}|\\\\x[0-9A-Fa-f]{2}|\\\\[0-3][0-7]{0,2}|\\\\[0-7]{1,2}|\\\\[\\s\\S]|-|[^-\\\\]","g"));c=[];for(var a=[],k=d[0]==="^",e=k?1:0,h=d.length;e122)){s<65||g>90||a.push([Math.max(65,g)|32,Math.min(s,90)|32]);s<97||g>122||a.push([Math.max(97,g)&-33,Math.min(s,122)&-33])}}a.sort(function(v,w){return v[0]-w[0]||w[1]-v[1]});d=[];g=[NaN,NaN];for(e=0;eh[0]){h[1]+1>h[0]&&a.push("-"); + a.push(i(h[1]))}}a.push("]");return a.join("")}function l(c){for(var d=c.source.match(RegExp("(?:\\[(?:[^\\x5C\\x5D]|\\\\[\\s\\S])*\\]|\\\\u[A-Fa-f0-9]{4}|\\\\x[A-Fa-f0-9]{2}|\\\\[0-9]+|\\\\[^ux0-9]|\\(\\?[:!=]|[\\(\\)\\^]|[^\\x5B\\x5C\\(\\)\\^]+)","g")),a=d.length,k=[],e=0,h=0;e=2&&c==="[")d[e]=o(g);else if(c!=="\\")d[e]=g.replace(/[a-zA-Z]/g,function(s){s=s.charCodeAt(0);return"["+String.fromCharCode(s&-33,s|32)+"]"})}return d.join("")}for(var n=0,r=false,j=false,q=0,m=b.length;q=0;l-=16)o.push(" ".substring(0,l));l=n+1;break;case "\n":f=0;break;default:++f}if(!o)return i;o.push(i.substring(l));return o.join("")}}function I(b, + f,i,o){if(f){b={source:f,c:b};i(b);o.push.apply(o,b.d)}}function B(b,f){var i={},o;(function(){for(var r=b.concat(f),j=[],q={},m=0,t=r.length;m=0;)i[c.charAt(d)]=p;p=p[1];c=""+p;if(!q.hasOwnProperty(c)){j.push(p);q[c]=null}}j.push(/[\0-\uffff]/);o=O(j)})();var l=f.length;function n(r){for(var j=r.c,q=[j,z],m=0,t=r.source.match(o)||[],p={},c=0,d=t.length;c=5&&"lang-"===k.substring(0,5))&&!(e&&typeof e[1]==="string")){h=false;k=P}h||(p[a]=k)}g=m;m+=a.length;if(h){h=e[1];var s=a.indexOf(h),v=s+h.length;if(e[2]){v=a.length-e[2].length;s=v-h.length}k=k.substring(5);I(j+g,a.substring(0,s),n,q);I(j+g+s,h,Q(k,h),q);I(j+g+v,a.substring(v),n,q)}else q.push(j+g,k)}r.d=q}return n}function x(b){var f=[],i=[];if(b.tripleQuotedStrings)f.push([A,/^(?:\'\'\'(?:[^\'\\]|\\[\s\S]|\'{1,2}(?=[^\']))*(?:\'\'\'|$)|\"\"\"(?:[^\"\\]|\\[\s\S]|\"{1,2}(?=[^\"]))*(?:\"\"\"|$)|\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$))/, + null,"'\""]);else b.multiLineStrings?f.push([A,/^(?:\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$)|\`(?:[^\\\`]|\\[\s\S])*(?:\`|$))/,null,"'\"`"]):f.push([A,/^(?:\'(?:[^\\\'\r\n]|\\.)*(?:\'|$)|\"(?:[^\\\"\r\n]|\\.)*(?:\"|$))/,null,"\"'"]);b.verbatimStrings&&i.push([A,/^@\"(?:[^\"]|\"\")*(?:\"|$)/,null]);if(b.hashComments)if(b.cStyleComments){f.push([C,/^#(?:(?:define|elif|else|endif|error|ifdef|include|ifndef|line|pragma|undef|warning)\b|[^\r\n]*)/,null,"#"]);i.push([A,/^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h|[a-z]\w*)>/, + null])}else f.push([C,/^#[^\r\n]*/,null,"#"]);if(b.cStyleComments){i.push([C,/^\/\/[^\r\n]*/,null]);i.push([C,/^\/\*[\s\S]*?(?:\*\/|$)/,null])}b.regexLiterals&&i.push(["lang-regex",RegExp("^"+Z+"(/(?=[^/*])(?:[^/\\x5B\\x5C]|\\x5C[\\s\\S]|\\x5B(?:[^\\x5C\\x5D]|\\x5C[\\s\\S])*(?:\\x5D|$))+/)")]);b=b.keywords.replace(/^\s+|\s+$/g,"");b.length&&i.push([R,RegExp("^(?:"+b.replace(/\s+/g,"|")+")\\b"),null]);f.push([z,/^\s+/,null," \r\n\t\u00a0"]);i.push([J,/^@[a-z_$][a-z_$@0-9]*/i,null],[S,/^@?[A-Z]+[a-z][A-Za-z_$@0-9]*/, + null],[z,/^[a-z_$][a-z_$@0-9]*/i,null],[J,/^(?:0x[a-f0-9]+|(?:\d(?:_\d+)*\d*(?:\.\d*)?|\.\d\+)(?:e[+\-]?\d+)?)[a-z]*/i,null,"0123456789"],[E,/^.[^\s\w\.$@\'\"\`\/\#]*/,null]);return B(f,i)}function $(b){function f(D){if(D>r){if(j&&j!==q){n.push("");j=null}if(!j&&q){j=q;n.push('')}var T=y(p(i.substring(r,D))).replace(e?d:c,"$1 ");e=k.test(T);n.push(T.replace(a,s));r=D}}var i=b.source,o=b.g,l=b.d,n=[],r=0,j=null,q=null,m=0,t=0,p=Y(window.PR_TAB_WIDTH),c=/([\r\n ]) /g, + d=/(^| ) /gm,a=/\r\n?|\n/g,k=/[ \r\n]$/,e=true,h=window._pr_isIE6();h=h?b.b.tagName==="PRE"?h===6?" \r\n":h===7?" 
\r":" \r":" 
":"
";var g=b.b.className.match(/\blinenums\b(?::(\d+))?/),s;if(g){for(var v=[],w=0;w<10;++w)v[w]=h+'
  • ';var F=g[1]&&g[1].length?g[1]-1:0;n.push('
    1. ");s=function(){var D=v[++F%10];return j?""+D+'':D}}else s=h; + for(;;)if(m");j=null}n.push(o[m+1]);m+=2}else if(t");g&&n.push("
    ");b.a=n.join("")}function u(b,f){for(var i=f.length;--i>=0;){var o=f[i];if(G.hasOwnProperty(o))"console"in window&&console.warn("cannot override language handler %s",o);else G[o]=b}}function Q(b,f){b&&G.hasOwnProperty(b)||(b=/^\s*1&&m.charAt(0)==="<"){if(!ba.test(m))if(ca.test(m)){f.push(m.substring(9,m.length-3));n+=m.length-12}else if(da.test(m)){f.push("\n");++n}else if(m.indexOf(V)>=0&&m.replace(/\s(\w+)\s*=\s*(?:\"([^\"]*)\"|'([^\']*)'|(\S+))/g,' $1="$2$3$4"').match(/[cC][lL][aA][sS][sS]=\"[^\"]*\bnocode\b/)){var t=m.match(W)[2],p=1,c;c=j+1;a:for(;c=0;){var e=p.indexOf(";",k);if(e>=0){var h=p.substring(k+3,e),g=10;if(h&&h.charAt(0)==="x"){h=h.substring(1);g=16}var s=parseInt(h,g);isNaN(s)||(p=p.substring(0,k)+String.fromCharCode(s)+p.substring(e+1))}}a=p.replace(ea,"<").replace(fa,">").replace(ga,"'").replace(ha,'"').replace(ia," ").replace(ja, + "&")}f.push(a);n+=a.length}}o={source:f.join(""),h:r};var v=o.source;b.source=v;b.c=0;b.g=o.h;Q(i,v)(b);$(b)}catch(w){if("console"in window)console.log(w&&w.stack?w.stack:w)}}var A="str",R="kwd",C="com",S="typ",J="lit",E="pun",z="pln",P="src",V="nocode",Z=function(){for(var b=["!","!=","!==","#","%","%=","&","&&","&&=","&=","(","*","*=","+=",",","-=","->","/","/=",":","::",";","<","<<","<<=","<=","=","==","===",">",">=",">>",">>=",">>>",">>>=","?","@","[","^","^=","^^","^^=","{","|","|=","||","||=", + "~","break","case","continue","delete","do","else","finally","instanceof","return","throw","try","typeof"],f="(?:^^|[+-]",i=0;i:&a-z])/g,"\\$1");f+=")\\s*";return f}(),L=/&/g,M=//g,X=/\"/g,ea=/</g,fa=/>/g,ga=/'/g,ha=/"/g,ja=/&/g,ia=/ /g,ka=/[\r\n]/g,K=null,aa=RegExp("[^<]+|
  • <%= link_to (h @user.try(:name)), user_path(@user) if @user %> <%= l(:label_user_create_project) %> <%= link_to @project.name %> ! + <% + #判断是否显示真名 + if @canShowRealName + %> + <%= link_to (h @user.try(:realname)), user_path(@user) if @user %> + <% else %> + <%= link_to (h @user.try(:name)), user_path(@user) if @user %> + <% end %> + <%= l(:label_user_create_project) %> <%= link_to @project.name %> !
    <%= l :label_update_time %>: <%= format_time(@project.created_on) %> diff --git a/app/views/tags/_show_users.html.erb b/app/views/tags/_show_users.html.erb index bee210b2..6818bdd9 100644 --- a/app/views/tags/_show_users.html.erb +++ b/app/views/tags/_show_users.html.erb @@ -3,10 +3,9 @@
    <% users_results.each do |user| %>

    - <%= l(:label_tags_user_name) %><%= link_to ("#{user.firstname+user.lastname}"), + <%= l(:label_tags_user_name) %><%= link_to ("#{user.name}"), :controller => "users",:action => "show",:id => user.id%>
    - <%= l(:label_tags_user_mail) %><%= mail_to(h(user.mail)) %>

    <% end %> diff --git a/app/views/tags/_tagEx.html.erb b/app/views/tags/_tagEx.html.erb index 08800caa..3e7cd9c6 100644 --- a/app/views/tags/_tagEx.html.erb +++ b/app/views/tags/_tagEx.html.erb @@ -1,13 +1,3 @@ -
    <%#begin @@ -60,11 +50,7 @@ <%= text_field "tag_name" ,"name"%> - <%= button_tag "增加", :type=>"button", :onclick=>"tagAddClick(tags_show-"+obj.class.to_s + "-" +obj.id.to_s + ","+ obj.id.to_s + "," + object_flag.to_s + ")" %> - <%#= f.text_field :object_id,:value=> obj.id,:style=>"display:none"%> - <%#= f.text_field :object_flag,:value=> object_flag,:style=>"display:none"%> - - <%= submit_tag "增加", :name=>"add_tag" %> + <%= submit_tag l(:button_add), :name=>"add_tag",:remote=>"false", :format => 'js' %>
    @inst_d