diff --git a/Gemfile b/Gemfile index 70281fedf..976184793 100644 --- a/Gemfile +++ b/Gemfile @@ -1,139 +1,140 @@ -source 'http://ruby.taobao.org' -#source 'http://ruby.sdutlinux.org/' - -unless RUBY_PLATFORM =~ /w32/ - # unix-like only - gem 'iconv' - gem 'rubyzip' - gem 'zip-zip' -end - -gem 'grape', '~> 0.9.0' -gem 'grape-entity' -gem 'seems_rateable', '~> 1.0.13' -gem "rails", "3.2.13" -gem "jquery-rails", "~> 2.0.2" -gem "i18n", "~> 0.6.0" -gem 'coderay', '~> 1.1.0' -gem "fastercsv", "~> 1.5.0", :platforms => [:mri_18, :mingw_18, :jruby] -gem "builder", "3.0.0" -gem 'acts-as-taggable-on', '2.4.1' -gem 'spreadsheet' -gem 'ruby-ole' -#gem 'email_verifier', path: 'lib/email_verifier' -gem 'rufus-scheduler' -#gem 'dalli', path: 'lib/dalli-2.7.2' -gem 'rails_kindeditor',path:'lib/rails_kindeditor' -group :development do - gem 'grape-swagger' - #gem 'grape-swagger-ui', git: 'https://github.com/guange2015/grape-swagger-ui.git' - gem 'puma' if RbConfig::CONFIG['host_os'] =~ /linux/ - gem 'pry-rails' - if RUBY_VERSION >= '2.0.0' - gem 'pry-byebug' - else - # gem 'pry-debugger' - end - gem 'pry-stack_explorer' - gem 'better_errors', '~> 1.1.0' - gem 'rack-mini-profiler', '~> 0.9.3' -end - -group :test do - gem "shoulda", "~> 3.5.0" - gem "mocha", "~> 1.1.0" - gem 'capybara', '~> 2.4.1' - gem 'nokogiri', '~> 1.6.3' - gem 'factory_girl', '~> 4.4.0' - gem 'selenium-webdriver', '~> 2.42.0' - - gem "faker" - # platforms :mri, :mingw do - # group :rmagick do - # # RMagick 2 supports ruby 1.9 - # # RMagick 1 would be fine for ruby 1.8 but Bundler does not support - # # different requirements for the same gem on different platforms - # gem "rmagick", ">= 2.0.0" - # end - #end -end - -# Gems used only for assets and not required -# in production environments by default. -group :assets do - gem 'sass-rails', '~> 3.2.3' - gem 'coffee-rails', '~> 3.2.1' - - # See https://github.com/sstephenson/execjs#readme for more supported runtimes - gem 'therubyracer', :platforms => :ruby - - gem 'uglifier', '>= 1.0.3' -end - -# Optional gem for LDAP authentication -group :ldap do - gem "net-ldap", "~> 0.3.1" -end - - -# Optional gem for OpenID authentication -group :openid do - gem "ruby-openid", "~> 2.1.4", :require => "openid" - gem "rack-openid" -end - -# Optional gem for exporting the gantt to a PNG file, not supported with jruby -platforms :jruby do - # jruby-openssl is bundled with JRuby 1.7.0 - gem "jruby-openssl" if Object.const_defined?(:JRUBY_VERSION) && JRUBY_VERSION < '1.7.0' - gem "activerecord-jdbc-adapter", "1.2.5" -end - -# Include database gems for the adapters found in the database -# configuration file -require 'erb' -require 'yaml' -database_file = File.join(File.dirname(__FILE__), "config/database.yml") -if File.exist?(database_file) - database_config = YAML::load(ERB.new(IO.read(database_file)).result) - adapters = database_config.values.map {|c| c['adapter']}.compact.uniq - if adapters.any? - adapters.each do |adapter| - case adapter - when 'mysql2' - gem "mysql2", "= 0.3.18", :platforms => [:mri, :mingw] - gem "activerecord-jdbcmysql-adapter", :platforms => :jruby - when 'mysql' - gem "mysql", "~> 2.8.1", :platforms => [:mri, :mingw] - gem "activerecord-jdbcmysql-adapter", :platforms => :jruby - when /postgresql/ - gem "pg", ">= 0.11.0", :platforms => [:mri, :mingw] - gem "activerecord-jdbcpostgresql-adapter", :platforms => :jruby - when /sqlite3/ - gem "sqlite3", :platforms => [:mri, :mingw] - gem "activerecord-jdbcsqlite3-adapter", :platforms => :jruby - when /sqlserver/ - gem "tiny_tds", "~> 0.5.1", :platforms => [:mri, :mingw] - gem "activerecord-sqlserver-adapter", :platforms => [:mri, :mingw] - else - warn("Unknown database adapter `#{adapter}` found in config/database.yml, use Gemfile.local to load your own database gems") - end - end - else - warn("No adapter found in config/database.yml, please configure it first") - end -else - warn("Please configure your config/database.yml first") -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` - instance_eval File.read(local_gemfile) -end - -# Load plugins' Gemfiles -Dir.glob File.expand_path("../plugins/*/Gemfile", __FILE__) do |file| - puts "Loading #{file} ..." if $DEBUG # `ruby -d` or `bundle -v` - instance_eval File.read(file) -end +source 'http://ruby.taobao.org' +#source 'http://ruby.sdutlinux.org/' + +unless RUBY_PLATFORM =~ /w32/ + # unix-like only + gem 'iconv' +end + +gem 'rubyzip' +gem 'delayed_job_active_record'#, :group => :production +gem 'daemons' +gem 'grape', '~> 0.9.0' +gem 'grape-entity' +gem 'seems_rateable', '~> 1.0.13' +gem "rails", "3.2.13" +gem "jquery-rails", "~> 2.0.2" +gem "i18n", "~> 0.6.0" +gem 'coderay', '~> 1.1.0' +gem "fastercsv", "~> 1.5.0", :platforms => [:mri_18, :mingw_18, :jruby] +gem "builder", "3.0.0" +gem 'acts-as-taggable-on', '2.4.1' +gem 'spreadsheet' +gem 'ruby-ole' +#gem 'email_verifier', path: 'lib/email_verifier' +gem 'rufus-scheduler' +#gem 'dalli', path: 'lib/dalli-2.7.2' +gem 'rails_kindeditor',path:'lib/rails_kindeditor' +group :development do + gem 'grape-swagger' + #gem 'grape-swagger-ui', git: 'https://github.com/guange2015/grape-swagger-ui.git' + gem 'puma' if RbConfig::CONFIG['host_os'] =~ /linux/ + gem 'pry-rails' + if RUBY_VERSION >= '2.0.0' + gem 'pry-byebug' + else + # gem 'pry-debugger' + end + gem 'pry-stack_explorer' + gem 'better_errors', '~> 1.1.0' + gem 'rack-mini-profiler', '~> 0.9.3' +end + +group :test do + gem "shoulda", "~> 3.5.0" + gem "mocha", "~> 1.1.0" + gem 'capybara', '~> 2.4.1' + gem 'nokogiri', '~> 1.6.3' + gem 'factory_girl', '~> 4.4.0' + gem 'selenium-webdriver', '~> 2.42.0' + + gem "faker" + # platforms :mri, :mingw do + # group :rmagick do + # # RMagick 2 supports ruby 1.9 + # # RMagick 1 would be fine for ruby 1.8 but Bundler does not support + # # different requirements for the same gem on different platforms + # gem "rmagick", ">= 2.0.0" + # end + #end +end + +# Gems used only for assets and not required +# in production environments by default. +group :assets do + gem 'sass-rails', '~> 3.2.3' + gem 'coffee-rails', '~> 3.2.1' + + # See https://github.com/sstephenson/execjs#readme for more supported runtimes + gem 'therubyracer', :platforms => :ruby + + gem 'uglifier', '>= 1.0.3' +end + +# Optional gem for LDAP authentication +group :ldap do + gem "net-ldap", "~> 0.3.1" +end + + +# Optional gem for OpenID authentication +group :openid do + gem "ruby-openid", "~> 2.1.4", :require => "openid" + gem "rack-openid" +end + +# Optional gem for exporting the gantt to a PNG file, not supported with jruby +platforms :jruby do + # jruby-openssl is bundled with JRuby 1.7.0 + gem "jruby-openssl" if Object.const_defined?(:JRUBY_VERSION) && JRUBY_VERSION < '1.7.0' + gem "activerecord-jdbc-adapter", "1.2.5" +end + +# Include database gems for the adapters found in the database +# configuration file +require 'erb' +require 'yaml' +database_file = File.join(File.dirname(__FILE__), "config/database.yml") +if File.exist?(database_file) + database_config = YAML::load(ERB.new(IO.read(database_file)).result) + adapters = database_config.values.map {|c| c['adapter']}.compact.uniq + if adapters.any? + adapters.each do |adapter| + case adapter + when 'mysql2' + gem "mysql2", "= 0.3.18", :platforms => [:mri, :mingw] + gem "activerecord-jdbcmysql-adapter", :platforms => :jruby + when 'mysql' + gem "mysql", "~> 2.8.1", :platforms => [:mri, :mingw] + gem "activerecord-jdbcmysql-adapter", :platforms => :jruby + when /postgresql/ + gem "pg", ">= 0.11.0", :platforms => [:mri, :mingw] + gem "activerecord-jdbcpostgresql-adapter", :platforms => :jruby + when /sqlite3/ + gem "sqlite3", :platforms => [:mri, :mingw] + gem "activerecord-jdbcsqlite3-adapter", :platforms => :jruby + when /sqlserver/ + gem "tiny_tds", "~> 0.5.1", :platforms => [:mri, :mingw] + gem "activerecord-sqlserver-adapter", :platforms => [:mri, :mingw] + else + warn("Unknown database adapter `#{adapter}` found in config/database.yml, use Gemfile.local to load your own database gems") + end + end + else + warn("No adapter found in config/database.yml, please configure it first") + end +else + warn("Please configure your config/database.yml first") +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` + instance_eval File.read(local_gemfile) +end + +# Load plugins' Gemfiles +Dir.glob File.expand_path("../plugins/*/Gemfile", __FILE__) do |file| + puts "Loading #{file} ..." if $DEBUG # `ruby -d` or `bundle -v` + instance_eval File.read(file) +end diff --git a/app/api/mobile/api.rb b/app/api/mobile/api.rb index 8f7f0342f..a59b01776 100644 --- a/app/api/mobile/api.rb +++ b/app/api/mobile/api.rb @@ -1,49 +1,49 @@ -module Mobile - require_relative 'middleware/error_handler' - require_relative 'apis/auth' - require_relative 'apis/users' - require_relative 'apis/courses' - require_relative 'apis/watches' - require_relative 'apis/upgrade' - require_relative 'apis/homeworks' - require_relative 'apis/comments' - class API < Grape::API - version 'v1', using: :path - format :json - content_type :json, "application/json;charset=UTF-8" - use Mobile::Middleware::ErrorHandler - - helpers do - def logger - API.logger - end - - def authenticate! - raise('Unauthorized. Invalid or expired token.') unless current_user - end - - def current_user - token = ApiKey.where(access_token: params[:token]).first - if token && !token.expired? - @current_user = User.find(token.user_id) - else - nil - end - end - end - - mount Apis::Auth - mount Apis::Users - mount Apis::Courses - mount Apis::Watches - mount Apis::Upgrade - mount Apis::Homeworks - mount Apis::Comments - - #add_swagger_documentation ({api_version: 'v1', base_path: 'http://u06.shellinfo.cn/trustie/api'}) - #add_swagger_documentation ({api_version: 'v1', base_path: '/api'}) if Rails.env.development? - - end -end - - +module Mobile + require_relative 'middleware/error_handler' + require_relative 'apis/auth' + require_relative 'apis/users' + require_relative 'apis/courses' + require_relative 'apis/watches' + require_relative 'apis/upgrade' + require_relative 'apis/homeworks' + require_relative 'apis/comments' + class API < Grape::API + version 'v1', using: :path + format :json + content_type :json, "application/json;charset=UTF-8" + use Mobile::Middleware::ErrorHandler + + helpers do + def logger + API.logger + end + + def authenticate! + raise('Unauthorized. Invalid or expired token.') unless current_user + end + + def current_user + token = ApiKey.where(access_token: params[:token]).first + if token && !token.expired? + @current_user = User.find(token.user_id) + else + nil + end + end + end + + mount Apis::Auth + mount Apis::Users + mount Apis::Courses + mount Apis::Watches + mount Apis::Upgrade + mount Apis::Homeworks + mount Apis::Comments + + #add_swagger_documentation ({api_version: 'v1', base_path: 'http://u06.shellinfo.cn/trustie/api'}) + #add_swagger_documentation ({api_version: 'v1', base_path: '/api'}) if Rails.env.development? + + end +end + + diff --git a/app/controllers/account_controller.rb b/app/controllers/account_controller.rb index 2cf404ffb..f8d0f5dd1 100644 --- a/app/controllers/account_controller.rb +++ b/app/controllers/account_controller.rb @@ -88,9 +88,7 @@ class AccountController < ApplicationController # create a new token for password recovery token = Token.new(:user => user, :action => "recovery") if token.save - Thread.new do - Mailer.lost_password(token).deliver - end + Mailer.run.lost_password(token) flash[:notice] = l(:notice_account_lost_email_sent) redirect_to signin_url return @@ -228,7 +226,7 @@ class AccountController < ApplicationController user = User.find(params[:user]) if params[:user] token = Token.new(:user => user, :action => "register") if token.save - Mailer.register(token).deliver + Mailer.run.register(token) else yield if block_given? @@ -366,7 +364,7 @@ class AccountController < ApplicationController token = Token.new(:user => user, :action => "register") if user.save and token.save UserStatus.create(:user_id => user.id, :changsets_count => 0, :watchers_count => 0) - Mailer.register(token).deliver + Mailer.run.register(token) flash[:notice] = l(:notice_account_register_done) @@ -401,7 +399,7 @@ class AccountController < ApplicationController if user.save UserStatus.create(:user_id => user.id ,:changsets_count => 0, :watchers_count => 0) # Sends an email to the administrators - Mailer.account_activation_request(user).deliver + Mailer.run.account_activation_request(user) account_pending else yield if block_given? diff --git a/app/controllers/applied_project_controller.rb b/app/controllers/applied_project_controller.rb index f2c0eb056..b9824976b 100644 --- a/app/controllers/applied_project_controller.rb +++ b/app/controllers/applied_project_controller.rb @@ -1,59 +1,59 @@ -class AppliedProjectController < ApplicationController - - #申请加入项目 - def applied_join_project - @user_id = params[:user_id] - @project = Project.find_by_id(params[:project_id]) - if params[:project_join] - if @project - user = User.find @user_id - if user.member_of?(@project) - @status = 3 - else - @applieds = AppliedProject.where("user_id = ? and project_id = ?", params[:user_id],params[:project_id]) - if @applieds.count == 0 - appliedproject = AppliedProject.create(:user_id => params[:user_id], :project_id => params[:project_id]) - Mailer.applied_project(appliedproject).deliver - @status = 2 - else - @status = 1 - end - end - else - @status = 0 - end - respond_to do |format| - format.js - end - return - end - - @applieds = AppliedProject.where("user_id = ? and project_id = ?", params[:user_id],params[:project_id]) - if @applieds.count == 0 - appliedproject = AppliedProject.create(:user_id => params[:user_id], :project_id => params[:project_id]) - Mailer.applied_project(appliedproject).deliver - end - - #redirect_to project_path(params[:project_id]) - #redirect_to_referer_or {render :text => ( 'applied success.'), :layout => true} - respond_to do |format| - format.html { redirect_to_referer_or {render :text => (watching ? 'Watcher added.' : 'Watcher removed.'), :layout => true}} - format.js { render :partial => 'set_applied'} - end - end - - #取消申请 - def unapplied_join_project - @project = Project.find(params[:project_id]) - #@applied = AppliedProject.find(params[:id]) - #@applied.destroy - - AppliedProject.deleteappiled(params[:user_id], params[:project_id]) - - respond_to do |format| - format.html { redirect_to_referer_or {render :text => (watching ? 'Watcher added.' : 'Watcher removed.'), :layout => true}} - format.js { render :partial => 'set_applied' } - end - end - -end +class AppliedProjectController < ApplicationController + + #申请加入项目 + def applied_join_project + @user_id = params[:user_id] + @project = Project.find_by_id(params[:project_id]) + if params[:project_join] + if @project + user = User.find @user_id + if user.member_of?(@project) + @status = 3 + else + @applieds = AppliedProject.where("user_id = ? and project_id = ?", params[:user_id],params[:project_id]) + if @applieds.count == 0 + appliedproject = AppliedProject.create(:user_id => params[:user_id], :project_id => params[:project_id]) + Mailer.run.applied_project(appliedproject) + @status = 2 + else + @status = 1 + end + end + else + @status = 0 + end + respond_to do |format| + format.js + end + return + end + + @applieds = AppliedProject.where("user_id = ? and project_id = ?", params[:user_id],params[:project_id]) + if @applieds.count == 0 + appliedproject = AppliedProject.create(:user_id => params[:user_id], :project_id => params[:project_id]) + Mailer.run.applied_project(appliedproject) + end + + #redirect_to project_path(params[:project_id]) + #redirect_to_referer_or {render :text => ( 'applied success.'), :layout => true} + respond_to do |format| + format.html { redirect_to_referer_or {render :text => (watching ? 'Watcher added.' : 'Watcher removed.'), :layout => true}} + format.js { render :partial => 'set_applied'} + end + end + + #取消申请 + def unapplied_join_project + @project = Project.find(params[:project_id]) + #@applied = AppliedProject.find(params[:id]) + #@applied.destroy + + AppliedProject.deleteappiled(params[:user_id], params[:project_id]) + + respond_to do |format| + format.html { redirect_to_referer_or {render :text => (watching ? 'Watcher added.' : 'Watcher removed.'), :layout => true}} + format.js { render :partial => 'set_applied' } + end + end + +end diff --git a/app/controllers/attachments_controller.rb b/app/controllers/attachments_controller.rb index d458b73e4..d880ebe35 100644 --- a/app/controllers/attachments_controller.rb +++ b/app/controllers/attachments_controller.rb @@ -27,7 +27,7 @@ class AttachmentsController < ApplicationController accept_api_auth :show, :download, :upload require 'iconv' include AttachmentsHelper - + include ApplicationHelper def show respond_to do |format| @@ -65,36 +65,7 @@ class AttachmentsController < ApplicationController def download # modify by nwb # 下载添加权限设置 - candown = false - if @attachment.container.class.to_s != "HomeworkAttach" && (@attachment.container.has_attribute?(:project) || @attachment.container.has_attribute?(:project_id)) && @attachment.container.project - project = @attachment.container.project - candown= User.current.member_of?(project) || (project.is_public && @attachment.is_public == 1) - elsif @attachment.container.is_a?(Project) - project = @attachment.container - candown= User.current.member_of?(project) || (project.is_public && @attachment.is_public == 1) - elsif (@attachment.container.has_attribute?(:board) || @attachment.container.has_attribute?(:board_id)) && @attachment.container.board && - @attachment.container.board.project - project = @attachment.container.board.project - candown = User.current.member_of?(project) || (project.is_public && @attachment.is_public == 1) - elsif (@attachment.container.has_attribute?(:course) ||@attachment.container.has_attribute?(:course_id) ) && @attachment.container.course - course = @attachment.container.course - candown = User.current.member_of_course?(course) || (course.is_public==1 && @attachment.is_public == 1) - elsif @attachment.container.is_a?(Course) - course = @attachment.container - candown= User.current.member_of_course?(course) || (course.is_public==1 && @attachment.is_public == 1) - elsif (@attachment.container.has_attribute?(:board) || @attachment.container.has_attribute?(:board_id)) && @attachment.container.board && - @attachment.container.board.course - course = @attachment.container.board.course - candown= User.current.member_of_course?(course) || (course.is_public==1 && @attachment.is_public == 1) - elsif @attachment.container.class.to_s=="HomeworkAttach" && @attachment.container.bid.reward_type == 3 - candown = true - elsif @attachment.container_type == "Bid" && @attachment.container && @attachment.container.courses.first - course = @attachment.container.courses.first - candown = User.current.member_of_course?(course) || (course.is_public == 1 && @attachment.is_public == 1) - else - - candown = @attachment.is_public == 1 - end + candown = attachment_candown @attachment if candown || User.current.admin? || User.current.id == @attachment.author_id @attachment.increment_download if stale?(:etag => @attachment.digest) @@ -349,6 +320,46 @@ class AttachmentsController < ApplicationController end end + def add_exist_file_to_projects + file = Attachment.find(params[:file_id]) + projects = params[:projects][:project] + @message = "" + projects.each do |project| + c = Project.find(project); + if project_contains_attachment?(c,file) + if @message && @message == "" + @message += l(:label_project_prompt) + c.name + l(:label_contain_resource) + file.filename + l(:label_quote_resource_failed) + next + else + @message += "
" + l(:label_project_prompt) + c.name + l(:label_contain_resource) + file.filename + l(:label_quote_resource_failed) + next + end + end + attach_copied_obj = file.copy + attach_copied_obj.tag_list.add(file.tag_list) # tag关联 + attach_copied_obj.container = c + attach_copied_obj.created_on = Time.now + attach_copied_obj.author_id = User.current.id + attach_copied_obj.copy_from = file.copy_from.nil? ? file.id : file.copy_from + if attach_copied_obj.attachtype == nil + attach_copied_obj.attachtype = 4 + end + @obj = c + @save_flag = attach_copied_obj.save + @save_message = attach_copied_obj.errors.full_messages + update_quotes attach_copied_obj + end + respond_to do |format| + format.js + end + rescue NoMethodError + @save_flag = false + @save_message = [] << l(:label_course_empty_select) + respond_to do |format| + format.js + end + end + def add_exist_file_to_courses file = Attachment.find(params[:file_id]) courses = params[:courses][:course] diff --git a/app/controllers/comments_controller.rb b/app/controllers/comments_controller.rb index 9f61306ab..57ad9d4ce 100644 --- a/app/controllers/comments_controller.rb +++ b/app/controllers/comments_controller.rb @@ -17,6 +17,7 @@ class CommentsController < ApplicationController default_search_scope :news + include ApplicationHelper model_object News before_filter :find_model_object before_filter :find_project_from_association @@ -29,6 +30,10 @@ class CommentsController < ApplicationController @comment.safe_attributes = params[:comment] @comment.author = User.current if @news.comments << @comment + if params[:asset_id] + ids = params[:asset_id].split(',') + update_kindeditor_assets_owner ids,@comment.id,OwnerTypeHelper::COMMENT + end flash[:notice] = l(:label_comment_added) end diff --git a/app/controllers/courses_controller.rb b/app/controllers/courses_controller.rb index 581005bf7..a243018ba 100644 --- a/app/controllers/courses_controller.rb +++ b/app/controllers/courses_controller.rb @@ -1,6 +1,7 @@ class CoursesController < ApplicationController layout 'base_courses' include CoursesHelper + include ActivitiesHelper helper :activities helper :members helper :words @@ -680,26 +681,49 @@ class CoursesController < ApplicationController "show_course_files" => true, "show_course_news" => true, "show_course_messages" => true, - "show_bids" => true, "show_course_journals_for_messages" => true, + "show_bids" => true, "show_homeworks" => true } @date_to ||= Date.today + 1 @date_from = (@date_to - @days) > @course.created_at.to_date ? (@date_to - @days) : @course.created_at.to_date @author = (params[:user_id].blank? ? nil : User.active.find(params[:user_id])) - @author ||= @course.teacher - # 决定显示所用用户或单个用户活动 - @activity = Redmine::Activity::Fetcher.new(User.current, :course => @course, - :with_subprojects => false, - :author => @author) - @activity.scope_select {|t| has["show_#{t}"]} - # modify by nwb - # 添加私密性判断 - if User.current.member_of_course?(@course)|| User.current.admin? - events = @activity.events(@days, @course.created_at) + if @author.nil? + # 显示老师和助教的活动 + # @authors = searchTeacherAndAssistant(@course) + @authors = course_all_member(@course) + events = [] + @authors.each do |author| + @activity = Redmine::Activity::Fetcher.new(User.current, :course => @course, + :with_subprojects => false, + :author => author.user) + + @activity.scope_select {|t| has["show_#{t}"]} + # modify by nwb + # 添加私密性判断 + if User.current.member_of_course?(@course)|| User.current.admin? + events += @activity.events(@days, @course.created_at) + else + events += @activity.events(@days, @course.created_at, :is_public => 1) + end + + end else - events = @activity.events(@days, @course.created_at, :is_public => 1) + # @author = @course.teacher + @activity = Redmine::Activity::Fetcher.new(User.current, :course => @course, + :with_subprojects => false, + :author => @author) + + @activity.scope_select {|t| has["show_#{t}"]} + # modify by nwb + # 添加私密性判断 + if User.current.member_of_course?(@course)|| User.current.admin? + events = @activity.events(@days, @course.created_at) + else + events = @activity.events(@days, @course.created_at, :is_public => 1) + end end + # 无新动态时,显示老动态 if events.count == 0 if User.current.member_of_course?(@course)|| User.current.admin? @@ -708,13 +732,17 @@ class CoursesController < ApplicationController events = @activity.events(:is_public => 1) end end - events = paginateHelper events,10 - @events_by_day = events.group_by {|event| User.current.time_to_date(event.event_datetime)} - # documents @sort_by = %w(category date title author).include?(params[:sort_by]) ? params[:sort_by] : 'category' if(User.find_by_id(CourseInfos.find_by_course_id(@course.id).try(:user_id))) @user = User.find_by_id(CourseInfos.find_by_course_id(@course.id).user_id) end + + sorted_events = sort_activity_events_course(events); + events = paginateHelper sorted_events,10 + @events_by_day = events.group_by {|event| User.current.time_to_date(event.event_datetime)} + # documents + + respond_to do |format| format.html{render :layout => 'base_courses'} format.api diff --git a/app/controllers/discuss_demos_controller.rb b/app/controllers/discuss_demos_controller.rb deleted file mode 100644 index b0c9791cb..000000000 --- a/app/controllers/discuss_demos_controller.rb +++ /dev/null @@ -1,42 +0,0 @@ -class DiscussDemosController < ApplicationController - def index - - @discuss_demo_list = DiscussDemo.where("body is not null").order("created_at desc").page(params[:page] || 1).per(10) - end - - def new - @discuss_demo = DiscussDemo.create - @discuss_demo.save! - @discuss_demo - end - - def create - - end - - def update - @discuss_demo = DiscussDemo.find(params[:id]) - @discuss_demo.update_attributes(:title => params[:discuss_demo][:title],:body => params[:discuss_demo][:body]) - redirect_to :controller=> 'discuss_demos',:action => 'show',:id => params[:id] - end - - def delete - - end - - def destroy - asset = Kindeditor::Asset.find_by_owner_id(params[:id]) - if !asset.nil? - filepath = File.join(Rails.root,"public","files","uploads", - asset[:created_at].to_s.gsub("+0800","").to_datetime.strftime("%Y%m").to_s, - asset[:asset].to_s) - File.delete(filepath) if File.exist?filepath - end - DiscussDemo.destroy(params[:id]) - redirect_to :controller=> 'discuss_demos',:action => 'index' - end - - def show - @discuss_demo = DiscussDemo.find(params[:id]) - end -end diff --git a/app/controllers/documents_controller.rb b/app/controllers/documents_controller.rb index 9bf2ee846..570726320 100644 --- a/app/controllers/documents_controller.rb +++ b/app/controllers/documents_controller.rb @@ -110,7 +110,7 @@ class DocumentsController < ApplicationController render_attachment_warning_if_needed(@document) if attachments.present? && attachments[:files].present? && Setting.notified_events.include?('document_added') - Mailer.attachments_added(attachments[:files]).deliver + Mailer.run.attachments_added(attachments[:files]) end redirect_to document_url(@document) end diff --git a/app/controllers/files_controller.rb b/app/controllers/files_controller.rb index 28156ac0c..1ca3e2fcf 100644 --- a/app/controllers/files_controller.rb +++ b/app/controllers/files_controller.rb @@ -23,13 +23,14 @@ class FilesController < ApplicationController before_filter :auth_login1, :only => [:index] before_filter :logged_user_by_apptoken,:only => [:index] before_filter :find_project_by_project_id#, :except => [:getattachtype] - before_filter :authorize, :except => [:getattachtype,:quote_resource_show,:search] + before_filter :authorize, :except => [:getattachtype,:quote_resource_show,:search,:search_project,:quote_resource_show_project] helper :sort include SortHelper include FilesHelper helper :project_score include CoursesHelper + include ApplicationHelper def show_attachments obj @attachments = [] @@ -41,8 +42,8 @@ class FilesController < ApplicationController @feedback_count = @all_attachments.count @feedback_pages = Paginator.new @feedback_count, @limit, params['page'] @offset ||= @feedback_pages.offset - @curse_attachments_all = @all_attachments[@offset, @limit] - @curse_attachments = paginateHelper @all_attachments,10 + #@curse_attachments_all = @all_attachments[@offset, @limit] + @obj_attachments = paginateHelper @all_attachments,10 end def search @@ -78,6 +79,39 @@ class FilesController < ApplicationController end end + def search_project + sort = "" + @sort = "" + @order = "" + @is_remote = true + if params[:sort] + order_by = params[:sort].split(":") + @sort = order_by[0] + if order_by.count > 1 + @order = order_by[1] + end + sort = "#{@sort} #{@order}" + end + + begin + q = "%#{params[:name].strip}%" + #(redirect_to stores_url, :notice => l(:label_sumbit_empty);return) if params[:name].blank? + if params[:insite] + @result = find_public_attache q,sort + @result = visable_attachemnts_insite @result,@project + @searched_attach = paginateHelper @result,10 + else + @result = find_project_attache q,@project,sort + @result = visable_attachemnts @result + @searched_attach = paginateHelper @result,10 + end + + #rescue Exception => e + # #render 'stores' + # redirect_to search_course_files_url + end + end + def find_course_attache keywords,course,sort = "" if sort == "" sort = "created_on DESC" @@ -87,6 +121,26 @@ class FilesController < ApplicationController #resultSet = Attachment.find_by_sql("SELECT `attachments`.* FROM `attachments` LEFT OUTER JOIN `homework_attaches` ON `attachments`.container_type = 'HomeworkAttach' AND `attachments`.container_id = `homework_attaches`.id LEFT OUTER JOIN `homework_for_courses` ON `homework_attaches`.bid_id = `homework_for_courses`.bid_id LEFT OUTER JOIN `homework_for_courses` AS H_C ON `attachments`.container_type = 'Bid' AND `attachments`.container_id = H_C.bid_id WHERE (`homework_for_courses`.course_id = 117 OR H_C.course_id = 117 OR (`attachments`.container_type = 'Course' AND `attachments`.container_id = 117)) AND `attachments`.filename LIKE '%#{keywords}%'").reorder("created_on DESC") end + def find_project_attache keywords,project,sort = "" + if sort == "" + sort = "created_on DESC" + end + ids = "" + len = 0 + count = project.versions.count + project.versions.each do |version| + len = len + 1 + if len != count + ids += version.id.to_s + ',' + else + ids += version.id.to_s + end + end + resultSet = Attachment.where("((attachments.container_type = 'Project' And attachments.container_id = '#{project.id}') OR (container_type = 'Version' AND container_id IN (#{ids}))) AND filename LIKE :like ", like: "%#{keywords}%"). + reorder(sort) + #resultSet = Attachment.find_by_sql("SELECT `attachments`.* FROM `attachments` LEFT OUTER JOIN `homework_attaches` ON `attachments`.container_type = 'HomeworkAttach' AND `attachments`.container_id = `homework_attaches`.id LEFT OUTER JOIN `homework_for_courses` ON `homework_attaches`.bid_id = `homework_for_courses`.bid_id LEFT OUTER JOIN `homework_for_courses` AS H_C ON `attachments`.container_type = 'Bid' AND `attachments`.container_id = H_C.bid_id WHERE (`homework_for_courses`.course_id = 117 OR H_C.course_id = 117 OR (`attachments`.container_type = 'Course' AND `attachments`.container_id = 117)) AND `attachments`.filename LIKE '%#{keywords}%'").reorder("created_on DESC") + end + def find_public_attache keywords,sort = "" # StoresController#search 将每条文件都查出来,再次进行判断过滤。---> resultSet.to_a.map # 此时内容不多速度还可,但文件增长,每条判断多则进行3-4次表连接。 @@ -132,17 +186,27 @@ class FilesController < ApplicationController attribute = "downloads" when "created_on" attribute = "created_on" + when "quotes" + attribute = "quotes" + else + attribute = "created_on" end - - if order_by.count == 1 - sort += "#{Attachment.table_name}.#{attribute} asc " if attribute - elsif order_by.count == 2 - sort += "#{Attachment.table_name}.#{attribute} #{order_by[1]} " if attribute && order_by[1] - end - if sort_type != params[:sort].split(",").last - sort += "," + @sort = order_by[0] + @order = order_by[1] + if order_by.count == 1 && attribute + sort += "#{Attachment.table_name}.#{attribute} asc " + if sort_type != params[:sort].split(",").last + sort += "," + end + elsif order_by.count == 2 && order_by[1] + sort += "#{Attachment.table_name}.#{attribute} #{order_by[1]} " + if sort_type != params[:sort].split(",").last + sort += "," + end end end + else + sort = "#{Attachment.table_name}.created_on desc" end @containers = [ Project.includes(:attachments).find(@project.id)] @@ -184,6 +248,8 @@ class FilesController < ApplicationController attribute = "created_on" when "quotes" attribute = "quotes" + else + attribute = "created_on" end @sort = order_by[0] @order = order_by[1] @@ -199,6 +265,8 @@ class FilesController < ApplicationController end end end + else + sort = "#{Attachment.table_name}.created_on desc" end @containers = [ Course.includes(:attachments).reorder(sort).find(@course.id)] @@ -215,6 +283,11 @@ class FilesController < ApplicationController @can_quote = attachment_candown @file end + def quote_resource_show_project + @file = Attachment.find(params[:id]) + @can_quote = attachment_candown @file + end + def new @versions = @project.versions.sort @course_tag = @project.project_type @@ -241,7 +314,7 @@ class FilesController < ApplicationController render_attachment_warning_if_needed(container) if !attachments.empty? && !attachments[:files].blank? && Setting.notified_events.include?('file_added') - Mailer.attachments_added(attachments[:files]).deliver + Mailer.run.attachments_added(attachments[:files]) end # TODO: 临时用 nyan @@ -270,7 +343,7 @@ class FilesController < ApplicationController attachments = Attachment.attach_filesex(@course, params[:attachments], params[:attachment_type]) if !attachments.empty? && !attachments[:files].blank? && Setting.notified_events.include?('file_added') - Mailer.attachments_added(attachments[:files]).deliver + Mailer.run.attachments_added(attachments[:files]) end # TODO: 临时用 nyan diff --git a/app/controllers/forums_controller.rb b/app/controllers/forums_controller.rb index c347ba6b8..253e1d28d 100644 --- a/app/controllers/forums_controller.rb +++ b/app/controllers/forums_controller.rb @@ -63,8 +63,10 @@ class ForumsController < ApplicationController respond_to do |format| if @memo.save - ids = params[:asset_id].split(',') - update_kindeditor_assets_owner ids ,@memo.id,1 + if params[:asset_id] + ids = params[:asset_id].split(',') + update_kindeditor_assets_owner ids ,@memo.id,OwnerTypeHelper::MEMO + end #end format.html { redirect_to (forum_memo_url(@forum, (@memo.parent_id.nil? ? @memo : @memo.parent_id))), notice: "#{l :label_memo_create_succ}" } format.json { render json: @memo, status: :created, location: @memo } @@ -170,8 +172,10 @@ class ForumsController < ApplicationController # Author lizanle # Description after save后需要进行资源记录的更新 # owner_type = 2 对应的是 forum - ids = params[:asset_id].split(',') - update_kindeditor_assets_owner ids ,@forum.id,2 + if params[:asset_id] + ids = params[:asset_id].split(',') + update_kindeditor_assets_owner ids ,@forum.id,OwnerTypeHelper::FORUM + end #end respond_to do |format| diff --git a/app/controllers/messages_controller.rb b/app/controllers/messages_controller.rb index b50b2720f..c58ebf851 100644 --- a/app/controllers/messages_controller.rb +++ b/app/controllers/messages_controller.rb @@ -86,6 +86,11 @@ class MessagesController < ApplicationController if request.post? @message.save_attachments(params[:attachments]) if @message.save + # 更新kindeditor上传的图片资源所有者 + if params[:asset_id] + ids = params[:asset_id].split(',') + update_kindeditor_assets_owner ids,@message.id,OwnerTypeHelper::MESSAGE + end call_hook(:controller_messages_new_after_save, { :params => params, :message => @message}) render_attachment_warning_if_needed(@message) redirect_to board_message_url(@board, @message) @@ -117,6 +122,10 @@ class MessagesController < ApplicationController @topic.children << @reply #@topic.update_attribute(:updated_on, Time.now) if !@reply.new_record? + if params[:asset_id] + ids = params[:asset_id].split(',') + update_kindeditor_assets_owner ids,@reply.id,OwnerTypeHelper::MESSAGE + end call_hook(:controller_messages_reply_after_save, { :params => params, :message => @reply}) attachments = Attachment.attach_files(@reply, params[:attachments]) render_attachment_warning_if_needed(@reply) diff --git a/app/controllers/news_controller.rb b/app/controllers/news_controller.rb index 617e56575..d41491349 100644 --- a/app/controllers/news_controller.rb +++ b/app/controllers/news_controller.rb @@ -17,6 +17,7 @@ class NewsController < ApplicationController layout 'base_projects'# by young + include ApplicationHelper before_filter :authorize1, :only => [:show] default_search_scope :news model_object News @@ -136,6 +137,10 @@ class NewsController < ApplicationController @news.safe_attributes = params[:news] @news.save_attachments(params[:attachments]) if @news.save + if params[:asset_id] + ids = params[:asset_id].split(',') + update_kindeditor_assets_owner ids,@news.id,OwnerTypeHelper::NEWS + end render_attachment_warning_if_needed(@news) flash[:notice] = l(:notice_successful_create) redirect_to course_news_index_url(@course) diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 41876e041..8ed9fcfef 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -336,7 +336,7 @@ class ProjectsController < ApplicationController def send_mail_to_member if !params[:mail].blank? && User.find_by_mail(params[:mail].to_s).nil? email = params[:mail] - Mailer.send_invite_in_project(email, @project, User.current).deliver + Mailer.run.send_invite_in_project(email, @project, User.current) @is_zhuce =false flash[:notice] = l(:notice_email_sent, :value => email) else diff --git a/app/controllers/repositories_controller.rb b/app/controllers/repositories_controller.rb index 6c115fc11..01e470c91 100644 --- a/app/controllers/repositories_controller.rb +++ b/app/controllers/repositories_controller.rb @@ -131,7 +131,7 @@ update @project_path=@root_path+"htdocs/"+@repository_name @repository_tag=params[:repository][:upassword] || params[:repository][:password] @repo_name=User.current.login.to_s+"_"+params[:repository][:identifier] - logger.info "htpasswd -mb "+@root_path+"user.passwd "+@repo_name+": "+@repository_tag + logger.info "htpasswd -mb "+@root_path+"htdocs/user.passwd "+@repo_name+": "+@repository_tag logger.info "the value of create repository"+@root_path+": "+@repository_name+": "+@project_path+": "+@repo_name attrs = pickup_extra_info if((@repository_tag!="")&¶ms[:repository_scm]=="Git") @@ -147,9 +147,9 @@ update @repository.project = @project if request.post? && @repository.save if(params[:repository_scm]=="Git") - system "htpasswd -mb "+@root_path+"user.passwd "+@repo_name+" "+@repository_tag + system "htpasswd -mb "+@root_path+"htdocs/user.passwd "+@repo_name+" "+@repository_tag system "echo -e '"+@repo_name+"-write:"+ - " "+@repo_name+"' >> "+@root_path+"group.passwd" + " "+@repo_name+"' >> "+@root_path+"htdocs/group.passwd" system "mkdir "+@root_path+"htdocs/"+User.current.login.to_s system "git init --bare "+@project_path system "mv "+@project_path+"/hooks/post-update{.sample,}" @@ -243,8 +243,8 @@ update if(@repository.type=="Repository::Git") logger.info "destory the repository value"+"root path"+@root_path+"repo_name"+@repo_name+ "repository_name"+@repository_name+"user group"+@middle - system "sed -i /"+@repo_name+"/{d} "+@root_path+"user.passwd" - system "sed -i /"+@middle+"/{d} "+@root_path+"group.passwd" + system "sed -i /"+@repo_name+"/{d} "+@root_path+"htdocs/user.passwd" + system "sed -i /"+@middle+"/{d} "+@root_path+"htdocs/group.passwd" system "rm -r "+@root_path+"htdocs/"+@repository_name # if(@sed_user&&@sed_group&&@remove) # else diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 4db471ab4..a230688dc 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -553,7 +553,7 @@ class UsersController < ApplicationController @user.pref.save @user.notified_project_ids = (@user.mail_notification == 'selected' ? params[:notified_project_ids] : []) - Mailer.account_information(@user, params[:user][:password]).deliver if params[:send_information] + Mailer.run.account_information(@user, params[:user][:password]) if params[:send_information] respond_to do |format| format.html { @@ -620,9 +620,9 @@ class UsersController < ApplicationController @user.notified_project_ids = (@user.mail_notification == 'selected' ? params[:notified_project_ids] : []) if was_activated - Mailer.account_activated(@user).deliver + Mailer.run.account_activated(@user) elsif @user.active? && params[:send_information] && !params[:user][:password].blank? && @user.auth_source_id.nil? - Mailer.account_information(@user, params[:user][:password]).deliver + Mailer.run.account_information(@user, params[:user][:password]) end respond_to do |format| diff --git a/app/controllers/words_controller.rb b/app/controllers/words_controller.rb index 2fc4fef9a..680a13963 100644 --- a/app/controllers/words_controller.rb +++ b/app/controllers/words_controller.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #####leave message fq class WordsController < ApplicationController - + include ApplicationHelper before_filter :find_user, :only => [:new, :create, :destroy, :more, :back] def create if params[:new_form][:user_message].size>0 @@ -209,6 +209,10 @@ class WordsController < ApplicationController message = params[:new_form][:course_message] feedback = Course.add_new_jour(user, message, params[:id]) if(feedback.errors.empty?) + if params[:asset_id] + ids = params[:asset_id].split(',') + update_kindeditor_assets_owner ids,feedback[:id],OwnerTypeHelper::JOURNALSFORMESSAGE + end redirect_to course_feedback_url(params[:id]), notice: l(:label_feedback_success) else flash[:error] = feedback.errors.full_messages[0] diff --git a/app/controllers/zipdown_controller.rb b/app/controllers/zipdown_controller.rb index 9880a6382..df354f107 100644 --- a/app/controllers/zipdown_controller.rb +++ b/app/controllers/zipdown_controller.rb @@ -5,7 +5,7 @@ class ZipdownController < ApplicationController #检查权限 #勿删 before_filter :authorize, :only => [:assort,:download_user_homework] SAVE_FOLDER = "#{Rails.root}/files" - OUTPUT_FOLDER = "#{Rails.root}/tmp/archiveZip" + OUTPUT_FOLDER = "#{Rails.root}/files/archiveZip" #统一下载功能 def download @@ -16,9 +16,11 @@ class ZipdownController < ApplicationController end end + #一个作业下所有文件打包下载,只有admin和课程老师有权限 def assort if params[:obj_class] == "Bid" bid = Bid.find params[:obj_id] + render_403 if User.current.allowed_to?(:as_teacher,bid.courses.first) file_count = 0 bid.homeworks.map { |homework| file_count += homework.attachments.count} if file_count > 0 @@ -56,9 +58,9 @@ class ZipdownController < ApplicationController if homework != nil unless homework.attachments.empty? zipfile = zip_homework_by_user homework - send_file zipfile, :filename => ((homework.user.user_extensions.nil? || homework.user.user_extensions.student_id.nil?) ? "" : homework.user.user_extensions.student_id) + + send_file zipfile.file_path, :filename => ((homework.user.user_extensions.nil? || homework.user.user_extensions.student_id.nil?) ? "" : homework.user.user_extensions.student_id) + "_" + (homework.user.lastname.nil? ? "" : homework.user.lastname) + (homework.user.firstname.nil? ? "" : homework.user.firstname) + - "_" + homework.name + ".zip", :type => detect_content_type(zipfile) if(zipfile) + "_" + homework.name + ".zip", :type => detect_content_type(zipfile.file_path) if(zipfile) else render file: 'public/no_file_found.html' end @@ -88,85 +90,116 @@ class ZipdownController < ApplicationController def zip_bid(bid) # Todo: User Access Controll bid_homework_path = [] + digests = [] bid.homeworks.each do |homeattach| unless homeattach.attachments.empty? - bid_homework_path << zip_homework_by_user(homeattach) + out_file = zip_homework_by_user(homeattach) + bid_homework_path << out_file.file_path + digests << out_file.file_digest end end - zips = split_pack_files(bid_homework_path, Setting.pack_attachment_max_size.to_i*1024) - x = 0 + homework_id = bid.id + user_id = bid.author_id - zips.each { |o| - x += 1 - file = zipping "#{Time.now.to_i}_#{bid.name}_#{x}.zip", o[:files], OUTPUT_FOLDER - o[:real_file] = file - o[:file] = File.basename(file) - o[:size] = (File.size(file) / 1024.0 / 1024.0).round(2) + out_file = find_or_pack(homework_id, user_id, digests.sort){ + zipping("#{Time.now.to_i}_#{bid.name}.zip", + bid_homework_path, OUTPUT_FOLDER) } + + # zips = split_pack_files(bid_homework_path, Setting.pack_attachment_max_size.to_i*1024) + # x = 0 + # + # + # zips.each { |o| + # x += 1 + # file = zipping "#{Time.now.to_i}_#{bid.name}_#{x}.zip", o[:files], OUTPUT_FOLDER + # o[:real_file] = file + # o[:file] = File.basename(file) + # o[:size] = (File.size(file) / 1024.0 / 1024.0).round(2) + # } + + [{files:[out_file.file_path], count: 1, index: 1, + real_file: out_file.file_path, file: File.basename(out_file.file_path), + size:(out_file.pack_size / 1024.0 / 1024.0).round(2) + }] end - def zip_homework_by_user(homeattach) + def zip_homework_by_user(homework_attach) homeworks_attach_path = [] not_exist_file = [] # 需要将所有homework.attachments遍历加入zip - # 并且返回zip路径 - homeattach.attachments.each do |attach| + + + digests = [] + homework_attach.attachments.each do |attach| if File.exist?(attach.diskfile) homeworks_attach_path << attach.diskfile + digests << attach.digest else not_exist_file << attach.filename + digests << 'not_exist_file' end end - zipping("#{homeattach.user.lastname}#{homeattach.user.firstname}_#{((homeattach.user.user_extensions.nil? || homeattach.user.user_extensions.student_id.nil?) ? "" : homeattach.user.user_extensions.student_id)}_#{Time.now.to_i.to_s}.zip", homeworks_attach_path, OUTPUT_FOLDER, true, not_exist_file) + + out_file = find_or_pack(homework_attach.bid_id, homework_attach.user_id, digests.sort){ + zipping("#{homework_attach.user.lastname}#{homework_attach.user.firstname}_#{((homework_attach.user.user_extensions.nil? || homework_attach.user.user_extensions.student_id.nil?) ? "" : homework_attach.user.user_extensions.student_id)}_#{Time.now.to_i.to_s}.zip", + homeworks_attach_path, OUTPUT_FOLDER, true, not_exist_file) + } + end - def zipping(zip_name_refer, files_paths, output_path, is_attachment=false, not_exist_file=[]) - # 输入待打包的文件列表,已经打包文件定位到ouput_path - ic = Iconv.new('GBK//IGNORE', 'UTF-8//IGNORE') + def find_or_pack(homework_id, user_id, digests) + raise "please given a pack block" unless block_given? - rename_zipfile = zip_name_refer ||= "#{Time.now.to_i.to_s}.zip" - zipfile_name = "#{output_path}/#{rename_zipfile}" + out_file = ZipPack.packed?(homework_id, user_id, digests.sort) - Dir.mkdir(File.dirname(zipfile_name)) unless File.exist?(File.dirname(zipfile_name)) + unless out_file && out_file.file_valid? + file = yield + + ZipPack.where(homework_id: homework_id, + user_id: user_id).delete_all - unless is_attachment - #都是zip合并,没必要再费力压缩了 - Zip.default_compression = Zlib::NO_COMPRESSION + out_file = ZipPack.create(homework_id: homework_id, + user_id: user_id, + file_digest: Trustie::Utils.digest(file), + file_path: file, + pack_size: File.size(file), + file_digests: digests.join(',') + ) else - Zip.default_compression = Zlib::DEFAULT_COMPRESSION + out_file.pack_times = out_file.pack_times + 1 + out_file.save end + out_file + end + + + def zipping(zip_name_refer, files_paths, output_path, is_attachment=false, not_exist_file=[]) + rename_zipfile = zip_name_refer ||= "#{Time.now.to_i.to_s}.zip" + zipfile_name = "#{output_path}/#{rename_zipfile}" + + Dir.mkdir(File.dirname(zipfile_name)) unless File.exist?(File.dirname(zipfile_name)) Zip::File.open(zipfile_name, Zip::File::CREATE) do |zipfile| files_paths.each do |filename| - flag = true - index = 1 - rename_file = ic.iconv( (File.basename(filename)) ).to_s - rename_file = ic.iconv( filename_to_real( File.basename(filename))).to_s if is_attachment - + rename_file = File.basename(filename) + rename_file = filename_to_real( File.basename(filename)) if is_attachment begin zipfile.add(rename_file, filename) - flag = false rescue Exception => e - zipfile.get_output_stream('FILE_NOTICE.txt') do |os| - os.write l(:label_file_exist) - end + zipfile.get_output_stream('FILE_NOTICE.txt'){|os| os.write l(:label_file_exist)} next end end unless not_exist_file.empty? - zipfile.get_output_stream('FILE_LOST.txt') do |os| - os.write l(:label_file_lost) + not_exist_file.join(',').to_s - end + zipfile.get_output_stream('FILE_LOST.txt'){|os| os.write l(:label_file_lost) + not_exist_file.join(',').to_s} end end zipfile_name - #rescue Errno => e - # logger.error "[zipdown#zipping] ===> #{e}" - # @error = e end # 合理分配文件打包 diff --git a/app/helpers/account_helper.rb b/app/helpers/account_helper.rb index 8ef2d6095..7ad6fe65b 100644 --- a/app/helpers/account_helper.rb +++ b/app/helpers/account_helper.rb @@ -1,62 +1,62 @@ -# encoding: utf-8 -# -# Redmine - project management software -# Copyright (C) 2006-2013 Jean-Philippe Lang -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - -module AccountHelper - - def email_activation_register(user, &block) - token = Token.new(:user => user, :action => "register") - if user.save and token.save - UserStatus.create(:user_id => user.id, :changsets_count => 0, :watchers_count => 0) - Mailer.register(token).deliver - #flash[:notice] = l(:notice_account_register_done) - #render action: 'email_valid', locals: {:mail => user.mail} - else - yield if block_given? - end - user - end - - def automatically_register(user, &block) - # Automatic activation - user.activate - user.last_login_on = Time.now - if user.save - UserStatus.create(:user_id => user.id, :changsets_count => 0, :watchers_count => 0) - #self.logged_user = user - #flash[:notice] = l(:notice_account_activated) - #redirect_to my_account_url - else - yield if block_given? - end - user - end - - def administrator_manually__register(user, &block) - if user.save - UserStatus.create(:user_id => user.id ,:changsets_count => 0, :watchers_count => 0) - # Sends an email to the administrators - Mailer.account_activation_request(user).deliver - #account_pending - else - yield if block_given? - end - user - end - -end +# encoding: utf-8 +# +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +module AccountHelper + + def email_activation_register(user, &block) + token = Token.new(:user => user, :action => "register") + if user.save and token.save + UserStatus.create(:user_id => user.id, :changsets_count => 0, :watchers_count => 0) + Mailer.run.register(token) + #flash[:notice] = l(:notice_account_register_done) + #render action: 'email_valid', locals: {:mail => user.mail} + else + yield if block_given? + end + user + end + + def automatically_register(user, &block) + # Automatic activation + user.activate + user.last_login_on = Time.now + if user.save + UserStatus.create(:user_id => user.id, :changsets_count => 0, :watchers_count => 0) + #self.logged_user = user + #flash[:notice] = l(:notice_account_activated) + #redirect_to my_account_url + else + yield if block_given? + end + user + end + + def administrator_manually__register(user, &block) + if user.save + UserStatus.create(:user_id => user.id ,:changsets_count => 0, :watchers_count => 0) + # Sends an email to the administrators + Mailer.run.account_activation_request(user) + #account_pending + else + yield if block_given? + end + user + end + +end diff --git a/app/helpers/activities_helper.rb b/app/helpers/activities_helper.rb index c15d89e0c..ede2ed78a 100644 --- a/app/helpers/activities_helper.rb +++ b/app/helpers/activities_helper.rb @@ -1,33 +1,45 @@ -# encoding: utf-8 -# -# Redmine - project management software -# Copyright (C) 2006-2013 Jean-Philippe Lang -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - -module ActivitiesHelper - def sort_activity_events(events) - events_by_group = events.group_by(&:event_group) - sorted_events = [] - events.sort {|x, y| y.event_datetime <=> x.event_datetime}.each do |event| - if group_events = events_by_group.delete(event.event_group) - group_events.sort {|x, y| y.event_datetime <=> x.event_datetime}.each_with_index do |e, i| - sorted_events << [e, i > 0] unless e.event_description.nil? - end - end - end - sorted_events - end -end +# encoding: utf-8 +# +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +module ActivitiesHelper + def sort_activity_events(events) + events_by_group = events.group_by(&:event_group) + sorted_events = [] + events.sort {|x, y| y.event_datetime <=> x.event_datetime}.each do |event| + if group_events = events_by_group.delete(event.event_group) + group_events.sort {|x, y| y.event_datetime <=> x.event_datetime}.each_with_index do |e, i| + sorted_events << [e, i > 0] unless e.event_description.nil? + end + end + end + sorted_events + end + def sort_activity_events_course(events) + events_by_group = events.group_by(&:event_group) + sorted_events = [] + events.sort {|x, y| y.event_datetime <=> x.event_datetime}.each do |event| + if group_events = events_by_group.delete(event.event_group) + group_events.sort {|x, y| y.event_datetime <=> x.event_datetime}.each_with_index do |e, i| + sorted_events << e unless e.event_description.nil? + end + end + end + sorted_events + end +end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 5bff62dcd..7fb0dcb7e 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -53,6 +53,10 @@ module ApplicationHelper # Author lizanle # Description after save后需要进行资源记录的更新 # owner_type = 1 对应的是 memo + # owner_type = 2 对应的是forum + # owner_type = 3 对应的是message + # owner_type = 4 对应的是news + # owner_type = 5 对应的是comment def update_kindeditor_assets_owner ids,owner_id,owner_type ids.each do |id| asset = Kindeditor::Asset.find(id.to_i) @@ -1676,6 +1680,42 @@ module ApplicationHelper courses_doing end + + def attachment_candown attachment + candown = false + if attachment.container + if attachment.container.class.to_s != "HomeworkAttach" && (attachment.container.has_attribute?(:project) || attachment.container.has_attribute?(:project_id)) && attachment.container.project + project = attachment.container.project + candown= User.current.member_of?(project) || (project.is_public && attachment.is_public == 1) + elsif attachment.container.is_a?(Project) + project = attachment.container + candown= User.current.member_of?(project) || (project.is_public && attachment.is_public == 1) + elsif (attachment.container.has_attribute?(:board) || attachment.container.has_attribute?(:board_id)) && attachment.container.board && + attachment.container.board.project + project = attachment.container.board.project + candown = User.current.member_of?(project) || (project.is_public && attachment.is_public == 1) + elsif (attachment.container.has_attribute?(:course) ||attachment.container.has_attribute?(:course_id) ) && attachment.container.course + course = attachment.container.course + candown = User.current.member_of_course?(course) || (course.is_public==1 && attachment.is_public == 1) + elsif attachment.container.is_a?(Course) + course = attachment.container + candown= User.current.member_of_course?(course) || (course.is_public==1 && attachment.is_public == 1) + elsif (attachment.container.has_attribute?(:board) || attachment.container.has_attribute?(:board_id)) && attachment.container.board && + attachment.container.board.course + course = attachment.container.board.course + candown= User.current.member_of_course?(course) || (course.is_public==1 && attachment.is_public == 1) + elsif attachment.container.class.to_s=="HomeworkAttach" && attachment.container.bid.reward_type == 3 + candown = true + elsif attachment.container_type == "Bid" && attachment.container && attachment.container.courses + course = attachment.container.courses.first + candown = User.current.member_of_course?(attachment.container.courses.first) || (course.is_public == 1 && attachment.is_public == 1) + else + candown = (attachment.is_public == 1 || attachment.is_public == true) + end + end + candown + end + private def wiki_helper diff --git a/app/helpers/attachments_helper.rb b/app/helpers/attachments_helper.rb index 529a00191..be24629af 100644 --- a/app/helpers/attachments_helper.rb +++ b/app/helpers/attachments_helper.rb @@ -87,6 +87,7 @@ module AttachmentsHelper end end + #判断课程course中是否包含课件attachment,course中引用了attachment也算作包含 def course_contains_attachment? course,attachment course.attachments.each do |att| if att.id == attachment.id || (!att.copy_from.nil? && !attachment.copy_from.nil? && att.copy_from == attachment.copy_from) || att.copy_from == attachment.id || att.id == attachment.copy_from @@ -95,6 +96,15 @@ module AttachmentsHelper end false end + #判断项目project中是否包含课件attachment,project中引用了attachment也算作包含 + def project_contains_attachment? project,attachment + project.attachments.each do |att| + if att.id == attachment.id || (!att.copy_from.nil? && !attachment.copy_from.nil? && att.copy_from == attachment.copy_from) || att.copy_from == attachment.id || att.id == attachment.copy_from + return true + end + end + false + end def get_qute_number attachment if attachment.copy_from diff --git a/app/helpers/courses_helper.rb b/app/helpers/courses_helper.rb index 408783934..3adcdb09e 100644 --- a/app/helpers/courses_helper.rb +++ b/app/helpers/courses_helper.rb @@ -632,7 +632,7 @@ module CoursesHelper #获取课程动态 def get_course_activity courses, activities @course_ids=activities.keys() - + @bid_ids = [] days = Setting.activity_days_default.to_i date_to ||= Date.today + 1 date_from = date_to - days-1.years @@ -653,8 +653,27 @@ module CoursesHelper activities[news.course_id]+=1 end + #feedbackc_count + JournalsForMessage.where(jour_id: @course_ids, jour_type: Course).each do |jourformess| + activities[jourformess.jour_id]+=1 + end + + #homework_count + #HomeworkForCourse.where(course_id: @course_ids).each do |homework| + # @bid_ids<?",date_from).count + + #end + + #@bid_ids.each do |bid_id| + # activities[] +=Bid.where(id: bid_id ).where("created_on>?",date_from).count + + #end + + + # 动态数 + 1 ( 某某创建了该课程 ) - activities.each_pair { |key, value| activities[key] = value + 1 } + # activities.each_pair { |key, value| activities[key] = value + 1 } return activities end @@ -736,4 +755,14 @@ module CoursesHelper "未启用匿评".html_safe end end + + def visable_attachemnts_incourse course + result = [] + course.attachments.each do |attachment| + if attachment.is_public? || User.current.member_of_course?(course) + result << attachment + end + end + result + end end diff --git a/app/helpers/files_helper.rb b/app/helpers/files_helper.rb index 884ebc2eb..29a2e57cb 100644 --- a/app/helpers/files_helper.rb +++ b/app/helpers/files_helper.rb @@ -1,157 +1,144 @@ -# encoding: utf-8 -module FilesHelper - include AttachmentsHelper - - def downloadAll containers - paths = [] - files = [] - tmpfile = "tmp.zip" - - containers.each do |container| - next if container.attachments.empty? - if container.is_a?(Version);end - container.attachments.each do |attachment| - paths << attachment.diskfile - file = attachment.diskfile - # logger.error "[FilesHelper] downloadAll: #{e}" - begin - File.new(file, "r") - rescue Exception => e - logger.error e - next - end - files << file - # zip.add(file.path.dup.sub(directory, ''), file.path) - end - end - - zipfile_name = "archive.zip" - if File.exists? File.open(zipfile_name, "w+") - ff = File.open(zipfile_name, "w+") - ff.close - File.delete ff - end - Zip::ZipFile.open(zipfile_name, Zip::ZipFile::CREATE) do |zipfile| - files.each do |filename| - directory = File.dirname filename - # Two arguments: - # - The name of the file as it will appear in the archive - # - The original file, including the path to find it - dir = filename.sub(directory+"/", '') - zipfile.add(dir, filename) - - end - end - File.new(zipfile_name,'w+') - end - - def courses_check_box_tags(name,courses,current_course,attachment) - s = '' - courses.each do |course| - if !course_contains_attachment?(course,attachment) && is_course_teacher(User.current,course) && course_in_current_or_next_term(course) - s << " [#{get_course_term course}]
" - end - end - s.html_safe - end - - #判断用户是否拥有不包含当前资源的课程,需用户在该课程中角色为教师且该课程属于当前学期或下一学期 - def has_course? user,file - result = false - user.courses.each do |course| - if !course_contains_attachment?(course,file) && is_course_teacher(User.current,course) && course_in_current_or_next_term(course) - return true - end - end - result - end - - # 判断指定的资源时候符合类型 - def isTypeOk(attachment, type, contentType) - result = false - if type != 0 - if attachment.attachtype == type - result = true - end - else - result = true - end - if result - if contentType != '0' && contentType != attachment.suffix_type - result = false - end - end - result - end - - def visable_attachemnts attachments - result = [] - attachments.each do |attachment| - if attachment.is_public? || - (attachment.container_type == "Project" && User.current.member_of?(attachment.project)) || - (attachment.container_type == "Course" && User.current.member_of_course?(Course.find(attachment.container_id)))|| - attachment.author_id == User.current.id - result << attachment - end - end - result - end - - def visable_attachemnts_incourse attachments - result = [] - attachments.each do |attachment| - if attachment.is_public? || (attachment.author.member_of_course?(Course.find(attachment.container_id)))|| attachment.author_id == User.current.id - result << attachment - end - end - result - end - - def visable_attachemnts_insite attachments,course - result = [] - attachments.each do |attachment| - if attachment.is_public? || (attachment.container_type == "Course" && attachment.container_id == course.id && User.current.member_of_course?(Course.find(attachment.container_id)))|| attachment.author_id == User.current.id - result << attachment - end - end - result - end - - def attachment_candown attachment - candown = false - if attachment.container - if attachment.container.class.to_s != "HomeworkAttach" && (attachment.container.has_attribute?(:project) || attachment.container.has_attribute?(:project_id)) && attachment.container.project - project = attachment.container.project - candown= User.current.member_of?(project) || (project.is_public && attachment.is_public == 1) - elsif attachment.container.is_a?(Project) - project = attachment.container - candown= User.current.member_of?(project) || (project.is_public && attachment.is_public == 1) - elsif (attachment.container.has_attribute?(:board) || attachment.container.has_attribute?(:board_id)) && attachment.container.board && - attachment.container.board.project - project = attachment.container.board.project - candown = User.current.member_of?(project) || (project.is_public && attachment.is_public == 1) - elsif (attachment.container.has_attribute?(:course) ||attachment.container.has_attribute?(:course_id) ) && attachment.container.course - course = attachment.container.course - candown = User.current.member_of_course?(course) || (course.is_public==1 && attachment.is_public == 1) - elsif attachment.container.is_a?(Course) - course = attachment.container - candown= User.current.member_of_course?(course) || (course.is_public==1 && attachment.is_public == 1) - elsif (attachment.container.has_attribute?(:board) || attachment.container.has_attribute?(:board_id)) && attachment.container.board && - attachment.container.board.course - course = attachment.container.board.course - candown= User.current.member_of_course?(course) || (course.is_public==1 && attachment.is_public == 1) - elsif attachment.container.class.to_s=="HomeworkAttach" && attachment.container.bid.reward_type == 3 - candown = true - elsif attachment.container_type == "Bid" && attachment.container && attachment.container.courses - course = attachment.container.courses.first - candown = User.current.member_of_course?(attachment.container.courses.first) || (course.is_public == 1 && attachment.is_public == 1) - else - candown = (attachment.is_public == 1 || attachment.is_public == true) - end - end - candown - end - - - +# encoding: utf-8 +module FilesHelper + include AttachmentsHelper + + def downloadAll containers + paths = [] + files = [] + tmpfile = "tmp.zip" + + containers.each do |container| + next if container.attachments.empty? + if container.is_a?(Version);end + container.attachments.each do |attachment| + paths << attachment.diskfile + file = attachment.diskfile + # logger.error "[FilesHelper] downloadAll: #{e}" + begin + File.new(file, "r") + rescue Exception => e + logger.error e + next + end + files << file + # zip.add(file.path.dup.sub(directory, ''), file.path) + end + end + + zipfile_name = "archive.zip" + if File.exists? File.open(zipfile_name, "w+") + ff = File.open(zipfile_name, "w+") + ff.close + File.delete ff + end + Zip::ZipFile.open(zipfile_name, Zip::ZipFile::CREATE) do |zipfile| + files.each do |filename| + directory = File.dirname filename + # Two arguments: + # - The name of the file as it will appear in the archive + # - The original file, including the path to find it + dir = filename.sub(directory+"/", '') + zipfile.add(dir, filename) + + end + end + File.new(zipfile_name,'w+') + end + + #带勾选框的课程列表 + def courses_check_box_tags(name,courses,current_course,attachment) + s = '' + courses.each do |course| + if !course_contains_attachment?(course,attachment) && is_course_teacher(User.current,course) && course_in_current_or_next_term(course) + s << " [#{get_course_term course}]
" + end + end + s.html_safe + end + + #带勾选框的项目列表 + def projects_check_box_tags(name,projects,current_project,attachment) + s = '' + projects.each do |project| + if !project_contains_attachment?(project,attachment) && User.current.allowed_to?(:manage_files, project) + s << "" + end + end + s.html_safe + end + + #判断用户是否拥有不包含当前资源的课程,需用户在该课程中角色为教师且该课程属于当前学期或下一学期 + def has_course? user,file + result = false + user.courses.each do |course| + if !course_contains_attachment?(course,file) && is_course_teacher(User.current,course) && course_in_current_or_next_term(course) + return true + end + end + result + end + + #判断用户是否拥有不包含当前资源的项目,需用户在该项目中有资源管理相关资源 + def has_project? user,file + result = false + user.projects.each do |project| + if !project_contains_attachment?(project,file) && User.current.allowed_to?(:manage_files, project) + return true + end + end + result + end + + # 判断指定的资源时候符合类型 + def isTypeOk(attachment, type, contentType) + result = false + if type != 0 + if attachment.attachtype == type + result = true + end + else + result = true + end + if result + if contentType != '0' && contentType != attachment.suffix_type + result = false + end + end + result + end + + def visable_attachemnts attachments + result = [] + attachments.each do |attachment| + if attachment.is_public? || + (attachment.container_type == "Project" && User.current.member_of?(attachment.project)) || + (attachment.container_type == "Course" && User.current.member_of_course?(Course.find(attachment.container_id)))|| + attachment.author_id == User.current.id + result << attachment + end + end + result + end + + def visable_attachemnts_insite attachments,obj + result = [] + if obj.is_a?(Course) + attachments.each do |attachment| + if attachment.is_public? || (attachment.container_type == "Course" && attachment.container_id == obj.id && User.current.member_of_course?(Course.find(attachment.container_id)))|| attachment.author_id == User.current.id + result << attachment + end + end + else if obj.is_a?(Project) + attachments.each do |attachment| + if attachment.is_public? || (attachment.container_type == "Project" && attachment.container_id == obj.id && User.current.member_of_course?(Project.find(attachment.container_id)))|| attachment.author_id == User.current.id + result << attachment + end + end + end + end + result + end + + + end \ No newline at end of file diff --git a/app/helpers/owner_type_helper.rb b/app/helpers/owner_type_helper.rb new file mode 100644 index 000000000..dd5dbbbac --- /dev/null +++ b/app/helpers/owner_type_helper.rb @@ -0,0 +1,9 @@ +module OwnerTypeHelper + MEMO = 1 + FORUM = 2 + MESSAGE = 3 + NEWS = 4 + COMMENT = 5 + BID = 6 + JOURNALSFORMESSAGE = 7 +end \ No newline at end of file diff --git a/app/models/bid.rb b/app/models/bid.rb index 10476b4d3..ccd6198bb 100644 --- a/app/models/bid.rb +++ b/app/models/bid.rb @@ -17,7 +17,8 @@ class Bid < ActiveRecord::Base HomeworkProject = 2 attr_accessible :author_id, :budget, :deadline, :name, :description, :homework_type, :password include Redmine::SafeAttributes - + include ApplicationHelper + has_many_kindeditor_assets :assets, :dependent => :destroy belongs_to :author, :class_name => 'User', :foreign_key => :author_id belongs_to :course has_many :biding_projects, :dependent => :destroy @@ -47,7 +48,7 @@ class Bid < ActiveRecord::Base validate :validate_user validate :validate_reward_type after_create :act_as_activity - + after_destroy :delete_kindeditor_assets scope :visible, lambda {|*args| nil } @@ -157,5 +158,10 @@ class Bid < ActiveRecord::Base end end - + # Time 2015-04-01 14:19:06 + # Author lizanle + # Description 删除对应课程通知的图片资源 + def delete_kindeditor_assets + delete_kindeditor_assets_from_disk self.id,OwnerTypeHelper::BID + end end diff --git a/app/models/comment.rb b/app/models/comment.rb index 539c62e85..bdb642d3c 100644 --- a/app/models/comment.rb +++ b/app/models/comment.rb @@ -17,8 +17,25 @@ class Comment < ActiveRecord::Base include Redmine::SafeAttributes + include ApplicationHelper + has_many_kindeditor_assets :assets, :dependent => :destroy belongs_to :commented, :polymorphic => true, :counter_cache => true belongs_to :author, :class_name => 'User', :foreign_key => 'author_id' validates_presence_of :commented, :author, :comments safe_attributes 'comments' + after_create :send_mail + + def send_mail + if self.commented.is_a?(News) && Setting.notified_events.include?('news_comment_added') + Mailer.run.news_comment_added(self) + end + end + after_destroy :delete_kindeditor_assets + + # Time 2015-03-31 09:15:06 + # Author lizanle + # Description 删除对应评论的图片资源 + def delete_kindeditor_assets + delete_kindeditor_assets_from_disk self.id,OwnerTypeHelper::COMMENT + end end diff --git a/app/models/comment_observer.rb b/app/models/comment_observer.rb deleted file mode 100644 index a46e53ab9..000000000 --- a/app/models/comment_observer.rb +++ /dev/null @@ -1,27 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2013 Jean-Philippe Lang -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - -class CommentObserver < ActiveRecord::Observer - def after_create(comment) - if comment.commented.is_a?(News) && Setting.notified_events.include?('news_comment_added') - ##by senluo - thread3=Thread.new do - Mailer.news_comment_added(comment).deliver - end - end - end -end diff --git a/app/models/discuss_demo.rb b/app/models/discuss_demo.rb deleted file mode 100644 index 6ed8d15b6..000000000 --- a/app/models/discuss_demo.rb +++ /dev/null @@ -1,4 +0,0 @@ -class DiscussDemo < ActiveRecord::Base - attr_accessible :title, :body - has_many_kindeditor_assets :assets, :dependent => :destroy -end diff --git a/app/models/document.rb b/app/models/document.rb index 33ffdaa2f..6bfb4b8ff 100644 --- a/app/models/document.rb +++ b/app/models/document.rb @@ -1,90 +1,95 @@ -# Redmine - project management software -# Copyright (C) 2006-2013 Jean-Philippe Lang -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - -class Document < ActiveRecord::Base - include Redmine::SafeAttributes - belongs_to :project - belongs_to :user - belongs_to :category, :class_name => "DocumentCategory", :foreign_key => "category_id" - include UserScoreHelper - after_save :be_user_score # user_score - after_destroy :down_user_score - acts_as_attachable :delete_permission => :delete_documents - # 被ForgeActivity虚拟关联 - has_many :forge_acts, :class_name => 'ForgeActivity',:as =>:forge_act ,:dependent => :destroy - # end - acts_as_searchable :columns => ['title', "#{table_name}.description"], :include => :project - acts_as_event :title => Proc.new {|o| "#{l(:label_document)}: #{o.title}"}, - #:author => Proc.new {|o| o.attachments.reorder("#{Attachment.table_name}.created_on ASC").first.try(:author) }, - :author => Proc.new {|o| User.find(o.user_id)}, - :url => Proc.new {|o| {:controller => 'documents', :action => 'show', :id => o.id}} - acts_as_activity_provider :find_options => {:include => :project}, - :is_public => 'documents.is_public' - - validates_presence_of :project, :title, :category - validates_length_of :title, :maximum => 60 - after_create :act_as_forge_activity - scope :visible, lambda {|*args| - includes(:project).where(Project.allowed_to_condition(args.shift || User.current, :view_documents, *args)) - } - - safe_attributes 'category_id', 'title', 'description','is_public' - - def visible?(user=User.current) - !user.nil? && user.allowed_to?(:view_documents, project) - end - - def has_right?(project,user=User.current) - user.admin? || user.member_of?(project) || self.is_public==1 - end - - def initialize(attributes=nil, *args) - super - if new_record? - self.category ||= DocumentCategory.default - end - end - - def updated_on - unless @updated_on - a = attachments.last - @updated_on = (a && a.created_on) || created_on - end - @updated_on - end - - # update user score - def be_user_score - UserScore.project(:push_document, self.user,self,{ document_id: self.id }) - update_document(self.user,1) - update_document(self.user,2,self.project) - end - - def down_user_score - update_document(self.user,1) - update_document(self.user,2,self.project) - end - - # Time 2015-03-02 10:51:16 - # Author lizanle - # Description 新创建的document要在公共表ForgeActivity中记录 - def act_as_forge_activity - self.forge_acts << ForgeActivity.new(:user_id => self.user_id, - :project_id => self.project_id) - end - -end +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class Document < ActiveRecord::Base + include Redmine::SafeAttributes + belongs_to :project + belongs_to :user + belongs_to :category, :class_name => "DocumentCategory", :foreign_key => "category_id" + include UserScoreHelper + after_save :be_user_score # user_score + after_destroy :down_user_score + acts_as_attachable :delete_permission => :delete_documents + after_create :send_mail + # 被ForgeActivity虚拟关联 + has_many :forge_acts, :class_name => 'ForgeActivity',:as =>:forge_act ,:dependent => :destroy + # end + acts_as_searchable :columns => ['title', "#{table_name}.description"], :include => :project + acts_as_event :title => Proc.new {|o| "#{l(:label_document)}: #{o.title}"}, + #:author => Proc.new {|o| o.attachments.reorder("#{Attachment.table_name}.created_on ASC").first.try(:author) }, + :author => Proc.new {|o| User.find(o.user_id)}, + :url => Proc.new {|o| {:controller => 'documents', :action => 'show', :id => o.id}} + acts_as_activity_provider :find_options => {:include => :project}, + :is_public => 'documents.is_public' + + validates_presence_of :project, :title, :category + validates_length_of :title, :maximum => 60 + after_create :act_as_forge_activity + scope :visible, lambda {|*args| + includes(:project).where(Project.allowed_to_condition(args.shift || User.current, :view_documents, *args)) + } + + safe_attributes 'category_id', 'title', 'description','is_public' + + def visible?(user=User.current) + !user.nil? && user.allowed_to?(:view_documents, project) + end + + def has_right?(project,user=User.current) + user.admin? || user.member_of?(project) || self.is_public==1 + end + + def initialize(attributes=nil, *args) + super + if new_record? + self.category ||= DocumentCategory.default + end + end + + def updated_on + unless @updated_on + a = attachments.last + @updated_on = (a && a.created_on) || created_on + end + @updated_on + end + + # update user score + def be_user_score + UserScore.project(:push_document, self.user,self,{ document_id: self.id }) + update_document(self.user,1) + update_document(self.user,2,self.project) + end + + def down_user_score + update_document(self.user,1) + update_document(self.user,2,self.project) + end + + # Time 2015-03-02 10:51:16 + # Author lizanle + # Description 新创建的document要在公共表ForgeActivity中记录 + def act_as_forge_activity + self.forge_acts << ForgeActivity.new(:user_id => self.user_id, + :project_id => self.project_id) + end + + def send_mail + Mailer.run.document_added(self) if Setting.notified_events.include?('document_added') + end + +end diff --git a/app/models/document_observer.rb b/app/models/document_observer.rb deleted file mode 100644 index 447952c70..000000000 --- a/app/models/document_observer.rb +++ /dev/null @@ -1,25 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2013 Jean-Philippe Lang -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - -class DocumentObserver < ActiveRecord::Observer - def after_create(document) - ##by senluo - thread2=Thread.new do - Mailer.document_added(document).deliver if Setting.notified_events.include?('document_added') - end - end -end diff --git a/app/models/forum.rb b/app/models/forum.rb index 6843ab678..2af1abf9e 100644 --- a/app/models/forum.rb +++ b/app/models/forum.rb @@ -1,59 +1,58 @@ -class Forum < ActiveRecord::Base - include Redmine::SafeAttributes - include ApplicationHelper - has_many_kindeditor_assets :assets, :dependent => :destroy - has_many :topics, :class_name => 'Memo', :conditions => "#{Memo.table_name}.parent_id IS NULL", :order => "#{Memo.table_name}.created_at DESC", :dependent => :destroy - has_many :memos, :dependent => :destroy, conditions: "parent_id IS NULL" - belongs_to :creator, :class_name => "User", :foreign_key => 'creator_id' - safe_attributes 'name', - 'description', - 'topic_count', - 'memo_count', - 'last_memo_id', - 'creator_id', - 'sticky', - 'locked' - validates_presence_of :name, :creator_id, :description - validates_length_of :name, maximum: 50 - #validates_length_of :description, maximum: 255 - validates :name, :uniqueness => true - after_destroy :delete_kindeditor_assets - acts_as_taggable - scope :by_join_date, order("created_at DESC") - #after_create :send_email - def reset_counters! - self.class.reset_counters!(id) - end - - def editable_by? user - # user && user.logged? || (self.author == usr && usr.allowed_to?(:edit_own_messages, project)) - self.creator == user || user.admin? - end - - def destroyable_by? user - # user && user.logged? && Forum.find(self.forum_id).creator_id == user.id || user.admin? - self.creator == user || user.admin? - end - - def send_email - Thread.start do - Mailer.forum_add(self).deliver if Setting.notified_events.include?('forum_add') - end - end - # Updates topic_count, memo_count and last_memo_id attributes for +board_id+ - def self.reset_counters!(forum_id) - forum_id = forum_id.to_i - update_all("topic_count = (SELECT COUNT(*) FROM #{Memo.table_name} WHERE forum_id=#{forum_id} AND parent_id IS NULL)," + - " memo_count = (SELECT COUNT(*) FROM #{Memo.table_name} WHERE forum_id=#{forum_id} AND parent_id IS NOT NULL)," + - " last_memo_id = (SELECT MAX(id) FROM #{Memo.table_name} WHERE forum_id=#{forum_id})", - ["id = ?", forum_id]) - end - - # Time 2015-03-26 15:50:54 - # Author lizanle - # Description 删除论坛后删除对应的资源 - def delete_kindeditor_assets - delete_kindeditor_assets_from_disk self.id,2 - end - -end +class Forum < ActiveRecord::Base + include Redmine::SafeAttributes + include ApplicationHelper + has_many_kindeditor_assets :assets, :dependent => :destroy + has_many :topics, :class_name => 'Memo', :conditions => "#{Memo.table_name}.parent_id IS NULL", :order => "#{Memo.table_name}.created_at DESC", :dependent => :destroy + has_many :memos, :dependent => :destroy, conditions: "parent_id IS NULL" + belongs_to :creator, :class_name => "User", :foreign_key => 'creator_id' + safe_attributes 'name', + 'description', + 'topic_count', + 'memo_count', + 'last_memo_id', + 'creator_id', + 'sticky', + 'locked' + validates_presence_of :name, :creator_id, :description + validates_length_of :name, maximum: 50 + #validates_length_of :description, maximum: 255 + validates :name, :uniqueness => true + after_destroy :delete_kindeditor_assets + acts_as_taggable + scope :by_join_date, order("created_at DESC") + after_create :send_mail + def reset_counters! + self.class.reset_counters!(id) + end + + def editable_by? user + # user && user.logged? || (self.author == usr && usr.allowed_to?(:edit_own_messages, project)) + self.creator == user || user.admin? + end + + def destroyable_by? user + # user && user.logged? && Forum.find(self.forum_id).creator_id == user.id || user.admin? + self.creator == user || user.admin? + end + + def send_mail + logger.debug "send mail for forum add." + Mailer.run.forum_add(self) if Setting.notified_events.include?('forum_add') + end + # Updates topic_count, memo_count and last_memo_id attributes for +board_id+ + def self.reset_counters!(forum_id) + forum_id = forum_id.to_i + update_all("topic_count = (SELECT COUNT(*) FROM #{Memo.table_name} WHERE forum_id=#{forum_id} AND parent_id IS NULL)," + + " memo_count = (SELECT COUNT(*) FROM #{Memo.table_name} WHERE forum_id=#{forum_id} AND parent_id IS NOT NULL)," + + " last_memo_id = (SELECT MAX(id) FROM #{Memo.table_name} WHERE forum_id=#{forum_id})", + ["id = ?", forum_id]) + end + + # Time 2015-03-26 15:50:54 + # Author lizanle + # Description 删除论坛后删除对应的资源 + def delete_kindeditor_assets + delete_kindeditor_assets_from_disk self.id,OwnerTypeHelper::FORUM + end + +end diff --git a/app/models/forum_observer.rb b/app/models/forum_observer.rb deleted file mode 100644 index 6afcac824..000000000 --- a/app/models/forum_observer.rb +++ /dev/null @@ -1,8 +0,0 @@ -class ForumObserver < ActiveRecord::Observer - # def after_create(forum) - # Thread.start do - # Mailer.forum_add(forum).deliver if Setting.notified_events.include?('forum_add') - # end - # - # end -end diff --git a/app/models/issue_observer.rb b/app/models/issue_observer.rb index e404a4a1c..ea78349af 100644 --- a/app/models/issue_observer.rb +++ b/app/models/issue_observer.rb @@ -1,30 +1,28 @@ -# Redmine - project management software -# Copyright (C) 2006-2013 Jean-Philippe Lang -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - -class IssueObserver < ActiveRecord::Observer - - def after_create(issue) - Thread.start do - # 将跟踪者与本项目的其他成员都设为收件方,并去重,不在进行抄送, - recipients = issue.recipients - issue.watcher_recipients + issue.watcher_recipients - recipients.each do |rec| - Mailer.issue_add(issue,rec).deliver if Setting.notified_events.include?('issue_added') - end - end - - end -end +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class IssueObserver < ActiveRecord::Observer + + def after_create(issue) + # 将跟踪者与本项目的其他成员都设为收件方,并去重,不在进行抄送, + recipients = issue.recipients - issue.watcher_recipients + issue.watcher_recipients + recipients.each do |rec| + Mailer.run.issue_add(issue,rec) if Setting.notified_events.include?('issue_added') + end + + end +end diff --git a/app/models/journal_observer.rb b/app/models/journal_observer.rb index b58464a9b..c5b0e496b 100644 --- a/app/models/journal_observer.rb +++ b/app/models/journal_observer.rb @@ -1,36 +1,34 @@ -# Redmine - project management software -# Copyright (C) 2006-2013 Jean-Philippe Lang -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - -class JournalObserver < ActiveRecord::Observer - def after_create(journal) - if journal.notify? && - (Setting.notified_events.include?('issue_updated') || - (Setting.notified_events.include?('issue_note_added') && journal.notes.present?) || - (Setting.notified_events.include?('issue_status_updated') && journal.new_status.present?) || - (Setting.notified_events.include?('issue_priority_updated') && journal.new_value_for('priority_id').present?) - ) - Thread.start do - # 将跟踪者与本项目的其他成员都设为收件方,并去重,不在进行抄送, - recipients = journal.recipients - journal.watcher_recipients + journal.watcher_recipients - recipients.each do |rec| - - Mailer.issue_edit(journal,rec).deliver - end - end - end - end -end +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class JournalObserver < ActiveRecord::Observer + def after_create(journal) + if journal.notify? && + (Setting.notified_events.include?('issue_updated') || + (Setting.notified_events.include?('issue_note_added') && journal.notes.present?) || + (Setting.notified_events.include?('issue_status_updated') && journal.new_status.present?) || + (Setting.notified_events.include?('issue_priority_updated') && journal.new_value_for('priority_id').present?) + ) + # 将跟踪者与本项目的其他成员都设为收件方,并去重,不在进行抄送, + recipients = journal.recipients - journal.watcher_recipients + journal.watcher_recipients + recipients.each do |rec| + + Mailer.run.issue_edit(journal,rec) + end + end + end +end diff --git a/app/models/journals_for_message.rb b/app/models/journals_for_message.rb index 23d9c7753..e96bd93cd 100644 --- a/app/models/journals_for_message.rb +++ b/app/models/journals_for_message.rb @@ -1,167 +1,174 @@ -# fq -# 数据库字段中带有m前缀和is_readed是二次开发添加,之前的字段基本复用 -# 注意reply_id 是提到人的id,不是留言id, Base中叫做 at_user -class JournalsForMessage < ActiveRecord::Base - include Redmine::SafeAttributes - include UserScoreHelper - safe_attributes "jour_type", # 留言所属类型 - "jour_id", # 留言所属类型的id - "notes", # 留言内容 - "reply_id", # 留言被回复留言者的用户id(用户a回复了用户b,这是b的id,用以查询谁给b留言了) - "status", # 留言是否被查看(弃用) - "user_id", # 留言者的id - "m_parent_id", # 留言信息的父留言id - "is_readed", # 留言是否已读 - "m_reply_count", # 留言的回复数量 - "m_reply_id" # 回复某留言的留言id(a留言回复了b留言,这是b留言的id) - "is_comprehensive_evaluation" # 1 教师评论、2 匿评、3 留言 - acts_as_tree :foreign_key => 'm_parent_id', :counter_cache => :m_reply_count, :order => "#{JournalsForMessage.table_name}.created_on ASC" - - belongs_to :project, - :foreign_key => 'jour_id', - :conditions => "#{self.table_name}.jour_type = 'Project' " - belongs_to :course, - :foreign_key => 'jour_id' - - - belongs_to :jour, :polymorphic => true - belongs_to :user - belongs_to :homework_attach - belongs_to :at_user, :class_name => "User", :foreign_key => 'reply_id' - - acts_as_event :title => Proc.new {|o| "#{l(:label_my_message)}"}, - :datetime => Proc.new {|o| o.updated_on }, - :author => Proc.new {|o| o.user }, - :description => Proc.new{|o| o.notes }, - :type => Proc.new {|o| o.jour_type }, - :url => Proc.new {|o| - if o.jour.kind_of? Project - {:controller => 'projects', :action => 'feedback', :id => o.jour, :r => o.id, :anchor => "word_li_#{o.id}"} - elsif o.jour.kind_of? Course - {:controller => 'courses', :action => 'feedback', :id => o.jour, :r => o.id, :anchor => "word_li_#{o.id}"} - end - } - acts_as_activity_provider :author_key => :user_id, - :timestamp => "#{self.table_name}.updated_on", - :find_options => {:include => :project } - - acts_as_activity_provider :type => 'course_journals_for_messages', - :author_key => :user_id, - :timestamp => "#{self.table_name}.updated_on", - :find_options => {:include => :course } - - - has_many :acts, :class_name => 'Activity', :as => :act, :dependent => :destroy - - validates :notes, presence: true - after_create :act_as_activity #huang - after_create :reset_counters! - after_destroy :reset_counters! - after_save :be_user_score - after_destroy :down_user_score - - # default_scope { where('m_parent_id IS NULL') } - - def self.create_by_user? user - if user.anonymous? - return false - else - return true - end - end - - - def self.remove_by_user? user - if( self.user == user || - ( self.jour.kind_of?(User) && self.jour== user ) - ) - true - else - false - end - end - - def self.delete_message(message_id) - self.find(message_id).destroy - # self.destroy_all "id = #{message_id}" - end - - def reference_user - User.find(reply_id) - end - - def delete_by_user?user - # 用户可删除自己的留言 - if self.user.id == user.id || user.admin? - return true - else - return false - end - end - - def self.reference_message(user_id) - @user = User.find(user_id) - message = JournalsForMessage.find_by_sql("select * from journals_for_messages where reply_id = #{@user.id} - or (jour_type = 'Bid' and jour_id in (select id from bids where author_id = #{@user.id}))") - message - end - - def act_as_activity - if self.jour_type == 'Principal' - unless self.user_id == self.jour.id && self.user_id != self.reply_id && self.reply_id != 0 - # self.acts << Activity.new(:user_id => self.user_id) - self.acts << Activity.new(:user_id => self.jour_id) - end - elsif self.jour_type == 'Project' - self.acts << Activity.new(:user_id => self.reply_id) - elsif self.jour_type == 'Course' - self.acts << Activity.new(:user_id => self.reply_id) - else - end - end - - def reset_counters! - self.class.reset_counters!(self) - end - def self.reset_counters! journals_for_messages - # jfm_id = journals_for_messages.id.to_i - count = find_all_by_m_parent_id(journals_for_messages.m_parent_id).count #(SELECT COUNT(*) FROM #{JournalsForMessage.table_name} WHERE m_parent_id = #{jfm_id} ) - update_all("m_reply_count = #{count.to_i}", ["id = ?", journals_for_messages.m_parent_id]) - end - - #如果是在项目中留言则返回该项目否则返回nil - zjc - def project - if self.jour_type == 'Project' - Project.find(self.jour_id) - else - nil - end - end - - # 更新用户分数 -by zjc - def be_user_score - #新建了留言回复 - if self.reply_id != 0 - #协同得分加分 - UserScore.joint(:reply_message, self.user,User.find(self.reply_id),self, { journals_for_messages_id: self.id }) - update_replay_for_message(self.user,1) - if self.jour_type == "Project" - update_replay_for_message(self.user,2,self.jour) - end - end - end - # 更新用户分数 -by zjc - def down_user_score - #删除了留言回复 - if self.reply_id != 0 - #协同得分减分 - UserScore.joint(:reply_message_delete, self.user,User.find(self.reply_id), { journals_for_messages_id: self.id }) - update_replay_for_message(self.user,1) - if self.jour_type == "Project" - update_replay_for_message(self.user,2,self.jour) - end - end - end - - -end +# fq +# 数据库字段中带有m前缀和is_readed是二次开发添加,之前的字段基本复用 +# 注意reply_id 是提到人的id,不是留言id, Base中叫做 at_user +class JournalsForMessage < ActiveRecord::Base + include Redmine::SafeAttributes + include UserScoreHelper + include ApplicationHelper + has_many_kindeditor_assets :assets, :dependent => :destroy + safe_attributes "jour_type", # 留言所属类型 + "jour_id", # 留言所属类型的id + "notes", # 留言内容 + "reply_id", # 留言被回复留言者的用户id(用户a回复了用户b,这是b的id,用以查询谁给b留言了) + "status", # 留言是否被查看(弃用) + "user_id", # 留言者的id + "m_parent_id", # 留言信息的父留言id + "is_readed", # 留言是否已读 + "m_reply_count", # 留言的回复数量 + "m_reply_id" # 回复某留言的留言id(a留言回复了b留言,这是b留言的id) + "is_comprehensive_evaluation" # 1 教师评论、2 匿评、3 留言 + acts_as_tree :foreign_key => 'm_parent_id', :counter_cache => :m_reply_count, :order => "#{JournalsForMessage.table_name}.created_on ASC" + after_destroy :delete_kindeditor_assets + belongs_to :project, + :foreign_key => 'jour_id', + :conditions => "#{self.table_name}.jour_type = 'Project' " + belongs_to :course, + :foreign_key => 'jour_id' + + + belongs_to :jour, :polymorphic => true + belongs_to :user + belongs_to :homework_attach + belongs_to :at_user, :class_name => "User", :foreign_key => 'reply_id' + + acts_as_event :title => Proc.new {|o| "#{l(:label_my_message)}"}, + :datetime => Proc.new {|o| o.updated_on }, + :author => Proc.new {|o| o.user }, + :description => Proc.new{|o| o.notes }, + :type => Proc.new {|o| o.jour_type }, + :url => Proc.new {|o| + if o.jour.kind_of? Project + {:controller => 'projects', :action => 'feedback', :id => o.jour, :r => o.id, :anchor => "word_li_#{o.id}"} + elsif o.jour.kind_of? Course + {:controller => 'courses', :action => 'feedback', :id => o.jour, :r => o.id, :anchor => "word_li_#{o.id}"} + end + } + acts_as_activity_provider :author_key => :user_id, + :timestamp => "#{self.table_name}.updated_on", + :find_options => {:include => :project } + + acts_as_activity_provider :type => 'course_journals_for_messages', + :author_key => :user_id, + :timestamp => "#{self.table_name}.updated_on", + :find_options => {:include => :course } + + + has_many :acts, :class_name => 'Activity', :as => :act, :dependent => :destroy + + validates :notes, presence: true + after_create :act_as_activity #huang + after_create :reset_counters! + after_destroy :reset_counters! + after_save :be_user_score + after_destroy :down_user_score + + # default_scope { where('m_parent_id IS NULL') } + + def self.create_by_user? user + if user.anonymous? + return false + else + return true + end + end + + + def self.remove_by_user? user + if( self.user == user || + ( self.jour.kind_of?(User) && self.jour== user ) + ) + true + else + false + end + end + + def self.delete_message(message_id) + self.find(message_id).destroy + # self.destroy_all "id = #{message_id}" + end + + def reference_user + User.find(reply_id) + end + + def delete_by_user?user + # 用户可删除自己的留言 + if self.user.id == user.id || user.admin? + return true + else + return false + end + end + + def self.reference_message(user_id) + @user = User.find(user_id) + message = JournalsForMessage.find_by_sql("select * from journals_for_messages where reply_id = #{@user.id} + or (jour_type = 'Bid' and jour_id in (select id from bids where author_id = #{@user.id}))") + message + end + + def act_as_activity + if self.jour_type == 'Principal' + unless self.user_id == self.jour.id && self.user_id != self.reply_id && self.reply_id != 0 + # self.acts << Activity.new(:user_id => self.user_id) + self.acts << Activity.new(:user_id => self.jour_id) + end + elsif self.jour_type == 'Project' + self.acts << Activity.new(:user_id => self.reply_id) + elsif self.jour_type == 'Course' + self.acts << Activity.new(:user_id => self.reply_id) + else + end + end + + def reset_counters! + self.class.reset_counters!(self) + end + def self.reset_counters! journals_for_messages + # jfm_id = journals_for_messages.id.to_i + count = find_all_by_m_parent_id(journals_for_messages.m_parent_id).count #(SELECT COUNT(*) FROM #{JournalsForMessage.table_name} WHERE m_parent_id = #{jfm_id} ) + update_all("m_reply_count = #{count.to_i}", ["id = ?", journals_for_messages.m_parent_id]) + end + + #如果是在项目中留言则返回该项目否则返回nil - zjc + def project + if self.jour_type == 'Project' + Project.find(self.jour_id) + else + nil + end + end + + # 更新用户分数 -by zjc + def be_user_score + #新建了留言回复 + if self.reply_id != 0 + #协同得分加分 + UserScore.joint(:reply_message, self.user,User.find(self.reply_id),self, { journals_for_messages_id: self.id }) + update_replay_for_message(self.user,1) + if self.jour_type == "Project" + update_replay_for_message(self.user,2,self.jour) + end + end + end + # 更新用户分数 -by zjc + def down_user_score + #删除了留言回复 + if self.reply_id != 0 + #协同得分减分 + UserScore.joint(:reply_message_delete, self.user,User.find(self.reply_id), { journals_for_messages_id: self.id }) + update_replay_for_message(self.user,1) + if self.jour_type == "Project" + update_replay_for_message(self.user,2,self.jour) + end + end + end + + # Time 2015-04-01 14:15:06 + # Author lizanle + # Description 删除对应课程留言的图片资源 + def delete_kindeditor_assets + delete_kindeditor_assets_from_disk self.id,7 + end +end diff --git a/app/models/journals_for_message_observer.rb b/app/models/journals_for_message_observer.rb index 0e5f29d03..0db2e0043 100644 --- a/app/models/journals_for_message_observer.rb +++ b/app/models/journals_for_message_observer.rb @@ -1,9 +1,7 @@ -# Added by young -class JournalsForMessageObserver < ActiveRecord::Observer - def after_create(journals_for_message) - thread1 = Thread.start do - Mailer.journals_for_message_add(User.current, journals_for_message).deliver - end - end -end - +# Added by young +class JournalsForMessageObserver < ActiveRecord::Observer + def after_create(journals_for_message) + Mailer.run.journals_for_message_add(User.current, journals_for_message) + end +end + diff --git a/app/models/mail_handler.rb b/app/models/mail_handler.rb index 8421fb67d..bf268ad66 100644 --- a/app/models/mail_handler.rb +++ b/app/models/mail_handler.rb @@ -1,490 +1,490 @@ -# Redmine - project management software -# Copyright (C) 2006-2013 Jean-Philippe Lang -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - -class MailHandler < ActionMailer::Base - include ActionView::Helpers::SanitizeHelper - include Redmine::I18n - - class UnauthorizedAction < StandardError; end - class MissingInformation < StandardError; end - - attr_reader :email, :user - - def self.receive(email, options={}) - @@handler_options = options.dup - - @@handler_options[:issue] ||= {} - - if @@handler_options[:allow_override].is_a?(String) - @@handler_options[:allow_override] = @@handler_options[:allow_override].split(',').collect(&:strip) - end - @@handler_options[:allow_override] ||= [] - # Project needs to be overridable if not specified - @@handler_options[:allow_override] << 'project' unless @@handler_options[:issue].has_key?(:project) - # Status overridable by default - @@handler_options[:allow_override] << 'status' unless @@handler_options[:issue].has_key?(:status) - - @@handler_options[:no_account_notice] = (@@handler_options[:no_account_notice].to_s == '1') - @@handler_options[:no_notification] = (@@handler_options[:no_notification].to_s == '1') - @@handler_options[:no_permission_check] = (@@handler_options[:no_permission_check].to_s == '1') - - email.force_encoding('ASCII-8BIT') if email.respond_to?(:force_encoding) - super(email) - end - - def logger - Rails.logger - end - - cattr_accessor :ignored_emails_headers - @@ignored_emails_headers = { - 'X-Auto-Response-Suppress' => 'oof', - 'Auto-Submitted' => /^auto-/ - } - - # Processes incoming emails - # Returns the created object (eg. an issue, a message) or false - def receive(email) - @email = email - sender_email = email.from.to_a.first.to_s.strip - # Ignore emails received from the application emission address to avoid hell cycles - if sender_email.downcase == Setting.mail_from.to_s.strip.downcase - if logger && logger.info - logger.info "MailHandler: ignoring email from Redmine emission address [#{sender_email}]" - end - return false - end - # Ignore auto generated emails - self.class.ignored_emails_headers.each do |key, ignored_value| - value = email.header[key] - if value - value = value.to_s.downcase - if (ignored_value.is_a?(Regexp) && value.match(ignored_value)) || value == ignored_value - if logger && logger.info - logger.info "MailHandler: ignoring email with #{key}:#{value} header" - end - return false - end - end - end - @user = User.find_by_mail(sender_email) if sender_email.present? - if @user && !@user.active? - if logger && logger.info - logger.info "MailHandler: ignoring email from non-active user [#{@user.login}]" - end - return false - end - if @user.nil? - # Email was submitted by an unknown user - case @@handler_options[:unknown_user] - when 'accept' - @user = User.anonymous - when 'create' - @user = create_user_from_email - if @user - if logger && logger.info - logger.info "MailHandler: [#{@user.login}] account created" - end - add_user_to_group(@@handler_options[:default_group]) - unless @@handler_options[:no_account_notice] - Mailer.account_information(@user, @user.password).deliver - end - else - if logger && logger.error - logger.error "MailHandler: could not create account for [#{sender_email}]" - end - return false - end - else - # Default behaviour, emails from unknown users are ignored - if logger && logger.info - logger.info "MailHandler: ignoring email from unknown user [#{sender_email}]" - end - return false - end - end - User.current = @user - dispatch - end - - private - - MESSAGE_ID_RE = %r{^ e - # TODO: send a email to the user - logger.error e.message if logger - false - rescue MissingInformation => e - logger.error "MailHandler: missing information from #{user}: #{e.message}" if logger - false - rescue UnauthorizedAction => e - logger.error "MailHandler: unauthorized attempt from #{user}" if logger - false - end - - def dispatch_to_default - receive_issue - end - - # Creates a new issue - def receive_issue - project = target_project - # check permission - unless @@handler_options[:no_permission_check] - raise UnauthorizedAction unless user.allowed_to?(:add_issues, project) - end - - issue = Issue.new(:author => user, :project => project) - issue.safe_attributes = issue_attributes_from_keywords(issue) - issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)} - issue.subject = cleaned_up_subject - if issue.subject.blank? - issue.subject = '(no subject)' - end - issue.description = cleaned_up_text_body - - # add To and Cc as watchers before saving so the watchers can reply to Redmine - add_watchers(issue) - issue.save! - add_attachments(issue) - logger.info "MailHandler: issue ##{issue.id} created by #{user}" if logger && logger.info - issue - end - - # Adds a note to an existing issue - def receive_issue_reply(issue_id, from_journal=nil) - issue = Issue.find_by_id(issue_id) - return unless issue - # check permission - unless @@handler_options[:no_permission_check] - unless user.allowed_to?(:add_issue_notes, issue.project) || - user.allowed_to?(:edit_issues, issue.project) - raise UnauthorizedAction - end - end - - # ignore CLI-supplied defaults for new issues - @@handler_options[:issue].clear - - journal = issue.init_journal(user) - if from_journal && from_journal.private_notes? - # If the received email was a reply to a private note, make the added note private - issue.private_notes = true - end - issue.safe_attributes = issue_attributes_from_keywords(issue) - issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)} - journal.notes = cleaned_up_text_body - add_attachments(issue) - issue.save! - if logger && logger.info - logger.info "MailHandler: issue ##{issue.id} updated by #{user}" - end - journal - end - - # Reply will be added to the issue - def receive_journal_reply(journal_id) - journal = Journal.find_by_id(journal_id) - if journal && journal.journalized_type == 'Issue' - receive_issue_reply(journal.journalized_id, journal) - end - end - - # Receives a reply to a forum message - def receive_message_reply(message_id) - message = Message.find_by_id(message_id) - if message - message = message.root - - unless @@handler_options[:no_permission_check] - raise UnauthorizedAction unless user.allowed_to?(:add_messages, message.project) - end - - if !message.locked? - reply = Message.new(:subject => cleaned_up_subject.gsub(%r{^.*msg\d+\]}, '').strip, - :content => cleaned_up_text_body) - reply.author = user - reply.board = message.board - message.children << reply - add_attachments(reply) - reply - else - if logger && logger.info - logger.info "MailHandler: ignoring reply from [#{sender_email}] to a locked topic" - end - end - end - end - - def add_attachments(obj) - if email.attachments && email.attachments.any? - email.attachments.each do |attachment| - obj.attachments << Attachment.create(:container => obj, - :file => attachment.decoded, - :filename => attachment.filename, - :author => user, - :content_type => attachment.mime_type) - end - end - end - - # Adds To and Cc as watchers of the given object if the sender has the - # appropriate permission - def add_watchers(obj) - if user.allowed_to?("add_#{obj.class.name.underscore}_watchers".to_sym, obj.project) - addresses = [email.to, email.cc].flatten.compact.uniq.collect {|a| a.strip.downcase} - unless addresses.empty? - watchers = User.active.where('LOWER(mail) IN (?)', addresses).all - watchers.each {|w| obj.add_watcher(w)} - end - end - end - - def get_keyword(attr, options={}) - @keywords ||= {} - if @keywords.has_key?(attr) - @keywords[attr] - else - @keywords[attr] = begin - if (options[:override] || @@handler_options[:allow_override].include?(attr.to_s)) && - (v = extract_keyword!(plain_text_body, attr, options[:format])) - v - elsif !@@handler_options[:issue][attr].blank? - @@handler_options[:issue][attr] - end - end - end - end - - # Destructively extracts the value for +attr+ in +text+ - # Returns nil if no matching keyword found - def extract_keyword!(text, attr, format=nil) - keys = [attr.to_s.humanize] - if attr.is_a?(Symbol) - if user && user.language.present? - keys << l("field_#{attr}", :default => '', :locale => user.language) - end - if Setting.default_language.present? - keys << l("field_#{attr}", :default => '', :locale => Setting.default_language) - end - end - keys.reject! {|k| k.blank?} - keys.collect! {|k| Regexp.escape(k)} - format ||= '.+' - keyword = nil - regexp = /^(#{keys.join('|')})[ \t]*:[ \t]*(#{format})\s*$/i - if m = text.match(regexp) - keyword = m[2].strip - text.gsub!(regexp, '') - end - keyword - end - - def target_project - # TODO: other ways to specify project: - # * parse the email To field - # * specific project (eg. Setting.mail_handler_target_project) - target = Project.find_by_identifier(get_keyword(:project)) - raise MissingInformation.new('Unable to determine target project') if target.nil? - target - end - - # Returns a Hash of issue attributes extracted from keywords in the email body - def issue_attributes_from_keywords(issue) - assigned_to = (k = get_keyword(:assigned_to, :override => true)) && find_assignee_from_keyword(k, issue) - - attrs = { - 'tracker_id' => (k = get_keyword(:tracker)) && issue.project.trackers.named(k).first.try(:id), - 'status_id' => (k = get_keyword(:status)) && IssueStatus.named(k).first.try(:id), - 'priority_id' => (k = get_keyword(:priority)) && IssuePriority.named(k).first.try(:id), - 'category_id' => (k = get_keyword(:category)) && issue.project.issue_categories.named(k).first.try(:id), - 'assigned_to_id' => assigned_to.try(:id), - 'fixed_version_id' => (k = get_keyword(:fixed_version, :override => true)) && - issue.project.shared_versions.named(k).first.try(:id), - 'start_date' => get_keyword(:start_date, :override => true, :format => '\d{4}-\d{2}-\d{2}'), - 'due_date' => get_keyword(:due_date, :override => true, :format => '\d{4}-\d{2}-\d{2}'), - 'estimated_hours' => get_keyword(:estimated_hours, :override => true), - 'done_ratio' => get_keyword(:done_ratio, :override => true, :format => '(\d|10)?0') - }.delete_if {|k, v| v.blank? } - - if issue.new_record? && attrs['tracker_id'].nil? - attrs['tracker_id'] = issue.project.trackers.first.try(:id) - end - - attrs - end - - # Returns a Hash of issue custom field values extracted from keywords in the email body - def custom_field_values_from_keywords(customized) - customized.custom_field_values.inject({}) do |h, v| - if keyword = get_keyword(v.custom_field.name, :override => true) - h[v.custom_field.id.to_s] = v.custom_field.value_from_keyword(keyword, customized) - end - h - end - end - - # Returns the text/plain part of the email - # If not found (eg. HTML-only email), returns the body with tags removed - def plain_text_body - return @plain_text_body unless @plain_text_body.nil? - - part = email.text_part || email.html_part || email - @plain_text_body = Redmine::CodesetUtil.to_utf8(part.body.decoded, part.charset) - - # strip html tags and remove doctype directive - @plain_text_body = strip_tags(@plain_text_body.strip) - @plain_text_body.sub! %r{^$/) - addr, name = m[2], m[1] - end - if addr.present? - user = self.class.new_user_from_attributes(addr, name) - if @@handler_options[:no_notification] - user.mail_notification = 'none' - end - if user.save - user - else - logger.error "MailHandler: failed to create User: #{user.errors.full_messages}" if logger - nil - end - else - logger.error "MailHandler: failed to create User: no FROM address found" if logger - nil - end - end - - # Adds the newly created user to default group - def add_user_to_group(default_group) - if default_group.present? - default_group.split(',').each do |group_name| - if group = Group.named(group_name).first - group.users << @user - elsif logger - logger.warn "MailHandler: could not add user to [#{group_name}], group not found" - end - end - end - end - - # Removes the email body of text after the truncation configurations. - def cleanup_body(body) - delimiters = Setting.mail_handler_body_delimiters.to_s.split(/[\r\n]+/).reject(&:blank?).map {|s| Regexp.escape(s)} - unless delimiters.empty? - regex = Regexp.new("^[> ]*(#{ delimiters.join('|') })\s*[\r\n].*", Regexp::MULTILINE) - body = body.gsub(regex, '') - end - body.strip - end - - def find_assignee_from_keyword(keyword, issue) - keyword = keyword.to_s.downcase - assignable = issue.assignable_users - assignee = nil - assignee ||= assignable.detect {|a| - a.mail.to_s.downcase == keyword || - a.login.to_s.downcase == keyword - } - if assignee.nil? && keyword.match(/ /) - firstname, lastname = *(keyword.split) # "First Last Throwaway" - assignee ||= assignable.detect {|a| - a.is_a?(User) && a.firstname.to_s.downcase == firstname && - a.lastname.to_s.downcase == lastname - } - end - if assignee.nil? - assignee ||= assignable.detect {|a| a.name.downcase == keyword} - end - assignee - end -end +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class MailHandler < ActionMailer::Base + include ActionView::Helpers::SanitizeHelper + include Redmine::I18n + + class UnauthorizedAction < StandardError; end + class MissingInformation < StandardError; end + + attr_reader :email, :user + + def self.receive(email, options={}) + @@handler_options = options.dup + + @@handler_options[:issue] ||= {} + + if @@handler_options[:allow_override].is_a?(String) + @@handler_options[:allow_override] = @@handler_options[:allow_override].split(',').collect(&:strip) + end + @@handler_options[:allow_override] ||= [] + # Project needs to be overridable if not specified + @@handler_options[:allow_override] << 'project' unless @@handler_options[:issue].has_key?(:project) + # Status overridable by default + @@handler_options[:allow_override] << 'status' unless @@handler_options[:issue].has_key?(:status) + + @@handler_options[:no_account_notice] = (@@handler_options[:no_account_notice].to_s == '1') + @@handler_options[:no_notification] = (@@handler_options[:no_notification].to_s == '1') + @@handler_options[:no_permission_check] = (@@handler_options[:no_permission_check].to_s == '1') + + email.force_encoding('ASCII-8BIT') if email.respond_to?(:force_encoding) + super(email) + end + + def logger + Rails.logger + end + + cattr_accessor :ignored_emails_headers + @@ignored_emails_headers = { + 'X-Auto-Response-Suppress' => 'oof', + 'Auto-Submitted' => /^auto-/ + } + + # Processes incoming emails + # Returns the created object (eg. an issue, a message) or false + def receive(email) + @email = email + sender_email = email.from.to_a.first.to_s.strip + # Ignore emails received from the application emission address to avoid hell cycles + if sender_email.downcase == Setting.mail_from.to_s.strip.downcase + if logger && logger.info + logger.info "MailHandler: ignoring email from Redmine emission address [#{sender_email}]" + end + return false + end + # Ignore auto generated emails + self.class.ignored_emails_headers.each do |key, ignored_value| + value = email.header[key] + if value + value = value.to_s.downcase + if (ignored_value.is_a?(Regexp) && value.match(ignored_value)) || value == ignored_value + if logger && logger.info + logger.info "MailHandler: ignoring email with #{key}:#{value} header" + end + return false + end + end + end + @user = User.find_by_mail(sender_email) if sender_email.present? + if @user && !@user.active? + if logger && logger.info + logger.info "MailHandler: ignoring email from non-active user [#{@user.login}]" + end + return false + end + if @user.nil? + # Email was submitted by an unknown user + case @@handler_options[:unknown_user] + when 'accept' + @user = User.anonymous + when 'create' + @user = create_user_from_email + if @user + if logger && logger.info + logger.info "MailHandler: [#{@user.login}] account created" + end + add_user_to_group(@@handler_options[:default_group]) + unless @@handler_options[:no_account_notice] + Mailer.run.account_information(@user, @user.password) + end + else + if logger && logger.error + logger.error "MailHandler: could not create account for [#{sender_email}]" + end + return false + end + else + # Default behaviour, emails from unknown users are ignored + if logger && logger.info + logger.info "MailHandler: ignoring email from unknown user [#{sender_email}]" + end + return false + end + end + User.current = @user + dispatch + end + + private + + MESSAGE_ID_RE = %r{^ e + # TODO: send a email to the user + logger.error e.message if logger + false + rescue MissingInformation => e + logger.error "MailHandler: missing information from #{user}: #{e.message}" if logger + false + rescue UnauthorizedAction => e + logger.error "MailHandler: unauthorized attempt from #{user}" if logger + false + end + + def dispatch_to_default + receive_issue + end + + # Creates a new issue + def receive_issue + project = target_project + # check permission + unless @@handler_options[:no_permission_check] + raise UnauthorizedAction unless user.allowed_to?(:add_issues, project) + end + + issue = Issue.new(:author => user, :project => project) + issue.safe_attributes = issue_attributes_from_keywords(issue) + issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)} + issue.subject = cleaned_up_subject + if issue.subject.blank? + issue.subject = '(no subject)' + end + issue.description = cleaned_up_text_body + + # add To and Cc as watchers before saving so the watchers can reply to Redmine + add_watchers(issue) + issue.save! + add_attachments(issue) + logger.info "MailHandler: issue ##{issue.id} created by #{user}" if logger && logger.info + issue + end + + # Adds a note to an existing issue + def receive_issue_reply(issue_id, from_journal=nil) + issue = Issue.find_by_id(issue_id) + return unless issue + # check permission + unless @@handler_options[:no_permission_check] + unless user.allowed_to?(:add_issue_notes, issue.project) || + user.allowed_to?(:edit_issues, issue.project) + raise UnauthorizedAction + end + end + + # ignore CLI-supplied defaults for new issues + @@handler_options[:issue].clear + + journal = issue.init_journal(user) + if from_journal && from_journal.private_notes? + # If the received email was a reply to a private note, make the added note private + issue.private_notes = true + end + issue.safe_attributes = issue_attributes_from_keywords(issue) + issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)} + journal.notes = cleaned_up_text_body + add_attachments(issue) + issue.save! + if logger && logger.info + logger.info "MailHandler: issue ##{issue.id} updated by #{user}" + end + journal + end + + # Reply will be added to the issue + def receive_journal_reply(journal_id) + journal = Journal.find_by_id(journal_id) + if journal && journal.journalized_type == 'Issue' + receive_issue_reply(journal.journalized_id, journal) + end + end + + # Receives a reply to a forum message + def receive_message_reply(message_id) + message = Message.find_by_id(message_id) + if message + message = message.root + + unless @@handler_options[:no_permission_check] + raise UnauthorizedAction unless user.allowed_to?(:add_messages, message.project) + end + + if !message.locked? + reply = Message.new(:subject => cleaned_up_subject.gsub(%r{^.*msg\d+\]}, '').strip, + :content => cleaned_up_text_body) + reply.author = user + reply.board = message.board + message.children << reply + add_attachments(reply) + reply + else + if logger && logger.info + logger.info "MailHandler: ignoring reply from [#{sender_email}] to a locked topic" + end + end + end + end + + def add_attachments(obj) + if email.attachments && email.attachments.any? + email.attachments.each do |attachment| + obj.attachments << Attachment.create(:container => obj, + :file => attachment.decoded, + :filename => attachment.filename, + :author => user, + :content_type => attachment.mime_type) + end + end + end + + # Adds To and Cc as watchers of the given object if the sender has the + # appropriate permission + def add_watchers(obj) + if user.allowed_to?("add_#{obj.class.name.underscore}_watchers".to_sym, obj.project) + addresses = [email.to, email.cc].flatten.compact.uniq.collect {|a| a.strip.downcase} + unless addresses.empty? + watchers = User.active.where('LOWER(mail) IN (?)', addresses).all + watchers.each {|w| obj.add_watcher(w)} + end + end + end + + def get_keyword(attr, options={}) + @keywords ||= {} + if @keywords.has_key?(attr) + @keywords[attr] + else + @keywords[attr] = begin + if (options[:override] || @@handler_options[:allow_override].include?(attr.to_s)) && + (v = extract_keyword!(plain_text_body, attr, options[:format])) + v + elsif !@@handler_options[:issue][attr].blank? + @@handler_options[:issue][attr] + end + end + end + end + + # Destructively extracts the value for +attr+ in +text+ + # Returns nil if no matching keyword found + def extract_keyword!(text, attr, format=nil) + keys = [attr.to_s.humanize] + if attr.is_a?(Symbol) + if user && user.language.present? + keys << l("field_#{attr}", :default => '', :locale => user.language) + end + if Setting.default_language.present? + keys << l("field_#{attr}", :default => '', :locale => Setting.default_language) + end + end + keys.reject! {|k| k.blank?} + keys.collect! {|k| Regexp.escape(k)} + format ||= '.+' + keyword = nil + regexp = /^(#{keys.join('|')})[ \t]*:[ \t]*(#{format})\s*$/i + if m = text.match(regexp) + keyword = m[2].strip + text.gsub!(regexp, '') + end + keyword + end + + def target_project + # TODO: other ways to specify project: + # * parse the email To field + # * specific project (eg. Setting.mail_handler_target_project) + target = Project.find_by_identifier(get_keyword(:project)) + raise MissingInformation.new('Unable to determine target project') if target.nil? + target + end + + # Returns a Hash of issue attributes extracted from keywords in the email body + def issue_attributes_from_keywords(issue) + assigned_to = (k = get_keyword(:assigned_to, :override => true)) && find_assignee_from_keyword(k, issue) + + attrs = { + 'tracker_id' => (k = get_keyword(:tracker)) && issue.project.trackers.named(k).first.try(:id), + 'status_id' => (k = get_keyword(:status)) && IssueStatus.named(k).first.try(:id), + 'priority_id' => (k = get_keyword(:priority)) && IssuePriority.named(k).first.try(:id), + 'category_id' => (k = get_keyword(:category)) && issue.project.issue_categories.named(k).first.try(:id), + 'assigned_to_id' => assigned_to.try(:id), + 'fixed_version_id' => (k = get_keyword(:fixed_version, :override => true)) && + issue.project.shared_versions.named(k).first.try(:id), + 'start_date' => get_keyword(:start_date, :override => true, :format => '\d{4}-\d{2}-\d{2}'), + 'due_date' => get_keyword(:due_date, :override => true, :format => '\d{4}-\d{2}-\d{2}'), + 'estimated_hours' => get_keyword(:estimated_hours, :override => true), + 'done_ratio' => get_keyword(:done_ratio, :override => true, :format => '(\d|10)?0') + }.delete_if {|k, v| v.blank? } + + if issue.new_record? && attrs['tracker_id'].nil? + attrs['tracker_id'] = issue.project.trackers.first.try(:id) + end + + attrs + end + + # Returns a Hash of issue custom field values extracted from keywords in the email body + def custom_field_values_from_keywords(customized) + customized.custom_field_values.inject({}) do |h, v| + if keyword = get_keyword(v.custom_field.name, :override => true) + h[v.custom_field.id.to_s] = v.custom_field.value_from_keyword(keyword, customized) + end + h + end + end + + # Returns the text/plain part of the email + # If not found (eg. HTML-only email), returns the body with tags removed + def plain_text_body + return @plain_text_body unless @plain_text_body.nil? + + part = email.text_part || email.html_part || email + @plain_text_body = Redmine::CodesetUtil.to_utf8(part.body.decoded, part.charset) + + # strip html tags and remove doctype directive + @plain_text_body = strip_tags(@plain_text_body.strip) + @plain_text_body.sub! %r{^$/) + addr, name = m[2], m[1] + end + if addr.present? + user = self.class.new_user_from_attributes(addr, name) + if @@handler_options[:no_notification] + user.mail_notification = 'none' + end + if user.save + user + else + logger.error "MailHandler: failed to create User: #{user.errors.full_messages}" if logger + nil + end + else + logger.error "MailHandler: failed to create User: no FROM address found" if logger + nil + end + end + + # Adds the newly created user to default group + def add_user_to_group(default_group) + if default_group.present? + default_group.split(',').each do |group_name| + if group = Group.named(group_name).first + group.users << @user + elsif logger + logger.warn "MailHandler: could not add user to [#{group_name}], group not found" + end + end + end + end + + # Removes the email body of text after the truncation configurations. + def cleanup_body(body) + delimiters = Setting.mail_handler_body_delimiters.to_s.split(/[\r\n]+/).reject(&:blank?).map {|s| Regexp.escape(s)} + unless delimiters.empty? + regex = Regexp.new("^[> ]*(#{ delimiters.join('|') })\s*[\r\n].*", Regexp::MULTILINE) + body = body.gsub(regex, '') + end + body.strip + end + + def find_assignee_from_keyword(keyword, issue) + keyword = keyword.to_s.downcase + assignable = issue.assignable_users + assignee = nil + assignee ||= assignable.detect {|a| + a.mail.to_s.downcase == keyword || + a.login.to_s.downcase == keyword + } + if assignee.nil? && keyword.match(/ /) + firstname, lastname = *(keyword.split) # "First Last Throwaway" + assignee ||= assignable.detect {|a| + a.is_a?(User) && a.firstname.to_s.downcase == firstname && + a.lastname.to_s.downcase == lastname + } + end + if assignee.nil? + assignee ||= assignable.detect {|a| a.name.downcase == keyword} + end + assignee + end +end diff --git a/app/models/mailer.rb b/app/models/mailer.rb index e1c538fd0..fd22c645c 100644 --- a/app/models/mailer.rb +++ b/app/models/mailer.rb @@ -1,854 +1,908 @@ -# Redmine - project management software -# Copyright (C) 2006-2013 Jean-Philippe Lang -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - -class Mailer < ActionMailer::Base - layout 'mailer' - helper :application - helper :issues - helper :custom_fields - - include Redmine::I18n - include CoursesHelper - def self.default_url_options - { :host => Setting.host_name, :protocol => Setting.protocol } - end - - # author: alan - # 发送邀请未注册用户加入项目邮件 - # 功能: 在加入项目的同时自动注册用户 - def send_invite_in_project(email, project, invitor) - @email = email - @subject = "#{invitor.name} #{l(:label_invite_project)} #{project.name} " - @password = newpass(6) - @project_url = url_for(:controller => 'projects', :action => 'show', :id => project.id, - :password => @password, :login => email) - mail :to => email, :subject => @subject - end - - # author: alan - # 根据用户选择发送个人日报或周报 - # 发送内容: 项目【缺陷,讨论区,新闻】,课程【通知,留言,新闻】, 贴吧, 个人留言 - def send_for_user_activities(user, date_to, days) - date_from = date_to - days.days - - # 生成token用于直接点击登录 - @user = user - token = Token.new(:user =>user , :action => 'autologin') - token.save - @token = token - - @user_url = url_for(my_account_url(user,:token => @token.value)) - # 查询user参加的项目及课程 - projects = user.projects - courses = user.courses - project_ids = projects.map{|project| project.id}.join(",") - course_ids = courses.map {|course| course.id}.join(",") - - # 查询user的缺陷,包括发布的,跟踪的以及被指派的缺陷 - @issues = Issue.find_by_sql("select DISTINCT i.* from issues i, watchers w - where (i.assigned_to_id = #{user.id} or i.author_id = #{user.id} - or (w.watchable_type = 'Issue' and w.watchable_id = i.id and w.user_id = #{user.id})) - and (i.created_on between '#{date_from}' and '#{date_to}') order by i.created_on desc") - - # @bids 查询课程作业,包括老师发布的作业,以及user提交作业 - # @attachments查询课程课件更新 - @attachments ||= [] - - @bids ||= [] # 老师发布的作业 - - unless courses.first.nil? - count = courses.count - count = count - 1 - for i in 0..count do - bids = courses[i].homeworks.where("bids.created_on between '#{date_from}' and '#{date_to}'").order("bids.created_on desc") - attachments = courses[i].attachments.where("attachments.created_on between '#{date_from}' and '#{date_to}'").order('attachments.created_on DESC') - @bids += bids if bids.count > 0 - @attachments += attachments if attachments.count > 0 - end - end - # user 提交的作业 - @homeworks = HomeworkAttach.where("user_id=#{user.id} and (created_at between '#{date_from}' and '#{date_to}')").order("created_at desc") - - # 查询user在课程。项目中发布的讨论帖子 - messages = Message.find_by_sql("select DISTINCT * from messages where author_id = #{user.id} and (created_on between '#{date_from}' and '#{date_to}') order by created_on desc") - @course_messages ||= [] - @project_messages ||= [] - unless messages.first.nil? - messages.each do |msg| - if msg.project - @project_messages << msg - elsif msg.course - @course_messages << msg - end - end - end - # 查询user在课程中发布的通知,项目中发的新闻 - @course_news = News.find_by_sql("select DISTINCT n.* from news n - where n.course_id in (#{course_ids}) - and (created_on between '#{date_from}' and '#{date_to}') order by created_on desc") - @project_news = News.find_by_sql("select DISTINCT n.* from news n where n.project_id in (#{project_ids}) - and (created_on between '#{date_from}' and '#{date_to}') order by created_on desc") - - # 查询user在课程及个人中留言 - @course_journal_messages = JournalsForMessage.find_by_sql("select DISTINCT * from journals_for_messages where - jour_type='Course' and user_id = #{user.id} - and (created_on between '#{date_from}' and '#{date_to}') order by created_on desc") - @user_journal_messages = user.journals_for_messages.where("m_parent_id IS NULL and (created_on between '#{date_from}' and '#{date_to}')").order('created_on DESC') - - - # 查询user新建贴吧或发布帖子 - @forums = Forum.find_by_sql("select DISTINCT * from forums where creator_id = #{user.id} and (created_at between '#{date_from}' and '#{date_to}') order by created_at desc") - @memos = Memo.find_by_sql("select DISTINCT m.* from memos m, forums f where (m.author_id = #{user.id} or (m.forum_id = f.id and f.creator_id = #{user.id})) - and (m.created_at between '#{date_from}' and '#{date_to}') order by m.created_at desc") - if days == 1 - subject = "[ #{user.show_name} : #{l(:label_day_mail)}]" - @subject = " #{user.show_name} : #{date_to - 1.days} #{l(:label_day_mail)}" - else - subject = "[ #{user.show_name} : #{l(:label_week_mail)}]" - @subject = "#{user.show_name} : #{l(:label_week_mail)}" - end - mail :to => user.mail,:subject => subject - - end - - # 公共讨论区发帖、回帖添加邮件发送信息 - def forum_message_added(memo) - @memo = memo - redmine_headers 'Memo' => memo.id - @forum = memo.forum - @author = memo.author - @forum_url = url_for(:controller => 'forums', :action => 'show', :id => @forum.id) - @issue_author_url = url_for(user_activities_url(@author)) - recipients ||= [] - #将帖子创建者邮箱地址加入数组 - if @forum.creator.mail_notification != 'day' && @forum.creator.mail_notification != 'week' - recipients << @forum.creator.mail - end - #回复人邮箱地址加入数组 - if @author.mail_notification != 'day' && @author.mail_notification != 'week' - recipients << @author.mail - end - # cc = wiki_content.page.wiki.watcher_recipients - recipients - - @memo_url = url_for(forum_memo_url(@forum, (@memo.parent_id.nil? ? @memo : @memo.parent_id))) - mail :to => recipients,:subject => "[ #{l(:label_message_plural)} : #{memo.subject} #{l(:label_memo_create_succ)}]" - end - # Builds a Mail::Message object used to email recipients of the added journals for message. - - # 留言分为直接留言,和对留言人留言的回复 - # 字段说明在JournalsForMessage.rb - # 直接留言后 reply_id,m_parent_id 为空,相对应的at_user取值为nil - - def journals_for_message_add(user, journals_for_message) - @user = journals_for_message.user # 留言人 - @mail = journals_for_message.jour if journals_for_message.at_user.nil? # 留言 - @mail = journals_for_message.at_user if journals_for_message.at_user - @message = journals_for_message.notes - @title = "#@user #{t(:label_leave_your_message, :locale => 'zh')}" - @issue_author_url = url_for(user_activities_url(@user)) - @url = case journals_for_message.jour.class.to_s.to_sym # 判断留言的对象所属类型 - when :Bid - course_for_bid_url(journals_for_message.jour, anchor: "word_li_#{journals_for_message.id}") - when :Project - return -1 if journals_for_message.jour.project_type == Project::ProjectType_project - project_feedback_url(journals_for_message.jour, anchor: "word_li_#{journals_for_message.id}") - when :Course - course_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}") - when :User - user_newfeedback_user_url(journals_for_message.jour, anchor: "word_li_#{journals_for_message.id}") - else - Rails.logger.error "[Builds a Mail::Message ERROR] journalsForMessage's jour is unkown type, journalsForMessage.id = #{journals_for_message.id}" - return -1 - end - - # 验证用户的收取邮件的方式 - recipients ||= [] - recipients1 ||= [] - if @mail.mail_notification != 'week' && @mail.mail_notification != 'day' - recipients1 = @mail.mail - end - if journals_for_message.jour.author.mail_notification != 'week' && journals_for_message.jour.author.mail_notification != 'day' - recipients = journals_for_message.jour.author.mail - end - - # modify by nwb - #如果是直接留言并且留言对象是课程 - if !journals_for_message.at_user && journals_for_message.jour.class.to_s.to_sym == :Course - course = journals_for_message.jour - @author = journals_for_message.user - #课程的教师 - @members = course_all_member journals_for_message.jour - #收件人邮箱 - @recipients ||= [] - @members.each do |teacher| - if teacher.user.mail_notification != 'week' && teacher.user.mail_notification != 'day' - @recipients << teacher.user.mail - end - end - mail :to => @recipients, - :subject => "#{l(:label_your_course)}#{journals_for_message.jour.name}#{l(:label_have_message)} " - elsif journals_for_message.jour.class.to_s.to_sym == :Bid - if !journals_for_message.jour.author.notify_about? journals_for_message - return -1 - end - - mail :to => recipients, :subject => @title - elsif journals_for_message.jour.class.to_s.to_sym == :Contest - if !journals_for_message.jour.author.notify_about? journals_for_message - return -1 - end - mail :to => recipients, :subject => @title - else - mail :to => recipients1, :subject => @title - end - - - end - - # Builds a Mail::Message object used to email recipients of the added issue. - # - # Example: - # issue_add(issue) => Mail::Message object - # Mailer.issue_add(issue).deliver => sends an email to issue recipients - def issue_add(issue, recipients) - issue_id = issue.project_index - redmine_headers 'Project' => issue.project.identifier, - 'Issue-Id' => issue_id, - 'Issue-Author' => issue.author.login - redmine_headers 'Issue-Assignee' => issue.assigned_to.login if issue.assigned_to - message_id issue - - @author = issue.author - @issue = issue - user = User.find_by_mail(recipients) - token = Token.new(:user =>user , :action => 'autologin') - token.save - @token = token - @issue_url = url_for(:controller => 'issues', :action => 'show', :id => issue.id, :token => @token.value) - - # edit - @issue_author_url = url_for(user_activities_url(@author,:token => @token.value)) - @project_url = url_for(:controller => 'projects', :action => 'show', :id => issue.project_id, :token => @token.value) - - @user_url = url_for(my_account_url(user,:token => @token.value)) - - - subject = "[#{issue.project.name} - #{issue.tracker.name} ##{issue_id}] (#{issue.status.name}) #{issue.subject}" - mail(:to => recipients, - - :subject => subject) - end - # issue.attachments.each do |attach| - # attachments["#{attach.filename}"] = File.read("#{attach.disk_filename}") - # end - # cc = issue.watcher_recipients - recipients - #mail.attachments['test'] = File.read("#{RAILS.root}/files/2015/01/150114094010_libegl.dll") - - - - - # Builds a Mail::Message object used to email recipients of the edited issue. - # - # Example: - # issue_edit(journal) => Mail::Message object - # Mailer.issue_edit(journal).deliver => sends an email to issue recipients - def issue_edit(journal,recipients) - issue = journal.journalized.reload - issue_id = issue.project_index - redmine_headers 'Project' => issue.project.identifier, - 'Issue-Id' => issue_id.to_s, - 'Issue-Author' => issue.author.login - redmine_headers 'Issue-Assignee' => issue.assigned_to.login if issue.assigned_to - message_id journal - references issue - @author = journal.user - - user = User.find_by_mail(recipients) - - token = Token.new(:user =>user , :action => 'autologin') - token.save - @token = token - - - # edit - @issue_author_url = url_for(:controller => 'users', :action => 'show', :id => issue.author_id, :token => @token.value) - @project_url = url_for(:controller => 'projects', :action => 'show', :id => issue.project_id, :token => @token.value) - @user_url = url_for(my_account_url(user,:token => @token.value)) - - @issue_url = url_for(:controller => 'issues', :action => 'show', :id => issue.id, :anchor => "change-#{journal.id}", :token => @token.value) - s = "[#{issue.project.name} - #{issue.tracker.name} ##{issue_id}] " - s << "(#{issue.status.name}) " if journal.new_value_for('status_id') - s << issue.subject - @issue = issue - @journal = journal - # @issue_url = url_for(:controller => 'issues', :action => 'show', :id => issue, :anchor => "change-#{journal.id}") - mail(:to => recipients, - - :subject => s) - end - - def self.deliver_mailer(to,cc, subject) - mail :to => to, - :cc => cc, - :subject => subject - end - - # 用户申请加入项目邮件通知 - def applied_project(applied) - @project =applied.project - redmine_headers 'Project' => @project, - 'User' => applied.user - @user = applied.user - recipients = @project.manager_recipients - s = l(:text_applied_project, :id => "##{@user.show_name}", :project => @project.name) - @applied_url = url_for(:controller => 'projects', :action => 'settings', :id => @project.id,:tab=>'members') - mail :to => recipients, - :subject => s - end - - def reminder(user, issues, days) - set_language_if_valid user.language - @issues = issues - @days = days - @issues_url = url_for(:controller => 'issues', :action => 'index', - :set_filter => 1, :assigned_to_id => user.id, - :sort => 'due_date:asc') - mail :to => user.mail, - :subject => l(:mail_subject_reminder, :count => issues.size, :days => days) - end - - #缺陷到期邮件通知 - def issue_expire issue - issue_id = issue.project_index - redmine_headers 'Project' => issue.project.identifier, - 'Issue-Id' => issue_id, - 'Issue-Author' => issue.author.login - redmine_headers 'Issue-Assignee' => issue.assigned_to.login if issue.assigned_to - message_id issue - @author = issue.author - @issue = issue - @issue_url = url_for(:controller => 'issues', :action => 'show', :id => issue.id) - recipients = issue.recipients - s = l(:text_issue_expire,:issue => "[#{issue.project.name} - #{issue.tracker.name} ##{issue_id}] (#{issue.status.name}) #{issue.subject}") - mail :to => recipients, - :subject => s - ######################################################################################################### - #@issues = issues - #s = l(:text_issue_expire,:issue => "[#{issue.project.name} - #{issue.tracker.name} ##{issue_id}] (#{issue.status.name}) #{issue.subject}") - #puts s + "////" + issue.assigned_to.mail - #@issues_url = url_for(:controller => 'issues', :action => 'show',:id => issue.id) - #mail :to => issue.assigned_to.mail, - # :subject => s - ######################################################################################################### - #issue_id = issue.project_index - #redmine_headers 'Project' => issue.project.identifier, - # 'Issue-Id' => issue_id, - # 'Issue-Author' => issue.author.login - #redmine_headers 'Issue-Assignee' => issue.assigned_to.login if issue.assigned_to - #message_id issue - #@author = issue.author - #@issue = issue - #@issue_url = url_for(:controller => 'issues', :action => 'show', :id => issue) - #recipients = issue.recipients - #cc = issue.watcher_recipients - recipients - #mail :to => recipients, - # :cc => cc, - # :subject => "[#{issue.project.name} - #{issue.tracker.name} ##{issue_id}] (#{issue.status.name}) #{issue.subject}" - ###################################################################################################### - end - - - # Builds a Mail::Message object used to email users belonging to the added document's project. - # - # Example: - # document_added(document) => Mail::Message object - # Mailer.document_added(document).deliver => sends an email to the document's project recipients - def document_added(document) - redmine_headers 'Project' => document.project.identifier - @author = User.current - @document = document - @issue_author_url = url_for(user_activities_url(@author)) - @document_url = url_for(:controller => 'documents', :action => 'show', :id => document) - mail :to => document.recipients, - :subject => "[#{document.project.name}] #{l(:label_document_new)}: #{document.title}" - end - - # Builds a Mail::Message object used to email recipients of a project when an attachements are added. - # - # Example: - # attachments_added(attachments) => Mail::Message object - # Mailer.attachments_added(attachments).deliver => sends an email to the project's recipients - def attachments_added(attachments) - container = attachments.first.container - added_to = '' - added_to_url = '' - @author = attachments.first.author - @issue_author_url = url_for(user_activities_url(@author)) - case container.class.name - when 'Project' - added_to_url = url_for(:controller => 'files', :action => 'index', :project_id => container) - added_to = "#{l(:label_project)}: #{container}" - recipients = container.notified_users.select { |user| user.allowed_to?(:view_files, container) }.collect { |u| u.mail } - when 'Course' - added_to_url = url_for(:controller => 'files', :action => 'index', :course_id => container) - added_to = "#{l(:label_course)}: #{container.name}" - recipients = container.notified_users.select { |user| user.allowed_to?(:view_files, container) }.collect { |u| u.mail } - when 'Version' - added_to_url = url_for(:controller => 'files', :action => 'index', :project_id => container.project) - added_to = "#{l(:label_version)}: #{container.name}" - recipients = container.project.notified_users.select { |user| user.allowed_to?(:view_files, container.project) }.collect { |u| u.mail } - when 'Document' - added_to_url = url_for(:controller => 'documents', :action => 'show', :id => container.id) - added_to = "#{l(:label_document)}: #{container.title}" - recipients = container.recipients - end - if container.class.name == 'Course' - redmine_headers 'Course' => container.id - @attachments = attachments - @added_to = added_to - @added_to_url = added_to_url - mail :to => recipients, - :subject => "[#{container.name}] #{l(:label_attachment_new)}" - elsif container.class.name == 'Project' - redmine_headers 'Project' => container.id - @attachments = attachments - @added_to = added_to - @added_to_url = added_to_url - mail :to => recipients, - :subject => "[#{container.name}] #{l(:label_attachment_new)}" - else - redmine_headers 'Project' => container.project.identifier - @attachments = attachments - @added_to = added_to - @added_to_url = added_to_url - mail :to => recipients, - :subject => "[#{container.project.name}] #{l(:label_attachment_new)}" - end - end - - # Builds a Mail::Message object used to email recipients of a news' project when a news item is added. - # - # Example: - # news_added(news) => Mail::Message object - # Mailer.news_added(news).deliver => sends an email to the news' project recipients - def news_added(news) - - if news.project - redmine_headers 'Project' => news.project.identifier - @author = news.author - @issue_author_url = url_for(user_activities_url(@author)) - message_id news - @news = news - @news_url = url_for(:controller => 'news', :action => 'show', :id => news) - mail :to => news.recipients, - :subject => "[#{news.project.name}] #{l(:label_news)}: #{news.title}" - elsif news.course - redmine_headers 'Course' => news.course.id - @author = news.author - @issue_author_url = url_for(user_activities_url(@author)) - message_id news - @news = news - recipients = news.course.notified_users.select { |user| user.allowed_to?(:view_files, news.course) }.collect { |u| u.mail } - @news_url = url_for(:controller => 'news', :action => 'show', :id => news) - mail :to => recipients, - :subject => "[#{news.course.name}] #{l(:label_news)}: #{news.title}" - end - end - - # Builds a Mail::Message object used to email recipients of a news' project when a news comment is added. - # - # Example: - # news_comment_added(comment) => Mail::Message object - # Mailer.news_comment_added(comment) => sends an email to the news' project recipients - def news_comment_added(comment) - news = comment.commented - if news.project - redmine_headers 'Project' => news.project.identifier - @author = comment.author - @issue_author_url = url_for(user_activities_url(@author)) - message_id comment - @news = news - @comment = comment - @news_url = url_for(:controller => 'news', :action => 'show', :id => news) - mail :to => news.recipients, - :cc => news.watcher_recipients, - :subject => "Re: [#{news.project.name}] #{l(:label_news)}: #{news.title}" - elsif news.course - redmine_headers 'Course' => news.course.id - @author = comment.author - @issue_author_url = url_for(user_activities_url(@author)) - message_id comment - @news = news - @comment = comment - @news_url = url_for(:controller => 'news', :action => 'show', :id => news) - recipients = news.course.notified_users.select { |user| user.allowed_to?(:view_files, news.course) }.collect { |u| u.mail } - - mail :to => recipients, - :subject => "[#{news.course.name}] #{l(:label_news)}: #{news.title}" - end - end - - # Builds a Mail::Message object used to email the recipients of the specified message that was posted. - # - # Example: - # message_posted(message) => Mail::Message object - # Mailer.message_posted(message).deliver => sends an email to the recipients - def message_posted(message) - if message.project - redmine_headers 'Project' => message.project.identifier, - 'Topic-Id' => (message.parent_id || message.id) - @author = message.author - @issue_author_url = url_for(user_activities_url(@author)) - message_id message - references message.parent unless message.parent.nil? - recipients = message.recipients - cc = ((message.root.watcher_recipients + message.board.watcher_recipients).uniq - recipients) - @message = message - @message_url = url_for(message.event_url) - mail :to => recipients, - :cc => cc, - :subject => "[#{message.board.project.name} - #{message.board.name} - msg#{message.root.id}] #{message.subject}" - elsif message.course - redmine_headers 'Course' => message.course.id, - 'Topic-Id' => (message.parent_id || message.id) - @author = message.author - @issue_author_url = url_for(user_activities_url(@author)) - message_id message - references message.parent unless message.parent.nil? - recipients = message.course.notified_users.select { |user| user.allowed_to?(:view_files, message.course) }.collect { |u| u.mail } - cc = ((message.root.watcher_recipients + message.board.watcher_recipients).uniq - recipients) - @message = message - @message_url = url_for(message.event_url) - mail :to => recipients, - :cc => cc, - :subject => "[#{message.board.course.name} - #{message.board.name} - msg#{message.root.id}] #{message.subject}" - end - end - - # Builds a Mail::Message object used to email the recipients of a project of the specified wiki content was added. - # - # Example: - # wiki_content_added(wiki_content) => Mail::Message object - # Mailer.wiki_content_added(wiki_content).deliver => sends an email to the project's recipients - def wiki_content_added(wiki_content) - redmine_headers 'Project' => wiki_content.project.identifier, - 'Wiki-Page-Id' => wiki_content.page.id - @author = wiki_content.author - message_id wiki_content - recipients = wiki_content.recipients - cc = wiki_content.page.wiki.watcher_recipients - recipients - @wiki_content = wiki_content - @wiki_content_url = url_for(:controller => 'wiki', :action => 'show', - :project_id => wiki_content.project, - :id => wiki_content.page.title) - mail :to => recipients, - :cc => cc, - :subject => "[#{wiki_content.project.name}] #{l(:mail_subject_wiki_content_added, :id => wiki_content.page.pretty_title)}" - end - - # Builds a Mail::Message object used to email the recipients of a project of the specified wiki content was updated. - # - # Example: - # wiki_content_updated(wiki_content) => Mail::Message object - # Mailer.wiki_content_updated(wiki_content).deliver => sends an email to the project's recipients - def wiki_content_updated(wiki_content) - redmine_headers 'Project' => wiki_content.project.identifier, - 'Wiki-Page-Id' => wiki_content.page.id - @author = wiki_content.author - message_id wiki_content - recipients = wiki_content.recipients - cc = wiki_content.page.wiki.watcher_recipients + wiki_content.page.watcher_recipients - recipients - @wiki_content = wiki_content - @wiki_content_url = url_for(:controller => 'wiki', :action => 'show', - :project_id => wiki_content.project, - :id => wiki_content.page.title) - @wiki_diff_url = url_for(:controller => 'wiki', :action => 'diff', - :project_id => wiki_content.project, :id => wiki_content.page.title, - :version => wiki_content.version) - mail :to => recipients, - :cc => cc, - :subject => "[#{wiki_content.project.name}] #{l(:mail_subject_wiki_content_updated, :id => wiki_content.page.pretty_title)}" - end - - # Builds a Mail::Message object used to email the specified user their account information. - # - # Example: - # account_information(user, password) => Mail::Message object - # Mailer.account_information(user, password).deliver => sends account information to the user - def account_information(user, password) - set_language_if_valid user.language - @user = user - @password = password - @login_url = url_for(:controller => 'account', :action => 'login') - mail :to => user.mail, - :subject => l(:mail_subject_register, Setting.app_title) - end - - # Builds a Mail::Message object used to email all active administrators of an account activation request. - # - # Example: - # account_activation_request(user) => Mail::Message object - # Mailer.account_activation_request(user).deliver => sends an email to all active administrators - def account_activation_request(user) - # Send the email to all active administrators - recipients = User.active.where(:admin => true).all.collect { |u| u.mail }.compact - @user = user - @url = url_for(:controller => 'users', :action => 'index', - :status => User::STATUS_REGISTERED, - :sort_key => 'created_on', :sort_order => 'desc') - mail :to => recipients, - :subject => l(:mail_subject_account_activation_request, Setting.app_title) - end - - # Builds a Mail::Message object used to email the specified user that their account was activated by an administrator. - # - # Example: - # account_activated(user) => Mail::Message object - # Mailer.account_activated(user).deliver => sends an email to the registered user - def account_activated(user) - set_language_if_valid user.language - @user = user - @login_url = url_for(:controller => 'account', :action => 'login') - mail :to => user.mail, - :subject => l(:mail_subject_register, Setting.app_title) - end - - def lost_password(token) - set_language_if_valid(token.user.language) - @token = token - @url = url_for(:controller => 'account', :action => 'lost_password', :token => token.value) - mail :to => token.user.mail, - :subject => l(:mail_subject_lost_password, Setting.app_title) - end - - def register(token) - set_language_if_valid(token.user.language) - @token = token - @url = url_for(:controller => 'account', :action => 'activate', :token => token.value) - mail :to => token.user.mail, - :subject => l(:mail_subject_register, Setting.app_title) - end - - def test_email(user) - set_language_if_valid(user.language) - @url = url_for(:controller => 'welcome') - mail :to => user.mail, - :subject => 'forge test' - end - - # Overrides default deliver! method to prevent from sending an email - # with no recipient, cc or bcc - def deliver!(mail = @mail) - set_language_if_valid @initial_language - return false if (recipients.nil? || recipients.empty?) && - (cc.nil? || cc.empty?) && - (bcc.nil? || bcc.empty?) - - - # Log errors when raise_delivery_errors is set to false, Rails does not - raise_errors = self.class.raise_delivery_errors - self.class.raise_delivery_errors = true - begin - return super(mail) - rescue Exception => e - if raise_errors - raise e - elsif mylogger - mylogger.error "The following error occured while sending email notification: \"#{e.message}\". Check your configuration in config/configuration.yml." - end - ensure - self.class.raise_delivery_errors = raise_errors - end - end - - # Sends reminders to issue assignees - # Available options: - # * :days => how many days in the future to remind about (defaults to 7) - # * :tracker => id of tracker for filtering issues (defaults to all trackers) - # * :project => id or identifier of project to process (defaults to all projects) - # * :users => array of user/group ids who should be reminded - def self.reminders(options={}) - days = options[:days] || 7 - project = options[:project] ? Project.find(options[:project]) : nil - tracker = options[:tracker] ? Tracker.find(options[:tracker]) : nil - user_ids = options[:users] - - scope = Issue.open.where("#{Issue.table_name}.assigned_to_id IS NOT NULL" + - " AND #{Project.table_name}.status = #{Project::STATUS_ACTIVE}" + - " AND #{Issue.table_name}.due_date <= ?", days.day.from_now.to_date - ) - scope = scope.where(:assigned_to_id => user_ids) if user_ids.present? - scope = scope.where(:project_id => project.id) if project - scope = scope.where(:tracker_id => tracker.id) if tracker - - issues_by_assignee = scope.includes(:status, :assigned_to, :project, :tracker).all.group_by(&:assigned_to) - issues_by_assignee.keys.each do |assignee| - if assignee.is_a?(Group) - assignee.users.each do |user| - issues_by_assignee[user] ||= [] - issues_by_assignee[user] += issues_by_assignee[assignee] - end - end - end - - issues_by_assignee.each do |assignee, issues| - reminder(assignee, issues, days).deliver if assignee.is_a?(User) && assignee.active? - end - end - - # Activates/desactivates email deliveries during +block+ - def self.with_deliveries(enabled = true, &block) - was_enabled = ActionMailer::Base.perform_deliveries - ActionMailer::Base.perform_deliveries = !!enabled - yield - ensure - ActionMailer::Base.perform_deliveries = was_enabled - end - - # Sends emails synchronously in the given block - def self.with_synched_deliveries(&block) - saved_method = ActionMailer::Base.delivery_method - if m = saved_method.to_s.match(%r{^async_(.+)$}) - synched_method = m[1] - ActionMailer::Base.delivery_method = synched_method.to_sym - ActionMailer::Base.send "#{synched_method}_settings=", ActionMailer::Base.send("async_#{synched_method}_settings") - end - yield - ensure - ActionMailer::Base.delivery_method = saved_method - end - - def mail(headers={}) - headers.merge! 'X-Mailer' => 'Redmine', - 'X-Redmine-Host' => Setting.host_name, - 'X-Redmine-Site' => Setting.app_title, - 'X-Auto-Response-Suppress' => 'OOF', - 'Auto-Submitted' => 'auto-generated', - 'From' => Setting.mail_from, - 'List-Id' => "<#{Setting.mail_from.to_s.gsub('@', '.')}>" - - # Removes the author from the recipients and cc - # if he doesn't want to receive notifications about what he does - if @author && @author.logged? && @author.pref[:no_self_notified] - headers[:to].delete(@author.mail) if headers[:to].is_a?(Array) - headers[:cc].delete(@author.mail) if headers[:cc].is_a?(Array) - end - - if @author && @author.logged? - redmine_headers 'Sender' => @author.login - end - - # Blind carbon copy recipients - if Setting.bcc_recipients? - headers[:bcc] = [headers[:to], headers[:cc]].flatten.uniq.reject(&:blank?) - headers[:to] = nil - headers[:cc] = nil - end - - if @message_id_object - headers[:message_id] = "<#{self.class.message_id_for(@message_id_object)}>" - end - if @references_objects - headers[:references] = @references_objects.collect {|o| "<#{self.class.message_id_for(o)}>"}.join(' ') - end - - super headers do |format| - format.text - format.html unless Setting.plain_text_mail? - end - - set_language_if_valid @initial_language - end - - def initialize(*args) - @initial_language = current_language - set_language_if_valid Setting.default_language - super - end - - def self.deliver_mail(mail) - return false if mail.to.blank? && mail.cc.blank? && mail.bcc.blank? - Thread.new do - super - end - end - - def self.method_missing(method, *args, &block) - if m = method.to_s.match(%r{^deliver_(.+)$}) - ActiveSupport::Deprecation.warn "Mailer.deliver_#{m[1]}(*args) is deprecated. Use Mailer.#{m[1]}(*args).deliver instead." - send(m[1], *args).deliver - else - super - end - end - - - - private - - # Appends a Redmine header field (name is prepended with 'X-Redmine-') - def redmine_headers(h) - h.each { |k,v| headers["X-Redmine-#{k}"] = v.to_s } - end - - # Returns a predictable Message-Id for the given object - def self.message_id_for(object) - # id + timestamp should reduce the odds of a collision - # as far as we don't send multiple emails for the same object - timestamp = object.send(object.respond_to?(:created_on) ? :created_on : :updated_on) - hash = "redmine.#{object.class.name.demodulize.underscore}-#{object.id}.#{timestamp.strftime("%Y%m%d%H%M%S")}" - host = Setting.mail_from.to_s.gsub(%r{^.*@}, '') - host = "#{::Socket.gethostname}.redmine" if host.empty? - "#{hash}@#{host}" - end - - def message_id(object) - @message_id_object = object - end - - def references(object) - @references_objects ||= [] - @references_objects << object - end - - def mylogger - Rails.logger - end - - def add_attachments(obj) - if email.attachments && email.attachments.any? - email.attachments.each do |attachment| - obj.attachments << Attachment.create(:container => obj, - :file => attachment.decoded, - :filename => attachment.filename, - :author => user, - :content_type => attachment.mime_type) - end - end - end - - # author: alan - # 功能: 生成len位随机字符串 - def newpass(len) - chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a - newpass = "" - 1.upto(len) { |i| newpass << chars[rand(chars.size-1)] } - return newpass - end -end +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class Mailer < ActionMailer::Base + layout 'mailer' + helper :application + helper :issues + helper :custom_fields + + include Redmine::I18n + include CoursesHelper + def self.default_url_options + { :host => Setting.host_name, :protocol => Setting.protocol } + end + + + class MailerProxy + def initialize(cls) + @target = cls + end + def method_missing(name, *args, &block) + if Setting.delayjob_enabled && Object.const_defined?('Delayed') + @target.delay.send(name, *args, &block) + else + @target.send(name, *args, &block).deliver + end + end + end + + def self.run + MailerProxy.new(self) + end + + # author: alan + # 发送邀请未注册用户加入项目邮件 + # 功能: 在加入项目的同时自动注册用户 + def send_invite_in_project(email, project, invitor) + @email = email + @subject = "#{invitor.name} #{l(:label_invite_project)} #{project.name} " + @password = newpass(6) + @project_url = url_for(:controller => 'projects', :action => 'show', :id => project.id, + :password => @password, :login => email) + mail :to => email, :subject => @subject + end + + # author: alan + # 根据用户选择发送个人日报或周报 + # 发送内容: 项目【缺陷,讨论区,新闻】,课程【通知,留言,新闻】, 贴吧, 个人留言 + def send_for_user_activities(user, date_to, days) + date_from = date_to - days.days + + subject = "[ #{user.show_name} : #{l(:label_day_mail)}]" + @subject = " #{user.show_name} : #{date_to} #{l(:label_day_mail)}" + + date_from = "#{date_from} 23:59:59" + date_to = "#{date_to} 23:59:59" + + # 生成token用于直接点击登录 + @user = user + token = Token.new(:user =>user , :action => 'autologin') + token.save + @token = token + + @user_url = url_for(my_account_url(user,:token => @token.value)) + # 查询user参加的项目及课程 + projects = user.projects + courses = user.courses + project_ids = projects.map{|project| project.id}.join(",") + course_ids = courses.map {|course| course.id}.join(",") + + # 查询user的缺陷,包括发布的,跟踪的以及被指派的缺陷 + sql = "select DISTINCT i.* from issues i, watchers w + where (i.assigned_to_id = #{user.id} or i.author_id = #{user.id} + or (w.watchable_type = 'Issue' and w.watchable_id = i.id and w.user_id = #{user.id})) + and (i.created_on between '#{date_from}' and '#{date_to}') order by i.created_on desc" + @issues = Issue.find_by_sql(sql) + + # @bids 查询课程作业,包括老师发布的作业,以及user提交作业 + # @attachments查询课程课件更新 + @attachments ||= [] + + @bids ||= [] # 老师发布的作业 + + unless courses.first.nil? + count = courses.count + count = count - 1 + for i in 0..count do + bids = courses[i].homeworks.where("bids.created_on between '#{date_from}' and '#{date_to}'").order("bids.created_on desc") + attachments = courses[i].attachments.where("attachments.created_on between '#{date_from}' and '#{date_to}'").order('attachments.created_on DESC') + @bids += bids if bids.count > 0 + @attachments += attachments if attachments.count > 0 + end + end + # user 提交的作业 + @homeworks = HomeworkAttach.where("user_id=#{user.id} and (created_at between '#{date_from}' and '#{date_to}')").order("created_at desc") + + # 查询user在课程。项目中发布的讨论帖子 + messages = Message.find_by_sql("select DISTINCT * from messages where author_id = #{user.id} and (created_on between '#{date_from}' and '#{date_to}') order by created_on desc") + @course_messages ||= [] + @project_messages ||= [] + unless messages.first.nil? + messages.each do |msg| + if msg.project + @project_messages << msg + elsif msg.course + @course_messages << msg + end + end + end + # 查询user在课程中发布的通知,项目中发的新闻 + @course_news = News.find_by_sql("select DISTINCT n.* from news n + where n.course_id in (#{course_ids}) + and (created_on between '#{date_from}' and '#{date_to}') order by created_on desc") + @project_news = News.find_by_sql("select DISTINCT n.* from news n where n.project_id in (#{project_ids}) + and (created_on between '#{date_from}' and '#{date_to}') order by created_on desc") + + # 查询user在课程及个人中留言 + @course_journal_messages = JournalsForMessage.find_by_sql("select DISTINCT * from journals_for_messages where + jour_type='Course' and user_id = #{user.id} + and (created_on between '#{date_from}' and '#{date_to}') order by created_on desc") + @user_journal_messages = user.journals_for_messages.where("m_parent_id IS NULL and (created_on between '#{date_from}' and '#{date_to}')").order('created_on DESC') + + + # 查询user新建贴吧或发布帖子 + @forums = Forum.find_by_sql("select DISTINCT * from forums where creator_id = #{user.id} and (created_at between '#{date_from}' and '#{date_to}') order by created_at desc") + @memos = Memo.find_by_sql("select DISTINCT m.* from memos m, forums f where (m.author_id = #{user.id} or (m.forum_id = f.id and f.creator_id = #{user.id})) + and (m.created_at between '#{date_from}' and '#{date_to}') order by m.created_at desc") + + + has_content = [@issues,@homeworks,@course_messages,@project_messages,@course_news,@project_news, + @course_journal_messages,@user_journal_messages,@forums,@memos,@attachments,@bids].any? {|o| + !o.empty? + } + binding.pry if Rails.env.development? + #有内容才发,没有不发 + mail :to => user.mail,:subject => subject if has_content + end + + # 公共讨论区发帖、回帖添加邮件发送信息 + def forum_message_added(memo) + @memo = memo + redmine_headers 'Memo' => memo.id + @forum = memo.forum + @author = memo.author + @forum_url = url_for(:controller => 'forums', :action => 'show', :id => @forum.id) + @issue_author_url = url_for(user_activities_url(@author)) + recipients ||= [] + #将帖子创建者邮箱地址加入数组 + recipients << @forum.creator.mail + #回复人邮箱地址加入数组 + recipients << @author.mail + # cc = wiki_content.page.wiki.watcher_recipients - recipients + + @memo_url = url_for(forum_memo_url(@forum, (@memo.parent_id.nil? ? @memo : @memo.parent_id))) + mail :to => recipients, + :subject => "[ #{l(:label_message_plural)} : #{memo.subject} #{l(:label_memo_create_succ)}]", + :filter => true + end + # Builds a Mail::Message object used to email recipients of the added journals for message. + + # 留言分为直接留言,和对留言人留言的回复 + # 字段说明在JournalsForMessage.rb + # 直接留言后 reply_id,m_parent_id 为空,相对应的at_user取值为nil + + def journals_for_message_add(user, journals_for_message) + @user = journals_for_message.user # 留言人 + @mail = journals_for_message.jour if journals_for_message.at_user.nil? # 留言 + @mail = journals_for_message.at_user if journals_for_message.at_user + @message = journals_for_message.notes + @title = "#@user #{t(:label_leave_your_message, :locale => 'zh')}" + @issue_author_url = url_for(user_activities_url(@user)) + @url = case journals_for_message.jour.class.to_s.to_sym # 判断留言的对象所属类型 + when :Bid + course_for_bid_url(journals_for_message.jour, anchor: "word_li_#{journals_for_message.id}") + when :Project + return -1 if journals_for_message.jour.project_type == Project::ProjectType_project + project_feedback_url(journals_for_message.jour, anchor: "word_li_#{journals_for_message.id}") + when :Course + course_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}") + when :User + user_newfeedback_user_url(journals_for_message.jour, anchor: "word_li_#{journals_for_message.id}") + else + Rails.logger.error "[Builds a Mail::Message ERROR] journalsForMessage's jour is unkown type, journalsForMessage.id = #{journals_for_message.id}" + return -1 + end + + # 验证用户的收取邮件的方式 + recipients ||= [] + recipients1 ||= [] + recipients1 = @mail.mail + recipients = journals_for_message.jour.author.mail + + # modify by nwb + #如果是直接留言并且留言对象是课程 + if !journals_for_message.at_user && journals_for_message.jour.class.to_s.to_sym == :Course + course = journals_for_message.jour + @author = journals_for_message.user + #课程的教师 + @members = course_all_member journals_for_message.jour + #收件人邮箱 + @recipients ||= [] + @members.each do |teacher| + @recipients << teacher.user.mail + end + mail :to => @recipients, + :subject => "#{l(:label_your_course)}#{journals_for_message.jour.name}#{l(:label_have_message)} ", + :filter => true + elsif journals_for_message.jour.class.to_s.to_sym == :Bid + if !journals_for_message.jour.author.notify_about? journals_for_message + return -1 + end + + mail :to => recipients, :subject => @title,:filter => true + elsif journals_for_message.jour.class.to_s.to_sym == :Contest + if !journals_for_message.jour.author.notify_about? journals_for_message + return -1 + end + mail :to => recipients, :subject => @title,:filter => true + else + mail :to => recipients1, :subject => @title,:filter => true + end + + + end + + # Builds a Mail::Message object used to email recipients of the added issue. + # + # Example: + # issue_add(issue) => Mail::Message object + # Mailer.issue_add(issue).deliver => sends an email to issue recipients + def issue_add(issue, recipients) + issue_id = issue.project_index + redmine_headers 'Project' => issue.project.identifier, + 'Issue-Id' => issue_id, + 'Issue-Author' => issue.author.login + redmine_headers 'Issue-Assignee' => issue.assigned_to.login if issue.assigned_to + message_id issue + + @author = issue.author + @issue = issue + user = User.find_by_mail(recipients) + token = Token.new(:user =>user , :action => 'autologin') + token.save + @token = token + @issue_url = url_for(:controller => 'issues', :action => 'show', :id => issue.id, :token => @token.value) + + # edit + @issue_author_url = url_for(user_activities_url(@author,:token => @token.value)) + @project_url = url_for(:controller => 'projects', :action => 'show', :id => issue.project_id, :token => @token.value) + + @user_url = url_for(my_account_url(user,:token => @token.value)) + + + subject = "[#{issue.project.name} - #{issue.tracker.name} ##{issue_id}] (#{issue.status.name}) #{issue.subject}" + mail :to => recipients, + :subject => subject, + :filter => true + end + # issue.attachments.each do |attach| + # attachments["#{attach.filename}"] = File.read("#{attach.disk_filename}") + # end + # cc = issue.watcher_recipients - recipients + #mail.attachments['test'] = File.read("#{RAILS.root}/files/2015/01/150114094010_libegl.dll") + + + + + # Builds a Mail::Message object used to email recipients of the edited issue. + # + # Example: + # issue_edit(journal) => Mail::Message object + # Mailer.issue_edit(journal).deliver => sends an email to issue recipients + def issue_edit(journal,recipients) + issue = journal.journalized.reload + issue_id = issue.project_index + redmine_headers 'Project' => issue.project.identifier, + 'Issue-Id' => issue_id.to_s, + 'Issue-Author' => issue.author.login + redmine_headers 'Issue-Assignee' => issue.assigned_to.login if issue.assigned_to + message_id journal + references issue + @author = journal.user + + user = User.find_by_mail(recipients) + + token = Token.new(:user =>user , :action => 'autologin') + token.save + @token = token + + + # edit + @issue_author_url = url_for(:controller => 'users', :action => 'show', :id => issue.author_id, :token => @token.value) + @project_url = url_for(:controller => 'projects', :action => 'show', :id => issue.project_id, :token => @token.value) + @user_url = url_for(my_account_url(user,:token => @token.value)) + + @issue_url = url_for(:controller => 'issues', :action => 'show', :id => issue.id, :anchor => "change-#{journal.id}", :token => @token.value) + s = "[#{issue.project.name} - #{issue.tracker.name} ##{issue_id}] " + s << "(#{issue.status.name}) " if journal.new_value_for('status_id') + s << issue.subject + @issue = issue + @journal = journal + # @issue_url = url_for(:controller => 'issues', :action => 'show', :id => issue, :anchor => "change-#{journal.id}") + mail :to => recipients, + :subject => s, + :filter => true + end + + def self.deliver_mailer(to,cc, subject) + mail :to => to, + :cc => cc, + :subject => subject + end + + # 用户申请加入项目邮件通知 + def applied_project(applied) + @project =applied.project + redmine_headers 'Project' => @project, + 'User' => applied.user + @user = applied.user + recipients = @project.manager_recipients + s = l(:text_applied_project, :id => "##{@user.show_name}", :project => @project.name) + @applied_url = url_for(:controller => 'projects', :action => 'settings', :id => @project.id,:tab=>'members') + mail :to => recipients, + :subject => s + end + + def reminder(user, issues, days) + set_language_if_valid user.language + @issues = issues + @days = days + @issues_url = url_for(:controller => 'issues', :action => 'index', + :set_filter => 1, :assigned_to_id => user.id, + :sort => 'due_date:asc') + mail :to => user.mail, + :subject => l(:mail_subject_reminder, :count => issues.size, :days => days) + end + + #缺陷到期邮件通知 + def issue_expire issue + issue_id = issue.project_index + redmine_headers 'Project' => issue.project.identifier, + 'Issue-Id' => issue_id, + 'Issue-Author' => issue.author.login + redmine_headers 'Issue-Assignee' => issue.assigned_to.login if issue.assigned_to + message_id issue + @author = issue.author + @issue = issue + @issue_url = url_for(:controller => 'issues', :action => 'show', :id => issue.id) + recipients = issue.recipients + s = l(:text_issue_expire,:issue => "[#{issue.project.name} - #{issue.tracker.name} ##{issue_id}] (#{issue.status.name}) #{issue.subject}") + mail :to => recipients, + :subject => s + ######################################################################################################### + #@issues = issues + #s = l(:text_issue_expire,:issue => "[#{issue.project.name} - #{issue.tracker.name} ##{issue_id}] (#{issue.status.name}) #{issue.subject}") + #puts s + "////" + issue.assigned_to.mail + #@issues_url = url_for(:controller => 'issues', :action => 'show',:id => issue.id) + #mail :to => issue.assigned_to.mail, + # :subject => s + ######################################################################################################### + #issue_id = issue.project_index + #redmine_headers 'Project' => issue.project.identifier, + # 'Issue-Id' => issue_id, + # 'Issue-Author' => issue.author.login + #redmine_headers 'Issue-Assignee' => issue.assigned_to.login if issue.assigned_to + #message_id issue + #@author = issue.author + #@issue = issue + #@issue_url = url_for(:controller => 'issues', :action => 'show', :id => issue) + #recipients = issue.recipients + #cc = issue.watcher_recipients - recipients + #mail :to => recipients, + # :cc => cc, + # :subject => "[#{issue.project.name} - #{issue.tracker.name} ##{issue_id}] (#{issue.status.name}) #{issue.subject}" + ###################################################################################################### + end + + + # Builds a Mail::Message object used to email users belonging to the added document's project. + # + # Example: + # document_added(document) => Mail::Message object + # Mailer.document_added(document).deliver => sends an email to the document's project recipients + def document_added(document) + redmine_headers 'Project' => document.project.identifier + @author = User.current + @document = document + @issue_author_url = url_for(user_activities_url(@author)) + @document_url = url_for(:controller => 'documents', :action => 'show', :id => document) + mail :to => document.recipients, + :subject => "[#{document.project.name}] #{l(:label_document_new)}: #{document.title}", + :filter => true + end + + # Builds a Mail::Message object used to email recipients of a project when an attachements are added. + # + # Example: + # attachments_added(attachments) => Mail::Message object + # Mailer.attachments_added(attachments).deliver => sends an email to the project's recipients + def attachments_added(attachments) + container = attachments.first.container + added_to = '' + added_to_url = '' + @author = attachments.first.author + @issue_author_url = url_for(user_activities_url(@author)) + case container.class.name + when 'Project' + added_to_url = url_for(:controller => 'files', :action => 'index', :project_id => container) + added_to = "#{l(:label_project)}: #{container}" + recipients = container.notified_users.select { |user| user.allowed_to?(:view_files, container) }.collect { |u| u.mail } + when 'Course' + added_to_url = url_for(:controller => 'files', :action => 'index', :course_id => container) + added_to = "#{l(:label_course)}: #{container.name}" + recipients = container.notified_users.select { |user| user.allowed_to?(:view_files, container) }.collect { |u| u.mail } + when 'Version' + added_to_url = url_for(:controller => 'files', :action => 'index', :project_id => container.project) + added_to = "#{l(:label_version)}: #{container.name}" + recipients = container.project.notified_users.select { |user| user.allowed_to?(:view_files, container.project) }.collect { |u| u.mail } + when 'Document' + added_to_url = url_for(:controller => 'documents', :action => 'show', :id => container.id) + added_to = "#{l(:label_document)}: #{container.title}" + recipients = container.recipients + end + if container.class.name == 'Course' + redmine_headers 'Course' => container.id + @attachments = attachments + @added_to = added_to + @added_to_url = added_to_url + mail :to => recipients, + :subject => "[#{container.name}] #{l(:label_attachment_new)}", + :filter => true + elsif container.class.name == 'Project' + redmine_headers 'Project' => container.id + @attachments = attachments + @added_to = added_to + @added_to_url = added_to_url + mail :to => recipients, + :subject => "[#{container.name}] #{l(:label_attachment_new)}", + :filter => true + else + redmine_headers 'Project' => container.project.identifier + @attachments = attachments + @added_to = added_to + @added_to_url = added_to_url + mail :to => recipients, + :subject => "[#{container.project.name}] #{l(:label_attachment_new)}", + :filter => true + end + end + + # Builds a Mail::Message object used to email recipients of a news' project when a news item is added. + # + # Example: + # news_added(news) => Mail::Message object + # Mailer.news_added(news).deliver => sends an email to the news' project recipients + def news_added(news) + + if news.project + redmine_headers 'Project' => news.project.identifier + @author = news.author + @issue_author_url = url_for(user_activities_url(@author)) + message_id news + @news = news + @news_url = url_for(:controller => 'news', :action => 'show', :id => news) + mail :to => news.recipients, + :subject => "[#{news.project.name}] #{l(:label_news)}: #{news.title}", + :filter => true + elsif news.course + redmine_headers 'Course' => news.course.id + @author = news.author + @issue_author_url = url_for(user_activities_url(@author)) + message_id news + @news = news + recipients = news.course.notified_users.select { |user| user.allowed_to?(:view_files, news.course) }.collect { |u| u.mail } + @news_url = url_for(:controller => 'news', :action => 'show', :id => news) + mail :to => recipients, + :subject => "[#{news.course.name}] #{l(:label_news)}: #{news.title}", + :filter => true + end + end + + # Builds a Mail::Message object used to email recipients of a news' project when a news comment is added. + # + # Example: + # news_comment_added(comment) => Mail::Message object + # Mailer.news_comment_added(comment) => sends an email to the news' project recipients + def news_comment_added(comment) + news = comment.commented + if news.project + redmine_headers 'Project' => news.project.identifier + @author = comment.author + @issue_author_url = url_for(user_activities_url(@author)) + message_id comment + @news = news + @comment = comment + @news_url = url_for(:controller => 'news', :action => 'show', :id => news) + mail :to => news.recipients, + :cc => news.watcher_recipients, + :subject => "Re: [#{news.project.name}] #{l(:label_news)}: #{news.title}", + :filter => true + elsif news.course + redmine_headers 'Course' => news.course.id + @author = comment.author + @issue_author_url = url_for(user_activities_url(@author)) + message_id comment + @news = news + @comment = comment + @news_url = url_for(:controller => 'news', :action => 'show', :id => news) + recipients = news.course.notified_users.select { |user| user.allowed_to?(:view_files, news.course) }.collect { |u| u.mail } + + mail :to => recipients, + :subject => "[#{news.course.name}] #{l(:label_news)}: #{news.title}", + :filter => true + end + end + + # Builds a Mail::Message object used to email the recipients of the specified message that was posted. + # + # Example: + # message_posted(message) => Mail::Message object + # Mailer.message_posted(message).deliver => sends an email to the recipients + def message_posted(message) + if message.project + redmine_headers 'Project' => message.project.identifier, + 'Topic-Id' => (message.parent_id || message.id) + @author = message.author + @issue_author_url = url_for(user_activities_url(@author)) + message_id message + references message.parent unless message.parent.nil? + recipients = message.recipients + cc = ((message.root.watcher_recipients + message.board.watcher_recipients).uniq - recipients) + @message = message + @message_url = url_for(message.event_url) + mail :to => recipients, + :cc => cc, + :subject => "[#{message.board.project.name} - #{message.board.name} - msg#{message.root.id}] #{message.subject}", + :filter => true + elsif message.course + redmine_headers 'Course' => message.course.id, + 'Topic-Id' => (message.parent_id || message.id) + @author = message.author + @issue_author_url = url_for(user_activities_url(@author)) + message_id message + references message.parent unless message.parent.nil? + recipients = message.course.notified_users.select { |user| user.allowed_to?(:view_files, message.course) }.collect { |u| u.mail } + cc = ((message.root.watcher_recipients + message.board.watcher_recipients).uniq - recipients) + @message = message + @message_url = url_for(message.event_url) + mail :to => recipients, + :cc => cc, + :subject => "[#{message.board.course.name} - #{message.board.name} - msg#{message.root.id}] #{message.subject}", + :filter => true + end + end + + # Builds a Mail::Message object used to email the recipients of a project of the specified wiki content was added. + # + # Example: + # wiki_content_added(wiki_content) => Mail::Message object + # Mailer.wiki_content_added(wiki_content).deliver => sends an email to the project's recipients + def wiki_content_added(wiki_content) + redmine_headers 'Project' => wiki_content.project.identifier, + 'Wiki-Page-Id' => wiki_content.page.id + @author = wiki_content.author + message_id wiki_content + recipients = wiki_content.recipients + cc = wiki_content.page.wiki.watcher_recipients - recipients + @wiki_content = wiki_content + @wiki_content_url = url_for(:controller => 'wiki', :action => 'show', + :project_id => wiki_content.project, + :id => wiki_content.page.title) + mail :to => recipients, + :cc => cc, + :subject => "[#{wiki_content.project.name}] #{l(:mail_subject_wiki_content_added, :id => wiki_content.page.pretty_title)}", + :filter => true + end + + # Builds a Mail::Message object used to email the recipients of a project of the specified wiki content was updated. + # + # Example: + # wiki_content_updated(wiki_content) => Mail::Message object + # Mailer.wiki_content_updated(wiki_content).deliver => sends an email to the project's recipients + def wiki_content_updated(wiki_content) + redmine_headers 'Project' => wiki_content.project.identifier, + 'Wiki-Page-Id' => wiki_content.page.id + @author = wiki_content.author + message_id wiki_content + recipients = wiki_content.recipients + cc = wiki_content.page.wiki.watcher_recipients + wiki_content.page.watcher_recipients - recipients + @wiki_content = wiki_content + @wiki_content_url = url_for(:controller => 'wiki', :action => 'show', + :project_id => wiki_content.project, + :id => wiki_content.page.title) + @wiki_diff_url = url_for(:controller => 'wiki', :action => 'diff', + :project_id => wiki_content.project, :id => wiki_content.page.title, + :version => wiki_content.version) + mail :to => recipients, + :cc => cc, + :subject => "[#{wiki_content.project.name}] #{l(:mail_subject_wiki_content_updated, :id => wiki_content.page.pretty_title)}", + :filter => true + end + + # Builds a Mail::Message object used to email the specified user their account information. + # + # Example: + # account_information(user, password) => Mail::Message object + # Mailer.account_information(user, password).deliver => sends account information to the user + def account_information(user, password) + set_language_if_valid user.language + @user = user + @password = password + @login_url = url_for(:controller => 'account', :action => 'login') + mail :to => user.mail, + :subject => l(:mail_subject_register, Setting.app_title) + end + + # Builds a Mail::Message object used to email all active administrators of an account activation request. + # + # Example: + # account_activation_request(user) => Mail::Message object + # Mailer.account_activation_request(user).deliver => sends an email to all active administrators + def account_activation_request(user) + # Send the email to all active administrators + recipients = User.active.where(:admin => true).all.collect { |u| u.mail }.compact + @user = user + @url = url_for(:controller => 'users', :action => 'index', + :status => User::STATUS_REGISTERED, + :sort_key => 'created_on', :sort_order => 'desc') + mail :to => recipients, + :subject => l(:mail_subject_account_activation_request, Setting.app_title) + end + + # Builds a Mail::Message object used to email the specified user that their account was activated by an administrator. + # + # Example: + # account_activated(user) => Mail::Message object + # Mailer.account_activated(user).deliver => sends an email to the registered user + def account_activated(user) + set_language_if_valid user.language + @user = user + @login_url = url_for(:controller => 'account', :action => 'login') + mail :to => user.mail, + :subject => l(:mail_subject_register, Setting.app_title) + end + + def lost_password(token) + set_language_if_valid(token.user.language) + @token = token + @url = url_for(:controller => 'account', :action => 'lost_password', :token => token.value) + mail :to => token.user.mail, + :subject => l(:mail_subject_lost_password, Setting.app_title) + end + + def register(token) + set_language_if_valid(token.user.language) + @token = token + @url = url_for(:controller => 'account', :action => 'activate', :token => token.value) + mail :to => token.user.mail, + :subject => l(:mail_subject_register, Setting.app_title) + end + + def test_email(user) + set_language_if_valid(user.language) + @url = url_for(:controller => 'welcome') + mail :to => user.mail, + :subject => 'forge test' + end + + # Overrides default deliver! method to prevent from sending an email + # with no recipient, cc or bcc + def deliver!(mail = @mail) + set_language_if_valid @initial_language + return false if (recipients.nil? || recipients.empty?) && + (cc.nil? || cc.empty?) && + (bcc.nil? || bcc.empty?) + + + # Log errors when raise_delivery_errors is set to false, Rails does not + raise_errors = self.class.raise_delivery_errors + self.class.raise_delivery_errors = true + begin + return super(mail) + rescue Exception => e + if raise_errors + raise e + elsif mylogger + mylogger.error "The following error occured while sending email notification: \"#{e.message}\". Check your configuration in config/configuration.yml." + end + ensure + self.class.raise_delivery_errors = raise_errors + end + end + + # Sends reminders to issue assignees + # Available options: + # * :days => how many days in the future to remind about (defaults to 7) + # * :tracker => id of tracker for filtering issues (defaults to all trackers) + # * :project => id or identifier of project to process (defaults to all projects) + # * :users => array of user/group ids who should be reminded + def self.reminders(options={}) + days = options[:days] || 7 + project = options[:project] ? Project.find(options[:project]) : nil + tracker = options[:tracker] ? Tracker.find(options[:tracker]) : nil + user_ids = options[:users] + + scope = Issue.open.where("#{Issue.table_name}.assigned_to_id IS NOT NULL" + + " AND #{Project.table_name}.status = #{Project::STATUS_ACTIVE}" + + " AND #{Issue.table_name}.due_date <= ?", days.day.from_now.to_date + ) + scope = scope.where(:assigned_to_id => user_ids) if user_ids.present? + scope = scope.where(:project_id => project.id) if project + scope = scope.where(:tracker_id => tracker.id) if tracker + + issues_by_assignee = scope.includes(:status, :assigned_to, :project, :tracker).all.group_by(&:assigned_to) + issues_by_assignee.keys.each do |assignee| + if assignee.is_a?(Group) + assignee.users.each do |user| + issues_by_assignee[user] ||= [] + issues_by_assignee[user] += issues_by_assignee[assignee] + end + end + end + + issues_by_assignee.each do |assignee, issues| + reminder(assignee, issues, days).deliver if assignee.is_a?(User) && assignee.active? + end + end + + # Activates/desactivates email deliveries during +block+ + def self.with_deliveries(enabled = true, &block) + was_enabled = ActionMailer::Base.perform_deliveries + ActionMailer::Base.perform_deliveries = !!enabled + yield + ensure + ActionMailer::Base.perform_deliveries = was_enabled + end + + # Sends emails synchronously in the given block + def self.with_synched_deliveries(&block) + saved_method = ActionMailer::Base.delivery_method + if m = saved_method.to_s.match(%r{^async_(.+)$}) + synched_method = m[1] + ActionMailer::Base.delivery_method = synched_method.to_sym + ActionMailer::Base.send "#{synched_method}_settings=", ActionMailer::Base.send("async_#{synched_method}_settings") + end + yield + ensure + ActionMailer::Base.delivery_method = saved_method + end + + #过滤掉不是不合规则的收件人 + def filter(reps) + r_reps = [] + if reps.is_a? Array + reps.each do |r| + u = User.find_by_mail(r) + if u && u.mail_notification == 'all' + r_reps << r + end + end + elsif reps.is_a? String + u = User.find_by_mail(r) + if u && u.mail_notification == 'all' + r_reps << r + end + end + r_reps + end + + def mail(headers={}) + headers.merge! 'X-Mailer' => 'Redmine', + 'X-Redmine-Host' => Setting.host_name, + 'X-Redmine-Site' => Setting.app_title, + 'X-Auto-Response-Suppress' => 'OOF', + 'Auto-Submitted' => 'auto-generated', + 'From' => Setting.mail_from, + 'List-Id' => "<#{Setting.mail_from.to_s.gsub('@', '.')}>" + + # Removes the author from the recipients and cc + # if he doesn't want to receive notifications about what he does + if @author && @author.logged? && @author.pref[:no_self_notified] + headers[:to].delete(@author.mail) if headers[:to].is_a?(Array) + headers[:cc].delete(@author.mail) if headers[:cc].is_a?(Array) + end + + if headers[:filter] + headers[:to] = filter(headers[:to]) + headers[:cc] = filter(headers[:cc]) + end + + if @author && @author.logged? + redmine_headers 'Sender' => @author.login + end + + # Blind carbon copy recipients + if Setting.bcc_recipients? + headers[:bcc] = [headers[:to], headers[:cc]].flatten.uniq.reject(&:blank?) + headers[:to] = nil + headers[:cc] = nil + end + + if @message_id_object + headers[:message_id] = "<#{self.class.message_id_for(@message_id_object)}>" + end + if @references_objects + headers[:references] = @references_objects.collect {|o| "<#{self.class.message_id_for(o)}>"}.join(' ') + end + + super headers do |format| + format.text + format.html unless Setting.plain_text_mail? + end + + set_language_if_valid @initial_language + end + + def initialize(*args) + @initial_language = current_language + set_language_if_valid Setting.default_language + super + end + + def self.deliver_mail(mail) + return false if mail.to.blank? && mail.cc.blank? && mail.bcc.blank? + Thread.new do + super + end + end + + def self.method_missing(method, *args, &block) + if m = method.to_s.match(%r{^deliver_(.+)$}) + ActiveSupport::Deprecation.warn "Mailer.deliver_#{m[1]}(*args) is deprecated. Use Mailer.#{m[1]}(*args).deliver instead." + send(m[1], *args).deliver + else + super + end + end + + + + private + + # Appends a Redmine header field (name is prepended with 'X-Redmine-') + def redmine_headers(h) + h.each { |k,v| headers["X-Redmine-#{k}"] = v.to_s } + end + + # Returns a predictable Message-Id for the given object + def self.message_id_for(object) + # id + timestamp should reduce the odds of a collision + # as far as we don't send multiple emails for the same object + timestamp = object.send(object.respond_to?(:created_on) ? :created_on : :updated_on) + hash = "redmine.#{object.class.name.demodulize.underscore}-#{object.id}.#{timestamp.strftime("%Y%m%d%H%M%S")}" + host = Setting.mail_from.to_s.gsub(%r{^.*@}, '') + host = "#{::Socket.gethostname}.redmine" if host.empty? + "#{hash}@#{host}" + end + + def message_id(object) + @message_id_object = object + end + + def references(object) + @references_objects ||= [] + @references_objects << object + end + + def mylogger + Rails.logger + end + + def add_attachments(obj) + if email.attachments && email.attachments.any? + email.attachments.each do |attachment| + obj.attachments << Attachment.create(:container => obj, + :file => attachment.decoded, + :filename => attachment.filename, + :author => user, + :content_type => attachment.mime_type) + end + end + end + + # author: alan + # 功能: 生成len位随机字符串 + def newpass(len) + chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a + newpass = "" + 1.upto(len) { |i| newpass << chars[rand(chars.size-1)] } + return newpass + end +end diff --git a/app/models/memo.rb b/app/models/memo.rb index eb0c86855..2831effe3 100644 --- a/app/models/memo.rb +++ b/app/models/memo.rb @@ -1,181 +1,179 @@ -class Memo < ActiveRecord::Base - include Redmine::SafeAttributes - include UserScoreHelper - include ApplicationHelper - belongs_to :forum - has_many_kindeditor_assets :assets, :dependent => :destroy - belongs_to :author, :class_name => "User", :foreign_key => 'author_id' - validates_presence_of :author_id, :forum_id, :subject,:content - # 若是主题帖,则内容可以是空 - #validates :content, presence: true, if: Proc.new{|o| !o.parent_id.nil? } - validates_length_of :subject, maximum: 50 - #validates_length_of :content, maximum: 3072 - validate :cannot_reply_to_locked_topic, :on => :create - - acts_as_tree :counter_cache => :replies_count, :order => "#{Memo.table_name}.created_at ASC" - acts_as_attachable - has_many :user_score_details, :class_name => 'UserScoreDetails',:as => :score_changeable_obj - has_many :praise_tread, as: :praise_tread_object, dependent: :destroy - belongs_to :last_reply, :class_name => 'Memo', :foreign_key => 'last_reply_id' - # acts_as_searchable :column => ['subject', 'content'], - # #:include => { :forum => :p} - # #:project_key => "#{Forum.table_name}.project_id" - # :date_column => "#{table_name}.created_at" - acts_as_event :title => Proc.new {|o| "#{o.forum.name}: #{o.subject}"}, - :datetime => :updated_at, - # :datetime => :created_at, - :description => :content, - :author => :author, - :type => Proc.new {|o| o.parent_id.nil? ? 'Memo' : 'Reply'}, - :url => Proc.new {|o| {:controller => 'memos', :action => 'show', :forum_id => o.forum_id}.merge(o.parent_id.nil? ? {:id => o.id} : {:id => o.parent_id, :r => o.id, :anchor => "reply-#{o.id}"})} - acts_as_activity_provider :author_key => :author_id, - :func => 'memos', - :timestamp => 'created_at' - # :find_options => {:type => 'memos'} - # acts_as_watchable - - safe_attributes "author_id", - "subject", - "content", - "forum_id", - "last_memo_id", - "lock", - "sticky", - "parent_id", - "replies_count" - - after_create :add_author_as_watcher, :reset_counters!, :sendmail - # after_update :update_memos_forum - after_destroy :reset_counters!,:delete_kindeditor_assets#,:down_user_score -- 公共区发帖暂不计入得分 - # after_create :send_notification - # after_save :plusParentAndForum - # after_destroy :minusParentAndForum - #before_save :be_user_score - # scope :visible, lambda { |*args| - # includes(:forum => ).where() - # } - - def sendmail - thread1=Thread.new do - Mailer.forum_message_added(self).deliver if Setting.notified_events.include?('forum_message_added') - end - end - - def cannot_reply_to_locked_topic - errors.add :base, l(:label_memo_locked) if root.locked? && self != root - end - - # def update_memos_forum - # if forum_id_changed? - # Message.update_all({:board_id => board_id}, ["id = ? OR parent_id = ?", root.id, root.id ]) - # Forum.reset_counters!(forum_id_was) - # Forum.reset_counters!(forum_id) - # end - # end - - def reset_counters! - if parent && parent.id - Memo.update_all({:last_reply_id => parent.children.maximum(:id)}, {:id => parent.id}) - parent.update_attribute(:updated_at, Time.now) - end - forum.reset_counters! - end - - def sticky? - sticky == 1 - end - - def replies - Memo.where("parent_id = ?", id) - end - - def locked? - self.lock - end - - def editable_by? user - # user && user.logged? || (self.author == usr && usr.allowed_to?(:edit_own_messages, project)) - user.admin? || self.author == user - end - - def destroyable_by? user - (user && self.author == user) || user.admin? - #self.author == user || user.admin? - end - - def deleted_attach_able_by? user - (user && user.logged? && (self.author == user) ) || user.admin? - end - - private - - def add_author_as_watcher - Watcher.create(:watchable => self.root, :user => author) - end - - def send_notification - if Setting.notified_events.include?('message_posted') - Mailer.message_posted(self).deliver - end - end - - def plusParentAndForum - @forum = Forum.find(self.forum_id) - @forum.memo_count = @forum.memo_count.to_int + 1 - @forum.last_memo_id = self.id - if self.parent_id - @parent_memo = Memo.find_by_id(self.parent_id) - @parent_memo.last_reply_id = self - @parent_memo.replies_count = @parent_memo.replies_count.to_int + 1 - @parent_memo.save - else - @forum.topic_count = @forum.topic_count.to_int + 1 - end - @forum.save - end - - def minusParentAndForum - @forum = Forum.find(self.forum_id) - @forum.memo_count = @forum.memo_count.to_int - 1 - @forum.memo_count = 0 if @forum.memo_count.to_int < 0 - # @forum.last_memo_id = Memo.reorder('created_at ASC').find_all_by_forum_id(self.forum_id).last.id - if self.parent_id - @parent_memo = Memo.find_by_id(self.parent_id) - # @parent_memo.last_reply_id = Memo.reorder('created_at ASC').find_all_by_parent_id(self.parent_id).last.id - @parent_memo.replies_count = @parent_memo.replies_count.to_int - 1 - @parent_memo.replies_count = 0 if @parent_memo.replies_count.to_int < 0 - @parent_memo.save - else - @forum.topic_count = @forum.topic_count.to_int - 1 - @forum.topic_count = 0 if @forum.topic_count.to_int < 0 - end - @forum.save - end - - #更新用户分数 -by zjc - def be_user_score - #新建memo且无parent的为发帖 - if self.parent_id.nil? - UserScore.joint(:post_message, User.current,nil,self ,{ memo_id: self.id }) - update_memo_number(User.current,1) - - #新建memo且有parent的为回帖 - elsif !self.parent_id.nil? - UserScore.joint(:reply_posting, User.current,self.parent.author,self, { memo_id: self.id }) - update_replay_for_memo(User.current,1) - end - end - - #被删除时更新用户分数 - def down_user_score - update_memo_number(User.current,1) - update_replay_for_memo(User.current,1) - end - - # Time 2015-03-26 15:20:24 - # Author lizanle - # Description 从硬盘上删除资源 - def delete_kindeditor_assets - delete_kindeditor_assets_from_disk self.id,1 - end -end +class Memo < ActiveRecord::Base + include Redmine::SafeAttributes + include UserScoreHelper + include ApplicationHelper + belongs_to :forum + has_many_kindeditor_assets :assets, :dependent => :destroy + belongs_to :author, :class_name => "User", :foreign_key => 'author_id' + validates_presence_of :author_id, :forum_id, :subject,:content + # 若是主题帖,则内容可以是空 + #validates :content, presence: true, if: Proc.new{|o| !o.parent_id.nil? } + validates_length_of :subject, maximum: 50 + #validates_length_of :content, maximum: 3072 + validate :cannot_reply_to_locked_topic, :on => :create + + acts_as_tree :counter_cache => :replies_count, :order => "#{Memo.table_name}.created_at ASC" + acts_as_attachable + has_many :user_score_details, :class_name => 'UserScoreDetails',:as => :score_changeable_obj + has_many :praise_tread, as: :praise_tread_object, dependent: :destroy + belongs_to :last_reply, :class_name => 'Memo', :foreign_key => 'last_reply_id' + # acts_as_searchable :column => ['subject', 'content'], + # #:include => { :forum => :p} + # #:project_key => "#{Forum.table_name}.project_id" + # :date_column => "#{table_name}.created_at" + acts_as_event :title => Proc.new {|o| "#{o.forum.name}: #{o.subject}"}, + :datetime => :updated_at, + # :datetime => :created_at, + :description => :content, + :author => :author, + :type => Proc.new {|o| o.parent_id.nil? ? 'Memo' : 'Reply'}, + :url => Proc.new {|o| {:controller => 'memos', :action => 'show', :forum_id => o.forum_id}.merge(o.parent_id.nil? ? {:id => o.id} : {:id => o.parent_id, :r => o.id, :anchor => "reply-#{o.id}"})} + acts_as_activity_provider :author_key => :author_id, + :func => 'memos', + :timestamp => 'created_at' + # :find_options => {:type => 'memos'} + # acts_as_watchable + + safe_attributes "author_id", + "subject", + "content", + "forum_id", + "last_memo_id", + "lock", + "sticky", + "parent_id", + "replies_count" + + after_create :add_author_as_watcher, :reset_counters!, :send_mail + # after_update :update_memos_forum + after_destroy :reset_counters!,:delete_kindeditor_assets#,:down_user_score -- 公共区发帖暂不计入得分 + # after_create :send_notification + # after_save :plusParentAndForum + # after_destroy :minusParentAndForum + #before_save :be_user_score + # scope :visible, lambda { |*args| + # includes(:forum => ).where() + # } + + def send_mail + Mailer.run.forum_message_added(self) if Setting.notified_events.include?('forum_message_added') + end + + def cannot_reply_to_locked_topic + errors.add :base, l(:label_memo_locked) if root.locked? && self != root + end + + # def update_memos_forum + # if forum_id_changed? + # Message.update_all({:board_id => board_id}, ["id = ? OR parent_id = ?", root.id, root.id ]) + # Forum.reset_counters!(forum_id_was) + # Forum.reset_counters!(forum_id) + # end + # end + + def reset_counters! + if parent && parent.id + Memo.update_all({:last_reply_id => parent.children.maximum(:id)}, {:id => parent.id}) + parent.update_attribute(:updated_at, Time.now) + end + forum.reset_counters! + end + + def sticky? + sticky == 1 + end + + def replies + Memo.where("parent_id = ?", id) + end + + def locked? + self.lock + end + + def editable_by? user + # user && user.logged? || (self.author == usr && usr.allowed_to?(:edit_own_messages, project)) + user.admin? || self.author == user + end + + def destroyable_by? user + (user && self.author == user) || user.admin? + #self.author == user || user.admin? + end + + def deleted_attach_able_by? user + (user && user.logged? && (self.author == user) ) || user.admin? + end + + private + + def add_author_as_watcher + Watcher.create(:watchable => self.root, :user => author) + end + + def send_notification + if Setting.notified_events.include?('message_posted') + Mailer.run.message_posted(self) + end + end + + def plusParentAndForum + @forum = Forum.find(self.forum_id) + @forum.memo_count = @forum.memo_count.to_int + 1 + @forum.last_memo_id = self.id + if self.parent_id + @parent_memo = Memo.find_by_id(self.parent_id) + @parent_memo.last_reply_id = self + @parent_memo.replies_count = @parent_memo.replies_count.to_int + 1 + @parent_memo.save + else + @forum.topic_count = @forum.topic_count.to_int + 1 + end + @forum.save + end + + def minusParentAndForum + @forum = Forum.find(self.forum_id) + @forum.memo_count = @forum.memo_count.to_int - 1 + @forum.memo_count = 0 if @forum.memo_count.to_int < 0 + # @forum.last_memo_id = Memo.reorder('created_at ASC').find_all_by_forum_id(self.forum_id).last.id + if self.parent_id + @parent_memo = Memo.find_by_id(self.parent_id) + # @parent_memo.last_reply_id = Memo.reorder('created_at ASC').find_all_by_parent_id(self.parent_id).last.id + @parent_memo.replies_count = @parent_memo.replies_count.to_int - 1 + @parent_memo.replies_count = 0 if @parent_memo.replies_count.to_int < 0 + @parent_memo.save + else + @forum.topic_count = @forum.topic_count.to_int - 1 + @forum.topic_count = 0 if @forum.topic_count.to_int < 0 + end + @forum.save + end + + #更新用户分数 -by zjc + def be_user_score + #新建memo且无parent的为发帖 + if self.parent_id.nil? + UserScore.joint(:post_message, User.current,nil,self ,{ memo_id: self.id }) + update_memo_number(User.current,1) + + #新建memo且有parent的为回帖 + elsif !self.parent_id.nil? + UserScore.joint(:reply_posting, User.current,self.parent.author,self, { memo_id: self.id }) + update_replay_for_memo(User.current,1) + end + end + + #被删除时更新用户分数 + def down_user_score + update_memo_number(User.current,1) + update_replay_for_memo(User.current,1) + end + + # Time 2015-03-26 15:20:24 + # Author lizanle + # Description 从硬盘上删除资源 + def delete_kindeditor_assets + delete_kindeditor_assets_from_disk self.id,OwnerTypeHelper::MEMO + end +end diff --git a/app/models/memo_observer.rb b/app/models/memo_observer.rb deleted file mode 100644 index 66cabe923..000000000 --- a/app/models/memo_observer.rb +++ /dev/null @@ -1,8 +0,0 @@ -class MemoObserver < ActiveRecord::Observer - def after_create(memo) - - thread1=Thread.new do - Mailer.forum_message_added(memo).deliver if Setting.notified_events.include?('forum_message_added') - end - end -end diff --git a/app/models/message.rb b/app/models/message.rb index 85a87132d..bbf62b5dc 100644 --- a/app/models/message.rb +++ b/app/models/message.rb @@ -1,212 +1,221 @@ -# Redmine - project management software -# Copyright (C) 2006-2013 Jean-Philippe Lang -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - -class Message < ActiveRecord::Base - include Redmine::SafeAttributes - include UserScoreHelper - belongs_to :board - belongs_to :author, :class_name => 'User', :foreign_key => 'author_id' - has_many :praise_tread, as: :praise_tread_object, dependent: :destroy - - acts_as_tree :counter_cache => :replies_count, :order => "#{Message.table_name}.created_on ASC" - acts_as_attachable - belongs_to :last_reply, :class_name => 'Message', :foreign_key => 'last_reply_id' - - # added by fq - has_many :acts, :class_name => 'Activity', :as => :act, :dependent => :destroy - # 被ForgeActivity虚拟关联 - has_many :forge_acts, :class_name => 'ForgeActivity',:as =>:forge_act ,:dependent => :destroy - # end - - acts_as_searchable :columns => ['subject', 'content'], - :include => {:board => :project}, - :project_key => "#{Board.table_name}.project_id", - :date_column => "#{table_name}.created_on" - acts_as_searchable :columns => ['subject', 'content'], - :include => {:board => :course}, - :course_key => "#{Board.table_name}.course_id", - :date_column => "#{table_name}.created_at" - acts_as_event :title => Proc.new {|o| "#{o.board.name}: #{o.subject}"}, - :description => :content, - :datetime => :updated_on, - # :datetime => "#{Message.table_name}.created_on", - :group => :parent, - :type => Proc.new {|o| o.parent_id.nil? ? 'message' : 'reply'}, - :url => Proc.new {|o| {:controller => 'messages', :action => 'show', :board_id => o.board_id}.merge(o.parent_id.nil? ? {:id => o.id} : - {:id => o.parent_id, :r => o.id, :anchor => "message-#{o.id}"})} - - acts_as_activity_provider :find_options => {:include => [{:board => :project}, :author]}, - :author_key => :author_id - acts_as_activity_provider :find_options => {:include => [{:board => :course}, :author]}, - :type => 'course_messages', - :author_key => :author_id - acts_as_watchable - - validates_presence_of :board, :subject, :content - validates_length_of :subject, :maximum => 255 - validate :cannot_reply_to_locked_topic, :on => :create - - after_create :add_author_as_watcher, :reset_counters! - after_update :update_messages_board - after_destroy :reset_counters!,:down_user_score - - # fq - after_create :act_as_activity,:be_user_score,:act_as_forge_activity - #before_save :be_user_score - # end - - scope :visible, lambda {|*args| - includes(:board => :project).where(Project.allowed_to_condition(args.shift || User.current, :view_messages, *args)) - } - - scope :course_visible, lambda {|*args| - includes(:board => :course).where(Course.allowed_to_condition(args.shift || User.current, :view_course_messages, *args)) - } - - - safe_attributes 'subject', 'content' - safe_attributes 'board_id','locked', 'sticky', - :if => lambda {|message, user| - if message.project - user.allowed_to?(:edit_messages, message.project) - else - user.allowed_to?(:edit_messages, message.course) - end - } - - def visible?(user=User.current) - if project - !user.nil? && user.allowed_to?(:view_messages, project) - elsif course - !user.nil? && user.allowed_to?(:view_messages, course) - end - end - - def cannot_reply_to_locked_topic - # Can not reply to a locked topic - errors.add :base, 'Topic is locked' if root.locked? && self != root - end - - def update_messages_board - if board_id_changed? - Message.update_all({:board_id => board_id}, ["id = ? OR parent_id = ?", root.id, root.id]) - Board.reset_counters!(board_id_was) - Board.reset_counters!(board_id) - end - end - - def reset_counters! - if parent && parent.id - Message.update_all({:last_reply_id => parent.children.maximum(:id)}, {:id => parent.id}) - end - board.reset_counters! - end - - def sticky=(arg) - write_attribute :sticky, (arg == true || arg.to_s == '1' ? 1 : 0) - end - - def sticky? - sticky == 1 - end - - def project - board.project - end - - def course - board.course - end - - def course_editable_by?(usr) - usr && usr.logged? && (usr.allowed_to?(:edit_messages, course) || (self.author == usr && usr.allowed_to?(:edit_own_messages, course))) - end - - def course_destroyable_by?(usr) - usr && usr.logged? && (usr.allowed_to?(:delete_messages, course) || (self.author == usr && usr.allowed_to?(:delete_own_messages, course))) - end - - def editable_by?(usr) - usr && usr.logged? && (usr.allowed_to?(:edit_messages, project) || (self.author == usr && usr.allowed_to?(:edit_own_messages, project))) - end - - def destroyable_by?(usr) - usr && usr.logged? && (usr.allowed_to?(:delete_messages, project) || (self.author == usr && usr.allowed_to?(:delete_own_messages, project))) - end - - private - - def add_author_as_watcher - Watcher.create(:watchable => self.root, :user => author) - end - - # fq - def act_as_activity - self.acts << Activity.new(:user_id => self.author_id) - end - # end - - # Time 2015-02-27 14:32:25 - # Author lizanle - # Description - def act_as_forge_activity - # 如果project为空,那么就是课程相关的消息 - if !self.board.project.nil? - self.forge_acts << ForgeActivity.new(:user_id => self.author_id, - :project_id => self.board.project.id) - end - end - - #更新用户分数 -by zjc - def be_user_score - #新建message且无parent的为发帖 - - if self.parent_id.nil? && !self.board.project.nil? - UserScore.joint(:post_message, self.author,nil,self, { message_id: self.id }) - update_memo_number(self.author,1) - if self.board.project_id != -1 - update_memo_number(self.author,2,self.board.project) - end - #新建message且有parent的为回帖 - elsif !self.parent_id.nil? && !self.board.project.nil? - UserScore.joint(:reply_posting, self.author,self.parent.author,self, { message_id: self.id }) - update_replay_for_memo(self.author,1) - if self.board.project_id != -1 - update_replay_for_memo(self.author,2,self.board.project) - end - end - end - - #减少用户分数 - def down_user_score - if self.parent_id.nil? && !self.board.project.nil? - UserScore.joint(:delete_message, self.author,nil,self, { message_id: self.id }) - update_memo_number(User.current,1) - if self.board.project_id != -1 - update_memo_number(self.author,2,self.board.project) - end - elsif !self.parent_id.nil? && !self.board.project.nil? - UserScore.joint(:reply_deleting, self.author,self.parent.author,self, { message_id: self.id }) - update_replay_for_memo(User.current,1) - if self.board.project_id != -1 - update_replay_for_memo(self.author,2,self.board.project) - end - end - end - - -end +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class Message < ActiveRecord::Base + include Redmine::SafeAttributes + include UserScoreHelper + include ApplicationHelper + has_many_kindeditor_assets :assets, :dependent => :destroy + belongs_to :board + belongs_to :author, :class_name => 'User', :foreign_key => 'author_id' + has_many :praise_tread, as: :praise_tread_object, dependent: :destroy + + acts_as_tree :counter_cache => :replies_count, :order => "#{Message.table_name}.created_on ASC" + acts_as_attachable + belongs_to :last_reply, :class_name => 'Message', :foreign_key => 'last_reply_id' + + # added by fq + has_many :acts, :class_name => 'Activity', :as => :act, :dependent => :destroy + # 被ForgeActivity虚拟关联 + has_many :forge_acts, :class_name => 'ForgeActivity',:as =>:forge_act ,:dependent => :destroy + # end + + acts_as_searchable :columns => ['subject', 'content'], + :include => {:board => :project}, + :project_key => "#{Board.table_name}.project_id", + :date_column => "#{table_name}.created_on" + acts_as_searchable :columns => ['subject', 'content'], + :include => {:board => :course}, + :course_key => "#{Board.table_name}.course_id", + :date_column => "#{table_name}.created_at" + acts_as_event :title => Proc.new {|o| "#{o.board.name}: #{o.subject}"}, + :description => :content, + :datetime => :updated_on, + # :datetime => "#{Message.table_name}.created_on", + :group => :parent, + :type => Proc.new {|o| o.parent_id.nil? ? 'message' : 'reply'}, + :url => Proc.new {|o| {:controller => 'messages', :action => 'show', :board_id => o.board_id}.merge(o.parent_id.nil? ? {:id => o.id} : + {:id => o.parent_id, :r => o.id, :anchor => "message-#{o.id}"})} + + acts_as_activity_provider :find_options => {:include => [{:board => :project}, :author]}, + :author_key => :author_id + acts_as_activity_provider :find_options => {:include => [{:board => :course}, :author]}, + :type => 'course_messages', + :author_key => :author_id + acts_as_watchable + + validates_presence_of :board, :subject, :content + validates_length_of :subject, :maximum => 255 + validate :cannot_reply_to_locked_topic, :on => :create + + after_create :add_author_as_watcher, :reset_counters! + after_update :update_messages_board + after_destroy :reset_counters!,:down_user_score,:delete_kindeditor_assets + + after_create :act_as_activity,:be_user_score,:act_as_forge_activity, :send_mail + #before_save :be_user_score + + scope :visible, lambda {|*args| + includes(:board => :project).where(Project.allowed_to_condition(args.shift || User.current, :view_messages, *args)) + } + + scope :course_visible, lambda {|*args| + includes(:board => :course).where(Course.allowed_to_condition(args.shift || User.current, :view_course_messages, *args)) + } + + + safe_attributes 'subject', 'content' + safe_attributes 'board_id','locked', 'sticky', + :if => lambda {|message, user| + if message.project + user.allowed_to?(:edit_messages, message.project) + else + user.allowed_to?(:edit_messages, message.course) + end + } + + def visible?(user=User.current) + if project + !user.nil? && user.allowed_to?(:view_messages, project) + elsif course + !user.nil? && user.allowed_to?(:view_messages, course) + end + end + + def cannot_reply_to_locked_topic + # Can not reply to a locked topic + errors.add :base, 'Topic is locked' if root.locked? && self != root + end + + def update_messages_board + if board_id_changed? + Message.update_all({:board_id => board_id}, ["id = ? OR parent_id = ?", root.id, root.id]) + Board.reset_counters!(board_id_was) + Board.reset_counters!(board_id) + end + end + + def reset_counters! + if parent && parent.id + Message.update_all({:last_reply_id => parent.children.maximum(:id)}, {:id => parent.id}) + end + board.reset_counters! + end + + def sticky=(arg) + write_attribute :sticky, (arg == true || arg.to_s == '1' ? 1 : 0) + end + + def sticky? + sticky == 1 + end + + def project + board.project + end + + def course + board.course + end + + def course_editable_by?(usr) + usr && usr.logged? && (usr.allowed_to?(:edit_messages, course) || (self.author == usr && usr.allowed_to?(:edit_own_messages, course))) + end + + def course_destroyable_by?(usr) + usr && usr.logged? && (usr.allowed_to?(:delete_messages, course) || (self.author == usr && usr.allowed_to?(:delete_own_messages, course))) + end + + def editable_by?(usr) + usr && usr.logged? && (usr.allowed_to?(:edit_messages, project) || (self.author == usr && usr.allowed_to?(:edit_own_messages, project))) + end + + def destroyable_by?(usr) + usr && usr.logged? && (usr.allowed_to?(:delete_messages, project) || (self.author == usr && usr.allowed_to?(:delete_own_messages, project))) + end + + private + + def add_author_as_watcher + Watcher.create(:watchable => self.root, :user => author) + end + + # fq + def act_as_activity + self.acts << Activity.new(:user_id => self.author_id) + end + # end + + # Time 2015-02-27 14:32:25 + # Author lizanle + # Description + def act_as_forge_activity + # 如果project为空,那么就是课程相关的消息 + if !self.board.project.nil? + self.forge_acts << ForgeActivity.new(:user_id => self.author_id, + :project_id => self.board.project.id) + end + end + + #更新用户分数 -by zjc + def be_user_score + #新建message且无parent的为发帖 + + if self.parent_id.nil? && !self.board.project.nil? + UserScore.joint(:post_message, self.author,nil,self, { message_id: self.id }) + update_memo_number(self.author,1) + if self.board.project_id != -1 + update_memo_number(self.author,2,self.board.project) + end + #新建message且有parent的为回帖 + elsif !self.parent_id.nil? && !self.board.project.nil? + UserScore.joint(:reply_posting, self.author,self.parent.author,self, { message_id: self.id }) + update_replay_for_memo(self.author,1) + if self.board.project_id != -1 + update_replay_for_memo(self.author,2,self.board.project) + end + end + end + + #减少用户分数 + def down_user_score + if self.parent_id.nil? && !self.board.project.nil? + UserScore.joint(:delete_message, self.author,nil,self, { message_id: self.id }) + update_memo_number(User.current,1) + if self.board.project_id != -1 + update_memo_number(self.author,2,self.board.project) + end + elsif !self.parent_id.nil? && !self.board.project.nil? + UserScore.joint(:reply_deleting, self.author,self.parent.author,self, { message_id: self.id }) + update_replay_for_memo(User.current,1) + if self.board.project_id != -1 + update_replay_for_memo(self.author,2,self.board.project) + end + end + end + + def send_mail + Mailer.run.message_posted(self) if Setting.notified_events.include?('message_posted') + end + + # Time 2015-03-31 09:15:06 + # Author lizanle + # Description 删除对应消息的图片资源 + def delete_kindeditor_assets + delete_kindeditor_assets_from_disk self.id,OwnerTypeHelper::MESSAGE + end +end diff --git a/app/models/message_observer.rb b/app/models/message_observer.rb deleted file mode 100644 index 383301664..000000000 --- a/app/models/message_observer.rb +++ /dev/null @@ -1,25 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2013 Jean-Philippe Lang -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - -class MessageObserver < ActiveRecord::Observer - def after_create(message) - ##by senluo - thread5=Thread.new do - Mailer.message_posted(message).deliver if Setting.notified_events.include?('message_posted') - end - end -end diff --git a/app/models/news.rb b/app/models/news.rb index d2547fc02..7e809cbfd 100644 --- a/app/models/news.rb +++ b/app/models/news.rb @@ -1,108 +1,120 @@ -# Redmine - project management software -# Copyright (C) 2006-2013 Jean-Philippe Lang -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - -class News < ActiveRecord::Base - include Redmine::SafeAttributes - belongs_to :project - #added by nwb - belongs_to :course - belongs_to :author, :class_name => 'User', :foreign_key => 'author_id' - has_many :comments, :as => :commented, :dependent => :delete_all, :order => "created_on" - # fq - has_many :acts, :class_name => 'Activity', :as => :act, :dependent => :destroy - # 被ForgeActivity虚拟关联 - has_many :forge_acts, :class_name => 'ForgeActivity',:as =>:forge_act ,:dependent => :destroy - # end - - validates_presence_of :title, :description - validates_length_of :title, :maximum => 60 - validates_length_of :summary, :maximum => 255 - validates_length_of :description, :maximum => 10000 - - acts_as_attachable :delete_permission => :manage_news - acts_as_searchable :columns => ['title', 'summary', "#{table_name}.description"], :include => :project - acts_as_event :url => Proc.new {|o| {:controller => 'news', :action => 'show', :id => o.id}} - acts_as_activity_provider :find_options => {:include => [:project, :author]}, - :author_key => :author_id - #added by nwb - #课程新闻独立于项目 - acts_as_activity_provider :type => 'course_news', - :find_options => {:include => [:course, :author]}, - :author_key => :author_id - acts_as_watchable - - after_create :add_author_as_watcher - # fq - after_create :act_as_activity,:act_as_forge_activity - # end - - scope :visible, lambda {|*args| - includes(:project).where(Project.allowed_to_condition(args.shift || User.current, :view_news, *args)) - } - - scope :course_visible, lambda {|*args| - includes(:course).where(Course.allowed_to_condition(args.shift || User.current, :view_course_news, *args)) - } - safe_attributes 'title', 'summary', 'description' - - def visible?(user=User.current) - !user.nil? && user.allowed_to?(:view_news, project) - end - - # Returns true if the news can be commented by user - def commentable?(user=User.current) - user.allowed_to?(:comment_news, project) - end - - def recipients - project.users.select {|user| user.notify_about?(self)}.map(&:mail) - end - - # returns latest news for projects visible by user - def self.latest(user = User.current, count = 5) - visible(user).includes([:author, :project]).order("#{News.table_name}.created_on DESC").limit(count).all - end - - # 新闻的短描述信息 - def short_description(length = 255) - description.gsub(/<\/?.*?>/,"").html_safe if description - #description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description - #description - end - - private - - def add_author_as_watcher - Watcher.create(:watchable => self, :user => author) - end - ## fq - def act_as_activity - self.acts << Activity.new(:user_id => self.author_id) - end - - # Time 2015-02-27 15:48:17 - # Author lizanle - # Description 公用表中也要记录 - def act_as_forge_activity - # 如果是project为空,那么是课程相关的,不需要保存 - if !self.project.nil? - self.forge_acts << ForgeActivity.new(:user_id => self.author_id, - :project_id => self.project.id) - end - end - -end +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class News < ActiveRecord::Base + include Redmine::SafeAttributes + belongs_to :project + include ApplicationHelper + has_many_kindeditor_assets :assets, :dependent => :destroy + #added by nwb + belongs_to :course + belongs_to :author, :class_name => 'User', :foreign_key => 'author_id' + has_many :comments, :as => :commented, :dependent => :delete_all, :order => "created_on" + # fq + has_many :acts, :class_name => 'Activity', :as => :act, :dependent => :destroy + # 被ForgeActivity虚拟关联 + has_many :forge_acts, :class_name => 'ForgeActivity',:as =>:forge_act ,:dependent => :destroy + # end + + validates_presence_of :title, :description + validates_length_of :title, :maximum => 60 + validates_length_of :summary, :maximum => 255 + validates_length_of :description, :maximum => 10000 + + acts_as_attachable :delete_permission => :manage_news + acts_as_searchable :columns => ['title', 'summary', "#{table_name}.description"], :include => :project + acts_as_event :url => Proc.new {|o| {:controller => 'news', :action => 'show', :id => o.id}} + acts_as_activity_provider :find_options => {:include => [:project, :author]}, + :author_key => :author_id + #added by nwb + #课程新闻独立于项目 + acts_as_activity_provider :type => 'course_news', + :find_options => {:include => [:course, :author]}, + :author_key => :author_id + acts_as_watchable + + after_create :act_as_activity,:act_as_forge_activity,:add_author_as_watcher, :send_mail + + after_destroy :delete_kindeditor_assets + + scope :visible, lambda {|*args| + includes(:project).where(Project.allowed_to_condition(args.shift || User.current, :view_news, *args)) + } + + scope :course_visible, lambda {|*args| + includes(:course).where(Course.allowed_to_condition(args.shift || User.current, :view_course_news, *args)) + } + safe_attributes 'title', 'summary', 'description' + + def visible?(user=User.current) + !user.nil? && user.allowed_to?(:view_news, project) + end + + # Returns true if the news can be commented by user + def commentable?(user=User.current) + user.allowed_to?(:comment_news, project) + end + + def recipients + project.users.select {|user| user.notify_about?(self)}.map(&:mail) + end + + # returns latest news for projects visible by user + def self.latest(user = User.current, count = 5) + visible(user).includes([:author, :project]).order("#{News.table_name}.created_on DESC").limit(count).all + end + + # 新闻的短描述信息 + def short_description(length = 255) + description.gsub(/<\/?.*?>/,"").html_safe if description + #description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description + #description + end + + private + + def add_author_as_watcher + Watcher.create(:watchable => self, :user => author) + end + ## fq + def act_as_activity + self.acts << Activity.new(:user_id => self.author_id) + end + + # Time 2015-02-27 15:48:17 + # Author lizanle + # Description 公用表中也要记录 + def act_as_forge_activity + # 如果是project为空,那么是课程相关的,不需要保存 + if !self.project.nil? + self.forge_acts << ForgeActivity.new(:user_id => self.author_id, + :project_id => self.project.id) + end + end + + # Time 2015-03-31 13:50:54 + # Author lizanle + # Description 删除news后删除对应的资源 + def delete_kindeditor_assets + delete_kindeditor_assets_from_disk self.id,OwnerTypeHelper::NEWS + end + + def send_mail + Mailer.run.news_added(self) if Setting.notified_events.include?('news_added') + end + +end diff --git a/app/models/news_observer.rb b/app/models/news_observer.rb deleted file mode 100644 index 8b9bc7b4b..000000000 --- a/app/models/news_observer.rb +++ /dev/null @@ -1,25 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2013 Jean-Philippe Lang -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - -class NewsObserver < ActiveRecord::Observer - def after_create(news) - ##by senluo - thread6=Thread.new do - Mailer.news_added(news).deliver if Setting.notified_events.include?('news_added') - end - end -end diff --git a/app/models/relative_memo.rb b/app/models/relative_memo.rb index f087fce2b..07767c7d6 100644 --- a/app/models/relative_memo.rb +++ b/app/models/relative_memo.rb @@ -1,199 +1,199 @@ -class RelativeMemo < ActiveRecord::Base - # attr_accessible :title, :body - include Redmine::SafeAttributes - belongs_to :open_source_project, :class_name => "OpenSourceProject", :foreign_key => 'osp_id' - belongs_to :author, :class_name => "User", :foreign_key => 'author_id' - - has_many :tags, :through => :project_tags, :class_name => 'Tag' - has_many :project_tags, :class_name => 'ProjectTags' - - has_many :relation_topics, :class_name => 'RelativeMemoToOpenSourceProject' - - has_many :no_uses, :as => :no_use, :dependent => :delete_all - - has_many :bugs_to_osp, :class_name => 'BugToOsp', :foreign_key => 'relative_memo_id', :dependent => :destroy - - - acts_as_taggable - - validates_presence_of :subject - #validates :content, presence: true - # validates_length_of :subject, maximum: 50 - #validates_length_of :content, maximum: 3072 - validate :cannot_reply_to_locked_topic, :on => :create - validates_uniqueness_of :osp_id, :scope => [:subject, :content] - - acts_as_tree :counter_cache => :replies_count, :order => "#{RelativeMemo.table_name}.created_at ASC" - acts_as_attachable - belongs_to :last_reply, :class_name => 'RelativeMemo', :foreign_key => 'last_reply_id' - # acts_as_searchable :column => ['subject', 'content'], - # #:include => { :forum => :p} - # #:project_key => "#{Forum.table_name}.project_id" - # :date_column => "#{table_name}.created_at" - - # acts_as_event :title => Proc.new {|o| "#{o.forum.name}: #{o.subject}"}, - # :datetime => :updated_at, - # # :datetime => :created_at, - # :description => :content, - # :author => :author, - # :type => Proc.new {|o| o.parent_id.nil? ? 'Memo' : 'Reply'}, - # :url => Proc.new {|o| {:controller => 'memos', :action => 'show', :forum_id => o.forum_id}.merge(o.parent_id.nil? ? {:id => o.id} : {:id => o.parent_id, :r => o.id, :anchor => "reply-#{o.id}"})} - # acts_as_activity_provider :author_key => :author_id, - # :func => 'memos', - # :timestamp => 'created_at' - - # :find_options => {:type => 'memos'} - # acts_as_watchable - - safe_attributes "author_id", - "subject", - "content", - "osp_id", - "last_memo_id", - "lock", - "sticky", - "parent_id", - "replies_count", - "is_quote" - - after_create :add_author_as_watcher, :reset_counters! - # after_update :update_memos_forum - after_destroy :reset_counters! - # after_create :send_notification - # after_save :plusParentAndForum - # after_destroy :minusParentAndForum - - # scope :visible, lambda { |*args| - # includes(:forum => ).where() - # } - - def cannot_reply_to_locked_topic - errors.add :base, l(:label_memo_locked) if root.locked? && self != root - end - - def short_content(length = 25) - str = "^(.{,#{length}})[^\n\r]*.*$" - content.gsub(Regexp.new(str), '\1...').strip if content - end - - # def update_memos_forum - # if forum_id_changed? - # Message.update_all({:board_id => board_id}, ["id = ? OR parent_id = ?", root.id, root.id ]) - # Forum.reset_counters!(forum_id_was) - # Forum.reset_counters!(forum_id) - # end - # end - - - scope :no_use_for, lambda { |user_id| - { :include => :no_uses, - :conditions => ["#{NoUse.table_name}.user_id = ?", user_id] } - } - - # 获取帖子的回复 - def replies - memos = RelativeMemo.where("parent_id = ?", id) - end - - def no_use_for?(user) - self.no_uses.each do |no_use| - if no_use.user_id == user.id - return true - end - end - false - end - - def set_no_use(user, flag=true) - flag ? set_filter(user) : remove_filter(user) - end - - def set_filter(user) - self.no_uses << NoUse.new(:user => user) - end - - def remove_filter(user) - return nil unless user && user.is_a?(User) - NoUse.delete_all "no_use_type = '#{self.class}' AND no_use_id = #{self.id} AND user_id = #{user.id}" - end - - def reset_counters! - if parent && parent.id - RelativeMemo.update_all({:last_reply_id => parent.children.maximum(:id)}, {:id => parent.id}) - parent.update_attribute(:updated_at, Time.now) - end - # forum.reset_counters! - end - - def sticky? - sticky == 1 - end - - def replies - RelativeMemo.where("parent_id = ?", id) - end - - def locked? - self.lock - end - - def editable_by? user - # user && user.logged? || (self.author == usr && usr.allowed_to?(:edit_own_messages, project)) - user.admin? - end - - # def destroyable_by? user - # (user && user.logged? && (Forum.find(self.forum_id).creator_id == user.id) ) || user.admin? - # #self.author == user || user.admin? - # end - - def deleted_attach_able_by? user - (user && user.logged? && (self.author == user) ) || user.admin? - end - - private - - def add_author_as_watcher - Watcher.create(:watchable => self.root, :user => author) - end - - def send_notification - if Setting.notified_events.include?('message_posted') - Mailer.message_posted(self).deliver - end - end - - # def plusParentAndForum - # @forum = Forum.find(self.forum_id) - # @forum.memo_count = @forum.memo_count.to_int + 1 - # @forum.last_memo_id = self.id - # if self.parent_id - # @parent_memo = Memo.find_by_id(self.parent_id) - # @parent_memo.last_reply_id = self - # @parent_memo.replies_count = @parent_memo.replies_count.to_int + 1 - # @parent_memo.save - # else - # @forum.topic_count = @forum.topic_count.to_int + 1 - # end - # @forum.save - # end - - # def minusParentAndForum - # @forum = Forum.find(self.forum_id) - # @forum.memo_count = @forum.memo_count.to_int - 1 - # @forum.memo_count = 0 if @forum.memo_count.to_int < 0 - # # @forum.last_memo_id = Memo.reorder('created_at ASC').find_all_by_forum_id(self.forum_id).last.id - # if self.parent_id - # @parent_memo = Memo.find_by_id(self.parent_id) - # # @parent_memo.last_reply_id = Memo.reorder('created_at ASC').find_all_by_parent_id(self.parent_id).last.id - # @parent_memo.replies_count = @parent_memo.replies_count.to_int - 1 - # @parent_memo.replies_count = 0 if @parent_memo.replies_count.to_int < 0 - # @parent_memo.save - # else - # @forum.topic_count = @forum.topic_count.to_int - 1 - # @forum.topic_count = 0 if @forum.topic_count.to_int < 0 - # end - # @forum.save - # end -end - +class RelativeMemo < ActiveRecord::Base + # attr_accessible :title, :body + include Redmine::SafeAttributes + belongs_to :open_source_project, :class_name => "OpenSourceProject", :foreign_key => 'osp_id' + belongs_to :author, :class_name => "User", :foreign_key => 'author_id' + + has_many :tags, :through => :project_tags, :class_name => 'Tag' + has_many :project_tags, :class_name => 'ProjectTags' + + has_many :relation_topics, :class_name => 'RelativeMemoToOpenSourceProject' + + has_many :no_uses, :as => :no_use, :dependent => :delete_all + + has_many :bugs_to_osp, :class_name => 'BugToOsp', :foreign_key => 'relative_memo_id', :dependent => :destroy + + + acts_as_taggable + + validates_presence_of :subject + #validates :content, presence: true + # validates_length_of :subject, maximum: 50 + #validates_length_of :content, maximum: 3072 + validate :cannot_reply_to_locked_topic, :on => :create + validates_uniqueness_of :osp_id, :scope => [:subject, :content] + + acts_as_tree :counter_cache => :replies_count, :order => "#{RelativeMemo.table_name}.created_at ASC" + acts_as_attachable + belongs_to :last_reply, :class_name => 'RelativeMemo', :foreign_key => 'last_reply_id' + # acts_as_searchable :column => ['subject', 'content'], + # #:include => { :forum => :p} + # #:project_key => "#{Forum.table_name}.project_id" + # :date_column => "#{table_name}.created_at" + + # acts_as_event :title => Proc.new {|o| "#{o.forum.name}: #{o.subject}"}, + # :datetime => :updated_at, + # # :datetime => :created_at, + # :description => :content, + # :author => :author, + # :type => Proc.new {|o| o.parent_id.nil? ? 'Memo' : 'Reply'}, + # :url => Proc.new {|o| {:controller => 'memos', :action => 'show', :forum_id => o.forum_id}.merge(o.parent_id.nil? ? {:id => o.id} : {:id => o.parent_id, :r => o.id, :anchor => "reply-#{o.id}"})} + # acts_as_activity_provider :author_key => :author_id, + # :func => 'memos', + # :timestamp => 'created_at' + + # :find_options => {:type => 'memos'} + # acts_as_watchable + + safe_attributes "author_id", + "subject", + "content", + "osp_id", + "last_memo_id", + "lock", + "sticky", + "parent_id", + "replies_count", + "is_quote" + + after_create :add_author_as_watcher, :reset_counters! + # after_update :update_memos_forum + after_destroy :reset_counters! + # after_create :send_notification + # after_save :plusParentAndForum + # after_destroy :minusParentAndForum + + # scope :visible, lambda { |*args| + # includes(:forum => ).where() + # } + + def cannot_reply_to_locked_topic + errors.add :base, l(:label_memo_locked) if root.locked? && self != root + end + + def short_content(length = 25) + str = "^(.{,#{length}})[^\n\r]*.*$" + content.gsub(Regexp.new(str), '\1...').strip if content + end + + # def update_memos_forum + # if forum_id_changed? + # Message.update_all({:board_id => board_id}, ["id = ? OR parent_id = ?", root.id, root.id ]) + # Forum.reset_counters!(forum_id_was) + # Forum.reset_counters!(forum_id) + # end + # end + + + scope :no_use_for, lambda { |user_id| + { :include => :no_uses, + :conditions => ["#{NoUse.table_name}.user_id = ?", user_id] } + } + + # 获取帖子的回复 + def replies + memos = RelativeMemo.where("parent_id = ?", id) + end + + def no_use_for?(user) + self.no_uses.each do |no_use| + if no_use.user_id == user.id + return true + end + end + false + end + + def set_no_use(user, flag=true) + flag ? set_filter(user) : remove_filter(user) + end + + def set_filter(user) + self.no_uses << NoUse.new(:user => user) + end + + def remove_filter(user) + return nil unless user && user.is_a?(User) + NoUse.delete_all "no_use_type = '#{self.class}' AND no_use_id = #{self.id} AND user_id = #{user.id}" + end + + def reset_counters! + if parent && parent.id + RelativeMemo.update_all({:last_reply_id => parent.children.maximum(:id)}, {:id => parent.id}) + parent.update_attribute(:updated_at, Time.now) + end + # forum.reset_counters! + end + + def sticky? + sticky == 1 + end + + def replies + RelativeMemo.where("parent_id = ?", id) + end + + def locked? + self.lock + end + + def editable_by? user + # user && user.logged? || (self.author == usr && usr.allowed_to?(:edit_own_messages, project)) + user.admin? + end + + # def destroyable_by? user + # (user && user.logged? && (Forum.find(self.forum_id).creator_id == user.id) ) || user.admin? + # #self.author == user || user.admin? + # end + + def deleted_attach_able_by? user + (user && user.logged? && (self.author == user) ) || user.admin? + end + + private + + def add_author_as_watcher + Watcher.create(:watchable => self.root, :user => author) + end + + def send_notification + if Setting.notified_events.include?('message_posted') + Mailer.run.message_posted(self) + end + end + + # def plusParentAndForum + # @forum = Forum.find(self.forum_id) + # @forum.memo_count = @forum.memo_count.to_int + 1 + # @forum.last_memo_id = self.id + # if self.parent_id + # @parent_memo = Memo.find_by_id(self.parent_id) + # @parent_memo.last_reply_id = self + # @parent_memo.replies_count = @parent_memo.replies_count.to_int + 1 + # @parent_memo.save + # else + # @forum.topic_count = @forum.topic_count.to_int + 1 + # end + # @forum.save + # end + + # def minusParentAndForum + # @forum = Forum.find(self.forum_id) + # @forum.memo_count = @forum.memo_count.to_int - 1 + # @forum.memo_count = 0 if @forum.memo_count.to_int < 0 + # # @forum.last_memo_id = Memo.reorder('created_at ASC').find_all_by_forum_id(self.forum_id).last.id + # if self.parent_id + # @parent_memo = Memo.find_by_id(self.parent_id) + # # @parent_memo.last_reply_id = Memo.reorder('created_at ASC').find_all_by_parent_id(self.parent_id).last.id + # @parent_memo.replies_count = @parent_memo.replies_count.to_int - 1 + # @parent_memo.replies_count = 0 if @parent_memo.replies_count.to_int < 0 + # @parent_memo.save + # else + # @forum.topic_count = @forum.topic_count.to_int - 1 + # @forum.topic_count = 0 if @forum.topic_count.to_int < 0 + # end + # @forum.save + # end +end + diff --git a/app/models/user.rb b/app/models/user.rb index 9abe80779..a17652afa 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,1029 +1,1030 @@ -# Redmine - project management software -# Copyright (C) 2006-2013 Jean-Philippe Lang -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - -require "digest/sha1" - -class User < Principal - TEACHER = 0 - STUDENT = 1 - ENTERPRISE = 2 - DEVELOPER = 3 - - include Redmine::SafeAttributes - seems_rateable_rater - # Different ways of displaying/sorting users - USER_FORMATS = { - :firstname_lastname => { - :string => '#{firstname} #{lastname}', - :order => %w(firstname lastname id), - :setting_order => 1 - }, - :firstname_lastinitial => { - :string => '#{firstname} #{lastname.to_s.chars.first}.', - :order => %w(firstname lastname id), - :setting_order => 2 - }, - :firstname => { - :string => '#{firstname}', - :order => %w(firstname id), - :setting_order => 3 - }, - :lastname_firstname => { - :string => '#{lastname} #{firstname}', - :order => %w(lastname firstname id), - :setting_order => 4 - }, - :lastname_coma_firstname => { - :string => '#{lastname}, #{firstname}', - :order => %w(lastname firstname id), - :setting_order => 5 - }, - :lastname => { - :string => '#{lastname}', - :order => %w(lastname id), - :setting_order => 6 - }, - :username => { - :string => '#{login}', - :order => %w(login id), - :setting_order => 7 - }, - } - - MAIL_NOTIFICATION_OPTIONS = [ - ['all', :label_user_mail_option_all], - ['week', :label_user_mail_option_week], - ['day', :label_user_mail_option_day], - ['none', :label_user_mail_option_none] - ] - - has_many :homework_users - has_many :homework_attaches, :through => :homework_users - has_many :homework_evaluations - - #问卷相关关关系 - has_many :poll_users, :dependent => :destroy - has_many :poll_votes, :dependent => :destroy - has_many :poll, :dependent => :destroy #用户创建的问卷 - has_many :answers, :source => :poll, :through => :poll_users, :dependent => :destroy #用户已经完成问答的问卷 - # end - - has_and_belongs_to_many :groups, :after_add => Proc.new {|user, group| group.user_added(user)}, - :after_remove => Proc.new {|user, group| group.user_removed(user)} - has_many :changesets, :dependent => :nullify - has_one :preference, :dependent => :destroy, :class_name => 'UserPreference' - has_one :rss_token, :class_name => 'Token', :conditions => "action='feeds'" - has_one :api_token, :class_name => 'Token', :conditions => "action='api'" - belongs_to :auth_source - belongs_to :ucourse, :class_name => 'Course', :foreign_key => :id #huang -## added by xianbo for delete - has_many :biding_projects, :dependent => :destroy - has_many :contesting_projects, :dependent => :destroy - belongs_to :softapplication, :foreign_key => 'id', :dependent => :destroy -##ended by xianbo - -#####fq - has_many :jours, :class_name => 'JournalsForMessage', :dependent => :destroy - has_many :journals_messages, :class_name => 'JournalsForMessage', :foreign_key => "user_id", :dependent => :destroy - has_many :bids, :foreign_key => 'author_id', :dependent => :destroy - has_many :contests, :foreign_key => 'author_id', :dependent => :destroy - has_many :softapplications, :foreign_key => 'user_id', :dependent => :destroy - has_many :journals_for_messages, :as => :jour, :dependent => :destroy - has_many :new_jours, :as => :jour, :class_name => 'JournalsForMessage', :conditions => "status=1" - has_many :journal_replies, :dependent => :destroy - has_many :activities, :dependent => :destroy - has_many :students_for_courses - #has_many :courses, :through => :students_for_courses, :source => :project - has_many :acts, :class_name => 'Activity', :as => :act, :dependent => :destroy - has_many :file_commit, :class_name => 'Attachment', :foreign_key => 'author_id', :conditions => "container_type = 'Project' or container_type = 'Version'" -#### -# added by bai - has_many :join_in_contests, :dependent => :destroy - has_many :news, :foreign_key => 'author_id' - has_many :contestnotification, :foreign_key => 'author_id' - has_many :comments, :foreign_key => 'author_id' - has_many :notificationcomments, :foreign_key => 'author_id' - has_many :wiki_contents, :foreign_key => 'author_id' - has_many :journals - has_many :messages, :foreign_key => 'author_id' - has_one :user_score, :dependent => :destroy - has_many :documents # 项目中关联的文档再次与人关联 -# end - -######added by nie - has_many :project_infos, :dependent => :destroy - has_one :user_status, :dependent => :destroy - ##### - has_many :shares ,:dependent => :destroy - - # add by zjc - has_one :level, :class_name => 'UserLevels', :dependent => :destroy - has_many :memos , :foreign_key => 'author_id' - ##### - scope :logged, lambda { where("#{User.table_name}.status <> #{STATUS_ANONYMOUS}") } - scope :status, lambda {|arg| where(arg.blank? ? nil : {:status => arg.to_i}) } - scope :visible, lambda {|*args| - nil - } - - - acts_as_customizable - ############################added by william - acts_as_taggable - scope :by_join_date, order("created_on DESC") - ############################# added by liuping 关注 - acts_as_watchable - - has_one :user_extensions,:dependent => :destroy - ## end - - # default_scope -> { includes(:user_extensions, :user_score) } - scope :teacher, -> { - joins(:user_extensions).where('user_extensions.identity = ?', UserExtensions::TEACHER) - } - scope :student, -> { - joins(:user_extensions).where('user_extensions.identity = ?', UserExtensions::STUDENT) - } - scope :developer, -> { - joins(:user_extensions).where('user_extensions.identity = ?', UserExtensions::DEVELOPER) - } - scope :enterprise, -> { - joins(:user_extensions).where('user_extensions.identity = ?', UserExtensions::ENTERPRISE) - } - - attr_accessor :password, :password_confirmation - attr_accessor :last_before_login_on - # Prevents unauthorized assignments - attr_protected :login, :admin, :password, :password_confirmation, :hashed_password - - LOGIN_LENGTH_LIMIT = 25 - MAIL_LENGTH_LIMIT = 60 - - validates_presence_of :login, :mail, :if => Proc.new { |user| !user.is_a?(AnonymousUser) } - validates_uniqueness_of :login, :if => Proc.new { |user| user.login_changed? && user.login.present? }, :case_sensitive => false - validates_uniqueness_of :mail, :if => Proc.new { |user| user.mail_changed? && user.mail.present? }, :case_sensitive => false - # Login must contain letters, numbers, underscores only - validates_format_of :login, :with => /\A[a-z0-9_\-@\.]*\z/i - validates_length_of :login, :maximum => LOGIN_LENGTH_LIMIT - validates_length_of :firstname, :maximum => 30 - validates_length_of :lastname, :maximum => 30 - validates_format_of :mail, :with => /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i, :allow_blank => true - validates_length_of :mail, :maximum => MAIL_LENGTH_LIMIT, :allow_nil => true - validates_confirmation_of :password, :allow_nil => true - validates_inclusion_of :mail_notification, :in => MAIL_NOTIFICATION_OPTIONS.collect(&:first), :allow_blank => true - validate :validate_password_length - # validates_email_realness_of :mail - before_create :set_mail_notification - before_save :update_hashed_password - before_destroy :remove_references_before_destroy - # added by fq - after_create :act_as_activity - # end - - scope :in_group, lambda {|group| - group_id = group.is_a?(Group) ? group.id : group.to_i - where("#{User.table_name}.id IN (SELECT gu.user_id FROM #{table_name_prefix}groups_users#{table_name_suffix} gu WHERE gu.group_id = ?)", group_id) - } - scope :not_in_group, lambda {|group| - group_id = group.is_a?(Group) ? group.id : group.to_i - where("#{User.table_name}.id NOT IN (SELECT gu.user_id FROM #{table_name_prefix}groups_users#{table_name_suffix} gu WHERE gu.group_id = ?)", group_id) - } - scope :sorted, lambda { order(*User.fields_for_order_statement)} - - scope :like, lambda {|arg, type| - if arg.blank? - where(nil) - else - pattern = "%#{arg.to_s.strip.downcase}%" - #where(" LOWER(concat(lastname, firstname)) LIKE :p ", :p => pattern) - if type == "0" - where(" LOWER(login) LIKE '#{pattern}' ") - elsif type == "1" - where(" LOWER(concat(lastname, firstname)) LIKE '#{pattern}' ") - elsif type == "3" - where(" LOWER(concat(lastname, firstname,login)) LIKE '#{pattern}' ") - else - where(" LOWER(mail) LIKE '#{pattern}' ") - end - end - } - - - # ====================================================================== - - def extensions - self.user_extensions ||= UserExtensions.new - end - - def user_score_attr - self.user_score ||= UserScore.new - end - - # ====================================================================== - - #选择项目成员时显示的用户信息文字 - def userInfo - if self.realname.gsub(' ','') == "" || self.realname.nil? - info = self.nickname; - else - info=self.nickname + ' (' + self.realname + ')'; - end - info - end - - ###添加留言 fq - def add_jour(user, notes, reference_user_id = 0, options = {}) - if options.count == 0 - self.journals_for_messages << JournalsForMessage.new(:user_id => user.id, :notes => notes, :reply_id => reference_user_id, :status => true) - else - jfm = self.journals_for_messages.build(options) - jfm.save - jfm - end - end - - # 判断用户是否加入了竞赛中 fq - def join_in_contest?(bid) - joined = JoinInContest.where('user_id = ? and bid_id =?', self.id, bid.id) - if joined.size > 0 - true - else - false - end - end - - ### fq - def join_in?(course) - joined = StudentsForCourse.where('student_id = ? and course_id = ?', self.id, course.id) - if joined.size > 0 - true - else - false - end - end - - def show_name - unless self.user_extensions.nil? - if self.user_extensions.identity == 2 - firstname - else - lastname+firstname - end - else - lastname+firstname - end - end - ## end - - def count_new_jour - count = self.new_jours.count - end - - #added by nie - def count_new_journal_reply - count = self.journal_reply.count - end - - def set_mail_notification - ##add byxianbo - thread=Thread.new do - self.mail_notification = Setting.default_notification_option if self.mail_notification.blank? - true - end - end - - def update_hashed_password - # update hashed_password if password was set - if self.password && self.auth_source_id.blank? - salt_password(password) - end - end - - alias :base_reload :reload - def reload(*args) - @name = nil - @projects_by_role = nil - @courses_by_role = nil - @membership_by_project_id = nil - base_reload(*args) - end - - def mail=(arg) - write_attribute(:mail, arg.to_s.strip) - end - - def identity_url=(url) - if url.blank? - write_attribute(:identity_url, '') - else - begin - write_attribute(:identity_url, OpenIdAuthentication.normalize_identifier(url)) - rescue OpenIdAuthentication::InvalidOpenId - # Invalid url, don't save - end - end - self.read_attribute(:identity_url) - end - - VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z]+)*\.[a-z]+\z/i - # VALID_EMAIL_REGEX = /^[0-9a-zA-Z_-]+@[0-9a-zA-Z_-]+(\.[0-9a-zA-Z_-]+)+$/ - # Returns the user that matches provided login and password, or nil - #登录,返回用户名与密码匹配的用户 - def self.try_to_login(login, password) - login = login.to_s.lstrip.rstrip - password = password.to_s - - # Make sure no one can sign in with an empty login or password - return nil if login.empty? || password.empty? - if (login =~ VALID_EMAIL_REGEX) - user = find_by_mail(login) - else - user = find_by_login(login) - end - if user - # user is already in local database - #return nil unless user.active? - return nil unless user.check_password?(password) - else - # user is not yet registered, try to authenticate with available sources - attrs = AuthSource.authenticate(login, password) - if attrs - user = new(attrs) - user.login = login - user.language = Setting.default_language - if user.save - user.reload - logger.info("User '#{user.login}' created from external auth source: #{user.auth_source.type} - #{user.auth_source.name}") if logger && user.auth_source - end - end - end - if user && !user.new_record? - last_login_on = user.last_login_on.nil? ? '' : user.last_login_on.to_s - user.update_column(:last_login_on, Time.now) - end - [user, last_login_on] - rescue => text - raise text - end - - - def self.try_to_autologin(key) - user = Token.find_active_user('autologin', key, Setting.autologin.to_i) - if user - user.update_column(:last_login_on, Time.now) - user - end - end - - def self.name_formatter(formatter = nil) - USER_FORMATS[formatter || Setting.user_format] || USER_FORMATS[:firstname_lastname] - end - - # Returns an array of fields names than can be used to make an order statement for users - # according to how user names are displayed - # Examples: - # - # User.fields_for_order_statement => ['users.login', 'users.id'] - # User.fields_for_order_statement('authors') => ['authors.login', 'authors.id'] - def self.fields_for_order_statement(table=nil) - table ||= table_name - name_formatter[:order].map {|field| "#{table}.#{field}"} - end - - # Return user's full name for display - def realname(formatter = nil) - f = self.class.name_formatter(formatter) - if formatter - eval('"' + f[:string] + '"') - else - @name ||= eval('"' + f[:string] + '"') - end - end - - def nickname(formatter = nil) - login - end - - def name(formatter = nil) - login - end - - def active? - self.status == STATUS_ACTIVE - end - - def registered? - self.status == STATUS_REGISTERED - end - - def locked? - self.status == STATUS_LOCKED - end - - def activate - self.status = STATUS_ACTIVE - end - - def register - self.status = STATUS_REGISTERED - end - - def lock - self.status = STATUS_LOCKED - end - - def activate! - update_attribute(:status, STATUS_ACTIVE) - end - - def register! - update_attribute(:status, STATUS_REGISTERED) - end - - def lock! - update_attribute(:status, STATUS_LOCKED) - end - - # Returns true if +clear_password+ is the correct user's password, otherwise false - def check_password?(clear_password) - if auth_source_id.present? - auth_source.authenticate(self.login, clear_password) - else - User.hash_password("#{salt}#{User.hash_password clear_password}") == hashed_password - end - end - def check_password1?(clear_password) - - clear_password == hashed_password - - end - # Generates a random salt and computes hashed_password for +clear_password+ - # The hashed password is stored in the following form: SHA1(salt + SHA1(password)) - def salt_password(clear_password) - self.salt = User.generate_salt - self.hashed_password = User.hash_password("#{salt}#{User.hash_password clear_password}") - end - - # Does the backend storage allow this user to change their password? - def change_password_allowed? - return true if auth_source.nil? - return auth_source.allow_password_changes? - end - - # Generate and set a random password. Useful for automated user creation - # Based on Token#generate_token_value - # - def random_password - chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a - password = '' - 40.times { |i| password << chars[rand(chars.size-1)] } - self.password = password - self.password_confirmation = password - self - end - - def pref - self.preference ||= UserPreference.new(:user => self) - end - - def time_zone - @time_zone ||= (self.pref.time_zone.blank? ? nil : ActiveSupport::TimeZone[self.pref.time_zone]) - end - - def wants_comments_in_reverse_order? - self.pref[:comments_sorting] == 'desc' - end - - def wants_notificationcomments_in_reverse_order? - self.pref[:notificationcomments_sorting] == 'desc' - end - # Return user's RSS key (a 40 chars long string), used to access feeds - def rss_key - if rss_token.nil? - create_rss_token(:action => 'feeds') - end - rss_token.value - end - - # Return user's API key (a 40 chars long string), used to access the API - def api_key - if api_token.nil? - create_api_token(:action => 'api') - end - api_token.value - end - - # Return an array of project ids for which the user has explicitly turned mail notifications on - def notified_projects_ids - @notified_projects_ids ||= memberships.select {|m| m.mail_notification?}.collect(&:project_id) - end - - def notified_project_ids=(ids) - Member.update_all("mail_notification = #{connection.quoted_false}", ['user_id = ?', id]) - Member.update_all("mail_notification = #{connection.quoted_true}", ['user_id = ? AND project_id IN (?)', id, ids]) if ids && !ids.empty? - @notified_projects_ids = nil - notified_projects_ids - end - - def valid_notification_options - self.class.valid_notification_options(self) - end - - # Only users that belong to more than 1 project can select projects for which they are notified - def self.valid_notification_options(user=nil) - # Note that @user.membership.size would fail since AR ignores - # :include association option when doing a count - if user.nil? || user.memberships.length < 1 - MAIL_NOTIFICATION_OPTIONS.reject {|option| option.first == 'selected'} - else - MAIL_NOTIFICATION_OPTIONS - end - end - - # Find a user account by matching the exact login and then a case-insensitive - # version. Exact matches will be given priority. - #通过用户名查找相应的用户,若没有匹配到,则不区分大小写进行查询 - #修改:不再匹配不区分大小写情况 -zjc - def self.find_by_login(login) - if login.present? - login = login.to_s - # First look for an exact match - user = where(:login => login).all.detect {|u| u.login == login} - #unless user - # # Fail over to case-insensitive if none was found - # user = where("LOWER(login) = ?", login.downcase).first - #end - user - end - end - - def self.find_by_rss_key(key) - Token.find_active_user('feeds', key) - end - - def self.find_by_api_key(key) - Token.find_active_user('api', key) - end - - # Makes find_by_mail case-insensitive - def self.find_by_mail(mail) - where("LOWER(mail) = ?", mail.to_s.downcase).first - end - - # Returns true if the default admin account can no longer be used - def self.default_admin_account_changed? - !User.active.find_by_login("admin").try(:check_password?, "admin") - end - - def to_s - name - end - - CSS_CLASS_BY_STATUS = { - STATUS_ANONYMOUS => 'anon', - STATUS_ACTIVE => 'active', - STATUS_REGISTERED => 'registered', - STATUS_LOCKED => 'locked' - } - - def css_classes - "user #{CSS_CLASS_BY_STATUS[status]}" - end - - # Returns the current day according to user's time zone - def today - if time_zone.nil? - Date.today - else - Time.now.in_time_zone(time_zone).to_date - end - end - - # Returns the day of +time+ according to user's time zone - def time_to_date(time) - if time_zone.nil? - time.to_date - else - time.in_time_zone(time_zone).to_date - end - end - - def logged? - true - end - - def anonymous? - !logged? - end - - # Returns user's membership for the given project - # or nil if the user is not a member of project - def membership(project) - project_id = project.is_a?(Project) ? project.id : project - - @membership_by_project_id ||= Hash.new {|h, project_id| - h[project_id] = memberships.where(:project_id => project_id).first - } - @membership_by_project_id[project_id] - end - - def coursemembership(course) - course_id = course.is_a?(Course) ? course.id : course - - @membership_by_course_id ||= Hash.new {|h, course_id| - h[course_id] = coursememberships.where(:course_id => course_id).first - } - @membership_by_course_id[course_id] - end - - # Return user's roles for project - def roles_for_project(project) - roles = [] - # No role on archived projects - return roles if project.nil? || project.archived? - if logged? - # Find project membership - membership = membership(project) - if membership - roles = membership.roles - else - @role_non_member ||= Role.non_member - roles << @role_non_member - end - else - @role_anonymous ||= Role.anonymous - roles << @role_anonymous - end - roles - end - - # 用户课程权限判断 - def roles_for_course(course) - roles = [] - # No role on archived courses - return roles if course.nil? || course.archived? - if logged? - # Find course membership - membership = coursemembership(course) - if membership - roles = membership.roles - else - @role_non_member ||= Role.non_member - roles << @role_non_member - end - else - @role_anonymous ||= Role.anonymous - roles << @role_anonymous - end - roles - end - - # Return true if the user is a member of project - def member_of?(project) - projects.to_a.include?(project) - end - - def member_of_course?(course) - courses.to_a.include?(course) - end - - def member_of_course_group?(course_group) - course_groups.to_a.include?(course_group) - end - # Returns a hash of user's projects grouped by roles - def projects_by_role - return @projects_by_role if @projects_by_role - - @projects_by_role = Hash.new([]) - memberships.each do |membership| - if membership.project - membership.roles.each do |role| - @projects_by_role[role] = [] unless @projects_by_role.key?(role) - @projects_by_role[role] << membership.project - end - end - end - @projects_by_role.each do |role, projects| - projects.uniq! - end - - @projects_by_role - end - - # 课程的角色权限 - def courses_by_role - return @courses_by_role if @courses_by_role - - @courses_by_role = Hash.new([]) - coursememberships.each do |membership| - if membership.course - membership.roles.each do |role| - @courses_by_role[role] = [] unless @courses_by_role.key?(role) - @courses_by_role[role] << membership.course - end - end - end - @courses_by_role.each do |role, courses| - courses.uniq! - end - - @courses_by_role - end - # Returns true if user is arg or belongs to arg - def is_or_belongs_to?(arg) - if arg.is_a?(User) - self == arg - elsif arg.is_a?(Group) - arg.users.include?(self) - else - false - end - end - - - # Return true if the user is allowed to do the specified action on a specific context - # Action can be: - # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit') - # * a permission Symbol (eg. :edit_project) - # Context can be: - # * a project : returns true if user is allowed to do the specified action on this project - # * an array of projects : returns true if user is allowed on every project - # * nil with options[:global] set : check if user has at least one role allowed for this action, - # or falls back to Non Member / Anonymous permissions depending if the user is logged - def allowed_to?(action, context, options={}, &block) - if context && context.is_a?(Project) - return false unless context.allows_to?(action) - # Admin users are authorized for anything else - return true if admin? - - roles = roles_for_project(context) - return false unless roles - roles.any? {|role| - (context.is_public? || role.member?) && - role.allowed_to?(action) && - (block_given? ? yield(role, self) : true) - } - #添加课程相关的权限判断 - elsif context && context.is_a?(Course) - return false unless context.allows_to?(action) - # Admin users are authorized for anything else - return true if admin? - - roles = roles_for_course(context) - return false unless roles - roles.any? {|role| - (context.is_public? || role.member?) && - role.allowed_to?(action) && - (block_given? ? yield(role, self) : true) - } - elsif context && context.is_a?(Array) - if context.empty? - false - else - # Authorize if user is authorized on every element of the array - context.map {|project| allowed_to?(action, project, options, &block)}.reduce(:&) - end - elsif options[:global] - # Admin users are always authorized - return true if admin? - - # authorize if user has at least one role that has this permission - roles = memberships.collect {|m| m.roles}.flatten.uniq - if roles.count == 0 - roles = coursememberships.collect {|m| m.roles}.flatten.uniq - end - roles << (self.logged? ? Role.non_member : Role.anonymous) - roles.any? {|role| - role.allowed_to?(action) && - (block_given? ? yield(role, self) : true) - } - else - if admin? - return true - end - #无项目时 查看Non member(id为1)角色是否有权限执行action - Role.find('1').allowed_to?(action) - # false - end - end - - # Is the user allowed to do the specified action on any project? - # See allowed_to? for the actions and valid options. - def allowed_to_globally?(action, options, &block) - allowed_to?(action, nil, options.reverse_merge(:global => true), &block) - end - - # Returns true if the user is allowed to delete his own account - def own_account_deletable? - Setting.unsubscribe? && - (!admin? || User.active.where("admin = ? AND id <> ?", true, id).exists?) - end - - safe_attributes 'login', - 'firstname', - 'lastname', - 'mail', - 'mail_notification', - 'language', - 'custom_field_values', - 'custom_fields', - 'identity_url' - - safe_attributes 'status', - 'auth_source_id', - :if => lambda {|user, current_user| current_user.admin?} - - safe_attributes 'group_ids', - :if => lambda {|user, current_user| current_user.admin? && !user.new_record?} - - # Utility method to help check if a user should be notified about an - # event. - # - # TODO: only supports Issue events currently - def notify_about?(object) - if mail_notification == 'all' - true - elsif mail_notification.blank? || mail_notification == 'none' - false - else - case object - when Issue - case mail_notification - when 'selected', 'only_my_events' - # user receives notifications for created/assigned issues on unselected projects - object.author == self || is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.assigned_to_was) - when 'only_assigned' - is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.assigned_to_was) - when 'only_owner' - object.author == self - end - when News - # always send to project members except when mail_notification is set to 'none' - true - #判定用户是否接受留言提醒邮件 - when JournalsForMessage - ##如果是直接留言并且留言对象是Project并且Project类型是课程(课程留言) - if !object.at_user && object.jour.class.to_s.to_sym == :Project && object.jour.project_type == 1 - #根据用户设置邮件接收模式判定当前用户是否接受邮件提醒 - is_notified_project object.jour - end - - end - end - end - - #用户是否接收project的消息提醒 - def is_notified_project arg - if arg.is_a?(Project) - case mail_notification - when 'selected' - notified_projects_ids.include?(arg.id) - when 'only_my_events' - projects.include?(arg) - when 'only_assigned' - false - when 'only_owner' - course = Course.find_by_extra(arg.identifier) - course.teacher == self - end - #勾选的项目或用户的项目 TODO:需改 - #notified_projects_ids.include?(arg) || projects.include?(arg) - else - false - end - end - - def self.current=(user) - Thread.current[:current_user] = user - end - - def self.current - Thread.current[:current_user] ||= User.anonymous - end - - # Returns the anonymous user. If the anonymous user does not exist, it is created. There can be only - # one anonymous user per database. - def self.anonymous - anonymous_user = AnonymousUser.first - if anonymous_user.nil? - anonymous_user = AnonymousUser.create(:lastname => 'Anonymous', :firstname => '', :mail => '', :login => '', :status => 0) - raise 'Unable to create the anonymous user.' if anonymous_user.new_record? - end - anonymous_user - end - - # Salts all existing unsalted passwords - # It changes password storage scheme from SHA1(password) to SHA1(salt + SHA1(password)) - # This method is used in the SaltPasswords migration and is to be kept as is - def self.salt_unsalted_passwords! - transaction do - User.where("salt IS NULL OR salt = ''").find_each do |user| - next if user.hashed_password.blank? - salt = User.generate_salt - hashed_password = User.hash_password("#{salt}#{user.hashed_password}") - User.where(:id => user.id).update_all(:salt => salt, :hashed_password => hashed_password) - end - end - end - - protected - - def validate_password_length - # Password length validation based on setting - if !password.nil? && password.size < Setting.password_min_length.to_i - errors.add(:password, :too_short, :count => Setting.password_min_length.to_i) - end - end - private - - def act_as_activity - self.acts << Activity.new(:user_id => self.id) - end - - # Removes references that are not handled by associations - # Things that are not deleted are reassociated with the anonymous user - def remove_references_before_destroy - return if self.id.nil? - - substitute = User.anonymous - Attachment.update_all ['author_id = ?', substitute.id], ['author_id = ?', id] - Comment.update_all ['author_id = ?', substitute.id], ['author_id = ?', id] - Notificationcomment.update_all ['author_id = ?', substitute.id], ['author_id = ?', id] - Issue.update_all ['author_id = ?', substitute.id], ['author_id = ?', id] - Issue.update_all 'assigned_to_id = NULL', ['assigned_to_id = ?', id] - Journal.update_all ['user_id = ?', substitute.id], ['user_id = ?', id] - JournalDetail.update_all ['old_value = ?', substitute.id.to_s], ["property = 'attr' AND prop_key = 'assigned_to_id' AND old_value = ?", id.to_s] - JournalDetail.update_all ['value = ?', substitute.id.to_s], ["property = 'attr' AND prop_key = 'assigned_to_id' AND value = ?", id.to_s] - Message.update_all ['author_id = ?', substitute.id], ['author_id = ?', id] - News.update_all ['author_id = ?', substitute.id], ['author_id = ?', id] - # Remove private queries and keep public ones - ::Query.delete_all ['user_id = ? AND is_public = ?', id, false] - ::Query.update_all ['user_id = ?', substitute.id], ['user_id = ?', id] - TimeEntry.update_all ['user_id = ?', substitute.id], ['user_id = ?', id] - Token.delete_all ['user_id = ?', id] - Watcher.delete_all ['user_id = ?', id] - WikiContent.update_all ['author_id = ?', substitute.id], ['author_id = ?', id] - WikiContent::Version.update_all ['author_id = ?', substitute.id], ['author_id = ?', id] - end - - # Return password digest - def self.hash_password(clear_password) - Digest::SHA1.hexdigest(clear_password || "") - end - - # Returns a 128bits random salt as a hex string (32 chars long) - def self.generate_salt - Redmine::Utils.random_hex(16) - end - - - -end - -class AnonymousUser < User - validate :validate_anonymous_uniqueness, :on => :create - - def validate_anonymous_uniqueness - # There should be only one AnonymousUser in the database - errors.add :base, 'An anonymous user already exists.' if AnonymousUser.exists? - end - - def available_custom_fields - [] - end - - # Overrides a few properties - def logged?; false end - def admin; false end - def name(*args); I18n.t(:label_user_anonymous) end - def mail; nil end - def time_zone; nil end - def rss_key; nil end - - def pref - UserPreference.new(:user => self) - end - - # def member_of?(project) - # false - # end - - # Anonymous user can not be destroyed - def destroy - false - end -end +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require "digest/sha1" + +class User < Principal + TEACHER = 0 + STUDENT = 1 + ENTERPRISE = 2 + DEVELOPER = 3 + + include Redmine::SafeAttributes + seems_rateable_rater + # Different ways of displaying/sorting users + USER_FORMATS = { + :firstname_lastname => { + :string => '#{firstname} #{lastname}', + :order => %w(firstname lastname id), + :setting_order => 1 + }, + :firstname_lastinitial => { + :string => '#{firstname} #{lastname.to_s.chars.first}.', + :order => %w(firstname lastname id), + :setting_order => 2 + }, + :firstname => { + :string => '#{firstname}', + :order => %w(firstname id), + :setting_order => 3 + }, + :lastname_firstname => { + :string => '#{lastname} #{firstname}', + :order => %w(lastname firstname id), + :setting_order => 4 + }, + :lastname_coma_firstname => { + :string => '#{lastname}, #{firstname}', + :order => %w(lastname firstname id), + :setting_order => 5 + }, + :lastname => { + :string => '#{lastname}', + :order => %w(lastname id), + :setting_order => 6 + }, + :username => { + :string => '#{login}', + :order => %w(login id), + :setting_order => 7 + }, + } + + #每日一报、一事一报、不报 + MAIL_NOTIFICATION_OPTIONS = [ + ['all', :label_user_mail_option_all], + #['week', :label_user_mail_option_week], + ['day', :label_user_mail_option_day], + ['none', :label_user_mail_option_none] + ] + + has_many :homework_users + has_many :homework_attaches, :through => :homework_users + has_many :homework_evaluations + + #问卷相关关关系 + has_many :poll_users, :dependent => :destroy + has_many :poll_votes, :dependent => :destroy + has_many :poll, :dependent => :destroy #用户创建的问卷 + has_many :answers, :source => :poll, :through => :poll_users, :dependent => :destroy #用户已经完成问答的问卷 + # end + + has_and_belongs_to_many :groups, :after_add => Proc.new {|user, group| group.user_added(user)}, + :after_remove => Proc.new {|user, group| group.user_removed(user)} + has_many :changesets, :dependent => :nullify + has_one :preference, :dependent => :destroy, :class_name => 'UserPreference' + has_one :rss_token, :class_name => 'Token', :conditions => "action='feeds'" + has_one :api_token, :class_name => 'Token', :conditions => "action='api'" + belongs_to :auth_source + belongs_to :ucourse, :class_name => 'Course', :foreign_key => :id #huang +## added by xianbo for delete + has_many :biding_projects, :dependent => :destroy + has_many :contesting_projects, :dependent => :destroy + belongs_to :softapplication, :foreign_key => 'id', :dependent => :destroy +##ended by xianbo + +#####fq + has_many :jours, :class_name => 'JournalsForMessage', :dependent => :destroy + has_many :journals_messages, :class_name => 'JournalsForMessage', :foreign_key => "user_id", :dependent => :destroy + has_many :bids, :foreign_key => 'author_id', :dependent => :destroy + has_many :contests, :foreign_key => 'author_id', :dependent => :destroy + has_many :softapplications, :foreign_key => 'user_id', :dependent => :destroy + has_many :journals_for_messages, :as => :jour, :dependent => :destroy + has_many :new_jours, :as => :jour, :class_name => 'JournalsForMessage', :conditions => "status=1" + has_many :journal_replies, :dependent => :destroy + has_many :activities, :dependent => :destroy + has_many :students_for_courses + #has_many :courses, :through => :students_for_courses, :source => :project + has_many :acts, :class_name => 'Activity', :as => :act, :dependent => :destroy + has_many :file_commit, :class_name => 'Attachment', :foreign_key => 'author_id', :conditions => "container_type = 'Project' or container_type = 'Version'" +#### +# added by bai + has_many :join_in_contests, :dependent => :destroy + has_many :news, :foreign_key => 'author_id' + has_many :contestnotification, :foreign_key => 'author_id' + has_many :comments, :foreign_key => 'author_id' + has_many :notificationcomments, :foreign_key => 'author_id' + has_many :wiki_contents, :foreign_key => 'author_id' + has_many :journals + has_many :messages, :foreign_key => 'author_id' + has_one :user_score, :dependent => :destroy + has_many :documents # 项目中关联的文档再次与人关联 +# end + +######added by nie + has_many :project_infos, :dependent => :destroy + has_one :user_status, :dependent => :destroy + ##### + has_many :shares ,:dependent => :destroy + + # add by zjc + has_one :level, :class_name => 'UserLevels', :dependent => :destroy + has_many :memos , :foreign_key => 'author_id' + ##### + scope :logged, lambda { where("#{User.table_name}.status <> #{STATUS_ANONYMOUS}") } + scope :status, lambda {|arg| where(arg.blank? ? nil : {:status => arg.to_i}) } + scope :visible, lambda {|*args| + nil + } + + + acts_as_customizable + ############################added by william + acts_as_taggable + scope :by_join_date, order("created_on DESC") + ############################# added by liuping 关注 + acts_as_watchable + + has_one :user_extensions,:dependent => :destroy + ## end + + # default_scope -> { includes(:user_extensions, :user_score) } + scope :teacher, -> { + joins(:user_extensions).where('user_extensions.identity = ?', UserExtensions::TEACHER) + } + scope :student, -> { + joins(:user_extensions).where('user_extensions.identity = ?', UserExtensions::STUDENT) + } + scope :developer, -> { + joins(:user_extensions).where('user_extensions.identity = ?', UserExtensions::DEVELOPER) + } + scope :enterprise, -> { + joins(:user_extensions).where('user_extensions.identity = ?', UserExtensions::ENTERPRISE) + } + + attr_accessor :password, :password_confirmation + attr_accessor :last_before_login_on + # Prevents unauthorized assignments + attr_protected :login, :admin, :password, :password_confirmation, :hashed_password + + LOGIN_LENGTH_LIMIT = 25 + MAIL_LENGTH_LIMIT = 60 + + validates_presence_of :login, :mail, :if => Proc.new { |user| !user.is_a?(AnonymousUser) } + validates_uniqueness_of :login, :if => Proc.new { |user| user.login_changed? && user.login.present? }, :case_sensitive => false + validates_uniqueness_of :mail, :if => Proc.new { |user| user.mail_changed? && user.mail.present? }, :case_sensitive => false + # Login must contain letters, numbers, underscores only + validates_format_of :login, :with => /\A[a-z0-9_\-@\.]*\z/i + validates_length_of :login, :maximum => LOGIN_LENGTH_LIMIT + validates_length_of :firstname, :maximum => 30 + validates_length_of :lastname, :maximum => 30 + validates_format_of :mail, :with => /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i, :allow_blank => true + validates_length_of :mail, :maximum => MAIL_LENGTH_LIMIT, :allow_nil => true + validates_confirmation_of :password, :allow_nil => true + validates_inclusion_of :mail_notification, :in => MAIL_NOTIFICATION_OPTIONS.collect(&:first), :allow_blank => true + validate :validate_password_length + # validates_email_realness_of :mail + before_create :set_mail_notification + before_save :update_hashed_password + before_destroy :remove_references_before_destroy + # added by fq + after_create :act_as_activity + # end + + scope :in_group, lambda {|group| + group_id = group.is_a?(Group) ? group.id : group.to_i + where("#{User.table_name}.id IN (SELECT gu.user_id FROM #{table_name_prefix}groups_users#{table_name_suffix} gu WHERE gu.group_id = ?)", group_id) + } + scope :not_in_group, lambda {|group| + group_id = group.is_a?(Group) ? group.id : group.to_i + where("#{User.table_name}.id NOT IN (SELECT gu.user_id FROM #{table_name_prefix}groups_users#{table_name_suffix} gu WHERE gu.group_id = ?)", group_id) + } + scope :sorted, lambda { order(*User.fields_for_order_statement)} + + scope :like, lambda {|arg, type| + if arg.blank? + where(nil) + else + pattern = "%#{arg.to_s.strip.downcase}%" + #where(" LOWER(concat(lastname, firstname)) LIKE :p ", :p => pattern) + if type == "0" + where(" LOWER(login) LIKE '#{pattern}' ") + elsif type == "1" + where(" LOWER(concat(lastname, firstname)) LIKE '#{pattern}' ") + elsif type == "3" + where(" LOWER(concat(lastname, firstname,login)) LIKE '#{pattern}' ") + else + where(" LOWER(mail) LIKE '#{pattern}' ") + end + end + } + + + # ====================================================================== + + def extensions + self.user_extensions ||= UserExtensions.new + end + + def user_score_attr + self.user_score ||= UserScore.new + end + + # ====================================================================== + + #选择项目成员时显示的用户信息文字 + def userInfo + if self.realname.gsub(' ','') == "" || self.realname.nil? + info = self.nickname; + else + info=self.nickname + ' (' + self.realname + ')'; + end + info + end + + ###添加留言 fq + def add_jour(user, notes, reference_user_id = 0, options = {}) + if options.count == 0 + self.journals_for_messages << JournalsForMessage.new(:user_id => user.id, :notes => notes, :reply_id => reference_user_id, :status => true) + else + jfm = self.journals_for_messages.build(options) + jfm.save + jfm + end + end + + # 判断用户是否加入了竞赛中 fq + def join_in_contest?(bid) + joined = JoinInContest.where('user_id = ? and bid_id =?', self.id, bid.id) + if joined.size > 0 + true + else + false + end + end + + ### fq + def join_in?(course) + joined = StudentsForCourse.where('student_id = ? and course_id = ?', self.id, course.id) + if joined.size > 0 + true + else + false + end + end + + def show_name + unless self.user_extensions.nil? + if self.user_extensions.identity == 2 + firstname + else + lastname+firstname + end + else + lastname+firstname + end + end + ## end + + def count_new_jour + count = self.new_jours.count + end + + #added by nie + def count_new_journal_reply + count = self.journal_reply.count + end + + def set_mail_notification + ##add byxianbo + thread=Thread.new do + self.mail_notification = Setting.default_notification_option if self.mail_notification.blank? + true + end + end + + def update_hashed_password + # update hashed_password if password was set + if self.password && self.auth_source_id.blank? + salt_password(password) + end + end + + alias :base_reload :reload + def reload(*args) + @name = nil + @projects_by_role = nil + @courses_by_role = nil + @membership_by_project_id = nil + base_reload(*args) + end + + def mail=(arg) + write_attribute(:mail, arg.to_s.strip) + end + + def identity_url=(url) + if url.blank? + write_attribute(:identity_url, '') + else + begin + write_attribute(:identity_url, OpenIdAuthentication.normalize_identifier(url)) + rescue OpenIdAuthentication::InvalidOpenId + # Invalid url, don't save + end + end + self.read_attribute(:identity_url) + end + + VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z]+)*\.[a-z]+\z/i + # VALID_EMAIL_REGEX = /^[0-9a-zA-Z_-]+@[0-9a-zA-Z_-]+(\.[0-9a-zA-Z_-]+)+$/ + # Returns the user that matches provided login and password, or nil + #登录,返回用户名与密码匹配的用户 + def self.try_to_login(login, password) + login = login.to_s.lstrip.rstrip + password = password.to_s + + # Make sure no one can sign in with an empty login or password + return nil if login.empty? || password.empty? + if (login =~ VALID_EMAIL_REGEX) + user = find_by_mail(login) + else + user = find_by_login(login) + end + if user + # user is already in local database + #return nil unless user.active? + return nil unless user.check_password?(password) + else + # user is not yet registered, try to authenticate with available sources + attrs = AuthSource.authenticate(login, password) + if attrs + user = new(attrs) + user.login = login + user.language = Setting.default_language + if user.save + user.reload + logger.info("User '#{user.login}' created from external auth source: #{user.auth_source.type} - #{user.auth_source.name}") if logger && user.auth_source + end + end + end + if user && !user.new_record? + last_login_on = user.last_login_on.nil? ? '' : user.last_login_on.to_s + user.update_column(:last_login_on, Time.now) + end + [user, last_login_on] + rescue => text + raise text + end + + + def self.try_to_autologin(key) + user = Token.find_active_user('autologin', key, Setting.autologin.to_i) + if user + user.update_column(:last_login_on, Time.now) + user + end + end + + def self.name_formatter(formatter = nil) + USER_FORMATS[formatter || Setting.user_format] || USER_FORMATS[:firstname_lastname] + end + + # Returns an array of fields names than can be used to make an order statement for users + # according to how user names are displayed + # Examples: + # + # User.fields_for_order_statement => ['users.login', 'users.id'] + # User.fields_for_order_statement('authors') => ['authors.login', 'authors.id'] + def self.fields_for_order_statement(table=nil) + table ||= table_name + name_formatter[:order].map {|field| "#{table}.#{field}"} + end + + # Return user's full name for display + def realname(formatter = nil) + f = self.class.name_formatter(formatter) + if formatter + eval('"' + f[:string] + '"') + else + @name ||= eval('"' + f[:string] + '"') + end + end + + def nickname(formatter = nil) + login + end + + def name(formatter = nil) + login + end + + def active? + self.status == STATUS_ACTIVE + end + + def registered? + self.status == STATUS_REGISTERED + end + + def locked? + self.status == STATUS_LOCKED + end + + def activate + self.status = STATUS_ACTIVE + end + + def register + self.status = STATUS_REGISTERED + end + + def lock + self.status = STATUS_LOCKED + end + + def activate! + update_attribute(:status, STATUS_ACTIVE) + end + + def register! + update_attribute(:status, STATUS_REGISTERED) + end + + def lock! + update_attribute(:status, STATUS_LOCKED) + end + + # Returns true if +clear_password+ is the correct user's password, otherwise false + def check_password?(clear_password) + if auth_source_id.present? + auth_source.authenticate(self.login, clear_password) + else + User.hash_password("#{salt}#{User.hash_password clear_password}") == hashed_password + end + end + def check_password1?(clear_password) + + clear_password == hashed_password + + end + # Generates a random salt and computes hashed_password for +clear_password+ + # The hashed password is stored in the following form: SHA1(salt + SHA1(password)) + def salt_password(clear_password) + self.salt = User.generate_salt + self.hashed_password = User.hash_password("#{salt}#{User.hash_password clear_password}") + end + + # Does the backend storage allow this user to change their password? + def change_password_allowed? + return true if auth_source.nil? + return auth_source.allow_password_changes? + end + + # Generate and set a random password. Useful for automated user creation + # Based on Token#generate_token_value + # + def random_password + chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a + password = '' + 40.times { |i| password << chars[rand(chars.size-1)] } + self.password = password + self.password_confirmation = password + self + end + + def pref + self.preference ||= UserPreference.new(:user => self) + end + + def time_zone + @time_zone ||= (self.pref.time_zone.blank? ? nil : ActiveSupport::TimeZone[self.pref.time_zone]) + end + + def wants_comments_in_reverse_order? + self.pref[:comments_sorting] == 'desc' + end + + def wants_notificationcomments_in_reverse_order? + self.pref[:notificationcomments_sorting] == 'desc' + end + # Return user's RSS key (a 40 chars long string), used to access feeds + def rss_key + if rss_token.nil? + create_rss_token(:action => 'feeds') + end + rss_token.value + end + + # Return user's API key (a 40 chars long string), used to access the API + def api_key + if api_token.nil? + create_api_token(:action => 'api') + end + api_token.value + end + + # Return an array of project ids for which the user has explicitly turned mail notifications on + def notified_projects_ids + @notified_projects_ids ||= memberships.select {|m| m.mail_notification?}.collect(&:project_id) + end + + def notified_project_ids=(ids) + Member.update_all("mail_notification = #{connection.quoted_false}", ['user_id = ?', id]) + Member.update_all("mail_notification = #{connection.quoted_true}", ['user_id = ? AND project_id IN (?)', id, ids]) if ids && !ids.empty? + @notified_projects_ids = nil + notified_projects_ids + end + + def valid_notification_options + self.class.valid_notification_options(self) + end + + # Only users that belong to more than 1 project can select projects for which they are notified + def self.valid_notification_options(user=nil) + # Note that @user.membership.size would fail since AR ignores + # :include association option when doing a count + if user.nil? || user.memberships.length < 1 + MAIL_NOTIFICATION_OPTIONS.reject {|option| option.first == 'selected'} + else + MAIL_NOTIFICATION_OPTIONS + end + end + + # Find a user account by matching the exact login and then a case-insensitive + # version. Exact matches will be given priority. + #通过用户名查找相应的用户,若没有匹配到,则不区分大小写进行查询 + #修改:不再匹配不区分大小写情况 -zjc + def self.find_by_login(login) + if login.present? + login = login.to_s + # First look for an exact match + user = where(:login => login).all.detect {|u| u.login == login} + #unless user + # # Fail over to case-insensitive if none was found + # user = where("LOWER(login) = ?", login.downcase).first + #end + user + end + end + + def self.find_by_rss_key(key) + Token.find_active_user('feeds', key) + end + + def self.find_by_api_key(key) + Token.find_active_user('api', key) + end + + # Makes find_by_mail case-insensitive + def self.find_by_mail(mail) + where("LOWER(mail) = ?", mail.to_s.downcase).first + end + + # Returns true if the default admin account can no longer be used + def self.default_admin_account_changed? + !User.active.find_by_login("admin").try(:check_password?, "admin") + end + + def to_s + name + end + + CSS_CLASS_BY_STATUS = { + STATUS_ANONYMOUS => 'anon', + STATUS_ACTIVE => 'active', + STATUS_REGISTERED => 'registered', + STATUS_LOCKED => 'locked' + } + + def css_classes + "user #{CSS_CLASS_BY_STATUS[status]}" + end + + # Returns the current day according to user's time zone + def today + if time_zone.nil? + Date.today + else + Time.now.in_time_zone(time_zone).to_date + end + end + + # Returns the day of +time+ according to user's time zone + def time_to_date(time) + if time_zone.nil? + time.to_date + else + time.in_time_zone(time_zone).to_date + end + end + + def logged? + true + end + + def anonymous? + !logged? + end + + # Returns user's membership for the given project + # or nil if the user is not a member of project + def membership(project) + project_id = project.is_a?(Project) ? project.id : project + + @membership_by_project_id ||= Hash.new {|h, project_id| + h[project_id] = memberships.where(:project_id => project_id).first + } + @membership_by_project_id[project_id] + end + + def coursemembership(course) + course_id = course.is_a?(Course) ? course.id : course + + @membership_by_course_id ||= Hash.new {|h, course_id| + h[course_id] = coursememberships.where(:course_id => course_id).first + } + @membership_by_course_id[course_id] + end + + # Return user's roles for project + def roles_for_project(project) + roles = [] + # No role on archived projects + return roles if project.nil? || project.archived? + if logged? + # Find project membership + membership = membership(project) + if membership + roles = membership.roles + else + @role_non_member ||= Role.non_member + roles << @role_non_member + end + else + @role_anonymous ||= Role.anonymous + roles << @role_anonymous + end + roles + end + + # 用户课程权限判断 + def roles_for_course(course) + roles = [] + # No role on archived courses + return roles if course.nil? || course.archived? + if logged? + # Find course membership + membership = coursemembership(course) + if membership + roles = membership.roles + else + @role_non_member ||= Role.non_member + roles << @role_non_member + end + else + @role_anonymous ||= Role.anonymous + roles << @role_anonymous + end + roles + end + + # Return true if the user is a member of project + def member_of?(project) + projects.to_a.include?(project) + end + + def member_of_course?(course) + courses.to_a.include?(course) + end + + def member_of_course_group?(course_group) + course_groups.to_a.include?(course_group) + end + # Returns a hash of user's projects grouped by roles + def projects_by_role + return @projects_by_role if @projects_by_role + + @projects_by_role = Hash.new([]) + memberships.each do |membership| + if membership.project + membership.roles.each do |role| + @projects_by_role[role] = [] unless @projects_by_role.key?(role) + @projects_by_role[role] << membership.project + end + end + end + @projects_by_role.each do |role, projects| + projects.uniq! + end + + @projects_by_role + end + + # 课程的角色权限 + def courses_by_role + return @courses_by_role if @courses_by_role + + @courses_by_role = Hash.new([]) + coursememberships.each do |membership| + if membership.course + membership.roles.each do |role| + @courses_by_role[role] = [] unless @courses_by_role.key?(role) + @courses_by_role[role] << membership.course + end + end + end + @courses_by_role.each do |role, courses| + courses.uniq! + end + + @courses_by_role + end + # Returns true if user is arg or belongs to arg + def is_or_belongs_to?(arg) + if arg.is_a?(User) + self == arg + elsif arg.is_a?(Group) + arg.users.include?(self) + else + false + end + end + + + # Return true if the user is allowed to do the specified action on a specific context + # Action can be: + # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit') + # * a permission Symbol (eg. :edit_project) + # Context can be: + # * a project : returns true if user is allowed to do the specified action on this project + # * an array of projects : returns true if user is allowed on every project + # * nil with options[:global] set : check if user has at least one role allowed for this action, + # or falls back to Non Member / Anonymous permissions depending if the user is logged + def allowed_to?(action, context, options={}, &block) + if context && context.is_a?(Project) + return false unless context.allows_to?(action) + # Admin users are authorized for anything else + return true if admin? + + roles = roles_for_project(context) + return false unless roles + roles.any? {|role| + (context.is_public? || role.member?) && + role.allowed_to?(action) && + (block_given? ? yield(role, self) : true) + } + #添加课程相关的权限判断 + elsif context && context.is_a?(Course) + return false unless context.allows_to?(action) + # Admin users are authorized for anything else + return true if admin? + + roles = roles_for_course(context) + return false unless roles + roles.any? {|role| + (context.is_public? || role.member?) && + role.allowed_to?(action) && + (block_given? ? yield(role, self) : true) + } + elsif context && context.is_a?(Array) + if context.empty? + false + else + # Authorize if user is authorized on every element of the array + context.map {|project| allowed_to?(action, project, options, &block)}.reduce(:&) + end + elsif options[:global] + # Admin users are always authorized + return true if admin? + + # authorize if user has at least one role that has this permission + roles = memberships.collect {|m| m.roles}.flatten.uniq + if roles.count == 0 + roles = coursememberships.collect {|m| m.roles}.flatten.uniq + end + roles << (self.logged? ? Role.non_member : Role.anonymous) + roles.any? {|role| + role.allowed_to?(action) && + (block_given? ? yield(role, self) : true) + } + else + if admin? + return true + end + #无项目时 查看Non member(id为1)角色是否有权限执行action + Role.find('1').allowed_to?(action) + # false + end + end + + # Is the user allowed to do the specified action on any project? + # See allowed_to? for the actions and valid options. + def allowed_to_globally?(action, options, &block) + allowed_to?(action, nil, options.reverse_merge(:global => true), &block) + end + + # Returns true if the user is allowed to delete his own account + def own_account_deletable? + Setting.unsubscribe? && + (!admin? || User.active.where("admin = ? AND id <> ?", true, id).exists?) + end + + safe_attributes 'login', + 'firstname', + 'lastname', + 'mail', + 'mail_notification', + 'language', + 'custom_field_values', + 'custom_fields', + 'identity_url' + + safe_attributes 'status', + 'auth_source_id', + :if => lambda {|user, current_user| current_user.admin?} + + safe_attributes 'group_ids', + :if => lambda {|user, current_user| current_user.admin? && !user.new_record?} + + # Utility method to help check if a user should be notified about an + # event. + # + # TODO: only supports Issue events currently + def notify_about?(object) + if mail_notification == 'all' + true + elsif mail_notification.blank? || mail_notification == 'none' + false + else + case object + when Issue + case mail_notification + when 'selected', 'only_my_events' + # user receives notifications for created/assigned issues on unselected projects + object.author == self || is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.assigned_to_was) + when 'only_assigned' + is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.assigned_to_was) + when 'only_owner' + object.author == self + end + when News + # always send to project members except when mail_notification is set to 'none' + true + #判定用户是否接受留言提醒邮件 + when JournalsForMessage + ##如果是直接留言并且留言对象是Project并且Project类型是课程(课程留言) + if !object.at_user && object.jour.class.to_s.to_sym == :Project && object.jour.project_type == 1 + #根据用户设置邮件接收模式判定当前用户是否接受邮件提醒 + is_notified_project object.jour + end + + end + end + end + + #用户是否接收project的消息提醒 + def is_notified_project arg + if arg.is_a?(Project) + case mail_notification + when 'selected' + notified_projects_ids.include?(arg.id) + when 'only_my_events' + projects.include?(arg) + when 'only_assigned' + false + when 'only_owner' + course = Course.find_by_extra(arg.identifier) + course.teacher == self + end + #勾选的项目或用户的项目 TODO:需改 + #notified_projects_ids.include?(arg) || projects.include?(arg) + else + false + end + end + + def self.current=(user) + Thread.current[:current_user] = user + end + + def self.current + Thread.current[:current_user] ||= User.anonymous + end + + # Returns the anonymous user. If the anonymous user does not exist, it is created. There can be only + # one anonymous user per database. + def self.anonymous + anonymous_user = AnonymousUser.first + if anonymous_user.nil? + anonymous_user = AnonymousUser.create(:lastname => 'Anonymous', :firstname => '', :mail => '', :login => '', :status => 0) + raise 'Unable to create the anonymous user.' if anonymous_user.new_record? + end + anonymous_user + end + + # Salts all existing unsalted passwords + # It changes password storage scheme from SHA1(password) to SHA1(salt + SHA1(password)) + # This method is used in the SaltPasswords migration and is to be kept as is + def self.salt_unsalted_passwords! + transaction do + User.where("salt IS NULL OR salt = ''").find_each do |user| + next if user.hashed_password.blank? + salt = User.generate_salt + hashed_password = User.hash_password("#{salt}#{user.hashed_password}") + User.where(:id => user.id).update_all(:salt => salt, :hashed_password => hashed_password) + end + end + end + + protected + + def validate_password_length + # Password length validation based on setting + if !password.nil? && password.size < Setting.password_min_length.to_i + errors.add(:password, :too_short, :count => Setting.password_min_length.to_i) + end + end + private + + def act_as_activity + self.acts << Activity.new(:user_id => self.id) + end + + # Removes references that are not handled by associations + # Things that are not deleted are reassociated with the anonymous user + def remove_references_before_destroy + return if self.id.nil? + + substitute = User.anonymous + Attachment.update_all ['author_id = ?', substitute.id], ['author_id = ?', id] + Comment.update_all ['author_id = ?', substitute.id], ['author_id = ?', id] + Notificationcomment.update_all ['author_id = ?', substitute.id], ['author_id = ?', id] + Issue.update_all ['author_id = ?', substitute.id], ['author_id = ?', id] + Issue.update_all 'assigned_to_id = NULL', ['assigned_to_id = ?', id] + Journal.update_all ['user_id = ?', substitute.id], ['user_id = ?', id] + JournalDetail.update_all ['old_value = ?', substitute.id.to_s], ["property = 'attr' AND prop_key = 'assigned_to_id' AND old_value = ?", id.to_s] + JournalDetail.update_all ['value = ?', substitute.id.to_s], ["property = 'attr' AND prop_key = 'assigned_to_id' AND value = ?", id.to_s] + Message.update_all ['author_id = ?', substitute.id], ['author_id = ?', id] + News.update_all ['author_id = ?', substitute.id], ['author_id = ?', id] + # Remove private queries and keep public ones + ::Query.delete_all ['user_id = ? AND is_public = ?', id, false] + ::Query.update_all ['user_id = ?', substitute.id], ['user_id = ?', id] + TimeEntry.update_all ['user_id = ?', substitute.id], ['user_id = ?', id] + Token.delete_all ['user_id = ?', id] + Watcher.delete_all ['user_id = ?', id] + WikiContent.update_all ['author_id = ?', substitute.id], ['author_id = ?', id] + WikiContent::Version.update_all ['author_id = ?', substitute.id], ['author_id = ?', id] + end + + # Return password digest + def self.hash_password(clear_password) + Digest::SHA1.hexdigest(clear_password || "") + end + + # Returns a 128bits random salt as a hex string (32 chars long) + def self.generate_salt + Redmine::Utils.random_hex(16) + end + + + +end + +class AnonymousUser < User + validate :validate_anonymous_uniqueness, :on => :create + + def validate_anonymous_uniqueness + # There should be only one AnonymousUser in the database + errors.add :base, 'An anonymous user already exists.' if AnonymousUser.exists? + end + + def available_custom_fields + [] + end + + # Overrides a few properties + def logged?; false end + def admin; false end + def name(*args); I18n.t(:label_user_anonymous) end + def mail; nil end + def time_zone; nil end + def rss_key; nil end + + def pref + UserPreference.new(:user => self) + end + + # def member_of?(project) + # false + # end + + # Anonymous user can not be destroyed + def destroy + false + end +end diff --git a/app/models/wiki_content_observer.rb b/app/models/wiki_content_observer.rb index 187c02288..3ded4da86 100644 --- a/app/models/wiki_content_observer.rb +++ b/app/models/wiki_content_observer.rb @@ -1,34 +1,28 @@ -# Redmine - project management software -# Copyright (C) 2006-2013 Jean-Philippe Lang -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - -class WikiContentObserver < ActiveRecord::Observer - def after_create(wiki_content) - ##by senluo - thread7=Thread.new do - Mailer.wiki_content_added(wiki_content).deliver if Setting.notified_events.include?('wiki_content_added') - end - end - - def after_update(wiki_content) - if wiki_content.text_changed? - ##by senluo - thread8=Thread.new do - Mailer.wiki_content_updated(wiki_content).deliver if Setting.notified_events.include?('wiki_content_updated') - end - end - end -end +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class WikiContentObserver < ActiveRecord::Observer + def after_create(wiki_content) + Mailer.run.wiki_content_added(wiki_content) if Setting.notified_events.include?('wiki_content_added') + end + + def after_update(wiki_content) + if wiki_content.text_changed? + Mailer.run.wiki_content_updated(wiki_content) if Setting.notified_events.include?('wiki_content_updated') + end + end +end diff --git a/app/models/zip_pack.rb b/app/models/zip_pack.rb new file mode 100644 index 000000000..df6ad593f --- /dev/null +++ b/app/models/zip_pack.rb @@ -0,0 +1,18 @@ +class ZipPack < ActiveRecord::Base + # attr_accessible :title, :body + + def self.packed?(bid_id, user_id, digests) + zip_pack = ZipPack.where(homework_id: bid_id, user_id: user_id).first + return false unless zip_pack && zip_pack.digests == digests + zip_pack + end + + def file_valid? + return false unless File.exist?(self.file_path) + Trustie::Utils.digest(self.file_path) == self.file_digest + end + + def digests + self.file_digests.split(',').sort + end +end diff --git a/app/services/courses_service.rb b/app/services/courses_service.rb index 7c816ec19..7c1f14104 100644 --- a/app/services/courses_service.rb +++ b/app/services/courses_service.rb @@ -194,6 +194,8 @@ class CoursesService @course.setup_time = params[:setup_time] @course.endup_time = params[:endup_time] @course.class_period = params[:class_period] + params[:course][:is_public] ? @course.is_public = 1 : @course.is_public = 0 + params[:course][:open_student] ? @course.open_student = 1 : @course.open_student = 0 end @issue_custom_fields = IssueCustomField.sorted.all diff --git a/app/services/homework_service.rb b/app/services/homework_service.rb index f9be6f79d..d22c62a5a 100644 --- a/app/services/homework_service.rb +++ b/app/services/homework_service.rb @@ -20,7 +20,7 @@ class HomeworkService many_times = course.homeworks.index(@bid) + 1 name = @bid.name homework_count = @bid.homeworks.count #已提交的作业数量 - student_questions_count = @bid.commit.nil? ? 0 : @bid.commit + student_questions_count = @bid.journals_for_messages.where('m_parent_id IS NULL').count description = @bid.description #if is_course_teacher(User.current, course) && @bid.open_anonymous_evaluation == 1 && @bid.homeworks.count >= 2 state = @bid.comment_status @@ -282,7 +282,7 @@ class HomeworkService many_times = course.homeworks.index(@bid) + 1 name = @bid.name homework_count = @bid.homeworks.count #已提交的作业数量 - student_questions_count = @bid.commit.nil? ? 0 : @bid.commit + student_questions_count = @bid.journals_for_messages.where('m_parent_id IS NULL').count description = @bid.description #if is_course_teacher(User.current, course) && @bid.open_anonymous_evaluation == 1 && @bid.homeworks.count >= 2 state = @bid.comment_status diff --git a/app/services/users_service.rb b/app/services/users_service.rb index 39ca37d26..80aa34d45 100644 --- a/app/services/users_service.rb +++ b/app/services/users_service.rb @@ -95,9 +95,7 @@ class UsersService # create a new token for password recovery token = Token.new(:user => user, :action => "recovery") if token.save - Thread.new do - Mailer.lost_password(token).deliver - end + Mailer.run.lost_password(token) return l(:notice_account_lost_email_sent,:locale => user.language) end end diff --git a/app/views/attachments/add_exist_file_to_projects.js.erb b/app/views/attachments/add_exist_file_to_projects.js.erb new file mode 100644 index 000000000..88f45e2cf --- /dev/null +++ b/app/views/attachments/add_exist_file_to_projects.js.erb @@ -0,0 +1,7 @@ +<% if !@save_flag && @save_message %> +$("#error_show").html("<%= @save_message.join(', ') %>"); +<% elsif @message && @message != "" %> +$("#error_show").html("<%= @message.html_safe %>"); +<% else %> +closeModal(); +<% end %> \ No newline at end of file diff --git a/app/views/bids/_homework_list.html.erb b/app/views/bids/_homework_list.html.erb index a59997a62..577cd280f 100644 --- a/app/views/bids/_homework_list.html.erb +++ b/app/views/bids/_homework_list.html.erb @@ -22,8 +22,8 @@ <%= link_to "留言", get_homework_jours_homework_attach_index_path(:bid_id => @bid.id), {:remote => true}%> (<%= @jours_count %>) -
  • - <%#= link_to "作品打包下载", zipdown_assort_path(obj_class: @bid.class, obj_id: @bid), class: "tb_all" unless @bid.homeworks.empty? %> +
  • <%= link_to "作品打包下载", zipdown_assort_path(obj_class: @bid.class, obj_id: @bid, format: :json), + remote: true, class: "tb_all" unless @bid.homeworks.empty? %>
  • <% else %> diff --git a/app/views/bids/_new_homework_form.html.erb b/app/views/bids/_new_homework_form.html.erb index 9f3bf30c7..69d8b3138 100644 --- a/app/views/bids/_new_homework_form.html.erb +++ b/app/views/bids/_new_homework_form.html.erb @@ -1,47 +1,53 @@ -<%= stylesheet_link_tag 'jquery/jquery-ui-1.9.2', :media => 'all' %> -<%= error_messages_for 'bid' %> -<%= hidden_field_tag 'course_id', @course.id %> -
    -

    <%= l(:label_course_homework_new)%>

    -
    -
    - -
    -
    +<%= stylesheet_link_tag 'jquery/jquery-ui-1.9.2', :media => 'all' %> +<%= error_messages_for 'bid' %> +<%= hidden_field_tag 'course_id', @course.id %> +
    +

    <%= l(:label_course_homework_new)%>

    +
    +
    + +
    +
    diff --git a/app/views/bids/edit.html.erb b/app/views/bids/edit.html.erb index 80a023ac1..4133a24b2 100644 --- a/app/views/bids/edit.html.erb +++ b/app/views/bids/edit.html.erb @@ -1,3 +1,4 @@ +<%= javascript_include_tag "/assets/kindeditor/kindeditor" %> <%= labelled_form_for @bid do |f| %> - <%= render :partial => 'new_homework_form', :locals => { :bid => @bid, :bid_id => "edit_bid_#{@bid.id}"} %> + <%= render :partial => 'new_homework_form', :locals => { :bid => @bid, :bid_id => "edit_bid_#{@bid.id}",:f=>f,:edit_mode => true} %> <% end %> \ No newline at end of file diff --git a/app/views/boards/show.html.erb b/app/views/boards/show.html.erb index 6e5888363..c9a8645de 100644 --- a/app/views/boards/show.html.erb +++ b/app/views/boards/show.html.erb @@ -1,5 +1,6 @@ -<% if @project %> - <%= render :partial => 'project_show', locals: {project: @project} %> -<% elsif @course %> - <%= render :partial => 'course_show', locals: {course: @course} %> -<% end %> + +<% if @project %> + <%= render :partial => 'project_show', locals: {project: @project} %> +<% elsif @course %> + <%= render :partial => 'course_show', locals: {course: @course} %> +<% end %> diff --git a/app/views/courses/_courses_jours.html.erb b/app/views/courses/_courses_jours.html.erb index 5f2a67ebb..c10e93153 100644 --- a/app/views/courses/_courses_jours.html.erb +++ b/app/views/courses/_courses_jours.html.erb @@ -1,28 +1,29 @@ - -
    - <%# reply_allow = JournalsForMessage.create_by_user? User.current %> -

    <%= l(:label_leave_message) %>

    - - <% if !User.current.logged?%> -
    - <%= l(:label_user_login_tips) %> - <%= link_to l(:label_user_login_new), signin_path %> -
    -
    - <% else %> - <%= form_for('new_form', :method => :post, - :url => {:controller => 'words', :action => 'leave_course_message'}) do |f|%> - <%= f.text_area 'course_message',:id => "leave_meassge",:style => "resize: none;", - :placeholder => "#{l(:label_welcome_my_respond)}",:maxlength => 250%> - 取  消 - <%= l(:button_leave_meassge)%> - <% end %> - <% end %> -
    - -
    - <%= render :partial => 'history',:locals => { :contest => @contest, :journals => @jour, :state => false} %> -
    - <% html_title @topic.subject %> - \ No newline at end of file diff --git a/app/views/messages/_form_course.html.erb b/app/views/messages/_form_course.html.erb index 8f806ac97..dc350bcb3 100644 --- a/app/views/messages/_form_course.html.erb +++ b/app/views/messages/_form_course.html.erb @@ -1,3 +1,4 @@ +<%= javascript_include_tag "/assets/kindeditor/kindeditor" %> <%= error_messages_for 'message' %> <% replying ||= false %> <% extra_option = replying ? { readonly: true} : { maxlength: 200 } %> @@ -28,7 +29,25 @@
    <%= text_area :quote,:quote,:style => 'display:none' %> - <%= f.text_area :content, :class => 'talk_text fl', :id => 'message_content', :onkeyup => "regexContent();", :maxlength => 5000,:placeholder => "最多3000个汉字(或6000个英文字符)" %> + <%= hidden_field_tag :asset_id,params[:asset_id],:required => false,:style => 'display:none' %> + <% if replying %> + <%= f.kindeditor :content, :editor_id => 'message_content_editor', + :width => '89%', + :height => 300, + :input_html => { :id => 'message_content', + :class => 'talk_text fl', + :maxlength => 5000 }%> + <% else %> + <%= f.kindeditor :content, :editor_id => 'message_content_editor', + :owner_id => @message.nil? ? 0: @message.id, + :owner_type => OwnerTypeHelper::MESSAGE, + :width => '91%', + :height => 300, + :class => 'talk_text fl', + :input_html => { :id => 'message_content', + :class => 'talk_text fl', + :maxlength => 5000 }%> + <% end %>

    diff --git a/app/views/news/_course_form.html.erb b/app/views/news/_course_form.html.erb index b32401d7c..5c0fc8274 100644 --- a/app/views/news/_course_form.html.erb +++ b/app/views/news/_course_form.html.erb @@ -1,12 +1,21 @@ +<%= javascript_include_tag "/assets/kindeditor/kindeditor" %>
  • - - -

    + <% if is_new %> + <%= hidden_field_tag :asset_id,params[:asset_id],:required => false,:style => 'display:none' %> + + <%= f.kindeditor :description,:width=>'91%',:editor_id=>'news_description_editor' %> +

    + <% else %> + + <%= f.kindeditor :description,:width=>'91%',:editor_id=>'news_description_editor',:owner_id => @news.id,:owner_type => OwnerTypeHelper::NEWS %> +

    + <% end %> +
  • @@ -19,7 +28,7 @@ <%= link_to l(:button_create), "#", :onclick => 'submitNews();', :onmouseover => 'submitFocus(this);', :class => 'blue_btn fl c_white' %> <%= link_to l(:button_cancel), course_news_index_path(@course), :onclick => '$("#add-news").hide()', :class => 'blue_btn grey_btn fl c_white' %> <% else %> - <%= link_to l(:button_save), "#", :onclick => 'submitNews();',:onmouseover => 'this.focus()',:class => 'blue_btn fl c_white' %> + <%= link_to l(:button_save), "#", :onclick => "submitNews();",:onmouseover => 'this.focus()',:class => 'blue_btn fl c_white' %> <%= link_to l(:button_cancel), "#", :onclick => '$("#edit-news").hide(); return false;',:class => 'blue_btn grey_btn fl c_white' %> <% end %>
    diff --git a/app/views/news/_course_news.html.erb b/app/views/news/_course_news.html.erb index a9af6551c..c6940ce10 100644 --- a/app/views/news/_course_news.html.erb +++ b/app/views/news/_course_news.html.erb @@ -51,7 +51,7 @@ <%= link_to_user_header(news.author,false,{:class=> 'problem_name c_orange fl'}) if news.respond_to?(:author) %> <%= l(:label_release_news) %>:<%= link_to h(news.title), news_path(news),:class => 'problem_tit fl fb c_dblue' %>
    -

    <%= news.description %>
    <%= l(:label_create_time) %> :<%= format_time(news.created_on) %>

    +

    <%= textAreailizable news.description %>
    <%= l(:label_create_time) %> :<%= format_time(news.created_on) %>

    diff --git a/app/views/news/_course_show.html.erb b/app/views/news/_course_show.html.erb index ed6011e29..3334f7c6a 100644 --- a/app/views/news/_course_show.html.erb +++ b/app/views/news/_course_show.html.erb @@ -44,6 +44,7 @@ function submitNews() { + news_description_editor.sync(); if(regexTitle() && regexDescription()) { $("#news-form").submit(); @@ -51,6 +52,7 @@ } function submitComment() { + comment_editor.sync(); $("#add_comment_form").submit(); } function clearMessage() @@ -58,7 +60,7 @@ $("#comment_comments").val(""); } - +<%= javascript_include_tag "/assets/kindeditor/kindeditor" %>

    <%= l(:label_course_news) %>

    @@ -84,7 +86,7 @@ :onclick => '$("#edit-news").show(); return false;') if User.current.allowed_to?(:manage_news, @course) %> <%= delete_link(news_path(@news),:class => 'talk_edit fr') if User.current.allowed_to?(:manage_news, @course) %>
    -
    <%= textilizable(@news, :description) %>
    <%= l(:label_create_time) %> : <%= format_time(@news.created_on) %>
    +
    <%= textAreailizable(@news, :description) %>
    <%= l(:label_create_time) %> : <%= format_time(@news.created_on) %>
    <%= link_to_attachments_course @news %> @@ -95,8 +97,9 @@

    <%= l(:label_comment_add) %>

    <%= form_tag({:controller => 'comments', :action => 'create', :id => @news}, :id => "add_comment_form") do %> -
    - <%= text_area 'comment', 'comments', :placeholder=>"最多250个字"%> +
    + <%= hidden_field_tag :asset_id,params[:asset_id],:required => false,:style => 'display:none' %> + <%= kindeditor_tag :comment, '',:height=>'100',:editor_id =>'comment_editor', :placeholder=>"最多250个字"%>

    <%= l(:label_cancel_with_space) %> @@ -106,6 +109,7 @@ <% end %>

    <% end %> + <% comments = @comments.reverse %> <% comments.each do |comment| %> <% next if comment.new_record? %> @@ -115,7 +119,7 @@
    <%= link_to_user_header(comment.author,false,:class => 'c_blue fb fl mb10 ') if comment.respond_to?(:author) %><%= format_time(comment.created_on) %>
    -

    <%= textilizable(comment.comments) %>

    +

    <%= textAreailizable(comment.comments) %>

    <%= link_to_if_authorized_course image_tag('delete.png'), {:controller => 'comments', :action => 'destroy', :id => @news, :comment_id => comment}, :data => {:confirm => l(:text_are_you_sure)}, :method => :delete, :title => l(:button_delete) %> diff --git a/app/views/projects/show.html.erb b/app/views/projects/show.html.erb index 13f91aedf..b3ddf6a26 100644 --- a/app/views/projects/show.html.erb +++ b/app/views/projects/show.html.erb @@ -130,4 +130,4 @@ <% end %> <% end %> <% end %> -<%= paginate @events_pages, :left => 3, :right => 3%> \ No newline at end of file +<%= paginate @events_pages, :left => 3, :right => 3 %> \ No newline at end of file diff --git a/config/application.rb b/config/application.rb index 3b36d7cb7..487223584 100644 --- a/config/application.rb +++ b/config/application.rb @@ -1,78 +1,77 @@ -require File.expand_path('../boot', __FILE__) - -require 'rails/all' -require 'sprockets/railtie' - -if defined?(Bundler) - # If you precompile assets before deploying to production, use this line - Bundler.require(*Rails.groups(:assets => %w(development test))) - # If you want your assets lazily compiled in production, use this line - # Bundler.require(:default, :assets, Rails.env) -end - -module RedmineApp - class Application < Rails::Application - # Settings in config/environments/* take precedence over those specified here. - # Application configuration should go into files in config/initializers - # -- all .rb files in that directory are automatically loaded. - - #verifier if email is real - - - config.generators do |g| - g.test_framework :rspec, - fixtures: true, - view_specs: false, - helper_specs: false, - routing_specs: false, - controller_specs: true, - request_specs: false - g.fixture_replacement :factory_girl, dir: "spec/factories" - end - # Custom directories with classes and modules you want to be autoloadable. - config.autoload_paths += %W(#{config.root}/lib) - - # Only load the plugins named here, in the order given (default is alphabetical). - # :all can be used as a placeholder for all plugins not explicitly named. - # config.plugins = [ :exception_notification, :ssl_requirement, :all ] - - # Activate observers that should always be running. - config.active_record.observers = :journals_for_message_observer, :message_observer, :issue_observer, :journal_observer, :news_observer, - :document_observer, :wiki_content_observer, :comment_observer, :forum_observer, :memo_observer - - config.active_record.store_full_sti_class = true - config.active_record.default_timezone = :local - - # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. - # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. - # config.time_zone = 'Central Time (US & Canada)' - - # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. - # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] - # config.i18n.default_locale = :de - - # Configure the default encoding used in templates for Ruby 1.9. - config.encoding = "utf-8" - - # Configure sensitive parameters which will be filtered from the log file. - config.filter_parameters += [:password] - - # Enable the asset pipeline - config.assets.enabled = false - - # Version of your assets, change this if you want to expire all your assets - config.assets.version = '1.0' - - config.action_mailer.perform_deliveries = false - - # Do not include all helpers - config.action_controller.include_all_helpers = false - - config.session_store :cookie_store, :key => '_redmine_session' - - if File.exists?(File.join(File.dirname(__FILE__), 'additional_environment.rb')) - instance_eval File.read(File.join(File.dirname(__FILE__), 'additional_environment.rb')) - end - - end -end +require File.expand_path('../boot', __FILE__) + +require 'rails/all' +require 'sprockets/railtie' + +if defined?(Bundler) + # If you precompile assets before deploying to production, use this line + Bundler.require(*Rails.groups(:assets => %w(development test))) + # If you want your assets lazily compiled in production, use this line + # Bundler.require(:default, :assets, Rails.env) +end + +module RedmineApp + class Application < Rails::Application + # Settings in config/environments/* take precedence over those specified here. + # Application configuration should go into files in config/initializers + # -- all .rb files in that directory are automatically loaded. + + #verifier if email is real + + + config.generators do |g| + g.test_framework :rspec, + fixtures: true, + view_specs: false, + helper_specs: false, + routing_specs: false, + controller_specs: true, + request_specs: false + g.fixture_replacement :factory_girl, dir: "spec/factories" + end + # Custom directories with classes and modules you want to be autoloadable. + config.autoload_paths += %W(#{config.root}/lib) + + # Only load the plugins named here, in the order given (default is alphabetical). + # :all can be used as a placeholder for all plugins not explicitly named. + # config.plugins = [ :exception_notification, :ssl_requirement, :all ] + + # Activate observers that should always be running. + config.active_record.observers = :journals_for_message_observer, :issue_observer, :journal_observer, :wiki_content_observer + + config.active_record.store_full_sti_class = true + config.active_record.default_timezone = :local + + # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. + # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. + # config.time_zone = 'Central Time (US & Canada)' + + # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. + # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] + # config.i18n.default_locale = :de + + # Configure the default encoding used in templates for Ruby 1.9. + config.encoding = "utf-8" + + # Configure sensitive parameters which will be filtered from the log file. + config.filter_parameters += [:password] + + # Enable the asset pipeline + config.assets.enabled = false + + # Version of your assets, change this if you want to expire all your assets + config.assets.version = '1.0' + + config.action_mailer.perform_deliveries = false + + # Do not include all helpers + config.action_controller.include_all_helpers = false + + config.session_store :cookie_store, :key => '_redmine_session' + + if File.exists?(File.join(File.dirname(__FILE__), 'additional_environment.rb')) + instance_eval File.read(File.join(File.dirname(__FILE__), 'additional_environment.rb')) + end + + end +end diff --git a/config/configuration.yml b/config/configuration.yml index f17881485..eee45c5d5 100644 --- a/config/configuration.yml +++ b/config/configuration.yml @@ -1,212 +1,209 @@ -# = Redmine configuration file -# -# Each environment has it's own configuration options. If you are only -# running in production, only the production block needs to be configured. -# Environment specific configuration options override the default ones. -# -# Note that this file needs to be a valid YAML file. -# DO NOT USE TABS! Use 2 spaces instead of tabs for identation. -# -# == Outgoing email settings (email_delivery setting) -# -# === Common configurations -# -# ==== Sendmail command -# -# production: -# email_delivery: -# delivery_method: :sendmail -# -# ==== Simple SMTP server at localhost -# -# production: -# email_delivery: -# delivery_method: :smtp -# smtp_settings: -# address: smtp.163.com -# port: 25 -# -# ==== SMTP server at example.com using LOGIN authentication and checking HELO for foo.com -# -# production: -# email_delivery: -# delivery_method: :smtp -# smtp_settings: -# address: smtp.gmail.com -# port: 587 -# authentication: :login -# domain: 'foo.com' -# user_name: senluowanxiangt@gmail.com -# password: 1913TXBja -# -# ==== SMTP server at example.com using PLAIN authentication -# -# production: -# email_delivery: -# delivery_method: :smtp -# smtp_settings: -# address: smtp.gmail.com -# port: 587 -# authentication: :plain -# domain: 'example.com' -# user_name: senluowanxiangt@gmail.com -# password: 1913TXBja -# -# ==== SMTP server at using TLS (GMail) -# -# This might require some additional configuration. See the guides at: -# http://www.redmine.org/projects/redmine/wiki/EmailConfiguration -# -# production: -# email_delivery: -# delivery_method: :smtp -# smtp_settings: -# enable_starttls_auto: true -# address: smtp.gmail.com -# port: 587 -# domain: "smtp.gmail.com" # 'your.domain.com' for GoogleApps -# authentication: :plain -# user_name: senluowanxiangt@gmail.com -# password: 1913TXBja -# -# -# === More configuration options -# -# See the "Configuration options" at the following website for a list of the -# full options allowed: -# -# http://wiki.rubyonrails.org/rails/pages/HowToSendEmailsWithActionMailer - - -# default configuration options for all environments -default: - # Outgoing emails configuration (see examples above) - email_delivery: - delivery_method: :smtp - smtp_settings: - - - - address: smtp.126.com - port: 25 - domain: smtp.126.com - authentication: :plain - user_name: "alanlong9278@126.com" - password: 'alanlong8788786' - - # Absolute path to the directory where attachments are stored. - # The default is the 'files' directory in your Redmine instance. - # Your Redmine instance needs to have write permission on this - # directory. - # Examples: - # attachments_storage_path: /var/redmine/files - # attachments_storage_path: D:/redmine/files - attachments_storage_path: - - # Configuration of the autologin cookie. - # autologin_cookie_name: the name of the cookie (default: autologin) - # autologin_cookie_path: the cookie path (default: /) - # autologin_cookie_secure: true sets the cookie secure flag (default: false) - autologin_cookie_name: - autologin_cookie_path: - autologin_cookie_secure: - - # Configuration of SCM executable command. - # - # Absolute path (e.g. /usr/local/bin/hg) or command name (e.g. hg.exe, bzr.exe) - # On Windows + CRuby, *.cmd, *.bat (e.g. hg.cmd, bzr.bat) does not work. - # - # On Windows + JRuby 1.6.2, path which contains spaces does not work. - # For example, "C:\Program Files\TortoiseHg\hg.exe". - # If you want to this feature, you need to install to the path which does not contains spaces. - # For example, "C:\TortoiseHg\hg.exe". - # - # Examples: - # scm_subversion_command: svn # (default: svn) - # scm_mercurial_command: C:\Program Files\TortoiseHg\hg.exe # (default: hg) - # scm_git_command: /usr/local/bin/git # (default: git) - # scm_cvs_command: cvs # (default: cvs) - # scm_bazaar_command: bzr.exe # (default: bzr) - # scm_darcs_command: darcs-1.0.9-i386-linux # (default: darcs) - # - scm_subversion_command: - scm_mercurial_command: - scm_git_command: - scm_cvs_command: - scm_bazaar_command: - scm_darcs_command: - - # Absolute path to the SCM commands errors (stderr) log file. - # The default is to log in the 'log' directory of your Redmine instance. - # Example: - # scm_stderr_log_file: /var/log/redmine_scm_stderr.log - scm_stderr_log_file: - - # Key used to encrypt sensitive data in the database (SCM and LDAP passwords). - # If you don't want to enable data encryption, just leave it blank. - # WARNING: losing/changing this key will make encrypted data unreadable. - # - # If you want to encrypt existing passwords in your database: - # * set the cipher key here in your configuration file - # * encrypt data using 'rake db:encrypt RAILS_ENV=production' - # - # If you have encrypted data and want to change this key, you have to: - # * decrypt data using 'rake db:decrypt RAILS_ENV=production' first - # * change the cipher key here in your configuration file - # * encrypt data using 'rake db:encrypt RAILS_ENV=production' - database_cipher_key: - - # Set this to false to disable plugins' assets mirroring on startup. - # You can use `rake redmine:plugins:assets` to manually mirror assets - # to public/plugin_assets when you install/upgrade a Redmine plugin. - # - #mirror_plugins_assets_on_startup: false - - # Your secret key for verifying cookie session data integrity. If you - # change this key, all old sessions will become invalid! Make sure the - # secret is at least 30 characters and all random, no regular words or - # you'll be exposed to dictionary attacks. - # - # If you have a load-balancing Redmine cluster, you have to use the - # same secret token on each machine. - #secret_token: 'change it to a long random string' - - # Absolute path (e.g. /usr/bin/convert, c:/im/convert.exe) to - # the ImageMagick's `convert` binary. Used to generate attachment thumbnails. - imagemagick_convert_command: '/home/pdl/redmine-2.3.2-0/common/bin/convert' - - # Configuration of RMagcik font. - # - # Redmine uses RMagcik in order to export gantt png. - # You don't need this setting if you don't install RMagcik. - # - # In CJK (Chinese, Japanese and Korean), - # in order to show CJK characters correctly, - # you need to set this configuration. - # - # Because there is no standard font across platforms in CJK, - # you need to set a font installed in your server. - # - # This setting is not necessary in non CJK. - # - # Examples for Japanese: - # Windows: - # rmagick_font_path: C:\windows\fonts\msgothic.ttc - # Linux: - # rmagick_font_path: /usr/share/fonts/ipa-mincho/ipam.ttf - # - rmagick_font_path: - - # Maximum number of simultaneous AJAX uploads - #max_concurrent_ajax_uploads: 2 - #pic_types: "bmp,jpeg,jpg,png,gif" - -# specific configuration options for production environment -# that overrides the default ones -production: - # CJK support - rmagick_font_path: /usr/share/fonts/ipa-mincho/ipam.ttf - -# specific configuration options for development environment -# that overrides the default ones -development: +# = Redmine configuration file +# +# Each environment has it's own configuration options. If you are only +# running in production, only the production block needs to be configured. +# Environment specific configuration options override the default ones. +# +# Note that this file needs to be a valid YAML file. +# DO NOT USE TABS! Use 2 spaces instead of tabs for identation. +# +# == Outgoing email settings (email_delivery setting) +# +# === Common configurations +# +# ==== Sendmail command +# +# production: +# email_delivery: +# delivery_method: :sendmail +# +# ==== Simple SMTP server at localhost +# +# production: +# email_delivery: +# delivery_method: :smtp +# smtp_settings: +# address: smtp.163.com +# port: 25 +# +# ==== SMTP server at example.com using LOGIN authentication and checking HELO for foo.com +# +# production: +# email_delivery: +# delivery_method: :smtp +# smtp_settings: +# address: smtp.gmail.com +# port: 587 +# authentication: :login +# domain: 'foo.com' +# user_name: senluowanxiangt@gmail.com +# password: 1913TXBja +# +# ==== SMTP server at example.com using PLAIN authentication +# +# production: +# email_delivery: +# delivery_method: :smtp +# smtp_settings: +# address: smtp.gmail.com +# port: 587 +# authentication: :plain +# domain: 'example.com' +# user_name: senluowanxiangt@gmail.com +# password: 1913TXBja +# +# ==== SMTP server at using TLS (GMail) +# +# This might require some additional configuration. See the guides at: +# http://www.redmine.org/projects/redmine/wiki/EmailConfiguration +# +# production: +# email_delivery: +# delivery_method: :smtp +# smtp_settings: +# enable_starttls_auto: true +# address: smtp.gmail.com +# port: 587 +# domain: "smtp.gmail.com" # 'your.domain.com' for GoogleApps +# authentication: :plain +# user_name: senluowanxiangt@gmail.com +# password: 1913TXBja +# +# +# === More configuration options +# +# See the "Configuration options" at the following website for a list of the +# full options allowed: +# +# http://wiki.rubyonrails.org/rails/pages/HowToSendEmailsWithActionMailer + + +# default configuration options for all environments +default: + # Outgoing emails configuration (see examples above) + email_delivery: + delivery_method: :smtp + smtp_settings: + address: smtp.126.com + port: 25 + domain: smtp.126.com + authentication: :plain + user_name: "alanlong9278@126.com" + password: 'alanlong8788786' + + # Absolute path to the directory where attachments are stored. + # The default is the 'files' directory in your Redmine instance. + # Your Redmine instance needs to have write permission on this + # directory. + # Examples: + # attachments_storage_path: /var/redmine/files + # attachments_storage_path: D:/redmine/files + attachments_storage_path: + + # Configuration of the autologin cookie. + # autologin_cookie_name: the name of the cookie (default: autologin) + # autologin_cookie_path: the cookie path (default: /) + # autologin_cookie_secure: true sets the cookie secure flag (default: false) + autologin_cookie_name: + autologin_cookie_path: + autologin_cookie_secure: + + # Configuration of SCM executable command. + # + # Absolute path (e.g. /usr/local/bin/hg) or command name (e.g. hg.exe, bzr.exe) + # On Windows + CRuby, *.cmd, *.bat (e.g. hg.cmd, bzr.bat) does not work. + # + # On Windows + JRuby 1.6.2, path which contains spaces does not work. + # For example, "C:\Program Files\TortoiseHg\hg.exe". + # If you want to this feature, you need to install to the path which does not contains spaces. + # For example, "C:\TortoiseHg\hg.exe". + # + # Examples: + # scm_subversion_command: svn # (default: svn) + # scm_mercurial_command: C:\Program Files\TortoiseHg\hg.exe # (default: hg) + # scm_git_command: /usr/local/bin/git # (default: git) + # scm_cvs_command: cvs # (default: cvs) + # scm_bazaar_command: bzr.exe # (default: bzr) + # scm_darcs_command: darcs-1.0.9-i386-linux # (default: darcs) + # + scm_subversion_command: + scm_mercurial_command: + scm_git_command: + scm_cvs_command: + scm_bazaar_command: + scm_darcs_command: + + # Absolute path to the SCM commands errors (stderr) log file. + # The default is to log in the 'log' directory of your Redmine instance. + # Example: + # scm_stderr_log_file: /var/log/redmine_scm_stderr.log + scm_stderr_log_file: + + # Key used to encrypt sensitive data in the database (SCM and LDAP passwords). + # If you don't want to enable data encryption, just leave it blank. + # WARNING: losing/changing this key will make encrypted data unreadable. + # + # If you want to encrypt existing passwords in your database: + # * set the cipher key here in your configuration file + # * encrypt data using 'rake db:encrypt RAILS_ENV=production' + # + # If you have encrypted data and want to change this key, you have to: + # * decrypt data using 'rake db:decrypt RAILS_ENV=production' first + # * change the cipher key here in your configuration file + # * encrypt data using 'rake db:encrypt RAILS_ENV=production' + database_cipher_key: + + # Set this to false to disable plugins' assets mirroring on startup. + # You can use `rake redmine:plugins:assets` to manually mirror assets + # to public/plugin_assets when you install/upgrade a Redmine plugin. + # + #mirror_plugins_assets_on_startup: false + + # Your secret key for verifying cookie session data integrity. If you + # change this key, all old sessions will become invalid! Make sure the + # secret is at least 30 characters and all random, no regular words or + # you'll be exposed to dictionary attacks. + # + # If you have a load-balancing Redmine cluster, you have to use the + # same secret token on each machine. + #secret_token: 'change it to a long random string' + + # Absolute path (e.g. /usr/bin/convert, c:/im/convert.exe) to + # the ImageMagick's `convert` binary. Used to generate attachment thumbnails. + imagemagick_convert_command: '/home/pdl/redmine-2.3.2-0/common/bin/convert' + + # Configuration of RMagcik font. + # + # Redmine uses RMagcik in order to export gantt png. + # You don't need this setting if you don't install RMagcik. + # + # In CJK (Chinese, Japanese and Korean), + # in order to show CJK characters correctly, + # you need to set this configuration. + # + # Because there is no standard font across platforms in CJK, + # you need to set a font installed in your server. + # + # This setting is not necessary in non CJK. + # + # Examples for Japanese: + # Windows: + # rmagick_font_path: C:\windows\fonts\msgothic.ttc + # Linux: + # rmagick_font_path: /usr/share/fonts/ipa-mincho/ipam.ttf + # + rmagick_font_path: + + # Maximum number of simultaneous AJAX uploads + #max_concurrent_ajax_uploads: 2 + #pic_types: "bmp,jpeg,jpg,png,gif" + +# specific configuration options for production environment +# that overrides the default ones +production: + # CJK support + rmagick_font_path: /usr/share/fonts/ipa-mincho/ipam.ttf + +# specific configuration options for development environment +# that overrides the default ones +development: diff --git a/config/initializers/30-redmine.rb b/config/initializers/30-redmine.rb index 1018ca18c..5a5f7c55a 100644 --- a/config/initializers/30-redmine.rb +++ b/config/initializers/30-redmine.rb @@ -1,15 +1,16 @@ -I18n.default_locale = 'en' -I18n.backend = Redmine::I18n::Backend.new - -require 'redmine' - -# Load the secret token from the Redmine configuration file -secret = Redmine::Configuration['secret_token'] -if secret.present? - RedmineApp::Application.config.secret_token = secret -end - -Redmine::Plugin.load -unless Redmine::Configuration['mirror_plugins_assets_on_startup'] == false - Redmine::Plugin.mirror_assets -end +I18n.default_locale = 'en' +I18n.backend = Redmine::I18n::Backend.new + +require 'redmine' +require 'trustie' + +# Load the secret token from the Redmine configuration file +secret = Redmine::Configuration['secret_token'] +if secret.present? + RedmineApp::Application.config.secret_token = secret +end + +Redmine::Plugin.load +unless Redmine::Configuration['mirror_plugins_assets_on_startup'] == false + Redmine::Plugin.mirror_assets +end diff --git a/config/initializers/send_mail.rb b/config/initializers/send_mail.rb index 86b3a53fd..da3ca3f59 100644 --- a/config/initializers/send_mail.rb +++ b/config/initializers/send_mail.rb @@ -1,27 +1,15 @@ -#!/usr/bin/env ruby - -require 'rubygems' -require 'rufus-scheduler' - -#users = User.where("mail_notification = 'week' or mail_notification = 'day'") - -scheduler = Rufus::Scheduler.new -scheduler.cron('0 0 * * 1') do - users = User.where("mail_notification = 'week'") - users.each do |user| - #Rails.logger.info "send mail to #{user.show_name}(#{user.mail}) at #{Time.now}" - Thread.start do - Mailer.send_for_user_activities(user, Date.today, 7).deliver - end - end -end -scheduler.cron('0 0 * * *') do - users = User.where("mail_notification = 'day'") - users.each do |user| - #Rails.logger.info "send mail to #{user.show_name}(#{user.mail}) at #{Time.now}" - Thread.start do - Mailer.send_for_user_activities(user, Date.today, 1).deliver - end - end -end - +#coding=utf-8 + + +## 移入crontab + +# scheduler = Rufus::Scheduler.new +# +# #每天18:00发送当天的邮件汇总 +# scheduler.cron('0 18 * * *') do +# users = User.where(mail_notification: 'day') +# users.each do |user| +# mailer = Mailer.send_for_user_activities(user, Date.today, 1) +# mailer.deliver if mailer +# end +# end diff --git a/config/initializers/url_helper.rb b/config/initializers/url_helper.rb deleted file mode 100644 index 7f54df604..000000000 --- a/config/initializers/url_helper.rb +++ /dev/null @@ -1,22 +0,0 @@ -# Time 2015-01-30 17:02:41 -# Author lizanle -# Description 打开redmine\ruby\lib\ruby\gems\1.9.1\gems\actionpack-3.2.13\lib\action_view\helpers\UrlHelper,重写link_to_unless方法 -# 如果是当前页,则给当前页添加样式:class=>'current-page' -module ActionView - # = Action View URL Helpers - module Helpers #:nodoc: - # Provides a set of methods for making links and getting URLs that - # depend on the routing subsystem (see ActionDispatch::Routing). - # This allows you to use the same format for links in views - # and controllers. - module UrlHelper - def link_to_unless(condition, name, options = {}, html_options = {}, &block) - if condition - link_to(name, options, html_options.merge(:class => 'current-page')) - else - link_to(name, options, html_options) - end - end - end - end -end \ No newline at end of file diff --git a/config/locales/commons/en.yml b/config/locales/commons/en.yml index 296e8bc30..1386aef6e 100644 --- a/config/locales/commons/en.yml +++ b/config/locales/commons/en.yml @@ -184,6 +184,11 @@ en: label_anonymous: Anonymous #作业和留言 模块 text_are_you_sure: Are you sure? #js 提示 + text_are_you_sure_out: 你确定要退出该课程吗? + text_are_you_sure_out_group: 你确定要退出该分班吗? + + + label_no_data: No data to display # 项目、课程、用户公用 diff --git a/config/locales/commons/zh.yml b/config/locales/commons/zh.yml index 6fd77c08c..b2224063e 100644 --- a/config/locales/commons/zh.yml +++ b/config/locales/commons/zh.yml @@ -187,7 +187,8 @@ zh: text_are_you_sure: 您确定要删除吗? #js 提示 - + text_are_you_sure_out: 你确定要退出该课程吗? + text_are_you_sure_out_group: 你确定要退出该分班吗? label_no_data: 没有任何数据可供显示 diff --git a/config/locales/contacts/en.yml b/config/locales/contacts/en.yml index e72f9f46a..ecce15507 100644 --- a/config/locales/contacts/en.yml +++ b/config/locales/contacts/en.yml @@ -4,7 +4,7 @@ en: # 托管平台主页 > 底部承办单位 - label_hosted_by: Organizer + label_hosted_organization: Organizer label_hosted_by: National Key Laboratory for Parallel and Distributed Processing, NUDT label_sponsor: Department of Computer Science and Technology, NUDT label_co_organizer_NUDT: College of Computer, NUDT diff --git a/config/locales/courses/en.yml b/config/locales/courses/en.yml index f21029944..fd8ba2366 100644 --- a/config/locales/courses/en.yml +++ b/config/locales/courses/en.yml @@ -6,10 +6,13 @@ en: # # 课程公共标签 # - label_course_join_student: 加入课程 - label_course_new: 新建课程 - - label_homework: 课程作业 + label_course_join_student: Join a course + label_course_exit_student: Exit a course + label_course_new: New course + label_course_name: Course name + + + label_homework: Task label_course_news: 课程通知 label_main_teacher: 主讲教师 label_course_term: 开课学期 diff --git a/config/locales/courses/zh.yml b/config/locales/courses/zh.yml index 3e64b61f3..30144bf1d 100644 --- a/config/locales/courses/zh.yml +++ b/config/locales/courses/zh.yml @@ -13,6 +13,7 @@ zh: label_course_join_student: 加入课程 label_course_exit_student: 退出课程 label_course_new: 新建课程 + label_course_name: 课程名称 label_homework: 课程作业 label_course_news: 课程通知 diff --git a/config/locales/en.yml b/config/locales/en.yml index 6c5ab903c..ddde2e075 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -331,7 +331,7 @@ en: # edit by meng lable_hot_course: Hot Courses - label_course_join_student: Join a course + label_contest_modify_settings: Configuration bale_news_notice: Add a notification label_field_correct: correct input @@ -1259,11 +1259,11 @@ en: #end label_course: Course - label_course_new: New course + label_public_info: "If you don't choose public, only the project's members can see the project." label_course_public_info: "If you don't choose public, only the course's members can see the course." label_course_student: Student - label_homework: Task + label_course_new_homework: New homework label_course_homework_list: Homework List label_course_homework_new: new homework diff --git a/config/locales/my/en.yml b/config/locales/my/en.yml index 6e17c3e62..a1a3dd275 100644 --- a/config/locales/my/en.yml +++ b/config/locales/my/en.yml @@ -15,7 +15,7 @@ en: # top_menu 个人相关 label_my_course: My Course label_my_message: Msgs - label_my_projects: My projects + label_my_projects: My projectsed # diff --git a/config/locales/projects/zh.yml b/config/locales/projects/zh.yml index 9e41e4de5..93cf3a776 100644 --- a/config/locales/projects/zh.yml +++ b/config/locales/projects/zh.yml @@ -119,7 +119,8 @@ zh: lable_file_sharingarea: 资源共享区 label_upload_files: 上传文件 - + label_slected_to_other_project: 选入我的其他项目 + label_slected_to_project: 选入我的项目 # 资源库(附件)公用 > 关联资源 label_relation_files: 关联已有资源 diff --git a/config/locales/zh.yml b/config/locales/zh.yml index fd0fa1d06..95e93debc 100644 --- a/config/locales/zh.yml +++ b/config/locales/zh.yml @@ -80,7 +80,7 @@ zh: field_enterprise_name: 组织 label_week_mail: 一周动态 - label_day_mail: 一日动态 + label_day_mail: 今日动态 #added by huang field_tea_name: 教师 field_couurse_time: 学时 @@ -932,9 +932,7 @@ zh: text_project_destroy_confirmation: 您确信要删除这个项目以及所有相关的数据吗? text_subprojects_destroy_warning: "以下子项目也将被同时删除:%{value}" text_workflow_edit: 选择角色和跟踪标签来编辑工作流程 - text_are_you_sure: 您确定要删除吗? - text_are_you_sure_out: 你确定要退出该课程吗? - text_are_you_sure_out_group: 你确定要退出该分班吗? + text_journal_changed: "%{label} 从 %{old} 变更为 %{new}" text_journal_set_to: "%{label} 被设置为 %{value}" text_journal_deleted: "%{label} 已删除 (%{old})" @@ -1967,6 +1965,7 @@ zh: field_open_anonymous_evaluation: 是否使用匿评 label_course_empty_select: 尚未选择课程! label_course_prompt: 课程: + label_project_prompt: 项目: label_contain_resource: 已包含资源: label_quote_resource_failed: ",此资源引用失败! " label_file_lost: 以下无法成功下载,请联系相关人员重新上传: @@ -2032,5 +2031,5 @@ zh: label_name_not_null: 名称不能为空 modal_valid_unpassing: 该分班已经存在 - + mail_footer: 点击修改邮件发送设置 diff --git a/config/routes.rb b/config/routes.rb index 27bc59ffc..c9220769c 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -27,7 +27,6 @@ RedmineApp::Application.routes.draw do - resources :discuss_demos #match '/discuss_demos/new',:to => 'discuss_demo#create',:via =>[:post] #match '/discuss_demo/show',:to => 'discuss_demo#show' mount Mobile::API => '/api' @@ -102,11 +101,11 @@ RedmineApp::Application.routes.draw do mount SeemsRateable::Engine => '/rateable', :as => :rateable - # namespace :zipdown do - # match 'assort' - # match 'download_user_homework', :as => :download_user_homework - # match 'download' - # end + namespace :zipdown do + match 'assort' + match 'download_user_homework', :as => :download_user_homework + match 'download' + end namespace :test do match 'courselist' match 'zip' @@ -431,8 +430,12 @@ RedmineApp::Application.routes.draw do match 'issues/update_form', :to => 'issues#update_form', :via => [:put, :post], :as => 'issue_form' resources :files, :only => [:index, :new, :create] do + member do + match "quote_resource_show_project",:via => [:get] + end collection do match "getattachtype" , :via => [:get, :post] + match "search_project",:via => [:post,:get] #match 'getattachtype/:attachtype', :to => 'files#getattachtype', :via => [:get, :post] end end @@ -590,6 +593,7 @@ RedmineApp::Application.routes.draw do get 'attachments/autocomplete' match 'attachments/autocomplete', :to => 'attachments#autocomplete', :via => [:post] post 'attachments/relationfile', to: 'attachments#add_exist_file_to_project', as: 'attach_relation' + post 'attachments/relationfiles', to: 'attachments#add_exist_file_to_projects', as: 'attach_relations' post 'attachments/courserelationfile', to: 'attachments#add_exist_file_to_course', as: 'course_attach_relation' post 'attachments/courserelationfiles', to: 'attachments#add_exist_file_to_courses', as: 'course_attach_relations' get 'attachments/renderTag/:attchmentId', :to => 'attachments#renderTag', :attchmentId => /\d+/ diff --git a/config/settings.yml b/config/settings.yml index 56cb8ac6c..cd3f2974e 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -60,6 +60,11 @@ per_page_options: default: '25,50,100' mail_from: default: trustieforge@gmail.com + +### delayjob for send email. +delayjob_enabled: + default: 1 + bcc_recipients: default: 1 plain_text_mail: diff --git a/db/migrate/20150328115230_chnage_mail_notification_week_to_day.rb b/db/migrate/20150328115230_chnage_mail_notification_week_to_day.rb new file mode 100644 index 000000000..226a91707 --- /dev/null +++ b/db/migrate/20150328115230_chnage_mail_notification_week_to_day.rb @@ -0,0 +1,8 @@ +class ChnageMailNotificationWeekToDay < ActiveRecord::Migration + def up + User.where(mail_notification: 'week').update_all(mail_notification: 'day') + end + + def down + end +end diff --git a/db/migrate/20150331031554_create_delayed_jobs.rb b/db/migrate/20150331031554_create_delayed_jobs.rb new file mode 100644 index 000000000..3bcf4a853 --- /dev/null +++ b/db/migrate/20150331031554_create_delayed_jobs.rb @@ -0,0 +1,22 @@ +class CreateDelayedJobs < ActiveRecord::Migration + def self.up + create_table :delayed_jobs, force: true do |table| + table.integer :priority, default: 0, null: false # Allows some jobs to jump to the front of the queue + table.integer :attempts, default: 0, null: false # Provides for retries, but still fail eventually. + table.text :handler, null: false # YAML-encoded string of the object that will do work + table.text :last_error # reason for last failure (See Note below) + table.datetime :run_at # When to run. Could be Time.zone.now for immediately, or sometime in the future. + table.datetime :locked_at # Set when a client is working on this object + table.datetime :failed_at # Set when all retries have failed (actually, by default, the record is deleted instead) + table.string :locked_by # Who is working on this object (if locked) + table.string :queue # The name of the queue this job is in + table.timestamps null: true + end + + add_index :delayed_jobs, [:priority, :run_at], name: "delayed_jobs_priority" + end + + def self.down + drop_table :delayed_jobs + end +end diff --git a/db/migrate/20150331032810_add_delayjob_enabled_to_settings.rb b/db/migrate/20150331032810_add_delayjob_enabled_to_settings.rb new file mode 100644 index 000000000..f0ca0b6f0 --- /dev/null +++ b/db/migrate/20150331032810_add_delayjob_enabled_to_settings.rb @@ -0,0 +1,5 @@ +class AddDelayjobEnabledToSettings < ActiveRecord::Migration + def change + Setting.create(name: 'delayjob_enabled', value: 1 ) + end +end diff --git a/db/migrate/20150402015402_create_zip_packs.rb b/db/migrate/20150402015402_create_zip_packs.rb new file mode 100644 index 000000000..cf5e06d3f --- /dev/null +++ b/db/migrate/20150402015402_create_zip_packs.rb @@ -0,0 +1,14 @@ +class CreateZipPacks < ActiveRecord::Migration + def change + create_table :zip_packs do |t| + t.integer :user_id + t.integer :homework_id + t.string :file_digest + t.string :file_path + t.integer :pack_times, default: 1 + t.integer :pack_size, default: 0 + t.string :file_digests + t.timestamps + end + end +end diff --git a/lib/rails_kindeditor/lib/rails_kindeditor/active_record.rb b/lib/rails_kindeditor/lib/rails_kindeditor/active_record.rb index 5120acf0a..3893fe724 100644 --- a/lib/rails_kindeditor/lib/rails_kindeditor/active_record.rb +++ b/lib/rails_kindeditor/lib/rails_kindeditor/active_record.rb @@ -1,14 +1,22 @@ -if defined?(ActiveRecord) - ActiveRecord::Base.class_eval do - def self.has_many_kindeditor_assets(*args) - options = args.extract_options! - asset_name = args[0] ? args[0].to_s : 'assets' - has_many asset_name.to_sym, :class_name => 'Kindeditor::Asset', :foreign_key => 'owner_id', :dependent => options[:dependent] - - class_name = self.name - Kindeditor::Asset.class_eval do - belongs_to :owner, :class_name => class_name, :foreign_key => 'owner_id' - end - end - end +if defined?(ActiveRecord) + ActiveRecord::Base.class_eval do + def self.has_many_kindeditor_assets(*args) + options = args.extract_options! + asset_name = args[0] ? args[0].to_s : 'assets' + class_name = self.name + # Time 2015-04-01 14:20:32 + # Author lizanle + # Description 对象类型对应的数值,kindeditor_assets表里owner_type的值 + + has_many asset_name.to_sym, + :class_name => 'Kindeditor::Asset', + :conditions => "owner_type = #{('OwnerTypeHelper::'+class_name.upcase).constantize}", + :foreign_key => 'owner_id', :dependent => options[:dependent] + + + Kindeditor::Asset.class_eval do + belongs_to :owner, :class_name => class_name, :foreign_key => 'owner_id' + end + end + end end \ No newline at end of file diff --git a/lib/rails_kindeditor/lib/rails_kindeditor/helper.rb b/lib/rails_kindeditor/lib/rails_kindeditor/helper.rb index db2be85fb..33d44a2ec 100644 --- a/lib/rails_kindeditor/lib/rails_kindeditor/helper.rb +++ b/lib/rails_kindeditor/lib/rails_kindeditor/helper.rb @@ -1,100 +1,104 @@ -module RailsKindeditor - module Helper - - def kindeditor_tag(name, content = nil, options = {}) - id = sanitize_to_id(name) - input_html = { :id => id }.merge(options.delete(:input_html) || {}) - output = ActiveSupport::SafeBuffer.new - output << text_area_tag(name, content, input_html) - output << javascript_tag(js_replace(id, options)) - end - - def kindeditor(name, method, options = {}) - # TODO: Refactory options: 1. kindeditor_option 2. html_option - input_html = (options.delete(:input_html) || {}).stringify_keys - output_buffer = ActiveSupport::SafeBuffer.new - output_buffer << build_text_area_tag(name, method, self, options, input_html) - output_buffer << javascript_tag(js_replace(input_html['id'], options)) - end - - def kindeditor_upload_json_path(*args) - options = args.extract_options! - owner_id_query_string = options[:owner_id] ? "?owner_id=#{options[:owner_id]}" : '' - owner_type_query_string = options[:owner_type] ? owner_id_query_string + "&owner_type=#{options[:owner_type]}" : owner_id_query_string - "#{main_app_root_url}kindeditor/upload#{owner_type_query_string}" - end - - def kindeditor_file_manager_json_path - "#{main_app_root_url}kindeditor/filemanager" - end - - private - - def main_app_root_url - begin - main_app.root_url.slice(0, main_app.root_url.rindex(main_app.root_path)) + '/' - rescue - '/' - end - end - - def js_replace(dom_id, options = {}) - editor_id = options[:editor_id].nil? ? '' : "#{options[:editor_id].to_s.downcase} = " - if options[:window_onload] - require 'securerandom' - random_name = SecureRandom.hex; - "var old_onload_#{random_name}; - if(typeof window.onload == 'function') old_onload_#{random_name} = window.onload; - window.onload = function() { - #{editor_id}KindEditor.create('##{dom_id}', #{get_options(options).to_json}); - if(old_onload_#{random_name}) old_onload_#{random_name}(); - }" - else - "KindEditor.ready(function(K){ - #{editor_id}K.create('##{dom_id}', #{get_options(options).to_json}); - });" - end - end - - def get_options(options) - options.delete(:editor_id) - options.delete(:window_onload) - options.reverse_merge!(:width => '100%') - options.reverse_merge!(:height => 300) - options.reverse_merge!(:allowFileManager => true) - options.merge!(:uploadJson => kindeditor_upload_json_path(:owner_id => options.delete(:owner_id),:owner_type => options.delete(:owner_type))) - options.merge!(:fileManagerJson => kindeditor_file_manager_json_path) - if options[:simple_mode] == true - options.merge!(:items => %w{fontname fontsize | forecolor hilitecolor bold italic underline removeformat | justifyleft justifycenter justifyright insertorderedlist insertunorderedlist | emoticons image link}) - end - options.delete(:simple_mode) - options - end - - - - def build_text_area_tag(name, method, template, options, input_html) - if Rails.version >= '4.0.0' - text_area_tag = ActionView::Helpers::Tags::TextArea.new(name, method, template, options) - text_area_tag.send(:add_default_name_and_id, input_html) - text_area_tag.render - elsif Rails.version >= '3.1.0' - text_area_tag = ActionView::Base::InstanceTag.new(name, method, template, options.delete(:object)) - text_area_tag.send(:add_default_name_and_id, input_html) - text_area_tag.to_text_area_tag(input_html) - elsif Rails.version >= '3.0.0' - raise 'Please use rails_kindeditor v0.2.8 for Rails v3.0.x' - else - raise 'Please upgrade your Rails !' - end - end - end - - - - module Builder - def kindeditor(method, options = {}) - @template.send("kindeditor", @object_name, method, objectify_options(options)) - end - end +module RailsKindeditor + module Helper + + def kindeditor_tag(name, content = nil, options = {}) + id = sanitize_to_id(name) + input_html = { :id => id }.merge(options.delete(:input_html) || {}) + output = ActiveSupport::SafeBuffer.new + output << text_area_tag(name, content, input_html) + output << javascript_tag(js_replace(id, options)) + end + + def kindeditor(name, method, options = {}) + # TODO: Refactory options: 1. kindeditor_option 2. html_option + input_html = (options.delete(:input_html) || {}).stringify_keys + output_buffer = ActiveSupport::SafeBuffer.new + output_buffer << build_text_area_tag(name, method, self, options, input_html) + output_buffer << javascript_tag(js_replace(input_html['id'], options)) + end + + def kindeditor_upload_json_path(*args) + options = args.extract_options! + owner_id_query_string = options[:owner_id] ? "?owner_id=#{options[:owner_id]}" : '' + if owner_id_query_string.blank? + owner_type_query_string = options[:owner_type] ? "?owner_type=#{options[:owner_type]}" : "" + else + owner_type_query_string = options[:owner_type] ? owner_id_query_string + "&owner_type=#{options[:owner_type]}" : owner_id_query_string + end + "#{main_app_root_url}kindeditor/upload#{owner_type_query_string}" + end + + def kindeditor_file_manager_json_path + "#{main_app_root_url}kindeditor/filemanager" + end + + private + + def main_app_root_url + begin + main_app.root_url.slice(0, main_app.root_url.rindex(main_app.root_path)) + '/' + rescue + '/' + end + end + + def js_replace(dom_id, options = {}) + editor_id = options[:editor_id].nil? ? '' : "#{options[:editor_id].to_s.downcase} = " + if options[:window_onload] + require 'securerandom' + random_name = SecureRandom.hex; + "var old_onload_#{random_name}; + if(typeof window.onload == 'function') old_onload_#{random_name} = window.onload; + window.onload = function() { + #{editor_id}KindEditor.create('##{dom_id}', #{get_options(options).to_json}); + if(old_onload_#{random_name}) old_onload_#{random_name}(); + }" + else + "KindEditor.ready(function(K){ + #{editor_id}K.create('##{dom_id}', #{get_options(options).to_json}); + });" + end + end + + def get_options(options) + options.delete(:editor_id) + options.delete(:window_onload) + options.reverse_merge!(:width => '100%') + options.reverse_merge!(:height => 300) + options.reverse_merge!(:allowFileManager => true) + options.merge!(:uploadJson => kindeditor_upload_json_path(:owner_id => options.delete(:owner_id),:owner_type => options.delete(:owner_type))) + options.merge!(:fileManagerJson => kindeditor_file_manager_json_path) + if options[:simple_mode] == true + options.merge!(:items => %w{fontname fontsize | forecolor hilitecolor bold italic underline removeformat | justifyleft justifycenter justifyright insertorderedlist insertunorderedlist | emoticons image link}) + end + options.delete(:simple_mode) + options + end + + + + def build_text_area_tag(name, method, template, options, input_html) + if Rails.version >= '4.0.0' + text_area_tag = ActionView::Helpers::Tags::TextArea.new(name, method, template, options) + text_area_tag.send(:add_default_name_and_id, input_html) + text_area_tag.render + elsif Rails.version >= '3.1.0' + text_area_tag = ActionView::Base::InstanceTag.new(name, method, template, options.delete(:object)) + text_area_tag.send(:add_default_name_and_id, input_html) + text_area_tag.to_text_area_tag(input_html) + elsif Rails.version >= '3.0.0' + raise 'Please use rails_kindeditor v0.2.8 for Rails v3.0.x' + else + raise 'Please upgrade your Rails !' + end + end + end + + + + module Builder + def kindeditor(method, options = {}) + @template.send("kindeditor", @object_name, method, objectify_options(options)) + end + end end \ No newline at end of file diff --git a/lib/tasks/email.rake b/lib/tasks/email.rake index 934070e54..5f9bf7769 100644 --- a/lib/tasks/email.rake +++ b/lib/tasks/email.rake @@ -1,198 +1,206 @@ -# Redmine - project management software -# Copyright (C) 2006-2013 Jean-Philippe Lang -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - -namespace :redmine do - namespace :email do - - desc <<-END_DESC -Read an email from standard input. - -General options: - unknown_user=ACTION how to handle emails from an unknown user - ACTION can be one of the following values: - ignore: email is ignored (default) - accept: accept as anonymous user - create: create a user account - no_permission_check=1 disable permission checking when receiving - the email - no_account_notice=1 disable new user account notification - default_group=foo,bar adds created user to foo and bar groups - -Issue attributes control options: - project=PROJECT identifier of the target project - status=STATUS name of the target status - tracker=TRACKER name of the target tracker - category=CATEGORY name of the target category - priority=PRIORITY name of the target priority - allow_override=ATTRS allow email content to override attributes - specified by previous options - ATTRS is a comma separated list of attributes - -Examples: - # No project specified. Emails MUST contain the 'Project' keyword: - rake redmine:email:read RAILS_ENV="production" < raw_email - - # Fixed project and default tracker specified, but emails can override - # both tracker and priority attributes: - rake redmine:email:read RAILS_ENV="production" \\ - project=foo \\ - tracker=bug \\ - allow_override=tracker,priority < raw_email -END_DESC - - task :read => :environment do - options = { :issue => {} } - %w(project status tracker category priority).each { |a| options[:issue][a.to_sym] = ENV[a] if ENV[a] } - options[:allow_override] = ENV['allow_override'] if ENV['allow_override'] - options[:unknown_user] = ENV['unknown_user'] if ENV['unknown_user'] - options[:no_permission_check] = ENV['no_permission_check'] if ENV['no_permission_check'] - options[:no_account_notice] = ENV['no_account_notice'] if ENV['no_account_notice'] - options[:default_group] = ENV['default_group'] if ENV['default_group'] - - MailHandler.receive(STDIN.read, options) - end - - desc <<-END_DESC -Read emails from an IMAP server. - -General options: - unknown_user=ACTION how to handle emails from an unknown user - ACTION can be one of the following values: - ignore: email is ignored (default) - accept: accept as anonymous user - create: create a user account - no_permission_check=1 disable permission checking when receiving - the email - no_account_notice=1 disable new user account notification - default_group=foo,bar adds created user to foo and bar groups - -Available IMAP options: - host=HOST IMAP server host (default: 127.0.0.1) - port=PORT IMAP server port (default: 143) - ssl=SSL Use SSL? (default: false) - username=USERNAME IMAP account - password=PASSWORD IMAP password - folder=FOLDER IMAP folder to read (default: INBOX) - -Issue attributes control options: - project=PROJECT identifier of the target project - status=STATUS name of the target status - tracker=TRACKER name of the target tracker - category=CATEGORY name of the target category - priority=PRIORITY name of the target priority - allow_override=ATTRS allow email content to override attributes - specified by previous options - ATTRS is a comma separated list of attributes - -Processed emails control options: - move_on_success=MAILBOX move emails that were successfully received - to MAILBOX instead of deleting them - move_on_failure=MAILBOX move emails that were ignored to MAILBOX - -Examples: - # No project specified. Emails MUST contain the 'Project' keyword: - - rake redmine:email:receive_imap RAILS_ENV="production" \\ - host=imap.foo.bar username=redmine@example.net password=xxx - - - # Fixed project and default tracker specified, but emails can override - # both tracker and priority attributes: - - rake redmine:email:receive_imap RAILS_ENV="production" \\ - host=imap.foo.bar username=redmine@example.net password=xxx ssl=1 \\ - project=foo \\ - tracker=bug \\ - allow_override=tracker,priority -END_DESC - - task :receive_imap => :environment do - imap_options = {:host => ENV['host'], - :port => ENV['port'], - :ssl => ENV['ssl'], - :username => ENV['username'], - :password => ENV['password'], - :folder => ENV['folder'], - :move_on_success => ENV['move_on_success'], - :move_on_failure => ENV['move_on_failure']} - - options = { :issue => {} } - %w(project status tracker category priority).each { |a| options[:issue][a.to_sym] = ENV[a] if ENV[a] } - options[:allow_override] = ENV['allow_override'] if ENV['allow_override'] - options[:unknown_user] = ENV['unknown_user'] if ENV['unknown_user'] - options[:no_permission_check] = ENV['no_permission_check'] if ENV['no_permission_check'] - options[:no_account_notice] = ENV['no_account_notice'] if ENV['no_account_notice'] - options[:default_group] = ENV['default_group'] if ENV['default_group'] - - Redmine::IMAP.check(imap_options, options) - end - - desc <<-END_DESC -Read emails from an POP3 server. - -Available POP3 options: - host=HOST POP3 server host (default: 127.0.0.1) - port=PORT POP3 server port (default: 110) - username=USERNAME POP3 account - password=PASSWORD POP3 password - apop=1 use APOP authentication (default: false) - delete_unprocessed=1 delete messages that could not be processed - successfully from the server (default - behaviour is to leave them on the server) - -See redmine:email:receive_imap for more options and examples. -END_DESC - - task :receive_pop3 => :environment do - pop_options = {:host => ENV['host'], - :port => ENV['port'], - :apop => ENV['apop'], - :username => ENV['username'], - :password => ENV['password'], - :delete_unprocessed => ENV['delete_unprocessed']} - - options = { :issue => {} } - %w(project status tracker category priority).each { |a| options[:issue][a.to_sym] = ENV[a] if ENV[a] } - options[:allow_override] = ENV['allow_override'] if ENV['allow_override'] - options[:unknown_user] = ENV['unknown_user'] if ENV['unknown_user'] - options[:no_permission_check] = ENV['no_permission_check'] if ENV['no_permission_check'] - options[:no_account_notice] = ENV['no_account_notice'] if ENV['no_account_notice'] - options[:default_group] = ENV['default_group'] if ENV['default_group'] - - Redmine::POP3.check(pop_options, options) - end - - desc "Send a test email to the user with the provided login name" - task :test, [:login] => :environment do |task, args| - include Redmine::I18n - abort l(:notice_email_error, "Please include the user login to test with. Example: rake redmine:email:test[login]") if args[:login].blank? - - user = User.find_by_login(args[:login]) - abort l(:notice_email_error, "User #{args[:login]} not found") unless user && user.logged? - - ActionMailer::Base.raise_delivery_errors = true - begin - Mailer.with_synched_deliveries do - Mailer.test_email(user).deliver - end - puts l(:notice_email_sent, user.mail) - rescue Exception => e - abort l(:notice_email_error, e.message) - end - end - end -end +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +namespace :redmine do + namespace :email do + + desc <<-END_DESC +Read an email from standard input. + +General options: + unknown_user=ACTION how to handle emails from an unknown user + ACTION can be one of the following values: + ignore: email is ignored (default) + accept: accept as anonymous user + create: create a user account + no_permission_check=1 disable permission checking when receiving + the email + no_account_notice=1 disable new user account notification + default_group=foo,bar adds created user to foo and bar groups + +Issue attributes control options: + project=PROJECT identifier of the target project + status=STATUS name of the target status + tracker=TRACKER name of the target tracker + category=CATEGORY name of the target category + priority=PRIORITY name of the target priority + allow_override=ATTRS allow email content to override attributes + specified by previous options + ATTRS is a comma separated list of attributes + +Examples: + # No project specified. Emails MUST contain the 'Project' keyword: + rake redmine:email:read RAILS_ENV="production" < raw_email + + # Fixed project and default tracker specified, but emails can override + # both tracker and priority attributes: + rake redmine:email:read RAILS_ENV="production" \\ + project=foo \\ + tracker=bug \\ + allow_override=tracker,priority < raw_email +END_DESC + + task :read => :environment do + options = { :issue => {} } + %w(project status tracker category priority).each { |a| options[:issue][a.to_sym] = ENV[a] if ENV[a] } + options[:allow_override] = ENV['allow_override'] if ENV['allow_override'] + options[:unknown_user] = ENV['unknown_user'] if ENV['unknown_user'] + options[:no_permission_check] = ENV['no_permission_check'] if ENV['no_permission_check'] + options[:no_account_notice] = ENV['no_account_notice'] if ENV['no_account_notice'] + options[:default_group] = ENV['default_group'] if ENV['default_group'] + + MailHandler.receive(STDIN.read, options) + end + + desc <<-END_DESC +Read emails from an IMAP server. + +General options: + unknown_user=ACTION how to handle emails from an unknown user + ACTION can be one of the following values: + ignore: email is ignored (default) + accept: accept as anonymous user + create: create a user account + no_permission_check=1 disable permission checking when receiving + the email + no_account_notice=1 disable new user account notification + default_group=foo,bar adds created user to foo and bar groups + +Available IMAP options: + host=HOST IMAP server host (default: 127.0.0.1) + port=PORT IMAP server port (default: 143) + ssl=SSL Use SSL? (default: false) + username=USERNAME IMAP account + password=PASSWORD IMAP password + folder=FOLDER IMAP folder to read (default: INBOX) + +Issue attributes control options: + project=PROJECT identifier of the target project + status=STATUS name of the target status + tracker=TRACKER name of the target tracker + category=CATEGORY name of the target category + priority=PRIORITY name of the target priority + allow_override=ATTRS allow email content to override attributes + specified by previous options + ATTRS is a comma separated list of attributes + +Processed emails control options: + move_on_success=MAILBOX move emails that were successfully received + to MAILBOX instead of deleting them + move_on_failure=MAILBOX move emails that were ignored to MAILBOX + +Examples: + # No project specified. Emails MUST contain the 'Project' keyword: + + rake redmine:email:receive_imap RAILS_ENV="production" \\ + host=imap.foo.bar username=redmine@example.net password=xxx + + + # Fixed project and default tracker specified, but emails can override + # both tracker and priority attributes: + + rake redmine:email:receive_imap RAILS_ENV="production" \\ + host=imap.foo.bar username=redmine@example.net password=xxx ssl=1 \\ + project=foo \\ + tracker=bug \\ + allow_override=tracker,priority +END_DESC + + task :receive_imap => :environment do + imap_options = {:host => ENV['host'], + :port => ENV['port'], + :ssl => ENV['ssl'], + :username => ENV['username'], + :password => ENV['password'], + :folder => ENV['folder'], + :move_on_success => ENV['move_on_success'], + :move_on_failure => ENV['move_on_failure']} + + options = { :issue => {} } + %w(project status tracker category priority).each { |a| options[:issue][a.to_sym] = ENV[a] if ENV[a] } + options[:allow_override] = ENV['allow_override'] if ENV['allow_override'] + options[:unknown_user] = ENV['unknown_user'] if ENV['unknown_user'] + options[:no_permission_check] = ENV['no_permission_check'] if ENV['no_permission_check'] + options[:no_account_notice] = ENV['no_account_notice'] if ENV['no_account_notice'] + options[:default_group] = ENV['default_group'] if ENV['default_group'] + + Redmine::IMAP.check(imap_options, options) + end + + desc <<-END_DESC +Read emails from an POP3 server. + +Available POP3 options: + host=HOST POP3 server host (default: 127.0.0.1) + port=PORT POP3 server port (default: 110) + username=USERNAME POP3 account + password=PASSWORD POP3 password + apop=1 use APOP authentication (default: false) + delete_unprocessed=1 delete messages that could not be processed + successfully from the server (default + behaviour is to leave them on the server) + +See redmine:email:receive_imap for more options and examples. +END_DESC + + task :receive_pop3 => :environment do + pop_options = {:host => ENV['host'], + :port => ENV['port'], + :apop => ENV['apop'], + :username => ENV['username'], + :password => ENV['password'], + :delete_unprocessed => ENV['delete_unprocessed']} + + options = { :issue => {} } + %w(project status tracker category priority).each { |a| options[:issue][a.to_sym] = ENV[a] if ENV[a] } + options[:allow_override] = ENV['allow_override'] if ENV['allow_override'] + options[:unknown_user] = ENV['unknown_user'] if ENV['unknown_user'] + options[:no_permission_check] = ENV['no_permission_check'] if ENV['no_permission_check'] + options[:no_account_notice] = ENV['no_account_notice'] if ENV['no_account_notice'] + options[:default_group] = ENV['default_group'] if ENV['default_group'] + + Redmine::POP3.check(pop_options, options) + end + + desc "Send a test email to the user with the provided login name" + task :test, [:login] => :environment do |task, args| + include Redmine::I18n + abort l(:notice_email_error, "Please include the user login to test with. Example: rake redmine:email:test[login]") if args[:login].blank? + + user = User.find_by_login(args[:login]) + abort l(:notice_email_error, "User #{args[:login]} not found") unless user && user.logged? + + ActionMailer::Base.raise_delivery_errors = true + begin + Mailer.with_synched_deliveries do + Mailer.test_email(user).deliver + end + puts l(:notice_email_sent, user.mail) + rescue Exception => e + abort l(:notice_email_error, e.message) + end + end + + desc "send a email for day" + task :day => :environment do + users = User.where(mail_notification: 'day') + users.each do |user| + Mailer.run.send_for_user_activities(user, Date.today, 1) + end + end + end +end diff --git a/lib/trustie.rb b/lib/trustie.rb new file mode 100644 index 000000000..ff70d118c --- /dev/null +++ b/lib/trustie.rb @@ -0,0 +1 @@ +require 'trustie/utils' \ No newline at end of file diff --git a/lib/trustie/utils.rb b/lib/trustie/utils.rb new file mode 100644 index 000000000..7d0b2a272 --- /dev/null +++ b/lib/trustie/utils.rb @@ -0,0 +1,20 @@ +#coding=utf-8 + +module Trustie + module Utils + def self.digest(diskfile) + md5 = Digest::MD5.new + File.open(diskfile, "rb") do |f| + buffer = "" + while (buffer = f.read(8192)) + md5.update(buffer) + end + end + md5.hexdigest + end + end +end + +if __FILE__ == $0 + puts Trustie::Utils.digest('/Users/guange/Downloads/QQ_V4.0.2.dmg') +end \ No newline at end of file diff --git a/public/assets/kindeditor/kindeditor.js b/public/assets/kindeditor/kindeditor.js index e39c6e19e..e3922fff3 100644 --- a/public/assets/kindeditor/kindeditor.js +++ b/public/assets/kindeditor/kindeditor.js @@ -1,5962 +1,5958 @@ -/******************************************************************************* -* KindEditor - WYSIWYG HTML Editor for Internet -* Copyright (C) 2006-2013 kindsoft.net -* -* @author Roddy -* @website http://www.kindsoft.net/ -* @licence http://www.kindsoft.net/license.php -* @version 4.1.10 (2013-11-23) -*******************************************************************************/ -(function (window, undefined) { - if (window.KindEditor) { - return; - } -if (!window.console) { - window.console = {}; -} -if (!console.log) { - console.log = function () {}; -} -var _VERSION = '4.1.10 (2013-11-23)', - _ua = navigator.userAgent.toLowerCase(), - _IE = _ua.indexOf('msie') > -1 && _ua.indexOf('opera') == -1, - _NEWIE = _ua.indexOf('msie') == -1 && _ua.indexOf('trident') > -1, - _GECKO = _ua.indexOf('gecko') > -1 && _ua.indexOf('khtml') == -1, - _WEBKIT = _ua.indexOf('applewebkit') > -1, - _OPERA = _ua.indexOf('opera') > -1, - _MOBILE = _ua.indexOf('mobile') > -1, - _IOS = /ipad|iphone|ipod/.test(_ua), - _QUIRKS = document.compatMode != 'CSS1Compat', - _IERANGE = !window.getSelection, - _matches = /(?:msie|firefox|webkit|opera)[\/:\s](\d+)/.exec(_ua), - _V = _matches ? _matches[1] : '0', - _TIME = new Date().getTime(); -function _isArray(val) { - if (!val) { - return false; - } - return Object.prototype.toString.call(val) === '[object Array]'; -} -function _isFunction(val) { - if (!val) { - return false; - } - return Object.prototype.toString.call(val) === '[object Function]'; -} -function _inArray(val, arr) { - for (var i = 0, len = arr.length; i < len; i++) { - if (val === arr[i]) { - return i; - } - } - return -1; -} -function _each(obj, fn) { - if (_isArray(obj)) { - for (var i = 0, len = obj.length; i < len; i++) { - if (fn.call(obj[i], i, obj[i]) === false) { - break; - } - } - } else { - for (var key in obj) { - if (obj.hasOwnProperty(key)) { - if (fn.call(obj[key], key, obj[key]) === false) { - break; - } - } - } - } -} -function _trim(str) { - return str.replace(/(?:^[ \t\n\r]+)|(?:[ \t\n\r]+$)/g, ''); -} -function _inString(val, str, delimiter) { - delimiter = delimiter === undefined ? ',' : delimiter; - return (delimiter + str + delimiter).indexOf(delimiter + val + delimiter) >= 0; -} -function _addUnit(val, unit) { - unit = unit || 'px'; - return val && /^\d+$/.test(val) ? val + unit : val; -} -function _removeUnit(val) { - var match; - return val && (match = /(\d+)/.exec(val)) ? parseInt(match[1], 10) : 0; -} -function _escape(val) { - return val.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); -} -function _unescape(val) { - return val.replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/&/g, '&'); -} -function _toCamel(str) { - var arr = str.split('-'); - str = ''; - _each(arr, function(key, val) { - str += (key > 0) ? val.charAt(0).toUpperCase() + val.substr(1) : val; - }); - return str; -} -function _toHex(val) { - function hex(d) { - var s = parseInt(d, 10).toString(16).toUpperCase(); - return s.length > 1 ? s : '0' + s; - } - return val.replace(/rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)/ig, - function($0, $1, $2, $3) { - return '#' + hex($1) + hex($2) + hex($3); - } - ); -} -function _toMap(val, delimiter) { - delimiter = delimiter === undefined ? ',' : delimiter; - var map = {}, arr = _isArray(val) ? val : val.split(delimiter), match; - _each(arr, function(key, val) { - if ((match = /^(\d+)\.\.(\d+)$/.exec(val))) { - for (var i = parseInt(match[1], 10); i <= parseInt(match[2], 10); i++) { - map[i.toString()] = true; - } - } else { - map[val] = true; - } - }); - return map; -} -function _toArray(obj, offset) { - return Array.prototype.slice.call(obj, offset || 0); -} -function _undef(val, defaultVal) { - return val === undefined ? defaultVal : val; -} -function _invalidUrl(url) { - return !url || /[<>"]/.test(url); -} -function _addParam(url, param) { - return url.indexOf('?') >= 0 ? url + '&' + param : url + '?' + param; -} -function _extend(child, parent, proto) { - if (!proto) { - proto = parent; - parent = null; - } - var childProto; - if (parent) { - var fn = function () {}; - fn.prototype = parent.prototype; - childProto = new fn(); - _each(proto, function(key, val) { - childProto[key] = val; - }); - } else { - childProto = proto; - } - childProto.constructor = child; - child.prototype = childProto; - child.parent = parent ? parent.prototype : null; -} -function _json(text) { - var match; - if ((match = /\{[\s\S]*\}|\[[\s\S]*\]/.exec(text))) { - text = match[0]; - } - var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g; - cx.lastIndex = 0; - if (cx.test(text)) { - text = text.replace(cx, function (a) { - return '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); - }); - } - if (/^[\],:{}\s]*$/. - test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@'). - replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']'). - replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) { - return eval('(' + text + ')'); - } - throw 'JSON parse error'; -} -var _round = Math.round; -var K = { - DEBUG : false, - VERSION : _VERSION, - IE : _IE, - GECKO : _GECKO, - WEBKIT : _WEBKIT, - OPERA : _OPERA, - V : _V, - TIME : _TIME, - each : _each, - isArray : _isArray, - isFunction : _isFunction, - inArray : _inArray, - inString : _inString, - trim : _trim, - addUnit : _addUnit, - removeUnit : _removeUnit, - escape : _escape, - unescape : _unescape, - toCamel : _toCamel, - toHex : _toHex, - toMap : _toMap, - toArray : _toArray, - undef : _undef, - invalidUrl : _invalidUrl, - addParam : _addParam, - extend : _extend, - json : _json -}; -var _INLINE_TAG_MAP = _toMap('a,abbr,acronym,b,basefont,bdo,big,br,button,cite,code,del,dfn,em,font,i,img,input,ins,kbd,label,map,q,s,samp,select,small,span,strike,strong,sub,sup,textarea,tt,u,var'), - _BLOCK_TAG_MAP = _toMap('address,applet,blockquote,body,center,dd,dir,div,dl,dt,fieldset,form,frameset,h1,h2,h3,h4,h5,h6,head,hr,html,iframe,ins,isindex,li,map,menu,meta,noframes,noscript,object,ol,p,pre,script,style,table,tbody,td,tfoot,th,thead,title,tr,ul'), - _SINGLE_TAG_MAP = _toMap('area,base,basefont,br,col,frame,hr,img,input,isindex,link,meta,param,embed'), - _STYLE_TAG_MAP = _toMap('b,basefont,big,del,em,font,i,s,small,span,strike,strong,sub,sup,u'), - _CONTROL_TAG_MAP = _toMap('img,table,input,textarea,button'), - _PRE_TAG_MAP = _toMap('pre,style,script'), - _NOSPLIT_TAG_MAP = _toMap('html,head,body,td,tr,table,ol,ul,li'), - _AUTOCLOSE_TAG_MAP = _toMap('colgroup,dd,dt,li,options,p,td,tfoot,th,thead,tr'), - _FILL_ATTR_MAP = _toMap('checked,compact,declare,defer,disabled,ismap,multiple,nohref,noresize,noshade,nowrap,readonly,selected'), - _VALUE_TAG_MAP = _toMap('input,button,textarea,select'); -// Begining of modification by Macrow -function _getBasePath() { - var refPath = '/assets/kindeditor/'; - var els = document.getElementsByTagName('script'), src; - for (var i = 0, len = els.length; i < len; i++) { - src = els[i].src || ''; - if (/(kindeditor|application)[\w\-\.]*\.js/.test(src)) { - return src.substring(0, src.indexOf('assets')) + refPath; - } - } - return refPath; -} -// End of modification by Macrow -K.basePath = _getBasePath(); -K.options = { - designMode : true, - fullscreenMode : false, - filterMode : true, - wellFormatMode : true, - shadowMode : true, - loadStyleMode : true, - basePath : K.basePath, - themesPath : K.basePath + 'themes/', - langPath : K.basePath + 'lang/', - pluginsPath : K.basePath + 'plugins/', - themeType : 'default', - langType : 'zh_CN', - urlType : '', - newlineTag : 'p', - resizeType : 2, - syncType : 'form', - pasteType : 2, - dialogAlignType : 'page', - useContextmenu : true, - fullscreenShortcut : false, - bodyClass : 'ke-content', - indentChar : '\t', - cssPath : '', - cssData : '', - minWidth : 650, - minHeight : 100, - minChangeSize : 50, - zIndex : 811213, - items : [ - 'source', '|', 'undo', 'redo', '|', 'preview', 'print', 'template', 'code', - 'plainpaste', 'wordpaste', '|', 'justifyleft', 'justifycenter', 'justifyright', - 'justifyfull', 'insertorderedlist', 'insertunorderedlist', 'indent', 'outdent', 'subscript', - 'superscript', 'quickformat', 'fullscreen','|', '/', - 'formatblock', 'fontname', 'fontsize', '|', 'forecolor', 'hilitecolor', 'bold', - 'italic', 'underline', 'strikethrough', 'lineheight', 'removeformat', '|', 'image', 'table', 'hr', 'emoticons', 'baidumap', 'pagebreak', - 'anchor', 'link', '|' - ], - noDisableItems : ['source', 'fullscreen'], - colorTable : [ - ['#E53333', '#E56600', '#FF9900', '#64451D', '#DFC5A4', '#FFE500'], - ['#009900', '#006600', '#99BB00', '#B8D100', '#60D978', '#00D5FF'], - ['#337FE5', '#003399', '#4C33E5', '#9933E5', '#CC33E5', '#EE33EE'], - ['#FFFFFF', '#CCCCCC', '#999999', '#666666', '#333333', '#000000'] - ], - fontSizeTable : ['9px', '10px', '12px', '14px', '16px', '18px', '24px', '32px'], - htmlTags : { - font : ['id', 'class', 'color', 'size', 'face', '.background-color'], - span : [ - 'id', 'class', '.color', '.background-color', '.font-size', '.font-family', '.background', - '.font-weight', '.font-style', '.text-decoration', '.vertical-align', '.line-height' - ], - div : [ - 'id', 'class', 'align', '.border', '.margin', '.padding', '.text-align', '.color', - '.background-color', '.font-size', '.font-family', '.font-weight', '.background', - '.font-style', '.text-decoration', '.vertical-align', '.margin-left' - ], - table: [ - 'id', 'class', 'border', 'cellspacing', 'cellpadding', 'width', 'height', 'align', 'bordercolor', - '.padding', '.margin', '.border', 'bgcolor', '.text-align', '.color', '.background-color', - '.font-size', '.font-family', '.font-weight', '.font-style', '.text-decoration', '.background', - '.width', '.height', '.border-collapse' - ], - 'td,th': [ - 'id', 'class', 'align', 'valign', 'width', 'height', 'colspan', 'rowspan', 'bgcolor', - '.text-align', '.color', '.background-color', '.font-size', '.font-family', '.font-weight', - '.font-style', '.text-decoration', '.vertical-align', '.background', '.border' - ], - a : ['id', 'class', 'href', 'target', 'name'], - embed : ['id', 'class', 'src', 'width', 'height', 'type', 'loop', 'autostart', 'quality', '.width', '.height', 'align', 'allowscriptaccess'], - img : ['id', 'class', 'src', 'width', 'height', 'border', 'alt', 'title', 'align', '.width', '.height', '.border'], - 'p,ol,ul,li,blockquote,h1,h2,h3,h4,h5,h6' : [ - 'id', 'class', 'align', '.text-align', '.color', '.background-color', '.font-size', '.font-family', '.background', - '.font-weight', '.font-style', '.text-decoration', '.vertical-align', '.text-indent', '.margin-left' - ], - pre : ['id', 'class'], - hr : ['id', 'class', '.page-break-after'], - 'br,tbody,tr,strong,b,sub,sup,em,i,u,strike,s,del' : ['id', 'class'], - iframe : ['id', 'class', 'src', 'frameborder', 'width', 'height', '.width', '.height'] - }, - layout : '
    ' -}; -var _useCapture = false; -var _INPUT_KEY_MAP = _toMap('8,9,13,32,46,48..57,59,61,65..90,106,109..111,188,190..192,219..222'); -var _CURSORMOVE_KEY_MAP = _toMap('33..40'); -var _CHANGE_KEY_MAP = {}; -_each(_INPUT_KEY_MAP, function(key, val) { - _CHANGE_KEY_MAP[key] = val; -}); -_each(_CURSORMOVE_KEY_MAP, function(key, val) { - _CHANGE_KEY_MAP[key] = val; -}); -function _bindEvent(el, type, fn) { - if (el.addEventListener){ - el.addEventListener(type, fn, _useCapture); - } else if (el.attachEvent){ - el.attachEvent('on' + type, fn); - } -} -function _unbindEvent(el, type, fn) { - if (el.removeEventListener){ - el.removeEventListener(type, fn, _useCapture); - } else if (el.detachEvent){ - el.detachEvent('on' + type, fn); - } -} -var _EVENT_PROPS = ('altKey,attrChange,attrName,bubbles,button,cancelable,charCode,clientX,clientY,ctrlKey,currentTarget,' + - 'data,detail,eventPhase,fromElement,handler,keyCode,metaKey,newValue,offsetX,offsetY,originalTarget,pageX,' + - 'pageY,prevValue,relatedNode,relatedTarget,screenX,screenY,shiftKey,srcElement,target,toElement,view,wheelDelta,which').split(','); -function KEvent(el, event) { - this.init(el, event); -} -_extend(KEvent, { - init : function(el, event) { - var self = this, doc = el.ownerDocument || el.document || el; - self.event = event; - _each(_EVENT_PROPS, function(key, val) { - self[val] = event[val]; - }); - if (!self.target) { - self.target = self.srcElement || doc; - } - if (self.target.nodeType === 3) { - self.target = self.target.parentNode; - } - if (!self.relatedTarget && self.fromElement) { - self.relatedTarget = self.fromElement === self.target ? self.toElement : self.fromElement; - } - if (self.pageX == null && self.clientX != null) { - var d = doc.documentElement, body = doc.body; - self.pageX = self.clientX + (d && d.scrollLeft || body && body.scrollLeft || 0) - (d && d.clientLeft || body && body.clientLeft || 0); - self.pageY = self.clientY + (d && d.scrollTop || body && body.scrollTop || 0) - (d && d.clientTop || body && body.clientTop || 0); - } - if (!self.which && ((self.charCode || self.charCode === 0) ? self.charCode : self.keyCode)) { - self.which = self.charCode || self.keyCode; - } - if (!self.metaKey && self.ctrlKey) { - self.metaKey = self.ctrlKey; - } - if (!self.which && self.button !== undefined) { - self.which = (self.button & 1 ? 1 : (self.button & 2 ? 3 : (self.button & 4 ? 2 : 0))); - } - switch (self.which) { - case 186 : - self.which = 59; - break; - case 187 : - case 107 : - case 43 : - self.which = 61; - break; - case 189 : - case 45 : - self.which = 109; - break; - case 42 : - self.which = 106; - break; - case 47 : - self.which = 111; - break; - case 78 : - self.which = 110; - break; - } - if (self.which >= 96 && self.which <= 105) { - self.which -= 48; - } - }, - preventDefault : function() { - var ev = this.event; - if (ev.preventDefault) { - ev.preventDefault(); - } else { - ev.returnValue = false; - } - }, - stopPropagation : function() { - var ev = this.event; - if (ev.stopPropagation) { - ev.stopPropagation(); - } else { - ev.cancelBubble = true; - } - }, - stop : function() { - this.preventDefault(); - this.stopPropagation(); - } -}); -var _eventExpendo = 'kindeditor_' + _TIME, _eventId = 0, _eventData = {}; -function _getId(el) { - return el[_eventExpendo] || null; -} -function _setId(el) { - el[_eventExpendo] = ++_eventId; - return _eventId; -} -function _removeId(el) { - try { - delete el[_eventExpendo]; - } catch(e) { - if (el.removeAttribute) { - el.removeAttribute(_eventExpendo); - } - } -} -function _bind(el, type, fn) { - if (type.indexOf(',') >= 0) { - _each(type.split(','), function() { - _bind(el, this, fn); - }); - return; - } - var id = _getId(el); - if (!id) { - id = _setId(el); - } - if (_eventData[id] === undefined) { - _eventData[id] = {}; - } - var events = _eventData[id][type]; - if (events && events.length > 0) { - _unbindEvent(el, type, events[0]); - } else { - _eventData[id][type] = []; - _eventData[id].el = el; - } - events = _eventData[id][type]; - if (events.length === 0) { - events[0] = function(e) { - var kevent = e ? new KEvent(el, e) : undefined; - _each(events, function(i, event) { - if (i > 0 && event) { - event.call(el, kevent); - } - }); - }; - } - if (_inArray(fn, events) < 0) { - events.push(fn); - } - _bindEvent(el, type, events[0]); -} -function _unbind(el, type, fn) { - if (type && type.indexOf(',') >= 0) { - _each(type.split(','), function() { - _unbind(el, this, fn); - }); - return; - } - var id = _getId(el); - if (!id) { - return; - } - if (type === undefined) { - if (id in _eventData) { - _each(_eventData[id], function(key, events) { - if (key != 'el' && events.length > 0) { - _unbindEvent(el, key, events[0]); - } - }); - delete _eventData[id]; - _removeId(el); - } - return; - } - if (!_eventData[id]) { - return; - } - var events = _eventData[id][type]; - if (events && events.length > 0) { - if (fn === undefined) { - _unbindEvent(el, type, events[0]); - delete _eventData[id][type]; - } else { - _each(events, function(i, event) { - if (i > 0 && event === fn) { - events.splice(i, 1); - } - }); - if (events.length == 1) { - _unbindEvent(el, type, events[0]); - delete _eventData[id][type]; - } - } - var count = 0; - _each(_eventData[id], function() { - count++; - }); - if (count < 2) { - delete _eventData[id]; - _removeId(el); - } - } -} -function _fire(el, type) { - if (type.indexOf(',') >= 0) { - _each(type.split(','), function() { - _fire(el, this); - }); - return; - } - var id = _getId(el); - if (!id) { - return; - } - var events = _eventData[id][type]; - if (_eventData[id] && events && events.length > 0) { - events[0](); - } -} -function _ctrl(el, key, fn) { - var self = this; - key = /^\d{2,}$/.test(key) ? key : key.toUpperCase().charCodeAt(0); - _bind(el, 'keydown', function(e) { - if (e.ctrlKey && e.which == key && !e.shiftKey && !e.altKey) { - fn.call(el); - e.stop(); - } - }); -} -var _readyFinished = false; -function _ready(fn) { - if (_readyFinished) { - fn(KindEditor); - return; - } - var loaded = false; - function readyFunc() { - if (!loaded) { - loaded = true; - fn(KindEditor); - _readyFinished = true; - } - } - function ieReadyFunc() { - if (!loaded) { - try { - document.documentElement.doScroll('left'); - } catch(e) { - setTimeout(ieReadyFunc, 100); - return; - } - readyFunc(); - } - } - function ieReadyStateFunc() { - if (document.readyState === 'complete') { - readyFunc(); - } - } - if (document.addEventListener) { - _bind(document, 'DOMContentLoaded', readyFunc); - } else if (document.attachEvent) { - _bind(document, 'readystatechange', ieReadyStateFunc); - var toplevel = false; - try { - toplevel = window.frameElement == null; - } catch(e) {} - if (document.documentElement.doScroll && toplevel) { - ieReadyFunc(); - } - } - _bind(window, 'load', readyFunc); -} -if (_IE) { - window.attachEvent('onunload', function() { - _each(_eventData, function(key, events) { - if (events.el) { - _unbind(events.el); - } - }); - }); -} -K.ctrl = _ctrl; -K.ready = _ready; -function _getCssList(css) { - var list = {}, - reg = /\s*([\w\-]+)\s*:([^;]*)(;|$)/g, - match; - while ((match = reg.exec(css))) { - var key = _trim(match[1].toLowerCase()), - val = _trim(_toHex(match[2])); - list[key] = val; - } - return list; -} -function _getAttrList(tag) { - var list = {}, - reg = /\s+(?:([\w\-:]+)|(?:([\w\-:]+)=([^\s"'<>]+))|(?:([\w\-:"]+)="([^"]*)")|(?:([\w\-:"]+)='([^']*)'))(?=(?:\s|\/|>)+)/g, - match; - while ((match = reg.exec(tag))) { - var key = (match[1] || match[2] || match[4] || match[6]).toLowerCase(), - val = (match[2] ? match[3] : (match[4] ? match[5] : match[7])) || ''; - list[key] = val; - } - return list; -} -function _addClassToTag(tag, className) { - if (/\s+class\s*=/.test(tag)) { - tag = tag.replace(/(\s+class=["']?)([^"']*)(["']?[\s>])/, function($0, $1, $2, $3) { - if ((' ' + $2 + ' ').indexOf(' ' + className + ' ') < 0) { - return $2 === '' ? $1 + className + $3 : $1 + $2 + ' ' + className + $3; - } else { - return $0; - } - }); - } else { - tag = tag.substr(0, tag.length - 1) + ' class="' + className + '">'; - } - return tag; -} -function _formatCss(css) { - var str = ''; - _each(_getCssList(css), function(key, val) { - str += key + ':' + val + ';'; - }); - return str; -} -function _formatUrl(url, mode, host, pathname) { - mode = _undef(mode, '').toLowerCase(); - if (url.substr(0, 5) != 'data:') { - url = url.replace(/([^:])\/\//g, '$1/'); - } - if (_inArray(mode, ['absolute', 'relative', 'domain']) < 0) { - return url; - } - host = host || location.protocol + '//' + location.host; - if (pathname === undefined) { - var m = location.pathname.match(/^(\/.*)\//); - pathname = m ? m[1] : ''; - } - var match; - if ((match = /^(\w+:\/\/[^\/]*)/.exec(url))) { - if (match[1] !== host) { - return url; - } - } else if (/^\w+:/.test(url)) { - return url; - } - function getRealPath(path) { - var parts = path.split('/'), paths = []; - for (var i = 0, len = parts.length; i < len; i++) { - var part = parts[i]; - if (part == '..') { - if (paths.length > 0) { - paths.pop(); - } - } else if (part !== '' && part != '.') { - paths.push(part); - } - } - return '/' + paths.join('/'); - } - if (/^\//.test(url)) { - url = host + getRealPath(url.substr(1)); - } else if (!/^\w+:\/\//.test(url)) { - url = host + getRealPath(pathname + '/' + url); - } - function getRelativePath(path, depth) { - if (url.substr(0, path.length) === path) { - var arr = []; - for (var i = 0; i < depth; i++) { - arr.push('..'); - } - var prefix = '.'; - if (arr.length > 0) { - prefix += '/' + arr.join('/'); - } - if (pathname == '/') { - prefix += '/'; - } - return prefix + url.substr(path.length); - } else { - if ((match = /^(.*)\//.exec(path))) { - return getRelativePath(match[1], ++depth); - } - } - } - if (mode === 'relative') { - url = getRelativePath(host + pathname, 0).substr(2); - } else if (mode === 'absolute') { - if (url.substr(0, host.length) === host) { - url = url.substr(host.length); - } - } - return url; -} -function _formatHtml(html, htmlTags, urlType, wellFormatted, indentChar) { - if (html == null) { - html = ''; - } - urlType = urlType || ''; - wellFormatted = _undef(wellFormatted, false); - indentChar = _undef(indentChar, '\t'); - var fontSizeList = 'xx-small,x-small,small,medium,large,x-large,xx-large'.split(','); - html = html.replace(/(<(?:pre|pre\s[^>]*)>)([\s\S]*?)(<\/pre>)/ig, function($0, $1, $2, $3) { - return $1 + $2.replace(/<(?:br|br\s[^>]*)>/ig, '\n') + $3; - }); - html = html.replace(/<(?:br|br\s[^>]*)\s*\/?>\s*<\/p>/ig, '

    '); - html = html.replace(/(<(?:p|p\s[^>]*)>)\s*(<\/p>)/ig, '$1
    $2'); - html = html.replace(/\u200B/g, ''); - html = html.replace(/\u00A9/g, '©'); - html = html.replace(/\u00AE/g, '®'); - html = html.replace(/<[^>]+/g, function($0) { - return $0.replace(/\s+/g, ' '); - }); - var htmlTagMap = {}; - if (htmlTags) { - _each(htmlTags, function(key, val) { - var arr = key.split(','); - for (var i = 0, len = arr.length; i < len; i++) { - htmlTagMap[arr[i]] = _toMap(val); - } - }); - if (!htmlTagMap.script) { - html = html.replace(/(<(?:script|script\s[^>]*)>)([\s\S]*?)(<\/script>)/ig, ''); - } - if (!htmlTagMap.style) { - html = html.replace(/(<(?:style|style\s[^>]*)>)([\s\S]*?)(<\/style>)/ig, ''); - } - } - var re = /(\s*)<(\/)?([\w\-:]+)((?:\s+|(?:\s+[\w\-:]+)|(?:\s+[\w\-:]+=[^\s"'<>]+)|(?:\s+[\w\-:"]+="[^"]*")|(?:\s+[\w\-:"]+='[^']*'))*)(\/)?>(\s*)/g; - var tagStack = []; - html = html.replace(re, function($0, $1, $2, $3, $4, $5, $6) { - var full = $0, - startNewline = $1 || '', - startSlash = $2 || '', - tagName = $3.toLowerCase(), - attr = $4 || '', - endSlash = $5 ? ' ' + $5 : '', - endNewline = $6 || ''; - if (htmlTags && !htmlTagMap[tagName]) { - return ''; - } - if (endSlash === '' && _SINGLE_TAG_MAP[tagName]) { - endSlash = ' /'; - } - if (_INLINE_TAG_MAP[tagName]) { - if (startNewline) { - startNewline = ' '; - } - if (endNewline) { - endNewline = ' '; - } - } - if (_PRE_TAG_MAP[tagName]) { - if (startSlash) { - endNewline = '\n'; - } else { - startNewline = '\n'; - } - } - if (wellFormatted && tagName == 'br') { - endNewline = '\n'; - } - if (_BLOCK_TAG_MAP[tagName] && !_PRE_TAG_MAP[tagName]) { - if (wellFormatted) { - if (startSlash && tagStack.length > 0 && tagStack[tagStack.length - 1] === tagName) { - tagStack.pop(); - } else { - tagStack.push(tagName); - } - startNewline = '\n'; - endNewline = '\n'; - for (var i = 0, len = startSlash ? tagStack.length : tagStack.length - 1; i < len; i++) { - startNewline += indentChar; - if (!startSlash) { - endNewline += indentChar; - } - } - if (endSlash) { - tagStack.pop(); - } else if (!startSlash) { - endNewline += indentChar; - } - } else { - startNewline = endNewline = ''; - } - } - if (attr !== '') { - var attrMap = _getAttrList(full); - if (tagName === 'font') { - var fontStyleMap = {}, fontStyle = ''; - _each(attrMap, function(key, val) { - if (key === 'color') { - fontStyleMap.color = val; - delete attrMap[key]; - } - if (key === 'size') { - fontStyleMap['font-size'] = fontSizeList[parseInt(val, 10) - 1] || ''; - delete attrMap[key]; - } - if (key === 'face') { - fontStyleMap['font-family'] = val; - delete attrMap[key]; - } - if (key === 'style') { - fontStyle = val; - } - }); - if (fontStyle && !/;$/.test(fontStyle)) { - fontStyle += ';'; - } - _each(fontStyleMap, function(key, val) { - if (val === '') { - return; - } - if (/\s/.test(val)) { - val = "'" + val + "'"; - } - fontStyle += key + ':' + val + ';'; - }); - attrMap.style = fontStyle; - } - _each(attrMap, function(key, val) { - if (_FILL_ATTR_MAP[key]) { - attrMap[key] = key; - } - if (_inArray(key, ['src', 'href']) >= 0) { - attrMap[key] = _formatUrl(val, urlType); - } - if (htmlTags && key !== 'style' && !htmlTagMap[tagName]['*'] && !htmlTagMap[tagName][key] || - tagName === 'body' && key === 'contenteditable' || - /^kindeditor_\d+$/.test(key)) { - delete attrMap[key]; - } - if (key === 'style' && val !== '') { - var styleMap = _getCssList(val); - _each(styleMap, function(k, v) { - if (htmlTags && !htmlTagMap[tagName].style && !htmlTagMap[tagName]['.' + k]) { - delete styleMap[k]; - } - }); - var style = ''; - _each(styleMap, function(k, v) { - style += k + ':' + v + ';'; - }); - attrMap.style = style; - } - }); - attr = ''; - _each(attrMap, function(key, val) { - if (key === 'style' && val === '') { - return; - } - val = val.replace(/"/g, '"'); - attr += ' ' + key + '="' + val + '"'; - }); - } - if (tagName === 'font') { - tagName = 'span'; - } - return startNewline + '<' + startSlash + tagName + attr + endSlash + '>' + endNewline; - }); - html = html.replace(/(<(?:pre|pre\s[^>]*)>)([\s\S]*?)(<\/pre>)/ig, function($0, $1, $2, $3) { - return $1 + $2.replace(/\n/g, '\n') + $3; - }); - html = html.replace(/\n\s*\n/g, '\n'); - html = html.replace(/\n/g, '\n'); - return _trim(html); -} -function _clearMsWord(html, htmlTags) { - html = html.replace(//ig, '') - .replace(//ig, '') - .replace(/]*>[\s\S]*?<\/style>/ig, '') - .replace(/]*>[\s\S]*?<\/script>/ig, '') - .replace(/]+>[\s\S]*?<\/w:[^>]+>/ig, '') - .replace(/]+>[\s\S]*?<\/o:[^>]+>/ig, '') - .replace(/[\s\S]*?<\/xml>/ig, '') - .replace(/<(?:table|td)[^>]*>/ig, function(full) { - return full.replace(/border-bottom:([#\w\s]+)/ig, 'border:$1'); - }); - return _formatHtml(html, htmlTags); -} -function _mediaType(src) { - if (/\.(rm|rmvb)(\?|$)/i.test(src)) { - return 'audio/x-pn-realaudio-plugin'; - } - if (/\.(swf|flv)(\?|$)/i.test(src)) { - return 'application/x-shockwave-flash'; - } - return 'video/x-ms-asf-plugin'; -} -function _mediaClass(type) { - if (/realaudio/i.test(type)) { - return 'ke-rm'; - } - if (/flash/i.test(type)) { - return 'ke-flash'; - } - return 'ke-media'; -} -function _mediaAttrs(srcTag) { - return _getAttrList(unescape(srcTag)); -} -function _mediaEmbed(attrs) { - var html = ' 0) { - style += 'width:' + width + 'px;'; - } - if (/\D/.test(height)) { - style += 'height:' + height + ';'; - } else if (height > 0) { - style += 'height:' + height + 'px;'; - } - var html = ''; - return html; -} -function _tmpl(str, data) { - var fn = new Function("obj", - "var p=[],print=function(){p.push.apply(p,arguments);};" + - "with(obj){p.push('" + - str.replace(/[\r\t\n]/g, " ") - .split("<%").join("\t") - .replace(/((^|%>)[^\t]*)'/g, "$1\r") - .replace(/\t=(.*?)%>/g, "',$1,'") - .split("\t").join("');") - .split("%>").join("p.push('") - .split("\r").join("\\'") + "');}return p.join('');"); - return data ? fn(data) : fn; -} -K.formatUrl = _formatUrl; -K.formatHtml = _formatHtml; -K.getCssList = _getCssList; -K.getAttrList = _getAttrList; -K.mediaType = _mediaType; -K.mediaAttrs = _mediaAttrs; -K.mediaEmbed = _mediaEmbed; -K.mediaImg = _mediaImg; -K.clearMsWord = _clearMsWord; -K.tmpl = _tmpl; -function _contains(nodeA, nodeB) { - if (nodeA.nodeType == 9 && nodeB.nodeType != 9) { - return true; - } - while ((nodeB = nodeB.parentNode)) { - if (nodeB == nodeA) { - return true; - } - } - return false; -} -var _getSetAttrDiv = document.createElement('div'); -_getSetAttrDiv.setAttribute('className', 't'); -var _GET_SET_ATTRIBUTE = _getSetAttrDiv.className !== 't'; -function _getAttr(el, key) { - key = key.toLowerCase(); - var val = null; - if (!_GET_SET_ATTRIBUTE && el.nodeName.toLowerCase() != 'script') { - var div = el.ownerDocument.createElement('div'); - div.appendChild(el.cloneNode(false)); - var list = _getAttrList(_unescape(div.innerHTML)); - if (key in list) { - val = list[key]; - } - } else { - try { - val = el.getAttribute(key, 2); - } catch(e) { - val = el.getAttribute(key, 1); - } - } - if (key === 'style' && val !== null) { - val = _formatCss(val); - } - return val; -} -function _queryAll(expr, root) { - var exprList = expr.split(','); - if (exprList.length > 1) { - var mergedResults = []; - _each(exprList, function() { - _each(_queryAll(this, root), function() { - if (_inArray(this, mergedResults) < 0) { - mergedResults.push(this); - } - }); - }); - return mergedResults; - } - root = root || document; - function escape(str) { - if (typeof str != 'string') { - return str; - } - return str.replace(/([^\w\-])/g, '\\$1'); - } - function stripslashes(str) { - return str.replace(/\\/g, ''); - } - function cmpTag(tagA, tagB) { - return tagA === '*' || tagA.toLowerCase() === escape(tagB.toLowerCase()); - } - function byId(id, tag, root) { - var arr = [], - doc = root.ownerDocument || root, - el = doc.getElementById(stripslashes(id)); - if (el) { - if (cmpTag(tag, el.nodeName) && _contains(root, el)) { - arr.push(el); - } - } - return arr; - } - function byClass(className, tag, root) { - var doc = root.ownerDocument || root, arr = [], els, i, len, el; - if (root.getElementsByClassName) { - els = root.getElementsByClassName(stripslashes(className)); - for (i = 0, len = els.length; i < len; i++) { - el = els[i]; - if (cmpTag(tag, el.nodeName)) { - arr.push(el); - } - } - } else if (doc.querySelectorAll) { - els = doc.querySelectorAll((root.nodeName !== '#document' ? root.nodeName + ' ' : '') + tag + '.' + className); - for (i = 0, len = els.length; i < len; i++) { - el = els[i]; - if (_contains(root, el)) { - arr.push(el); - } - } - } else { - els = root.getElementsByTagName(tag); - className = ' ' + className + ' '; - for (i = 0, len = els.length; i < len; i++) { - el = els[i]; - if (el.nodeType == 1) { - var cls = el.className; - if (cls && (' ' + cls + ' ').indexOf(className) > -1) { - arr.push(el); - } - } - } - } - return arr; - } - function byName(name, tag, root) { - var arr = [], doc = root.ownerDocument || root, - els = doc.getElementsByName(stripslashes(name)), el; - for (var i = 0, len = els.length; i < len; i++) { - el = els[i]; - if (cmpTag(tag, el.nodeName) && _contains(root, el)) { - if (el.getAttribute('name') !== null) { - arr.push(el); - } - } - } - return arr; - } - function byAttr(key, val, tag, root) { - var arr = [], els = root.getElementsByTagName(tag), el; - for (var i = 0, len = els.length; i < len; i++) { - el = els[i]; - if (el.nodeType == 1) { - if (val === null) { - if (_getAttr(el, key) !== null) { - arr.push(el); - } - } else { - if (val === escape(_getAttr(el, key))) { - arr.push(el); - } - } - } - } - return arr; - } - function select(expr, root) { - var arr = [], matches; - matches = /^((?:\\.|[^.#\s\[<>])+)/.exec(expr); - var tag = matches ? matches[1] : '*'; - if ((matches = /#((?:[\w\-]|\\.)+)$/.exec(expr))) { - arr = byId(matches[1], tag, root); - } else if ((matches = /\.((?:[\w\-]|\\.)+)$/.exec(expr))) { - arr = byClass(matches[1], tag, root); - } else if ((matches = /\[((?:[\w\-]|\\.)+)\]/.exec(expr))) { - arr = byAttr(matches[1].toLowerCase(), null, tag, root); - } else if ((matches = /\[((?:[\w\-]|\\.)+)\s*=\s*['"]?((?:\\.|[^'"]+)+)['"]?\]/.exec(expr))) { - var key = matches[1].toLowerCase(), val = matches[2]; - if (key === 'id') { - arr = byId(val, tag, root); - } else if (key === 'class') { - arr = byClass(val, tag, root); - } else if (key === 'name') { - arr = byName(val, tag, root); - } else { - arr = byAttr(key, val, tag, root); - } - } else { - var els = root.getElementsByTagName(tag), el; - for (var i = 0, len = els.length; i < len; i++) { - el = els[i]; - if (el.nodeType == 1) { - arr.push(el); - } - } - } - return arr; - } - var parts = [], arr, re = /((?:\\.|[^\s>])+|[\s>])/g; - while ((arr = re.exec(expr))) { - if (arr[1] !== ' ') { - parts.push(arr[1]); - } - } - var results = []; - if (parts.length == 1) { - return select(parts[0], root); - } - var isChild = false, part, els, subResults, val, v, i, j, k, length, len, l; - for (i = 0, lenth = parts.length; i < lenth; i++) { - part = parts[i]; - if (part === '>') { - isChild = true; - continue; - } - if (i > 0) { - els = []; - for (j = 0, len = results.length; j < len; j++) { - val = results[j]; - subResults = select(part, val); - for (k = 0, l = subResults.length; k < l; k++) { - v = subResults[k]; - if (isChild) { - if (val === v.parentNode) { - els.push(v); - } - } else { - els.push(v); - } - } - } - results = els; - } else { - results = select(part, root); - } - if (results.length === 0) { - return []; - } - } - return results; -} -function _query(expr, root) { - var arr = _queryAll(expr, root); - return arr.length > 0 ? arr[0] : null; -} -K.query = _query; -K.queryAll = _queryAll; -function _get(val) { - return K(val)[0]; -} -function _getDoc(node) { - if (!node) { - return document; - } - return node.ownerDocument || node.document || node; -} -function _getWin(node) { - if (!node) { - return window; - } - var doc = _getDoc(node); - return doc.parentWindow || doc.defaultView; -} -function _setHtml(el, html) { - if (el.nodeType != 1) { - return; - } - var doc = _getDoc(el); - try { - el.innerHTML = '' + html; - var temp = doc.getElementById('__kindeditor_temp_tag__'); - temp.parentNode.removeChild(temp); - } catch(e) { - K(el).empty(); - K('@' + html, doc).each(function() { - el.appendChild(this); - }); - } -} -function _hasClass(el, cls) { - return _inString(cls, el.className, ' '); -} -function _setAttr(el, key, val) { - if (_IE && _V < 8 && key.toLowerCase() == 'class') { - key = 'className'; - } - el.setAttribute(key, '' + val); -} -function _removeAttr(el, key) { - if (_IE && _V < 8 && key.toLowerCase() == 'class') { - key = 'className'; - } - _setAttr(el, key, ''); - el.removeAttribute(key); -} -function _getNodeName(node) { - if (!node || !node.nodeName) { - return ''; - } - return node.nodeName.toLowerCase(); -} -function _computedCss(el, key) { - var self = this, win = _getWin(el), camelKey = _toCamel(key), val = ''; - if (win.getComputedStyle) { - var style = win.getComputedStyle(el, null); - val = style[camelKey] || style.getPropertyValue(key) || el.style[camelKey]; - } else if (el.currentStyle) { - val = el.currentStyle[camelKey] || el.style[camelKey]; - } - return val; -} -function _hasVal(node) { - return !!_VALUE_TAG_MAP[_getNodeName(node)]; -} -function _docElement(doc) { - doc = doc || document; - return _QUIRKS ? doc.body : doc.documentElement; -} -function _docHeight(doc) { - var el = _docElement(doc); - return Math.max(el.scrollHeight, el.clientHeight); -} -function _docWidth(doc) { - var el = _docElement(doc); - return Math.max(el.scrollWidth, el.clientWidth); -} -function _getScrollPos(doc) { - doc = doc || document; - var x, y; - if (_IE || _NEWIE || _OPERA) { - x = _docElement(doc).scrollLeft; - y = _docElement(doc).scrollTop; - } else { - x = _getWin(doc).scrollX; - y = _getWin(doc).scrollY; - } - return {x : x, y : y}; -} -function KNode(node) { - this.init(node); -} -_extend(KNode, { - init : function(node) { - var self = this; - node = _isArray(node) ? node : [node]; - var length = 0; - for (var i = 0, len = node.length; i < len; i++) { - if (node[i]) { - self[i] = node[i].constructor === KNode ? node[i][0] : node[i]; - length++; - } - } - self.length = length; - self.doc = _getDoc(self[0]); - self.name = _getNodeName(self[0]); - self.type = self.length > 0 ? self[0].nodeType : null; - self.win = _getWin(self[0]); - }, - each : function(fn) { - var self = this; - for (var i = 0; i < self.length; i++) { - if (fn.call(self[i], i, self[i]) === false) { - return self; - } - } - return self; - }, - bind : function(type, fn) { - this.each(function() { - _bind(this, type, fn); - }); - return this; - }, - unbind : function(type, fn) { - this.each(function() { - _unbind(this, type, fn); - }); - return this; - }, - fire : function(type) { - if (this.length < 1) { - return this; - } - _fire(this[0], type); - return this; - }, - hasAttr : function(key) { - if (this.length < 1) { - return false; - } - return !!_getAttr(this[0], key); - }, - attr : function(key, val) { - var self = this; - if (key === undefined) { - return _getAttrList(self.outer()); - } - if (typeof key === 'object') { - _each(key, function(k, v) { - self.attr(k, v); - }); - return self; - } - if (val === undefined) { - val = self.length < 1 ? null : _getAttr(self[0], key); - return val === null ? '' : val; - } - self.each(function() { - _setAttr(this, key, val); - }); - return self; - }, - removeAttr : function(key) { - this.each(function() { - _removeAttr(this, key); - }); - return this; - }, - get : function(i) { - if (this.length < 1) { - return null; - } - return this[i || 0]; - }, - eq : function(i) { - if (this.length < 1) { - return null; - } - return this[i] ? new KNode(this[i]) : null; - }, - hasClass : function(cls) { - if (this.length < 1) { - return false; - } - return _hasClass(this[0], cls); - }, - addClass : function(cls) { - this.each(function() { - if (!_hasClass(this, cls)) { - this.className = _trim(this.className + ' ' + cls); - } - }); - return this; - }, - removeClass : function(cls) { - this.each(function() { - if (_hasClass(this, cls)) { - this.className = _trim(this.className.replace(new RegExp('(^|\\s)' + cls + '(\\s|$)'), ' ')); - } - }); - return this; - }, - html : function(val) { - var self = this; - if (val === undefined) { - if (self.length < 1 || self.type != 1) { - return ''; - } - return _formatHtml(self[0].innerHTML); - } - self.each(function() { - _setHtml(this, val); - }); - return self; - }, - text : function() { - var self = this; - if (self.length < 1) { - return ''; - } - return _IE ? self[0].innerText : self[0].textContent; - }, - hasVal : function() { - if (this.length < 1) { - return false; - } - return _hasVal(this[0]); - }, - val : function(val) { - var self = this; - if (val === undefined) { - if (self.length < 1) { - return ''; - } - return self.hasVal() ? self[0].value : self.attr('value'); - } else { - self.each(function() { - if (_hasVal(this)) { - this.value = val; - } else { - _setAttr(this, 'value' , val); - } - }); - return self; - } - }, - css : function(key, val) { - var self = this; - if (key === undefined) { - return _getCssList(self.attr('style')); - } - if (typeof key === 'object') { - _each(key, function(k, v) { - self.css(k, v); - }); - return self; - } - if (val === undefined) { - if (self.length < 1) { - return ''; - } - return self[0].style[_toCamel(key)] || _computedCss(self[0], key) || ''; - } - self.each(function() { - this.style[_toCamel(key)] = val; - }); - return self; - }, - width : function(val) { - var self = this; - if (val === undefined) { - if (self.length < 1) { - return 0; - } - return self[0].offsetWidth; - } - return self.css('width', _addUnit(val)); - }, - height : function(val) { - var self = this; - if (val === undefined) { - if (self.length < 1) { - return 0; - } - return self[0].offsetHeight; - } - return self.css('height', _addUnit(val)); - }, - opacity : function(val) { - this.each(function() { - if (this.style.opacity === undefined) { - this.style.filter = val == 1 ? '' : 'alpha(opacity=' + (val * 100) + ')'; - } else { - this.style.opacity = val == 1 ? '' : val; - } - }); - return this; - }, - data : function(key, val) { - var self = this; - key = 'kindeditor_data_' + key; - if (val === undefined) { - if (self.length < 1) { - return null; - } - return self[0][key]; - } - this.each(function() { - this[key] = val; - }); - return self; - }, - pos : function() { - var self = this, node = self[0], x = 0, y = 0; - if (node) { - if (node.getBoundingClientRect) { - var box = node.getBoundingClientRect(), - pos = _getScrollPos(self.doc); - x = box.left + pos.x; - y = box.top + pos.y; - } else { - while (node) { - x += node.offsetLeft; - y += node.offsetTop; - node = node.offsetParent; - } - } - } - return {x : _round(x), y : _round(y)}; - }, - clone : function(bool) { - if (this.length < 1) { - return new KNode([]); - } - return new KNode(this[0].cloneNode(bool)); - }, - append : function(expr) { - this.each(function() { - if (this.appendChild) { - this.appendChild(_get(expr)); - } - }); - return this; - }, - appendTo : function(expr) { - this.each(function() { - _get(expr).appendChild(this); - }); - return this; - }, - before : function(expr) { - this.each(function() { - this.parentNode.insertBefore(_get(expr), this); - }); - return this; - }, - after : function(expr) { - this.each(function() { - if (this.nextSibling) { - this.parentNode.insertBefore(_get(expr), this.nextSibling); - } else { - this.parentNode.appendChild(_get(expr)); - } - }); - return this; - }, - replaceWith : function(expr) { - var nodes = []; - this.each(function(i, node) { - _unbind(node); - var newNode = _get(expr); - node.parentNode.replaceChild(newNode, node); - nodes.push(newNode); - }); - return K(nodes); - }, - empty : function() { - var self = this; - self.each(function(i, node) { - var child = node.firstChild; - while (child) { - if (!node.parentNode) { - return; - } - var next = child.nextSibling; - child.parentNode.removeChild(child); - child = next; - } - }); - return self; - }, - remove : function(keepChilds) { - var self = this; - self.each(function(i, node) { - if (!node.parentNode) { - return; - } - _unbind(node); - if (keepChilds) { - var child = node.firstChild; - while (child) { - var next = child.nextSibling; - node.parentNode.insertBefore(child, node); - child = next; - } - } - node.parentNode.removeChild(node); - delete self[i]; - }); - self.length = 0; - return self; - }, - show : function(val) { - var self = this; - if (val === undefined) { - val = self._originDisplay || ''; - } - if (self.css('display') != 'none') { - return self; - } - return self.css('display', val); - }, - hide : function() { - var self = this; - if (self.length < 1) { - return self; - } - self._originDisplay = self[0].style.display; - return self.css('display', 'none'); - }, - outer : function() { - var self = this; - if (self.length < 1) { - return ''; - } - var div = self.doc.createElement('div'), html; - div.appendChild(self[0].cloneNode(true)); - html = _formatHtml(div.innerHTML); - div = null; - return html; - }, - isSingle : function() { - return !!_SINGLE_TAG_MAP[this.name]; - }, - isInline : function() { - return !!_INLINE_TAG_MAP[this.name]; - }, - isBlock : function() { - return !!_BLOCK_TAG_MAP[this.name]; - }, - isStyle : function() { - return !!_STYLE_TAG_MAP[this.name]; - }, - isControl : function() { - return !!_CONTROL_TAG_MAP[this.name]; - }, - contains : function(otherNode) { - if (this.length < 1) { - return false; - } - return _contains(this[0], _get(otherNode)); - }, - parent : function() { - if (this.length < 1) { - return null; - } - var node = this[0].parentNode; - return node ? new KNode(node) : null; - }, - children : function() { - if (this.length < 1) { - return new KNode([]); - } - var list = [], child = this[0].firstChild; - while (child) { - if (child.nodeType != 3 || _trim(child.nodeValue) !== '') { - list.push(child); - } - child = child.nextSibling; - } - return new KNode(list); - }, - first : function() { - var list = this.children(); - return list.length > 0 ? list.eq(0) : null; - }, - last : function() { - var list = this.children(); - return list.length > 0 ? list.eq(list.length - 1) : null; - }, - index : function() { - if (this.length < 1) { - return -1; - } - var i = -1, sibling = this[0]; - while (sibling) { - i++; - sibling = sibling.previousSibling; - } - return i; - }, - prev : function() { - if (this.length < 1) { - return null; - } - var node = this[0].previousSibling; - return node ? new KNode(node) : null; - }, - next : function() { - if (this.length < 1) { - return null; - } - var node = this[0].nextSibling; - return node ? new KNode(node) : null; - }, - scan : function(fn, order) { - if (this.length < 1) { - return; - } - order = (order === undefined) ? true : order; - function walk(node) { - var n = order ? node.firstChild : node.lastChild; - while (n) { - var next = order ? n.nextSibling : n.previousSibling; - if (fn(n) === false) { - return false; - } - if (walk(n) === false) { - return false; - } - n = next; - } - } - walk(this[0]); - return this; - } -}); -_each(('blur,focus,focusin,focusout,load,resize,scroll,unload,click,dblclick,' + - 'mousedown,mouseup,mousemove,mouseover,mouseout,mouseenter,mouseleave,' + - 'change,select,submit,keydown,keypress,keyup,error,contextmenu').split(','), function(i, type) { - KNode.prototype[type] = function(fn) { - return fn ? this.bind(type, fn) : this.fire(type); - }; -}); -var _K = K; -K = function(expr, root) { - if (expr === undefined || expr === null) { - return; - } - function newNode(node) { - if (!node[0]) { - node = []; - } - return new KNode(node); - } - if (typeof expr === 'string') { - if (root) { - root = _get(root); - } - var length = expr.length; - if (expr.charAt(0) === '@') { - expr = expr.substr(1); - } - if (expr.length !== length || /<.+>/.test(expr)) { - var doc = root ? root.ownerDocument || root : document, - div = doc.createElement('div'), list = []; - div.innerHTML = '' + expr; - for (var i = 0, len = div.childNodes.length; i < len; i++) { - var child = div.childNodes[i]; - if (child.id == '__kindeditor_temp_tag__') { - continue; - } - list.push(child); - } - return newNode(list); - } - return newNode(_queryAll(expr, root)); - } - if (expr && expr.constructor === KNode) { - return expr; - } - if (expr.toArray) { - expr = expr.toArray(); - } - if (_isArray(expr)) { - return newNode(expr); - } - return newNode(_toArray(arguments)); -}; -_each(_K, function(key, val) { - K[key] = val; -}); -K.NodeClass = KNode; -window.KindEditor = K; -var _START_TO_START = 0, - _START_TO_END = 1, - _END_TO_END = 2, - _END_TO_START = 3, - _BOOKMARK_ID = 0; -function _updateCollapsed(range) { - range.collapsed = (range.startContainer === range.endContainer && range.startOffset === range.endOffset); - return range; -} -function _copyAndDelete(range, isCopy, isDelete) { - var doc = range.doc, nodeList = []; - function splitTextNode(node, startOffset, endOffset) { - var length = node.nodeValue.length, centerNode; - if (isCopy) { - var cloneNode = node.cloneNode(true); - if (startOffset > 0) { - centerNode = cloneNode.splitText(startOffset); - } else { - centerNode = cloneNode; - } - if (endOffset < length) { - centerNode.splitText(endOffset - startOffset); - } - } - if (isDelete) { - var center = node; - if (startOffset > 0) { - center = node.splitText(startOffset); - range.setStart(node, startOffset); - } - if (endOffset < length) { - var right = center.splitText(endOffset - startOffset); - range.setEnd(right, 0); - } - nodeList.push(center); - } - return centerNode; - } - function removeNodes() { - if (isDelete) { - range.up().collapse(true); - } - for (var i = 0, len = nodeList.length; i < len; i++) { - var node = nodeList[i]; - if (node.parentNode) { - node.parentNode.removeChild(node); - } - } - } - var copyRange = range.cloneRange().down(); - var start = -1, incStart = -1, incEnd = -1, end = -1, - ancestor = range.commonAncestor(), frag = doc.createDocumentFragment(); - if (ancestor.nodeType == 3) { - var textNode = splitTextNode(ancestor, range.startOffset, range.endOffset); - if (isCopy) { - frag.appendChild(textNode); - } - removeNodes(); - return isCopy ? frag : range; - } - function extractNodes(parent, frag) { - var node = parent.firstChild, nextNode; - while (node) { - var testRange = new KRange(doc).selectNode(node); - start = testRange.compareBoundaryPoints(_START_TO_END, range); - if (start >= 0 && incStart <= 0) { - incStart = testRange.compareBoundaryPoints(_START_TO_START, range); - } - if (incStart >= 0 && incEnd <= 0) { - incEnd = testRange.compareBoundaryPoints(_END_TO_END, range); - } - if (incEnd >= 0 && end <= 0) { - end = testRange.compareBoundaryPoints(_END_TO_START, range); - } - if (end >= 0) { - return false; - } - nextNode = node.nextSibling; - if (start > 0) { - if (node.nodeType == 1) { - if (incStart >= 0 && incEnd <= 0) { - if (isCopy) { - frag.appendChild(node.cloneNode(true)); - } - if (isDelete) { - nodeList.push(node); - } - } else { - var childFlag; - if (isCopy) { - childFlag = node.cloneNode(false); - frag.appendChild(childFlag); - } - if (extractNodes(node, childFlag) === false) { - return false; - } - } - } else if (node.nodeType == 3) { - var textNode; - if (node == copyRange.startContainer) { - textNode = splitTextNode(node, copyRange.startOffset, node.nodeValue.length); - } else if (node == copyRange.endContainer) { - textNode = splitTextNode(node, 0, copyRange.endOffset); - } else { - textNode = splitTextNode(node, 0, node.nodeValue.length); - } - if (isCopy) { - try { - frag.appendChild(textNode); - } catch(e) {} - } - } - } - node = nextNode; - } - } - extractNodes(ancestor, frag); - if (isDelete) { - range.up().collapse(true); - } - for (var i = 0, len = nodeList.length; i < len; i++) { - var node = nodeList[i]; - if (node.parentNode) { - node.parentNode.removeChild(node); - } - } - return isCopy ? frag : range; -} -function _moveToElementText(range, el) { - var node = el; - while (node) { - var knode = K(node); - if (knode.name == 'marquee' || knode.name == 'select') { - return; - } - node = node.parentNode; - } - try { - range.moveToElementText(el); - } catch(e) {} -} -function _getStartEnd(rng, isStart) { - var doc = rng.parentElement().ownerDocument, - pointRange = rng.duplicate(); - pointRange.collapse(isStart); - var parent = pointRange.parentElement(), - nodes = parent.childNodes; - if (nodes.length === 0) { - return {node: parent.parentNode, offset: K(parent).index()}; - } - var startNode = doc, startPos = 0, cmp = -1; - var testRange = rng.duplicate(); - _moveToElementText(testRange, parent); - for (var i = 0, len = nodes.length; i < len; i++) { - var node = nodes[i]; - cmp = testRange.compareEndPoints('StartToStart', pointRange); - if (cmp === 0) { - return {node: node.parentNode, offset: i}; - } - if (node.nodeType == 1) { - var nodeRange = rng.duplicate(), dummy, knode = K(node), newNode = node; - if (knode.isControl()) { - dummy = doc.createElement('span'); - knode.after(dummy); - newNode = dummy; - startPos += knode.text().replace(/\r\n|\n|\r/g, '').length; - } - _moveToElementText(nodeRange, newNode); - testRange.setEndPoint('StartToEnd', nodeRange); - if (cmp > 0) { - startPos += nodeRange.text.replace(/\r\n|\n|\r/g, '').length; - } else { - startPos = 0; - } - if (dummy) { - K(dummy).remove(); - } - } else if (node.nodeType == 3) { - testRange.moveStart('character', node.nodeValue.length); - startPos += node.nodeValue.length; - } - if (cmp < 0) { - startNode = node; - } - } - if (cmp < 0 && startNode.nodeType == 1) { - return {node: parent, offset: K(parent.lastChild).index() + 1}; - } - if (cmp > 0) { - while (startNode.nextSibling && startNode.nodeType == 1) { - startNode = startNode.nextSibling; - } - } - testRange = rng.duplicate(); - _moveToElementText(testRange, parent); - testRange.setEndPoint('StartToEnd', pointRange); - startPos -= testRange.text.replace(/\r\n|\n|\r/g, '').length; - if (cmp > 0 && startNode.nodeType == 3) { - var prevNode = startNode.previousSibling; - while (prevNode && prevNode.nodeType == 3) { - startPos -= prevNode.nodeValue.length; - prevNode = prevNode.previousSibling; - } - } - return {node: startNode, offset: startPos}; -} -function _getEndRange(node, offset) { - var doc = node.ownerDocument || node, - range = doc.body.createTextRange(); - if (doc == node) { - range.collapse(true); - return range; - } - if (node.nodeType == 1 && node.childNodes.length > 0) { - var children = node.childNodes, isStart, child; - if (offset === 0) { - child = children[0]; - isStart = true; - } else { - child = children[offset - 1]; - isStart = false; - } - if (!child) { - return range; - } - if (K(child).name === 'head') { - if (offset === 1) { - isStart = true; - } - if (offset === 2) { - isStart = false; - } - range.collapse(isStart); - return range; - } - if (child.nodeType == 1) { - var kchild = K(child), span; - if (kchild.isControl()) { - span = doc.createElement('span'); - if (isStart) { - kchild.before(span); - } else { - kchild.after(span); - } - child = span; - } - _moveToElementText(range, child); - range.collapse(isStart); - if (span) { - K(span).remove(); - } - return range; - } - node = child; - offset = isStart ? 0 : child.nodeValue.length; - } - var dummy = doc.createElement('span'); - K(node).before(dummy); - _moveToElementText(range, dummy); - range.moveStart('character', offset); - K(dummy).remove(); - return range; -} -function _toRange(rng) { - var doc, range; - function tr2td(start) { - if (K(start.node).name == 'tr') { - start.node = start.node.cells[start.offset]; - start.offset = 0; - } - } - if (_IERANGE) { - if (rng.item) { - doc = _getDoc(rng.item(0)); - range = new KRange(doc); - range.selectNode(rng.item(0)); - return range; - } - doc = rng.parentElement().ownerDocument; - var start = _getStartEnd(rng, true), - end = _getStartEnd(rng, false); - tr2td(start); - tr2td(end); - range = new KRange(doc); - range.setStart(start.node, start.offset); - range.setEnd(end.node, end.offset); - return range; - } - var startContainer = rng.startContainer; - doc = startContainer.ownerDocument || startContainer; - range = new KRange(doc); - range.setStart(startContainer, rng.startOffset); - range.setEnd(rng.endContainer, rng.endOffset); - return range; -} -function KRange(doc) { - this.init(doc); -} -_extend(KRange, { - init : function(doc) { - var self = this; - self.startContainer = doc; - self.startOffset = 0; - self.endContainer = doc; - self.endOffset = 0; - self.collapsed = true; - self.doc = doc; - }, - commonAncestor : function() { - function getParents(node) { - var parents = []; - while (node) { - parents.push(node); - node = node.parentNode; - } - return parents; - } - var parentsA = getParents(this.startContainer), - parentsB = getParents(this.endContainer), - i = 0, lenA = parentsA.length, lenB = parentsB.length, parentA, parentB; - while (++i) { - parentA = parentsA[lenA - i]; - parentB = parentsB[lenB - i]; - if (!parentA || !parentB || parentA !== parentB) { - break; - } - } - return parentsA[lenA - i + 1]; - }, - setStart : function(node, offset) { - var self = this, doc = self.doc; - self.startContainer = node; - self.startOffset = offset; - if (self.endContainer === doc) { - self.endContainer = node; - self.endOffset = offset; - } - return _updateCollapsed(this); - }, - setEnd : function(node, offset) { - var self = this, doc = self.doc; - self.endContainer = node; - self.endOffset = offset; - if (self.startContainer === doc) { - self.startContainer = node; - self.startOffset = offset; - } - return _updateCollapsed(this); - }, - setStartBefore : function(node) { - return this.setStart(node.parentNode || this.doc, K(node).index()); - }, - setStartAfter : function(node) { - return this.setStart(node.parentNode || this.doc, K(node).index() + 1); - }, - setEndBefore : function(node) { - return this.setEnd(node.parentNode || this.doc, K(node).index()); - }, - setEndAfter : function(node) { - return this.setEnd(node.parentNode || this.doc, K(node).index() + 1); - }, - selectNode : function(node) { - return this.setStartBefore(node).setEndAfter(node); - }, - selectNodeContents : function(node) { - var knode = K(node); - if (knode.type == 3 || knode.isSingle()) { - return this.selectNode(node); - } - var children = knode.children(); - if (children.length > 0) { - return this.setStartBefore(children[0]).setEndAfter(children[children.length - 1]); - } - return this.setStart(node, 0).setEnd(node, 0); - }, - collapse : function(toStart) { - if (toStart) { - return this.setEnd(this.startContainer, this.startOffset); - } - return this.setStart(this.endContainer, this.endOffset); - }, - compareBoundaryPoints : function(how, range) { - var rangeA = this.get(), rangeB = range.get(); - if (_IERANGE) { - var arr = {}; - arr[_START_TO_START] = 'StartToStart'; - arr[_START_TO_END] = 'EndToStart'; - arr[_END_TO_END] = 'EndToEnd'; - arr[_END_TO_START] = 'StartToEnd'; - var cmp = rangeA.compareEndPoints(arr[how], rangeB); - if (cmp !== 0) { - return cmp; - } - var nodeA, nodeB, nodeC, posA, posB; - if (how === _START_TO_START || how === _END_TO_START) { - nodeA = this.startContainer; - posA = this.startOffset; - } - if (how === _START_TO_END || how === _END_TO_END) { - nodeA = this.endContainer; - posA = this.endOffset; - } - if (how === _START_TO_START || how === _START_TO_END) { - nodeB = range.startContainer; - posB = range.startOffset; - } - if (how === _END_TO_END || how === _END_TO_START) { - nodeB = range.endContainer; - posB = range.endOffset; - } - if (nodeA === nodeB) { - var diff = posA - posB; - return diff > 0 ? 1 : (diff < 0 ? -1 : 0); - } - nodeC = nodeB; - while (nodeC && nodeC.parentNode !== nodeA) { - nodeC = nodeC.parentNode; - } - if (nodeC) { - return K(nodeC).index() >= posA ? -1 : 1; - } - nodeC = nodeA; - while (nodeC && nodeC.parentNode !== nodeB) { - nodeC = nodeC.parentNode; - } - if (nodeC) { - return K(nodeC).index() >= posB ? 1 : -1; - } - nodeC = K(nodeB).next(); - if (nodeC && nodeC.contains(nodeA)) { - return 1; - } - nodeC = K(nodeA).next(); - if (nodeC && nodeC.contains(nodeB)) { - return -1; - } - } else { - return rangeA.compareBoundaryPoints(how, rangeB); - } - }, - cloneRange : function() { - return new KRange(this.doc).setStart(this.startContainer, this.startOffset).setEnd(this.endContainer, this.endOffset); - }, - toString : function() { - var rng = this.get(), str = _IERANGE ? rng.text : rng.toString(); - return str.replace(/\r\n|\n|\r/g, ''); - }, - cloneContents : function() { - return _copyAndDelete(this, true, false); - }, - deleteContents : function() { - return _copyAndDelete(this, false, true); - }, - extractContents : function() { - return _copyAndDelete(this, true, true); - }, - insertNode : function(node) { - var self = this, - sc = self.startContainer, so = self.startOffset, - ec = self.endContainer, eo = self.endOffset, - firstChild, lastChild, c, nodeCount = 1; - if (node.nodeName.toLowerCase() === '#document-fragment') { - firstChild = node.firstChild; - lastChild = node.lastChild; - nodeCount = node.childNodes.length; - } - if (sc.nodeType == 1) { - c = sc.childNodes[so]; - if (c) { - sc.insertBefore(node, c); - if (sc === ec) { - eo += nodeCount; - } - } else { - sc.appendChild(node); - } - } else if (sc.nodeType == 3) { - if (so === 0) { - sc.parentNode.insertBefore(node, sc); - if (sc.parentNode === ec) { - eo += nodeCount; - } - } else if (so >= sc.nodeValue.length) { - if (sc.nextSibling) { - sc.parentNode.insertBefore(node, sc.nextSibling); - } else { - sc.parentNode.appendChild(node); - } - } else { - if (so > 0) { - c = sc.splitText(so); - } else { - c = sc; - } - sc.parentNode.insertBefore(node, c); - if (sc === ec) { - ec = c; - eo -= so; - } - } - } - if (firstChild) { - self.setStartBefore(firstChild).setEndAfter(lastChild); - } else { - self.selectNode(node); - } - if (self.compareBoundaryPoints(_END_TO_END, self.cloneRange().setEnd(ec, eo)) >= 1) { - return self; - } - return self.setEnd(ec, eo); - }, - surroundContents : function(node) { - node.appendChild(this.extractContents()); - return this.insertNode(node).selectNode(node); - }, - isControl : function() { - var self = this, - sc = self.startContainer, so = self.startOffset, - ec = self.endContainer, eo = self.endOffset, rng; - return sc.nodeType == 1 && sc === ec && so + 1 === eo && K(sc.childNodes[so]).isControl(); - }, - get : function(hasControlRange) { - var self = this, doc = self.doc, node, rng; - if (!_IERANGE) { - rng = doc.createRange(); - try { - rng.setStart(self.startContainer, self.startOffset); - rng.setEnd(self.endContainer, self.endOffset); - } catch (e) {} - return rng; - } - if (hasControlRange && self.isControl()) { - rng = doc.body.createControlRange(); - rng.addElement(self.startContainer.childNodes[self.startOffset]); - return rng; - } - var range = self.cloneRange().down(); - rng = doc.body.createTextRange(); - rng.setEndPoint('StartToStart', _getEndRange(range.startContainer, range.startOffset)); - rng.setEndPoint('EndToStart', _getEndRange(range.endContainer, range.endOffset)); - return rng; - }, - html : function() { - return K(this.cloneContents()).outer(); - }, - down : function() { - var self = this; - function downPos(node, pos, isStart) { - if (node.nodeType != 1) { - return; - } - var children = K(node).children(); - if (children.length === 0) { - return; - } - var left, right, child, offset; - if (pos > 0) { - left = children.eq(pos - 1); - } - if (pos < children.length) { - right = children.eq(pos); - } - if (left && left.type == 3) { - child = left[0]; - offset = child.nodeValue.length; - } - if (right && right.type == 3) { - child = right[0]; - offset = 0; - } - if (!child) { - return; - } - if (isStart) { - self.setStart(child, offset); - } else { - self.setEnd(child, offset); - } - } - downPos(self.startContainer, self.startOffset, true); - downPos(self.endContainer, self.endOffset, false); - return self; - }, - up : function() { - var self = this; - function upPos(node, pos, isStart) { - if (node.nodeType != 3) { - return; - } - if (pos === 0) { - if (isStart) { - self.setStartBefore(node); - } else { - self.setEndBefore(node); - } - } else if (pos == node.nodeValue.length) { - if (isStart) { - self.setStartAfter(node); - } else { - self.setEndAfter(node); - } - } - } - upPos(self.startContainer, self.startOffset, true); - upPos(self.endContainer, self.endOffset, false); - return self; - }, - enlarge : function(toBlock) { - var self = this; - self.up(); - function enlargePos(node, pos, isStart) { - var knode = K(node), parent; - if (knode.type == 3 || _NOSPLIT_TAG_MAP[knode.name] || !toBlock && knode.isBlock()) { - return; - } - if (pos === 0) { - while (!knode.prev()) { - parent = knode.parent(); - if (!parent || _NOSPLIT_TAG_MAP[parent.name] || !toBlock && parent.isBlock()) { - break; - } - knode = parent; - } - if (isStart) { - self.setStartBefore(knode[0]); - } else { - self.setEndBefore(knode[0]); - } - } else if (pos == knode.children().length) { - while (!knode.next()) { - parent = knode.parent(); - if (!parent || _NOSPLIT_TAG_MAP[parent.name] || !toBlock && parent.isBlock()) { - break; - } - knode = parent; - } - if (isStart) { - self.setStartAfter(knode[0]); - } else { - self.setEndAfter(knode[0]); - } - } - } - enlargePos(self.startContainer, self.startOffset, true); - enlargePos(self.endContainer, self.endOffset, false); - return self; - }, - shrink : function() { - var self = this, child, collapsed = self.collapsed; - while (self.startContainer.nodeType == 1 && (child = self.startContainer.childNodes[self.startOffset]) && child.nodeType == 1 && !K(child).isSingle()) { - self.setStart(child, 0); - } - if (collapsed) { - return self.collapse(collapsed); - } - while (self.endContainer.nodeType == 1 && self.endOffset > 0 && (child = self.endContainer.childNodes[self.endOffset - 1]) && child.nodeType == 1 && !K(child).isSingle()) { - self.setEnd(child, child.childNodes.length); - } - return self; - }, - createBookmark : function(serialize) { - var self = this, doc = self.doc, endNode, - startNode = K('', doc)[0]; - startNode.id = '__kindeditor_bookmark_start_' + (_BOOKMARK_ID++) + '__'; - if (!self.collapsed) { - endNode = startNode.cloneNode(true); - endNode.id = '__kindeditor_bookmark_end_' + (_BOOKMARK_ID++) + '__'; - } - if (endNode) { - self.cloneRange().collapse(false).insertNode(endNode).setEndBefore(endNode); - } - self.insertNode(startNode).setStartAfter(startNode); - return { - start : serialize ? '#' + startNode.id : startNode, - end : endNode ? (serialize ? '#' + endNode.id : endNode) : null - }; - }, - moveToBookmark : function(bookmark) { - var self = this, doc = self.doc, - start = K(bookmark.start, doc), end = bookmark.end ? K(bookmark.end, doc) : null; - if (!start || start.length < 1) { - return self; - } - self.setStartBefore(start[0]); - start.remove(); - if (end && end.length > 0) { - self.setEndBefore(end[0]); - end.remove(); - } else { - self.collapse(true); - } - return self; - }, - dump : function() { - console.log('--------------------'); - console.log(this.startContainer.nodeType == 3 ? this.startContainer.nodeValue : this.startContainer, this.startOffset); - console.log(this.endContainer.nodeType == 3 ? this.endContainer.nodeValue : this.endContainer, this.endOffset); - } -}); -function _range(mixed) { - if (!mixed.nodeName) { - return mixed.constructor === KRange ? mixed : _toRange(mixed); - } - return new KRange(mixed); -} -K.RangeClass = KRange; -K.range = _range; -K.START_TO_START = _START_TO_START; -K.START_TO_END = _START_TO_END; -K.END_TO_END = _END_TO_END; -K.END_TO_START = _END_TO_START; -function _nativeCommand(doc, key, val) { - try { - doc.execCommand(key, false, val); - } catch(e) {} -} -function _nativeCommandValue(doc, key) { - var val = ''; - try { - val = doc.queryCommandValue(key); - } catch (e) {} - if (typeof val !== 'string') { - val = ''; - } - return val; -} -function _getSel(doc) { - var win = _getWin(doc); - return _IERANGE ? doc.selection : win.getSelection(); -} -function _getRng(doc) { - var sel = _getSel(doc), rng; - try { - if (sel.rangeCount > 0) { - rng = sel.getRangeAt(0); - } else { - rng = sel.createRange(); - } - } catch(e) {} - if (_IERANGE && (!rng || (!rng.item && rng.parentElement().ownerDocument !== doc))) { - return null; - } - return rng; -} -function _singleKeyMap(map) { - var newMap = {}, arr, v; - _each(map, function(key, val) { - arr = key.split(','); - for (var i = 0, len = arr.length; i < len; i++) { - v = arr[i]; - newMap[v] = val; - } - }); - return newMap; -} -function _hasAttrOrCss(knode, map) { - return _hasAttrOrCssByKey(knode, map, '*') || _hasAttrOrCssByKey(knode, map); -} -function _hasAttrOrCssByKey(knode, map, mapKey) { - mapKey = mapKey || knode.name; - if (knode.type !== 1) { - return false; - } - var newMap = _singleKeyMap(map); - if (!newMap[mapKey]) { - return false; - } - var arr = newMap[mapKey].split(','); - for (var i = 0, len = arr.length; i < len; i++) { - var key = arr[i]; - if (key === '*') { - return true; - } - var match = /^(\.?)([^=]+)(?:=([^=]*))?$/.exec(key); - var method = match[1] ? 'css' : 'attr'; - key = match[2]; - var val = match[3] || ''; - if (val === '' && knode[method](key) !== '') { - return true; - } - if (val !== '' && knode[method](key) === val) { - return true; - } - } - return false; -} -function _removeAttrOrCss(knode, map) { - if (knode.type != 1) { - return; - } - _removeAttrOrCssByKey(knode, map, '*'); - _removeAttrOrCssByKey(knode, map); -} -function _removeAttrOrCssByKey(knode, map, mapKey) { - mapKey = mapKey || knode.name; - if (knode.type !== 1) { - return; - } - var newMap = _singleKeyMap(map); - if (!newMap[mapKey]) { - return; - } - var arr = newMap[mapKey].split(','), allFlag = false; - for (var i = 0, len = arr.length; i < len; i++) { - var key = arr[i]; - if (key === '*') { - allFlag = true; - break; - } - var match = /^(\.?)([^=]+)(?:=([^=]*))?$/.exec(key); - key = match[2]; - if (match[1]) { - key = _toCamel(key); - if (knode[0].style[key]) { - knode[0].style[key] = ''; - } - } else { - knode.removeAttr(key); - } - } - if (allFlag) { - knode.remove(true); - } -} -function _getInnerNode(knode) { - var inner = knode; - while (inner.first()) { - inner = inner.first(); - } - return inner; -} -function _isEmptyNode(knode) { - if (knode.type != 1 || knode.isSingle()) { - return false; - } - return knode.html().replace(/<[^>]+>/g, '') === ''; -} -function _mergeWrapper(a, b) { - a = a.clone(true); - var lastA = _getInnerNode(a), childA = a, merged = false; - while (b) { - while (childA) { - if (childA.name === b.name) { - _mergeAttrs(childA, b.attr(), b.css()); - merged = true; - } - childA = childA.first(); - } - if (!merged) { - lastA.append(b.clone(false)); - } - merged = false; - b = b.first(); - } - return a; -} -function _wrapNode(knode, wrapper) { - wrapper = wrapper.clone(true); - if (knode.type == 3) { - _getInnerNode(wrapper).append(knode.clone(false)); - knode.replaceWith(wrapper); - return wrapper; - } - var nodeWrapper = knode, child; - while ((child = knode.first()) && child.children().length == 1) { - knode = child; - } - child = knode.first(); - var frag = knode.doc.createDocumentFragment(); - while (child) { - frag.appendChild(child[0]); - child = child.next(); - } - wrapper = _mergeWrapper(nodeWrapper, wrapper); - if (frag.firstChild) { - _getInnerNode(wrapper).append(frag); - } - nodeWrapper.replaceWith(wrapper); - return wrapper; -} -function _mergeAttrs(knode, attrs, styles) { - _each(attrs, function(key, val) { - if (key !== 'style') { - knode.attr(key, val); - } - }); - _each(styles, function(key, val) { - knode.css(key, val); - }); -} -function _inPreElement(knode) { - while (knode && knode.name != 'body') { - if (_PRE_TAG_MAP[knode.name] || knode.name == 'div' && knode.hasClass('ke-script')) { - return true; - } - knode = knode.parent(); - } - return false; -} -function KCmd(range) { - this.init(range); -} -_extend(KCmd, { - init : function(range) { - var self = this, doc = range.doc; - self.doc = doc; - self.win = _getWin(doc); - self.sel = _getSel(doc); - self.range = range; - }, - selection : function(forceReset) { - var self = this, doc = self.doc, rng = _getRng(doc); - self.sel = _getSel(doc); - if (rng) { - self.range = _range(rng); - if (K(self.range.startContainer).name == 'html') { - self.range.selectNodeContents(doc.body).collapse(false); - } - return self; - } - if (forceReset) { - self.range.selectNodeContents(doc.body).collapse(false); - } - return self; - }, - select : function(hasDummy) { - hasDummy = _undef(hasDummy, true); - var self = this, sel = self.sel, range = self.range.cloneRange().shrink(), - sc = range.startContainer, so = range.startOffset, - ec = range.endContainer, eo = range.endOffset, - doc = _getDoc(sc), win = self.win, rng, hasU200b = false; - if (hasDummy && sc.nodeType == 1 && range.collapsed) { - if (_IERANGE) { - var dummy = K(' ', doc); - range.insertNode(dummy[0]); - rng = doc.body.createTextRange(); - try { - rng.moveToElementText(dummy[0]); - } catch(ex) {} - rng.collapse(false); - rng.select(); - dummy.remove(); - win.focus(); - return self; - } - if (_WEBKIT) { - var children = sc.childNodes; - if (K(sc).isInline() || so > 0 && K(children[so - 1]).isInline() || children[so] && K(children[so]).isInline()) { - range.insertNode(doc.createTextNode('\u200B')); - hasU200b = true; - } - } - } - if (_IERANGE) { - try { - rng = range.get(true); - rng.select(); - } catch(e) {} - } else { - if (hasU200b) { - range.collapse(false); - } - rng = range.get(true); - sel.removeAllRanges(); - sel.addRange(rng); - if (doc !== document) { - var pos = K(rng.endContainer).pos(); - win.scrollTo(pos.x, pos.y); - } - } - win.focus(); - return self; - }, - wrap : function(val) { - var self = this, doc = self.doc, range = self.range, wrapper; - wrapper = K(val, doc); - if (range.collapsed) { - range.shrink(); - range.insertNode(wrapper[0]).selectNodeContents(wrapper[0]); - return self; - } - if (wrapper.isBlock()) { - var copyWrapper = wrapper.clone(true), child = copyWrapper; - while (child.first()) { - child = child.first(); - } - child.append(range.extractContents()); - range.insertNode(copyWrapper[0]).selectNode(copyWrapper[0]); - return self; - } - range.enlarge(); - var bookmark = range.createBookmark(), ancestor = range.commonAncestor(), isStart = false; - K(ancestor).scan(function(node) { - if (!isStart && node == bookmark.start) { - isStart = true; - return; - } - if (isStart) { - if (node == bookmark.end) { - return false; - } - var knode = K(node); - if (_inPreElement(knode)) { - return; - } - if (knode.type == 3 && _trim(node.nodeValue).length > 0) { - var parent; - while ((parent = knode.parent()) && parent.isStyle() && parent.children().length == 1) { - knode = parent; - } - _wrapNode(knode, wrapper); - } - } - }); - range.moveToBookmark(bookmark); - return self; - }, - split : function(isStart, map) { - var range = this.range, doc = range.doc; - var tempRange = range.cloneRange().collapse(isStart); - var node = tempRange.startContainer, pos = tempRange.startOffset, - parent = node.nodeType == 3 ? node.parentNode : node, - needSplit = false, knode; - while (parent && parent.parentNode) { - knode = K(parent); - if (map) { - if (!knode.isStyle()) { - break; - } - if (!_hasAttrOrCss(knode, map)) { - break; - } - } else { - if (_NOSPLIT_TAG_MAP[knode.name]) { - break; - } - } - needSplit = true; - parent = parent.parentNode; - } - if (needSplit) { - var dummy = doc.createElement('span'); - range.cloneRange().collapse(!isStart).insertNode(dummy); - if (isStart) { - tempRange.setStartBefore(parent.firstChild).setEnd(node, pos); - } else { - tempRange.setStart(node, pos).setEndAfter(parent.lastChild); - } - var frag = tempRange.extractContents(), - first = frag.firstChild, last = frag.lastChild; - if (isStart) { - tempRange.insertNode(frag); - range.setStartAfter(last).setEndBefore(dummy); - } else { - parent.appendChild(frag); - range.setStartBefore(dummy).setEndBefore(first); - } - var dummyParent = dummy.parentNode; - if (dummyParent == range.endContainer) { - var prev = K(dummy).prev(), next = K(dummy).next(); - if (prev && next && prev.type == 3 && next.type == 3) { - range.setEnd(prev[0], prev[0].nodeValue.length); - } else if (!isStart) { - range.setEnd(range.endContainer, range.endOffset - 1); - } - } - dummyParent.removeChild(dummy); - } - return this; - }, - remove : function(map) { - var self = this, doc = self.doc, range = self.range; - range.enlarge(); - if (range.startOffset === 0) { - var ksc = K(range.startContainer), parent; - while ((parent = ksc.parent()) && parent.isStyle() && parent.children().length == 1) { - ksc = parent; - } - range.setStart(ksc[0], 0); - ksc = K(range.startContainer); - if (ksc.isBlock()) { - _removeAttrOrCss(ksc, map); - } - var kscp = ksc.parent(); - if (kscp && kscp.isBlock()) { - _removeAttrOrCss(kscp, map); - } - } - var sc, so; - if (range.collapsed) { - self.split(true, map); - sc = range.startContainer; - so = range.startOffset; - if (so > 0) { - var sb = K(sc.childNodes[so - 1]); - if (sb && _isEmptyNode(sb)) { - sb.remove(); - range.setStart(sc, so - 1); - } - } - var sa = K(sc.childNodes[so]); - if (sa && _isEmptyNode(sa)) { - sa.remove(); - } - if (_isEmptyNode(sc)) { - range.startBefore(sc); - sc.remove(); - } - range.collapse(true); - return self; - } - self.split(true, map); - self.split(false, map); - var startDummy = doc.createElement('span'), endDummy = doc.createElement('span'); - range.cloneRange().collapse(false).insertNode(endDummy); - range.cloneRange().collapse(true).insertNode(startDummy); - var nodeList = [], cmpStart = false; - K(range.commonAncestor()).scan(function(node) { - if (!cmpStart && node == startDummy) { - cmpStart = true; - return; - } - if (node == endDummy) { - return false; - } - if (cmpStart) { - nodeList.push(node); - } - }); - K(startDummy).remove(); - K(endDummy).remove(); - sc = range.startContainer; - so = range.startOffset; - var ec = range.endContainer, eo = range.endOffset; - if (so > 0) { - var startBefore = K(sc.childNodes[so - 1]); - if (startBefore && _isEmptyNode(startBefore)) { - startBefore.remove(); - range.setStart(sc, so - 1); - if (sc == ec) { - range.setEnd(ec, eo - 1); - } - } - var startAfter = K(sc.childNodes[so]); - if (startAfter && _isEmptyNode(startAfter)) { - startAfter.remove(); - if (sc == ec) { - range.setEnd(ec, eo - 1); - } - } - } - var endAfter = K(ec.childNodes[range.endOffset]); - if (endAfter && _isEmptyNode(endAfter)) { - endAfter.remove(); - } - var bookmark = range.createBookmark(true); - _each(nodeList, function(i, node) { - _removeAttrOrCss(K(node), map); - }); - range.moveToBookmark(bookmark); - return self; - }, - commonNode : function(map) { - var range = this.range; - var ec = range.endContainer, eo = range.endOffset, - node = (ec.nodeType == 3 || eo === 0) ? ec : ec.childNodes[eo - 1]; - function find(node) { - var child = node, parent = node; - while (parent) { - if (_hasAttrOrCss(K(parent), map)) { - return K(parent); - } - parent = parent.parentNode; - } - while (child && (child = child.lastChild)) { - if (_hasAttrOrCss(K(child), map)) { - return K(child); - } - } - return null; - } - var cNode = find(node); - if (cNode) { - return cNode; - } - if (node.nodeType == 1 || (ec.nodeType == 3 && eo === 0)) { - var prev = K(node).prev(); - if (prev) { - return find(prev); - } - } - return null; - }, - commonAncestor : function(tagName) { - var range = this.range, - sc = range.startContainer, so = range.startOffset, - ec = range.endContainer, eo = range.endOffset, - startNode = (sc.nodeType == 3 || so === 0) ? sc : sc.childNodes[so - 1], - endNode = (ec.nodeType == 3 || eo === 0) ? ec : ec.childNodes[eo - 1]; - function find(node) { - while (node) { - if (node.nodeType == 1) { - if (node.tagName.toLowerCase() === tagName) { - return node; - } - } - node = node.parentNode; - } - return null; - } - var start = find(startNode), end = find(endNode); - if (start && end && start === end) { - return K(start); - } - return null; - }, - state : function(key) { - var self = this, doc = self.doc, bool = false; - try { - bool = doc.queryCommandState(key); - } catch (e) {} - return bool; - }, - val : function(key) { - var self = this, doc = self.doc, range = self.range; - function lc(val) { - return val.toLowerCase(); - } - key = lc(key); - var val = '', knode; - if (key === 'fontfamily' || key === 'fontname') { - val = _nativeCommandValue(doc, 'fontname'); - val = val.replace(/['"]/g, ''); - return lc(val); - } - if (key === 'formatblock') { - val = _nativeCommandValue(doc, key); - if (val === '') { - knode = self.commonNode({'h1,h2,h3,h4,h5,h6,p,div,pre,address' : '*'}); - if (knode) { - val = knode.name; - } - } - if (val === 'Normal') { - val = 'p'; - } - return lc(val); - } - if (key === 'fontsize') { - knode = self.commonNode({'*' : '.font-size'}); - if (knode) { - val = knode.css('font-size'); - } - return lc(val); - } - if (key === 'forecolor') { - knode = self.commonNode({'*' : '.color'}); - if (knode) { - val = knode.css('color'); - } - val = _toHex(val); - if (val === '') { - val = 'default'; - } - return lc(val); - } - if (key === 'hilitecolor') { - knode = self.commonNode({'*' : '.background-color'}); - if (knode) { - val = knode.css('background-color'); - } - val = _toHex(val); - if (val === '') { - val = 'default'; - } - return lc(val); - } - return val; - }, - toggle : function(wrapper, map) { - var self = this; - if (self.commonNode(map)) { - self.remove(map); - } else { - self.wrap(wrapper); - } - return self.select(); - }, - bold : function() { - return this.toggle('', { - span : '.font-weight=bold', - strong : '*', - b : '*' - }); - }, - italic : function() { - return this.toggle('', { - span : '.font-style=italic', - em : '*', - i : '*' - }); - }, - underline : function() { - return this.toggle('', { - span : '.text-decoration=underline', - u : '*' - }); - }, - strikethrough : function() { - return this.toggle('', { - span : '.text-decoration=line-through', - s : '*' - }); - }, - forecolor : function(val) { - return this.wrap('').select(); - }, - hilitecolor : function(val) { - return this.wrap('').select(); - }, - fontsize : function(val) { - return this.wrap('').select(); - }, - fontname : function(val) { - return this.fontfamily(val); - }, - fontfamily : function(val) { - return this.wrap('').select(); - }, - removeformat : function() { - var map = { - '*' : '.font-weight,.font-style,.text-decoration,.color,.background-color,.font-size,.font-family,.text-indent' - }, - tags = _STYLE_TAG_MAP; - _each(tags, function(key, val) { - map[key] = '*'; - }); - this.remove(map); - return this.select(); - }, - inserthtml : function(val, quickMode) { - var self = this, range = self.range; - if (val === '') { - return self; - } - function pasteHtml(range, val) { - val = '' + val; - var rng = range.get(); - if (rng.item) { - rng.item(0).outerHTML = val; - } else { - rng.pasteHTML(val); - } - var temp = range.doc.getElementById('__kindeditor_temp_tag__'); - temp.parentNode.removeChild(temp); - var newRange = _toRange(rng); - range.setEnd(newRange.endContainer, newRange.endOffset); - range.collapse(false); - self.select(false); - } - function insertHtml(range, val) { - var doc = range.doc, - frag = doc.createDocumentFragment(); - K('@' + val, doc).each(function() { - frag.appendChild(this); - }); - range.deleteContents(); - range.insertNode(frag); - range.collapse(false); - self.select(false); - } - if (_IERANGE && quickMode) { - try { - pasteHtml(range, val); - } catch(e) { - insertHtml(range, val); - } - return self; - } - insertHtml(range, val); - return self; - }, - hr : function() { - return this.inserthtml('
    '); - }, - print : function() { - this.win.print(); - return this; - }, - insertimage : function(url, title, width, height, border, align) { - title = _undef(title, ''); - border = _undef(border, 0); - var html = ''; - return self.inserthtml(html); - } - if (range.isControl()) { - var node = K(range.startContainer.childNodes[range.startOffset]); - html += '>'; - node.after(K(html, doc)); - node.next().append(node); - range.selectNode(node[0]); - return self.select(); - } - function setAttr(node, url, type) { - K(node).attr('href', url).attr('data-ke-src', url); - if (type) { - K(node).attr('target', type); - } else { - K(node).removeAttr('target'); - } - } - var sc = range.startContainer, so = range.startOffset, - ec = range.endContainer, eo = range.endOffset; - if (sc.nodeType == 1 && sc === ec && so + 1 === eo) { - var child = sc.childNodes[so]; - if (child.nodeName.toLowerCase() == 'a') { - setAttr(child, url, type); - return self; - } - } - _nativeCommand(doc, 'createlink', '__kindeditor_temp_url__'); - K('a[href="__kindeditor_temp_url__"]', doc).each(function() { - setAttr(this, url, type); - }); - return self; - }, - unlink : function() { - var self = this, doc = self.doc, range = self.range; - self.select(); - if (range.collapsed) { - var a = self.commonNode({ a : '*' }); - if (a) { - range.selectNode(a.get()); - self.select(); - } - _nativeCommand(doc, 'unlink', null); - if (_WEBKIT && K(range.startContainer).name === 'img') { - var parent = K(range.startContainer).parent(); - if (parent.name === 'a') { - parent.remove(true); - } - } - } else { - _nativeCommand(doc, 'unlink', null); - } - return self; - } -}); -_each(('formatblock,selectall,justifyleft,justifycenter,justifyright,justifyfull,insertorderedlist,' + - 'insertunorderedlist,indent,outdent,subscript,superscript').split(','), function(i, name) { - KCmd.prototype[name] = function(val) { - var self = this; - self.select(); - _nativeCommand(self.doc, name, val); - if (_IERANGE && _inArray(name, 'justifyleft,justifycenter,justifyright,justifyfull'.split(',')) >= 0) { - self.selection(); - } - if (!_IERANGE || _inArray(name, 'formatblock,selectall,insertorderedlist,insertunorderedlist'.split(',')) >= 0) { - self.selection(); - } - return self; - }; -}); -_each('cut,copy,paste'.split(','), function(i, name) { - KCmd.prototype[name] = function() { - var self = this; - if (!self.doc.queryCommandSupported(name)) { - throw 'not supported'; - } - self.select(); - _nativeCommand(self.doc, name, null); - return self; - }; -}); -function _cmd(mixed) { - if (mixed.nodeName) { - var doc = _getDoc(mixed); - mixed = _range(doc).selectNodeContents(doc.body).collapse(false); - } - return new KCmd(mixed); -} -K.CmdClass = KCmd; -K.cmd = _cmd; -function _drag(options) { - var moveEl = options.moveEl, - moveFn = options.moveFn, - clickEl = options.clickEl || moveEl, - beforeDrag = options.beforeDrag, - iframeFix = options.iframeFix === undefined ? true : options.iframeFix; - var docs = [document]; - if (iframeFix) { - K('iframe').each(function() { - var src = _formatUrl(this.src || '', 'absolute'); - if (/^https?:\/\//.test(src)) { - return; - } - var doc; - try { - doc = _iframeDoc(this); - } catch(e) {} - if (doc) { - var pos = K(this).pos(); - K(doc).data('pos-x', pos.x); - K(doc).data('pos-y', pos.y); - docs.push(doc); - } - }); - } - clickEl.mousedown(function(e) { - e.stopPropagation(); - var self = clickEl.get(), - x = _removeUnit(moveEl.css('left')), - y = _removeUnit(moveEl.css('top')), - width = moveEl.width(), - height = moveEl.height(), - pageX = e.pageX, - pageY = e.pageY; - if (beforeDrag) { - beforeDrag(); - } - function moveListener(e) { - e.preventDefault(); - var kdoc = K(_getDoc(e.target)); - var diffX = _round((kdoc.data('pos-x') || 0) + e.pageX - pageX); - var diffY = _round((kdoc.data('pos-y') || 0) + e.pageY - pageY); - moveFn.call(clickEl, x, y, width, height, diffX, diffY); - } - function selectListener(e) { - e.preventDefault(); - } - function upListener(e) { - e.preventDefault(); - K(docs).unbind('mousemove', moveListener) - .unbind('mouseup', upListener) - .unbind('selectstart', selectListener); - if (self.releaseCapture) { - self.releaseCapture(); - } - } - K(docs).mousemove(moveListener) - .mouseup(upListener) - .bind('selectstart', selectListener); - if (self.setCapture) { - self.setCapture(); - } - }); -} -function KWidget(options) { - this.init(options); -} -_extend(KWidget, { - init : function(options) { - var self = this; - self.name = options.name || ''; - self.doc = options.doc || document; - self.win = _getWin(self.doc); - self.x = _addUnit(options.x); - self.y = _addUnit(options.y); - self.z = options.z; - self.width = _addUnit(options.width); - self.height = _addUnit(options.height); - self.div = K('
    '); - self.options = options; - self._alignEl = options.alignEl; - if (self.width) { - self.div.css('width', self.width); - } - if (self.height) { - self.div.css('height', self.height); - } - if (self.z) { - self.div.css({ - position : 'absolute', - left : self.x, - top : self.y, - 'z-index' : self.z - }); - } - if (self.z && (self.x === undefined || self.y === undefined)) { - self.autoPos(self.width, self.height); - } - if (options.cls) { - self.div.addClass(options.cls); - } - if (options.shadowMode) { - self.div.addClass('ke-shadow'); - } - if (options.css) { - self.div.css(options.css); - } - if (options.src) { - K(options.src).replaceWith(self.div); - } else { - K(self.doc.body).append(self.div); - } - if (options.html) { - self.div.html(options.html); - } - if (options.autoScroll) { - if (_IE && _V < 7 || _QUIRKS) { - var scrollPos = _getScrollPos(); - K(self.win).bind('scroll', function(e) { - var pos = _getScrollPos(), - diffX = pos.x - scrollPos.x, - diffY = pos.y - scrollPos.y; - self.pos(_removeUnit(self.x) + diffX, _removeUnit(self.y) + diffY, false); - }); - } else { - self.div.css('position', 'fixed'); - } - } - }, - pos : function(x, y, updateProp) { - var self = this; - updateProp = _undef(updateProp, true); - if (x !== null) { - x = x < 0 ? 0 : _addUnit(x); - self.div.css('left', x); - if (updateProp) { - self.x = x; - } - } - if (y !== null) { - y = y < 0 ? 0 : _addUnit(y); - self.div.css('top', y); - if (updateProp) { - self.y = y; - } - } - return self; - }, - autoPos : function(width, height) { - var self = this, - w = _removeUnit(width) || 0, - h = _removeUnit(height) || 0, - scrollPos = _getScrollPos(); - if (self._alignEl) { - var knode = K(self._alignEl), - pos = knode.pos(), - diffX = _round(knode[0].clientWidth / 2 - w / 2), - diffY = _round(knode[0].clientHeight / 2 - h / 2); - x = diffX < 0 ? pos.x : pos.x + diffX; - y = diffY < 0 ? pos.y : pos.y + diffY; - } else { - var docEl = _docElement(self.doc); - x = _round(scrollPos.x + (docEl.clientWidth - w) / 2); - y = _round(scrollPos.y + (docEl.clientHeight - h) / 2); - } - if (!(_IE && _V < 7 || _QUIRKS)) { - x -= scrollPos.x; - y -= scrollPos.y; - } - return self.pos(x, y); - }, - remove : function() { - var self = this; - if (_IE && _V < 7 || _QUIRKS) { - K(self.win).unbind('scroll'); - } - self.div.remove(); - _each(self, function(i) { - self[i] = null; - }); - return this; - }, - show : function() { - this.div.show(); - return this; - }, - hide : function() { - this.div.hide(); - return this; - }, - draggable : function(options) { - var self = this; - options = options || {}; - options.moveEl = self.div; - options.moveFn = function(x, y, width, height, diffX, diffY) { - if ((x = x + diffX) < 0) { - x = 0; - } - if ((y = y + diffY) < 0) { - y = 0; - } - self.pos(x, y); - }; - _drag(options); - return self; - } -}); -function _widget(options) { - return new KWidget(options); -} -K.WidgetClass = KWidget; -K.widget = _widget; -function _iframeDoc(iframe) { - iframe = _get(iframe); - return iframe.contentDocument || iframe.contentWindow.document; -} -var html, _direction = ''; -if ((html = document.getElementsByTagName('html'))) { - _direction = html[0].dir; -} -function _getInitHtml(themesPath, bodyClass, cssPath, cssData) { - var arr = [ - (_direction === '' ? '' : ''), - '', - '' - ]; - if (!_isArray(cssPath)) { - cssPath = [cssPath]; - } - _each(cssPath, function(i, path) { - if (path) { - arr.push(''); - } - }); - if (cssData) { - arr.push(''); - } - arr.push(''); - return arr.join('\n'); -} -function _elementVal(knode, val) { - if (knode.hasVal()) { - if (val === undefined) { - var html = knode.val(); - html = html.replace(/(<(?:p|p\s[^>]*)>) *(<\/p>)/ig, ''); - return html; - } - return knode.val(val); - } - return knode.html(val); -} -function KEdit(options) { - this.init(options); -} -_extend(KEdit, KWidget, { - init : function(options) { - var self = this; - KEdit.parent.init.call(self, options); - self.srcElement = K(options.srcElement); - self.div.addClass('ke-edit'); - self.designMode = _undef(options.designMode, true); - self.beforeGetHtml = options.beforeGetHtml; - self.beforeSetHtml = options.beforeSetHtml; - self.afterSetHtml = options.afterSetHtml; - var themesPath = _undef(options.themesPath, ''), - bodyClass = options.bodyClass, - cssPath = options.cssPath, - cssData = options.cssData, - isDocumentDomain = location.protocol != 'res:' && location.host.replace(/:\d+/, '') !== document.domain, - srcScript = ('document.open();' + - (isDocumentDomain ? 'document.domain="' + document.domain + '";' : '') + - 'document.close();'), - iframeSrc = _IE ? ' src="javascript:void(function(){' + encodeURIComponent(srcScript) + '}())"' : ''; - self.iframe = K('').css('width', '100%'); - self.textarea = K('').css('width', '100%'); - self.tabIndex = isNaN(parseInt(options.tabIndex, 10)) ? self.srcElement.attr('tabindex') : parseInt(options.tabIndex, 10); - self.iframe.attr('tabindex', self.tabIndex); - self.textarea.attr('tabindex', self.tabIndex); - if (self.width) { - self.setWidth(self.width); - } - if (self.height) { - self.setHeight(self.height); - } - if (self.designMode) { - self.textarea.hide(); - } else { - self.iframe.hide(); - } - function ready() { - var doc = _iframeDoc(self.iframe); - doc.open(); - if (isDocumentDomain) { - doc.domain = document.domain; - } - doc.write(_getInitHtml(themesPath, bodyClass, cssPath, cssData)); - doc.close(); - self.win = self.iframe[0].contentWindow; - self.doc = doc; - var cmd = _cmd(doc); - self.afterChange(function(e) { - cmd.selection(); - }); - if (_WEBKIT) { - K(doc).click(function(e) { - if (K(e.target).name === 'img') { - cmd.selection(true); - cmd.range.selectNode(e.target); - cmd.select(); - } - }); - } - if (_IE) { - self._mousedownHandler = function() { - var newRange = cmd.range.cloneRange(); - newRange.shrink(); - if (newRange.isControl()) { - self.blur(); - } - }; - K(document).mousedown(self._mousedownHandler); - K(doc).keydown(function(e) { - if (e.which == 8) { - cmd.selection(); - var rng = cmd.range; - if (rng.isControl()) { - rng.collapse(true); - K(rng.startContainer.childNodes[rng.startOffset]).remove(); - e.preventDefault(); - } - } - }); - } - self.cmd = cmd; - self.html(_elementVal(self.srcElement)); - if (_IE) { - doc.body.disabled = true; - doc.body.contentEditable = true; - doc.body.removeAttribute('disabled'); - } else { - doc.designMode = 'on'; - } - if (options.afterCreate) { - options.afterCreate.call(self); - } - } - if (isDocumentDomain) { - self.iframe.bind('load', function(e) { - self.iframe.unbind('load'); - if (_IE) { - ready(); - } else { - setTimeout(ready, 0); - } - }); - } - self.div.append(self.iframe); - self.div.append(self.textarea); - self.srcElement.hide(); - !isDocumentDomain && ready(); - }, - setWidth : function(val) { - var self = this; - val = _addUnit(val); - self.width = val; - self.div.css('width', val); - return self; - }, - setHeight : function(val) { - var self = this; - val = _addUnit(val); - self.height = val; - self.div.css('height', val); - self.iframe.css('height', val); - if ((_IE && _V < 8) || _QUIRKS) { - val = _addUnit(_removeUnit(val) - 2); - } - self.textarea.css('height', val); - return self; - }, - remove : function() { - var self = this, doc = self.doc; - K(doc.body).unbind(); - K(doc).unbind(); - K(self.win).unbind(); - if (self._mousedownHandler) { - K(document).unbind('mousedown', self._mousedownHandler); - } - _elementVal(self.srcElement, self.html()); - self.srcElement.show(); - doc.write(''); - self.iframe.unbind(); - self.textarea.unbind(); - KEdit.parent.remove.call(self); - }, - html : function(val, isFull) { - var self = this, doc = self.doc; - if (self.designMode) { - var body = doc.body; - if (val === undefined) { - if (isFull) { - val = '' + body.parentNode.innerHTML + ''; - } else { - val = body.innerHTML; - } - if (self.beforeGetHtml) { - val = self.beforeGetHtml(val); - } - if (_GECKO && val == '
    ') { - val = ''; - } - return val; - } - if (self.beforeSetHtml) { - val = self.beforeSetHtml(val); - } - if (_IE && _V >= 9) { - val = val.replace(/(<.*?checked=")checked(".*>)/ig, '$1$2'); - } - K(body).html(val); - if (self.afterSetHtml) { - self.afterSetHtml(); - } - return self; - } - if (val === undefined) { - return self.textarea.val(); - } - self.textarea.val(val); - return self; - }, - design : function(bool) { - var self = this, val; - if (bool === undefined ? !self.designMode : bool) { - if (!self.designMode) { - val = self.html(); - self.designMode = true; - self.html(val); - self.textarea.hide(); - self.iframe.show(); - } - } else { - if (self.designMode) { - val = self.html(); - self.designMode = false; - self.html(val); - self.iframe.hide(); - self.textarea.show(); - } - } - return self.focus(); - }, - focus : function() { - var self = this; - self.designMode ? self.win.focus() : self.textarea[0].focus(); - return self; - }, - blur : function() { - var self = this; - if (_IE) { - var input = K('', self.div); - self.div.append(input); - input[0].focus(); - input.remove(); - } else { - self.designMode ? self.win.blur() : self.textarea[0].blur(); - } - return self; - }, - afterChange : function(fn) { - var self = this, doc = self.doc, body = doc.body; - K(doc).keyup(function(e) { - if (!e.ctrlKey && !e.altKey && _CHANGE_KEY_MAP[e.which]) { - fn(e); - } - }); - K(doc).mouseup(fn).contextmenu(fn); - K(self.win).blur(fn); - function timeoutHandler(e) { - setTimeout(function() { - fn(e); - }, 1); - } - K(body).bind('paste', timeoutHandler); - K(body).bind('cut', timeoutHandler); - return self; - } -}); -function _edit(options) { - return new KEdit(options); -} -K.EditClass = KEdit; -K.edit = _edit; -K.iframeDoc = _iframeDoc; -function _selectToolbar(name, fn) { - var self = this, - knode = self.get(name); - if (knode) { - if (knode.hasClass('ke-disabled')) { - return; - } - fn(knode); - } -} -function KToolbar(options) { - this.init(options); -} -_extend(KToolbar, KWidget, { - init : function(options) { - var self = this; - KToolbar.parent.init.call(self, options); - self.disableMode = _undef(options.disableMode, false); - self.noDisableItemMap = _toMap(_undef(options.noDisableItems, [])); - self._itemMap = {}; - self.div.addClass('ke-toolbar').bind('contextmenu,mousedown,mousemove', function(e) { - e.preventDefault(); - }).attr('unselectable', 'on'); - function find(target) { - var knode = K(target); - if (knode.hasClass('ke-outline')) { - return knode; - } - if (knode.hasClass('ke-toolbar-icon')) { - return knode.parent(); - } - } - function hover(e, method) { - var knode = find(e.target); - if (knode) { - if (knode.hasClass('ke-disabled')) { - return; - } - if (knode.hasClass('ke-selected')) { - return; - } - knode[method]('ke-on'); - } - } - self.div.mouseover(function(e) { - hover(e, 'addClass'); - }) - .mouseout(function(e) { - hover(e, 'removeClass'); - }) - .click(function(e) { - var knode = find(e.target); - if (knode) { - if (knode.hasClass('ke-disabled')) { - return; - } - self.options.click.call(this, e, knode.attr('data-name')); - } - }); - }, - get : function(name) { - if (this._itemMap[name]) { - return this._itemMap[name]; - } - return (this._itemMap[name] = K('span.ke-icon-' + name, this.div).parent()); - }, - select : function(name) { - _selectToolbar.call(this, name, function(knode) { - knode.addClass('ke-selected'); - }); - return self; - }, - unselect : function(name) { - _selectToolbar.call(this, name, function(knode) { - knode.removeClass('ke-selected').removeClass('ke-on'); - }); - return self; - }, - enable : function(name) { - var self = this, - knode = name.get ? name : self.get(name); - if (knode) { - knode.removeClass('ke-disabled'); - knode.opacity(1); - } - return self; - }, - disable : function(name) { - var self = this, - knode = name.get ? name : self.get(name); - if (knode) { - knode.removeClass('ke-selected').addClass('ke-disabled'); - knode.opacity(0.5); - } - return self; - }, - disableAll : function(bool, noDisableItems) { - var self = this, map = self.noDisableItemMap, item; - if (noDisableItems) { - map = _toMap(noDisableItems); - } - if (bool === undefined ? !self.disableMode : bool) { - K('span.ke-outline', self.div).each(function() { - var knode = K(this), - name = knode[0].getAttribute('data-name', 2); - if (!map[name]) { - self.disable(knode); - } - }); - self.disableMode = true; - } else { - K('span.ke-outline', self.div).each(function() { - var knode = K(this), - name = knode[0].getAttribute('data-name', 2); - if (!map[name]) { - self.enable(knode); - } - }); - self.disableMode = false; - } - return self; - } -}); -function _toolbar(options) { - return new KToolbar(options); -} -K.ToolbarClass = KToolbar; -K.toolbar = _toolbar; -function KMenu(options) { - this.init(options); -} -_extend(KMenu, KWidget, { - init : function(options) { - var self = this; - options.z = options.z || 811213; - KMenu.parent.init.call(self, options); - self.centerLineMode = _undef(options.centerLineMode, true); - self.div.addClass('ke-menu').bind('click,mousedown', function(e){ - e.stopPropagation(); - }).attr('unselectable', 'on'); - }, - addItem : function(item) { - var self = this; - if (item.title === '-') { - self.div.append(K('
    ')); - return; - } - var itemDiv = K('
    '), - leftDiv = K('
    '), - rightDiv = K('
    '), - height = _addUnit(item.height), - iconClass = _undef(item.iconClass, ''); - self.div.append(itemDiv); - if (height) { - itemDiv.css('height', height); - rightDiv.css('line-height', height); - } - var centerDiv; - if (self.centerLineMode) { - centerDiv = K('
    '); - if (height) { - centerDiv.css('height', height); - } - } - itemDiv.mouseover(function(e) { - K(this).addClass('ke-menu-item-on'); - if (centerDiv) { - centerDiv.addClass('ke-menu-item-center-on'); - } - }) - .mouseout(function(e) { - K(this).removeClass('ke-menu-item-on'); - if (centerDiv) { - centerDiv.removeClass('ke-menu-item-center-on'); - } - }) - .click(function(e) { - item.click.call(K(this)); - e.stopPropagation(); - }) - .append(leftDiv); - if (centerDiv) { - itemDiv.append(centerDiv); - } - itemDiv.append(rightDiv); - if (item.checked) { - iconClass = 'ke-icon-checked'; - } - if (iconClass !== '') { - leftDiv.html(''); - } - rightDiv.html(item.title); - return self; - }, - remove : function() { - var self = this; - if (self.options.beforeRemove) { - self.options.beforeRemove.call(self); - } - K('.ke-menu-item', self.div[0]).unbind(); - KMenu.parent.remove.call(self); - return self; - } -}); -function _menu(options) { - return new KMenu(options); -} -K.MenuClass = KMenu; -K.menu = _menu; -function KColorPicker(options) { - this.init(options); -} -_extend(KColorPicker, KWidget, { - init : function(options) { - var self = this; - options.z = options.z || 811213; - KColorPicker.parent.init.call(self, options); - var colors = options.colors || [ - ['#E53333', '#E56600', '#FF9900', '#64451D', '#DFC5A4', '#FFE500'], - ['#009900', '#006600', '#99BB00', '#B8D100', '#60D978', '#00D5FF'], - ['#337FE5', '#003399', '#4C33E5', '#9933E5', '#CC33E5', '#EE33EE'], - ['#FFFFFF', '#CCCCCC', '#999999', '#666666', '#333333', '#000000'] - ]; - self.selectedColor = (options.selectedColor || '').toLowerCase(); - self._cells = []; - self.div.addClass('ke-colorpicker').bind('click,mousedown', function(e){ - e.stopPropagation(); - }).attr('unselectable', 'on'); - var table = self.doc.createElement('table'); - self.div.append(table); - table.className = 'ke-colorpicker-table'; - table.cellPadding = 0; - table.cellSpacing = 0; - table.border = 0; - var row = table.insertRow(0), cell = row.insertCell(0); - cell.colSpan = colors[0].length; - self._addAttr(cell, '', 'ke-colorpicker-cell-top'); - for (var i = 0; i < colors.length; i++) { - row = table.insertRow(i + 1); - for (var j = 0; j < colors[i].length; j++) { - cell = row.insertCell(j); - self._addAttr(cell, colors[i][j], 'ke-colorpicker-cell'); - } - } - }, - _addAttr : function(cell, color, cls) { - var self = this; - cell = K(cell).addClass(cls); - if (self.selectedColor === color.toLowerCase()) { - cell.addClass('ke-colorpicker-cell-selected'); - } - cell.attr('title', color || self.options.noColor); - cell.mouseover(function(e) { - K(this).addClass('ke-colorpicker-cell-on'); - }); - cell.mouseout(function(e) { - K(this).removeClass('ke-colorpicker-cell-on'); - }); - cell.click(function(e) { - e.stop(); - self.options.click.call(K(this), color); - }); - if (color) { - cell.append(K('
    ').css('background-color', color)); - } else { - cell.html(self.options.noColor); - } - K(cell).attr('unselectable', 'on'); - self._cells.push(cell); - }, - remove : function() { - var self = this; - _each(self._cells, function() { - this.unbind(); - }); - KColorPicker.parent.remove.call(self); - return self; - } -}); -function _colorpicker(options) { - return new KColorPicker(options); -} -K.ColorPickerClass = KColorPicker; -K.colorpicker = _colorpicker; -function KUploadButton(options) { - this.init(options); -} -_extend(KUploadButton, { - init : function(options) { - var self = this, - button = K(options.button), - fieldName = options.fieldName || 'file', - url = options.url || '', - title = button.val(), - extraParams = options.extraParams || {}, - cls = button[0].className || '', - target = options.target || 'kindeditor_upload_iframe_' + new Date().getTime(); - options.afterError = options.afterError || function(str) { - alert(str); - }; - var hiddenElements = []; - for(var k in extraParams){ - hiddenElements.push(''); - } - var html = [ - '
    ', - (options.target ? '' : ''), - (options.form ? '
    ' : '
    '), - '', - hiddenElements.join(''), - '', - '', - '', - (options.form ? '
    ' : ''), - '
    '].join(''); - var div = K(html, button.doc); - button.hide(); - button.before(div); - self.div = div; - self.button = button; - self.iframe = options.target ? K('iframe[name="' + target + '"]') : K('iframe', div); - self.form = options.form ? K(options.form) : K('form', div); - self.fileBox = K('.ke-upload-file', div); - var width = options.width || K('.ke-button-common', div).width(); - K('.ke-upload-area', div).width(width); - self.options = options; - }, - submit : function() { - var self = this, - iframe = self.iframe; - iframe.bind('load', function() { - iframe.unbind(); - var tempForm = document.createElement('form'); - self.fileBox.before(tempForm); - K(tempForm).append(self.fileBox); - tempForm.reset(); - K(tempForm).remove(true); - var doc = K.iframeDoc(iframe), - pre = doc.getElementsByTagName('pre')[0], - str = '', data; - if (pre) { - str = pre.innerHTML; - } else { - str = doc.body.innerHTML; - } - str = _unescape(str); - iframe[0].src = 'javascript:false'; - try { - data = K.json(str); - } catch (e) { - self.options.afterError.call(self, '' + doc.body.parentNode.innerHTML + ''); - } - if (data) { - self.options.afterUpload.call(self, data); - } - }); - self.form[0].submit(); - return self; - }, - remove : function() { - var self = this; - if (self.fileBox) { - self.fileBox.unbind(); - } - self.iframe.remove(); - self.div.remove(); - self.button.show(); - return self; - } -}); -function _uploadbutton(options) { - return new KUploadButton(options); -} -K.UploadButtonClass = KUploadButton; -K.uploadbutton = _uploadbutton; -function _createButton(arg) { - arg = arg || {}; - var name = arg.name || '', - span = K(''), - btn = K(''); - if (arg.click) { - btn.click(arg.click); - } - span.append(btn); - return span; -} -function KDialog(options) { - this.init(options); -} -_extend(KDialog, KWidget, { - init : function(options) { - var self = this; - var shadowMode = _undef(options.shadowMode, true); - options.z = options.z || 811213; - options.shadowMode = false; - options.autoScroll = _undef(options.autoScroll, true); - KDialog.parent.init.call(self, options); - var title = options.title, - body = K(options.body, self.doc), - previewBtn = options.previewBtn, - yesBtn = options.yesBtn, - noBtn = options.noBtn, - closeBtn = options.closeBtn, - showMask = _undef(options.showMask, true); - self.div.addClass('ke-dialog').bind('click,mousedown', function(e){ - e.stopPropagation(); - }); - var contentDiv = K('
    ').appendTo(self.div); - if (_IE && _V < 7) { - self.iframeMask = K('').appendTo(self.div); - } else if (shadowMode) { - K('
    ').appendTo(self.div); - } - var headerDiv = K('
    '); - contentDiv.append(headerDiv); - headerDiv.html(title); - self.closeIcon = K('').click(closeBtn.click); - headerDiv.append(self.closeIcon); - self.draggable({ - clickEl : headerDiv, - beforeDrag : options.beforeDrag - }); - var bodyDiv = K('
    '); - contentDiv.append(bodyDiv); - bodyDiv.append(body); - var footerDiv = K(''); - if (previewBtn || yesBtn || noBtn) { - contentDiv.append(footerDiv); - } - _each([ - { btn : previewBtn, name : 'preview' }, - { btn : yesBtn, name : 'yes' }, - { btn : noBtn, name : 'no' } - ], function() { - if (this.btn) { - var button = _createButton(this.btn); - button.addClass('ke-dialog-' + this.name); - footerDiv.append(button); - } - }); - if (self.height) { - bodyDiv.height(_removeUnit(self.height) - headerDiv.height() - footerDiv.height()); - } - self.div.width(self.div.width()); - self.div.height(self.div.height()); - self.mask = null; - if (showMask) { - var docEl = _docElement(self.doc), - docWidth = Math.max(docEl.scrollWidth, docEl.clientWidth), - docHeight = Math.max(docEl.scrollHeight, docEl.clientHeight); - self.mask = _widget({ - x : 0, - y : 0, - z : self.z - 1, - cls : 'ke-dialog-mask', - width : docWidth, - height : docHeight - }); - } - self.autoPos(self.div.width(), self.div.height()); - self.footerDiv = footerDiv; - self.bodyDiv = bodyDiv; - self.headerDiv = headerDiv; - self.isLoading = false; - }, - setMaskIndex : function(z) { - var self = this; - self.mask.div.css('z-index', z); - }, - showLoading : function(msg) { - msg = _undef(msg, ''); - var self = this, body = self.bodyDiv; - self.loading = K('
    ' + msg + '
    ') - .width(body.width()).height(body.height()) - .css('top', self.headerDiv.height() + 'px'); - body.css('visibility', 'hidden').after(self.loading); - self.isLoading = true; - return self; - }, - hideLoading : function() { - this.loading && this.loading.remove(); - this.bodyDiv.css('visibility', 'visible'); - this.isLoading = false; - return this; - }, - remove : function() { - var self = this; - if (self.options.beforeRemove) { - self.options.beforeRemove.call(self); - } - self.mask && self.mask.remove(); - self.iframeMask && self.iframeMask.remove(); - self.closeIcon.unbind(); - K('input', self.div).unbind(); - K('button', self.div).unbind(); - self.footerDiv.unbind(); - self.bodyDiv.unbind(); - self.headerDiv.unbind(); - K('iframe', self.div).each(function() { - K(this).remove(); - }); - KDialog.parent.remove.call(self); - return self; - } -}); -function _dialog(options) { - return new KDialog(options); -} -K.DialogClass = KDialog; -K.dialog = _dialog; -function _tabs(options) { - var self = _widget(options), - remove = self.remove, - afterSelect = options.afterSelect, - div = self.div, - liList = []; - div.addClass('ke-tabs') - .bind('contextmenu,mousedown,mousemove', function(e) { - e.preventDefault(); - }); - var ul = K('
      '); - div.append(ul); - self.add = function(tab) { - var li = K('
    • ' + tab.title + '
    • '); - li.data('tab', tab); - liList.push(li); - ul.append(li); - }; - self.selectedIndex = 0; - self.select = function(index) { - self.selectedIndex = index; - _each(liList, function(i, li) { - li.unbind(); - if (i === index) { - li.addClass('ke-tabs-li-selected'); - K(li.data('tab').panel).show(''); - } else { - li.removeClass('ke-tabs-li-selected').removeClass('ke-tabs-li-on') - .mouseover(function() { - K(this).addClass('ke-tabs-li-on'); - }) - .mouseout(function() { - K(this).removeClass('ke-tabs-li-on'); - }) - .click(function() { - self.select(i); - }); - K(li.data('tab').panel).hide(); - } - }); - if (afterSelect) { - afterSelect.call(self, index); - } - }; - self.remove = function() { - _each(liList, function() { - this.remove(); - }); - ul.remove(); - remove.call(self); - }; - return self; -} -K.tabs = _tabs; -function _loadScript(url, fn) { - var head = document.getElementsByTagName('head')[0] || (_QUIRKS ? document.body : document.documentElement), - script = document.createElement('script'); - head.appendChild(script); - script.src = url; - script.charset = 'utf-8'; - script.onload = script.onreadystatechange = function() { - if (!this.readyState || this.readyState === 'loaded') { - if (fn) { - fn(); - } - script.onload = script.onreadystatechange = null; - head.removeChild(script); - } - }; -} -function _chopQuery(url) { - var index = url.indexOf('?'); - return index > 0 ? url.substr(0, index) : url; -} -function _loadStyle(url) { - var head = document.getElementsByTagName('head')[0] || (_QUIRKS ? document.body : document.documentElement), - link = document.createElement('link'), - absoluteUrl = _chopQuery(_formatUrl(url, 'absolute')); - var links = K('link[rel="stylesheet"]', head); - for (var i = 0, len = links.length; i < len; i++) { - if (_chopQuery(_formatUrl(links[i].href, 'absolute')) === absoluteUrl) { - return; - } - } - head.appendChild(link); - link.href = url; - link.rel = 'stylesheet'; -} -function _ajax(url, fn, method, param, dataType) { - method = method || 'GET'; - dataType = dataType || 'json'; - var xhr = window.XMLHttpRequest ? new window.XMLHttpRequest() : new ActiveXObject('Microsoft.XMLHTTP'); - xhr.open(method, url, true); - xhr.onreadystatechange = function () { - if (xhr.readyState == 4 && xhr.status == 200) { - if (fn) { - var data = _trim(xhr.responseText); - if (dataType == 'json') { - data = _json(data); - } - fn(data); - } - } - }; - if (method == 'POST') { - var params = []; - _each(param, function(key, val) { - params.push(encodeURIComponent(key) + '=' + encodeURIComponent(val)); - }); - try { - xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); - } catch (e) {} - xhr.send(params.join('&')); - } else { - xhr.send(null); - } -} -K.loadScript = _loadScript; -K.loadStyle = _loadStyle; -K.ajax = _ajax; -var _plugins = {}; -function _plugin(name, fn) { - if (name === undefined) { - return _plugins; - } - if (!fn) { - return _plugins[name]; - } - _plugins[name] = fn; -} -var _language = {}; -function _parseLangKey(key) { - var match, ns = 'core'; - if ((match = /^(\w+)\.(\w+)$/.exec(key))) { - ns = match[1]; - key = match[2]; - } - return { ns : ns, key : key }; -} -function _lang(mixed, langType) { - langType = langType === undefined ? K.options.langType : langType; - if (typeof mixed === 'string') { - if (!_language[langType]) { - return 'no language'; - } - var pos = mixed.length - 1; - if (mixed.substr(pos) === '.') { - return _language[langType][mixed.substr(0, pos)]; - } - var obj = _parseLangKey(mixed); - return _language[langType][obj.ns][obj.key]; - } - _each(mixed, function(key, val) { - var obj = _parseLangKey(key); - if (!_language[langType]) { - _language[langType] = {}; - } - if (!_language[langType][obj.ns]) { - _language[langType][obj.ns] = {}; - } - _language[langType][obj.ns][obj.key] = val; - }); -} -function _getImageFromRange(range, fn) { - if (range.collapsed) { - return; - } - range = range.cloneRange().up(); - var sc = range.startContainer, so = range.startOffset; - if (!_WEBKIT && !range.isControl()) { - return; - } - var img = K(sc.childNodes[so]); - if (!img || img.name != 'img') { - return; - } - if (fn(img)) { - return img; - } -} -function _bindContextmenuEvent() { - var self = this, doc = self.edit.doc; - K(doc).contextmenu(function(e) { - if (self.menu) { - self.hideMenu(); - } - if (!self.useContextmenu) { - e.preventDefault(); - return; - } - if (self._contextmenus.length === 0) { - return; - } - var maxWidth = 0, items = []; - _each(self._contextmenus, function() { - if (this.title == '-') { - items.push(this); - return; - } - if (this.cond && this.cond()) { - items.push(this); - if (this.width && this.width > maxWidth) { - maxWidth = this.width; - } - } - }); - while (items.length > 0 && items[0].title == '-') { - items.shift(); - } - while (items.length > 0 && items[items.length - 1].title == '-') { - items.pop(); - } - var prevItem = null; - _each(items, function(i) { - if (this.title == '-' && prevItem.title == '-') { - delete items[i]; - } - prevItem = this; - }); - if (items.length > 0) { - e.preventDefault(); - var pos = K(self.edit.iframe).pos(), - menu = _menu({ - x : pos.x + e.clientX, - y : pos.y + e.clientY, - width : maxWidth, - css : { visibility: 'hidden' }, - shadowMode : self.shadowMode - }); - _each(items, function() { - if (this.title) { - menu.addItem(this); - } - }); - var docEl = _docElement(menu.doc), - menuHeight = menu.div.height(); - if (e.clientY + menuHeight >= docEl.clientHeight - 100) { - menu.pos(menu.x, _removeUnit(menu.y) - menuHeight); - } - menu.div.css('visibility', 'visible'); - self.menu = menu; - } - }); -} -function _bindNewlineEvent() { - var self = this, doc = self.edit.doc, newlineTag = self.newlineTag; - if (_IE && newlineTag !== 'br') { - return; - } - if (_GECKO && _V < 3 && newlineTag !== 'p') { - return; - } - if (_OPERA && _V < 9) { - return; - } - var brSkipTagMap = _toMap('h1,h2,h3,h4,h5,h6,pre,li'), - pSkipTagMap = _toMap('p,h1,h2,h3,h4,h5,h6,pre,li,blockquote'); - function getAncestorTagName(range) { - var ancestor = K(range.commonAncestor()); - while (ancestor) { - if (ancestor.type == 1 && !ancestor.isStyle()) { - break; - } - ancestor = ancestor.parent(); - } - return ancestor.name; - } - K(doc).keydown(function(e) { - if (e.which != 13 || e.shiftKey || e.ctrlKey || e.altKey) { - return; - } - self.cmd.selection(); - var tagName = getAncestorTagName(self.cmd.range); - if (tagName == 'marquee' || tagName == 'select') { - return; - } - if (newlineTag === 'br' && !brSkipTagMap[tagName]) { - e.preventDefault(); - self.insertHtml('
      ' + (_IE && _V < 9 ? '' : '\u200B')); - return; - } - if (!pSkipTagMap[tagName]) { - _nativeCommand(doc, 'formatblock', '

      '); - } - }); - K(doc).keyup(function(e) { - if (e.which != 13 || e.shiftKey || e.ctrlKey || e.altKey) { - return; - } - if (newlineTag == 'br') { - return; - } - if (_GECKO) { - var root = self.cmd.commonAncestor('p'); - var a = self.cmd.commonAncestor('a'); - if (a && a.text() == '') { - a.remove(true); - self.cmd.range.selectNodeContents(root[0]).collapse(true); - self.cmd.select(); - } - return; - } - self.cmd.selection(); - var tagName = getAncestorTagName(self.cmd.range); - if (tagName == 'marquee' || tagName == 'select') { - return; - } - if (!pSkipTagMap[tagName]) { - _nativeCommand(doc, 'formatblock', '

      '); - } - var div = self.cmd.commonAncestor('div'); - if (div) { - var p = K('

      '), - child = div[0].firstChild; - while (child) { - var next = child.nextSibling; - p.append(child); - child = next; - } - div.before(p); - div.remove(); - self.cmd.range.selectNodeContents(p[0]); - self.cmd.select(); - } - }); -} -function _bindTabEvent() { - var self = this, doc = self.edit.doc; - K(doc).keydown(function(e) { - if (e.which == 9) { - e.preventDefault(); - if (self.afterTab) { - self.afterTab.call(self, e); - return; - } - var cmd = self.cmd, range = cmd.range; - range.shrink(); - if (range.collapsed && range.startContainer.nodeType == 1) { - range.insertNode(K('@ ', doc)[0]); - cmd.select(); - } - self.insertHtml('    '); - } - }); -} -function _bindFocusEvent() { - var self = this; - K(self.edit.textarea[0], self.edit.win).focus(function(e) { - if (self.afterFocus) { - self.afterFocus.call(self, e); - } - }).blur(function(e) { - if (self.afterBlur) { - self.afterBlur.call(self, e); - } - }); -} -function _removeBookmarkTag(html) { - return _trim(html.replace(/]*id="?__kindeditor_bookmark_\w+_\d+__"?[^>]*><\/span>/ig, '')); -} -function _removeTempTag(html) { - return html.replace(/]+class="?__kindeditor_paste__"?[^>]*>[\s\S]*?<\/div>/ig, ''); -} -function _addBookmarkToStack(stack, bookmark) { - if (stack.length === 0) { - stack.push(bookmark); - return; - } - var prev = stack[stack.length - 1]; - if (_removeBookmarkTag(bookmark.html) !== _removeBookmarkTag(prev.html)) { - stack.push(bookmark); - } -} -function _undoToRedo(fromStack, toStack) { - var self = this, edit = self.edit, - body = edit.doc.body, - range, bookmark; - if (fromStack.length === 0) { - return self; - } - if (edit.designMode) { - range = self.cmd.range; - bookmark = range.createBookmark(true); - bookmark.html = body.innerHTML; - } else { - bookmark = { - html : body.innerHTML - }; - } - _addBookmarkToStack(toStack, bookmark); - var prev = fromStack.pop(); - if (_removeBookmarkTag(bookmark.html) === _removeBookmarkTag(prev.html) && fromStack.length > 0) { - prev = fromStack.pop(); - } - if (edit.designMode) { - edit.html(prev.html); - if (prev.start) { - range.moveToBookmark(prev); - self.select(); - } - } else { - K(body).html(_removeBookmarkTag(prev.html)); - } - return self; -} -function KEditor(options) { - var self = this; - self.options = {}; - function setOption(key, val) { - if (KEditor.prototype[key] === undefined) { - self[key] = val; - } - self.options[key] = val; - } - _each(options, function(key, val) { - setOption(key, options[key]); - }); - _each(K.options, function(key, val) { - if (self[key] === undefined) { - setOption(key, val); - } - }); - var se = K(self.srcElement || '').css('width', '100%'); + self.tabIndex = isNaN(parseInt(options.tabIndex, 10)) ? self.srcElement.attr('tabindex') : parseInt(options.tabIndex, 10); + self.iframe.attr('tabindex', self.tabIndex); + self.textarea.attr('tabindex', self.tabIndex); + if (self.width) { + self.setWidth(self.width); + } + if (self.height) { + self.setHeight(self.height); + } + if (self.designMode) { + self.textarea.hide(); + } else { + self.iframe.hide(); + } + function ready() { + var doc = _iframeDoc(self.iframe); + doc.open(); + if (isDocumentDomain) { + doc.domain = document.domain; + } + doc.write(_getInitHtml(themesPath, bodyClass, cssPath, cssData)); + doc.close(); + self.win = self.iframe[0].contentWindow; + self.doc = doc; + var cmd = _cmd(doc); + self.afterChange(function(e) { + cmd.selection(); + }); + if (_WEBKIT) { + K(doc).click(function(e) { + if (K(e.target).name === 'img') { + cmd.selection(true); + cmd.range.selectNode(e.target); + cmd.select(); + } + }); + } + if (_IE) { + self._mousedownHandler = function() { + var newRange = cmd.range.cloneRange(); + newRange.shrink(); + if (newRange.isControl()) { + self.blur(); + } + }; + K(document).mousedown(self._mousedownHandler); + K(doc).keydown(function(e) { + if (e.which == 8) { + cmd.selection(); + var rng = cmd.range; + if (rng.isControl()) { + rng.collapse(true); + K(rng.startContainer.childNodes[rng.startOffset]).remove(); + e.preventDefault(); + } + } + }); + } + self.cmd = cmd; + self.html(_elementVal(self.srcElement)); + if (_IE) { + doc.body.disabled = true; + doc.body.contentEditable = true; + doc.body.removeAttribute('disabled'); + } else { + doc.designMode = 'on'; + } + if (options.afterCreate) { + options.afterCreate.call(self); + } + } + if (isDocumentDomain) { + self.iframe.bind('load', function(e) { + self.iframe.unbind('load'); + if (_IE) { + ready(); + } else { + setTimeout(ready, 0); + } + }); + } + self.div.append(self.iframe); + self.div.append(self.textarea); + self.srcElement.hide(); + !isDocumentDomain && ready(); + }, + setWidth : function(val) { + var self = this; + val = _addUnit(val); + self.width = val; + self.div.css('width', val); + return self; + }, + setHeight : function(val) { + var self = this; + val = _addUnit(val); + self.height = val; + self.div.css('height', val); + self.iframe.css('height', val); + if ((_IE && _V < 8) || _QUIRKS) { + val = _addUnit(_removeUnit(val) - 2); + } + self.textarea.css('height', val); + return self; + }, + remove : function() { + var self = this, doc = self.doc; + K(doc.body).unbind(); + K(doc).unbind(); + K(self.win).unbind(); + if (self._mousedownHandler) { + K(document).unbind('mousedown', self._mousedownHandler); + } + _elementVal(self.srcElement, self.html()); + self.srcElement.show(); + doc.write(''); + self.iframe.unbind(); + self.textarea.unbind(); + KEdit.parent.remove.call(self); + }, + html : function(val, isFull) { + var self = this, doc = self.doc; + if (self.designMode) { + var body = doc.body; + if (val === undefined) { + if (isFull) { + val = '' + body.parentNode.innerHTML + ''; + } else { + val = body.innerHTML; + } + if (self.beforeGetHtml) { + val = self.beforeGetHtml(val); + } + if (_GECKO && val == '
      ') { + val = ''; + } + return val; + } + if (self.beforeSetHtml) { + val = self.beforeSetHtml(val); + } + if (_IE && _V >= 9) { + val = val.replace(/(<.*?checked=")checked(".*>)/ig, '$1$2'); + } + K(body).html(val); + if (self.afterSetHtml) { + self.afterSetHtml(); + } + return self; + } + if (val === undefined) { + return self.textarea.val(); + } + self.textarea.val(val); + return self; + }, + design : function(bool) { + var self = this, val; + if (bool === undefined ? !self.designMode : bool) { + if (!self.designMode) { + val = self.html(); + self.designMode = true; + self.html(val); + self.textarea.hide(); + self.iframe.show(); + } + } else { + if (self.designMode) { + val = self.html(); + self.designMode = false; + self.html(val); + self.iframe.hide(); + self.textarea.show(); + } + } + return self.focus(); + }, + focus : function() { + var self = this; + self.designMode ? self.win.focus() : self.textarea[0].focus(); + return self; + }, + blur : function() { + var self = this; + if (_IE) { + var input = K('', self.div); + self.div.append(input); + input[0].focus(); + input.remove(); + } else { + self.designMode ? self.win.blur() : self.textarea[0].blur(); + } + return self; + }, + afterChange : function(fn) { + var self = this, doc = self.doc, body = doc.body; + K(doc).keyup(function(e) { + if (!e.ctrlKey && !e.altKey && _CHANGE_KEY_MAP[e.which]) { + fn(e); + } + }); + K(doc).mouseup(fn).contextmenu(fn); + K(self.win).blur(fn); + function timeoutHandler(e) { + setTimeout(function() { + fn(e); + }, 1); + } + K(body).bind('paste', timeoutHandler); + K(body).bind('cut', timeoutHandler); + return self; + } +}); +function _edit(options) { + return new KEdit(options); +} +K.EditClass = KEdit; +K.edit = _edit; +K.iframeDoc = _iframeDoc; +function _selectToolbar(name, fn) { + var self = this, + knode = self.get(name); + if (knode) { + if (knode.hasClass('ke-disabled')) { + return; + } + fn(knode); + } +} +function KToolbar(options) { + this.init(options); +} +_extend(KToolbar, KWidget, { + init : function(options) { + var self = this; + KToolbar.parent.init.call(self, options); + self.disableMode = _undef(options.disableMode, false); + self.noDisableItemMap = _toMap(_undef(options.noDisableItems, [])); + self._itemMap = {}; + self.div.addClass('ke-toolbar').bind('contextmenu,mousedown,mousemove', function(e) { + e.preventDefault(); + }).attr('unselectable', 'on'); + function find(target) { + var knode = K(target); + if (knode.hasClass('ke-outline')) { + return knode; + } + if (knode.hasClass('ke-toolbar-icon')) { + return knode.parent(); + } + } + function hover(e, method) { + var knode = find(e.target); + if (knode) { + if (knode.hasClass('ke-disabled')) { + return; + } + if (knode.hasClass('ke-selected')) { + return; + } + knode[method]('ke-on'); + } + } + self.div.mouseover(function(e) { + hover(e, 'addClass'); + }) + .mouseout(function(e) { + hover(e, 'removeClass'); + }) + .click(function(e) { + var knode = find(e.target); + if (knode) { + if (knode.hasClass('ke-disabled')) { + return; + } + self.options.click.call(this, e, knode.attr('data-name')); + } + }); + }, + get : function(name) { + if (this._itemMap[name]) { + return this._itemMap[name]; + } + return (this._itemMap[name] = K('span.ke-icon-' + name, this.div).parent()); + }, + select : function(name) { + _selectToolbar.call(this, name, function(knode) { + knode.addClass('ke-selected'); + }); + return self; + }, + unselect : function(name) { + _selectToolbar.call(this, name, function(knode) { + knode.removeClass('ke-selected').removeClass('ke-on'); + }); + return self; + }, + enable : function(name) { + var self = this, + knode = name.get ? name : self.get(name); + if (knode) { + knode.removeClass('ke-disabled'); + knode.opacity(1); + } + return self; + }, + disable : function(name) { + var self = this, + knode = name.get ? name : self.get(name); + if (knode) { + knode.removeClass('ke-selected').addClass('ke-disabled'); + knode.opacity(0.5); + } + return self; + }, + disableAll : function(bool, noDisableItems) { + var self = this, map = self.noDisableItemMap, item; + if (noDisableItems) { + map = _toMap(noDisableItems); + } + if (bool === undefined ? !self.disableMode : bool) { + K('span.ke-outline', self.div).each(function() { + var knode = K(this), + name = knode[0].getAttribute('data-name', 2); + if (!map[name]) { + self.disable(knode); + } + }); + self.disableMode = true; + } else { + K('span.ke-outline', self.div).each(function() { + var knode = K(this), + name = knode[0].getAttribute('data-name', 2); + if (!map[name]) { + self.enable(knode); + } + }); + self.disableMode = false; + } + return self; + } +}); +function _toolbar(options) { + return new KToolbar(options); +} +K.ToolbarClass = KToolbar; +K.toolbar = _toolbar; +function KMenu(options) { + this.init(options); +} +_extend(KMenu, KWidget, { + init : function(options) { + var self = this; + options.z = options.z || 811213; + KMenu.parent.init.call(self, options); + self.centerLineMode = _undef(options.centerLineMode, true); + self.div.addClass('ke-menu').bind('click,mousedown', function(e){ + e.stopPropagation(); + }).attr('unselectable', 'on'); + }, + addItem : function(item) { + var self = this; + if (item.title === '-') { + self.div.append(K('
      ')); + return; + } + var itemDiv = K('
      '), + leftDiv = K('
      '), + rightDiv = K('
      '), + height = _addUnit(item.height), + iconClass = _undef(item.iconClass, ''); + self.div.append(itemDiv); + if (height) { + itemDiv.css('height', height); + rightDiv.css('line-height', height); + } + var centerDiv; + if (self.centerLineMode) { + centerDiv = K('
      '); + if (height) { + centerDiv.css('height', height); + } + } + itemDiv.mouseover(function(e) { + K(this).addClass('ke-menu-item-on'); + if (centerDiv) { + centerDiv.addClass('ke-menu-item-center-on'); + } + }) + .mouseout(function(e) { + K(this).removeClass('ke-menu-item-on'); + if (centerDiv) { + centerDiv.removeClass('ke-menu-item-center-on'); + } + }) + .click(function(e) { + item.click.call(K(this)); + e.stopPropagation(); + }) + .append(leftDiv); + if (centerDiv) { + itemDiv.append(centerDiv); + } + itemDiv.append(rightDiv); + if (item.checked) { + iconClass = 'ke-icon-checked'; + } + if (iconClass !== '') { + leftDiv.html(''); + } + rightDiv.html(item.title); + return self; + }, + remove : function() { + var self = this; + if (self.options.beforeRemove) { + self.options.beforeRemove.call(self); + } + K('.ke-menu-item', self.div[0]).unbind(); + KMenu.parent.remove.call(self); + return self; + } +}); +function _menu(options) { + return new KMenu(options); +} +K.MenuClass = KMenu; +K.menu = _menu; +function KColorPicker(options) { + this.init(options); +} +_extend(KColorPicker, KWidget, { + init : function(options) { + var self = this; + options.z = options.z || 811213; + KColorPicker.parent.init.call(self, options); + var colors = options.colors || [ + ['#E53333', '#E56600', '#FF9900', '#64451D', '#DFC5A4', '#FFE500'], + ['#009900', '#006600', '#99BB00', '#B8D100', '#60D978', '#00D5FF'], + ['#337FE5', '#003399', '#4C33E5', '#9933E5', '#CC33E5', '#EE33EE'], + ['#FFFFFF', '#CCCCCC', '#999999', '#666666', '#333333', '#000000'] + ]; + self.selectedColor = (options.selectedColor || '').toLowerCase(); + self._cells = []; + self.div.addClass('ke-colorpicker').bind('click,mousedown', function(e){ + e.stopPropagation(); + }).attr('unselectable', 'on'); + var table = self.doc.createElement('table'); + self.div.append(table); + table.className = 'ke-colorpicker-table'; + table.cellPadding = 0; + table.cellSpacing = 0; + table.border = 0; + var row = table.insertRow(0), cell = row.insertCell(0); + cell.colSpan = colors[0].length; + self._addAttr(cell, '', 'ke-colorpicker-cell-top'); + for (var i = 0; i < colors.length; i++) { + row = table.insertRow(i + 1); + for (var j = 0; j < colors[i].length; j++) { + cell = row.insertCell(j); + self._addAttr(cell, colors[i][j], 'ke-colorpicker-cell'); + } + } + }, + _addAttr : function(cell, color, cls) { + var self = this; + cell = K(cell).addClass(cls); + if (self.selectedColor === color.toLowerCase()) { + cell.addClass('ke-colorpicker-cell-selected'); + } + cell.attr('title', color || self.options.noColor); + cell.mouseover(function(e) { + K(this).addClass('ke-colorpicker-cell-on'); + }); + cell.mouseout(function(e) { + K(this).removeClass('ke-colorpicker-cell-on'); + }); + cell.click(function(e) { + e.stop(); + self.options.click.call(K(this), color); + }); + if (color) { + cell.append(K('
      ').css('background-color', color)); + } else { + cell.html(self.options.noColor); + } + K(cell).attr('unselectable', 'on'); + self._cells.push(cell); + }, + remove : function() { + var self = this; + _each(self._cells, function() { + this.unbind(); + }); + KColorPicker.parent.remove.call(self); + return self; + } +}); +function _colorpicker(options) { + return new KColorPicker(options); +} +K.ColorPickerClass = KColorPicker; +K.colorpicker = _colorpicker; +function KUploadButton(options) { + this.init(options); +} +_extend(KUploadButton, { + init : function(options) { + var self = this, + button = K(options.button), + fieldName = options.fieldName || 'file', + url = options.url || '', + title = button.val(), + extraParams = options.extraParams || {}, + cls = button[0].className || '', + target = options.target || 'kindeditor_upload_iframe_' + new Date().getTime(); + options.afterError = options.afterError || function(str) { + alert(str); + }; + var hiddenElements = []; + for(var k in extraParams){ + hiddenElements.push(''); + } + var html = [ + '
      ', + (options.target ? '' : ''), + (options.form ? '
      ' : '
      '), + '', + hiddenElements.join(''), + '', + '', + '', + (options.form ? '
      ' : ''), + '
      '].join(''); + var div = K(html, button.doc); + button.hide(); + button.before(div); + self.div = div; + self.button = button; + self.iframe = options.target ? K('iframe[name="' + target + '"]') : K('iframe', div); + self.form = options.form ? K(options.form) : K('form', div); + self.fileBox = K('.ke-upload-file', div); + var width = options.width || K('.ke-button-common', div).width(); + K('.ke-upload-area', div).width(width); + self.options = options; + }, + submit : function() { + var self = this, + iframe = self.iframe; + iframe.bind('load', function() { + iframe.unbind(); + var tempForm = document.createElement('form'); + self.fileBox.before(tempForm); + K(tempForm).append(self.fileBox); + tempForm.reset(); + K(tempForm).remove(true); + var doc = K.iframeDoc(iframe), + pre = doc.getElementsByTagName('pre')[0], + str = '', data; + if (pre) { + str = pre.innerHTML; + } else { + str = doc.body.innerHTML; + } + str = _unescape(str); + iframe[0].src = 'javascript:false'; + try { + data = K.json(str); + } catch (e) { + self.options.afterError.call(self, '' + doc.body.parentNode.innerHTML + ''); + } + if (data) { + self.options.afterUpload.call(self, data); + } + }); + self.form[0].submit(); + return self; + }, + remove : function() { + var self = this; + if (self.fileBox) { + self.fileBox.unbind(); + } + self.iframe.remove(); + self.div.remove(); + self.button.show(); + return self; + } +}); +function _uploadbutton(options) { + return new KUploadButton(options); +} +K.UploadButtonClass = KUploadButton; +K.uploadbutton = _uploadbutton; +function _createButton(arg) { + arg = arg || {}; + var name = arg.name || '', + span = K(''), + btn = K(''); + if (arg.click) { + btn.click(arg.click); + } + span.append(btn); + return span; +} +function KDialog(options) { + this.init(options); +} +_extend(KDialog, KWidget, { + init : function(options) { + var self = this; + var shadowMode = _undef(options.shadowMode, true); + options.z = options.z || 811213; + options.shadowMode = false; + options.autoScroll = _undef(options.autoScroll, true); + KDialog.parent.init.call(self, options); + var title = options.title, + body = K(options.body, self.doc), + previewBtn = options.previewBtn, + yesBtn = options.yesBtn, + noBtn = options.noBtn, + closeBtn = options.closeBtn, + showMask = _undef(options.showMask, true); + self.div.addClass('ke-dialog').bind('click,mousedown', function(e){ + e.stopPropagation(); + }); + var contentDiv = K('
      ').appendTo(self.div); + if (_IE && _V < 7) { + self.iframeMask = K('').appendTo(self.div); + } else if (shadowMode) { + K('
      ').appendTo(self.div); + } + var headerDiv = K('
      '); + contentDiv.append(headerDiv); + headerDiv.html(title); + self.closeIcon = K('').click(closeBtn.click); + headerDiv.append(self.closeIcon); + self.draggable({ + clickEl : headerDiv, + beforeDrag : options.beforeDrag + }); + var bodyDiv = K('
      '); + contentDiv.append(bodyDiv); + bodyDiv.append(body); + var footerDiv = K(''); + if (previewBtn || yesBtn || noBtn) { + contentDiv.append(footerDiv); + } + _each([ + { btn : previewBtn, name : 'preview' }, + { btn : yesBtn, name : 'yes' }, + { btn : noBtn, name : 'no' } + ], function() { + if (this.btn) { + var button = _createButton(this.btn); + button.addClass('ke-dialog-' + this.name); + footerDiv.append(button); + } + }); + if (self.height) { + bodyDiv.height(_removeUnit(self.height) - headerDiv.height() - footerDiv.height()); + } + self.div.width(self.div.width()); + self.div.height(self.div.height()); + self.mask = null; + if (showMask) { + var docEl = _docElement(self.doc), + docWidth = Math.max(docEl.scrollWidth, docEl.clientWidth), + docHeight = Math.max(docEl.scrollHeight, docEl.clientHeight); + self.mask = _widget({ + x : 0, + y : 0, + z : self.z - 1, + cls : 'ke-dialog-mask', + width : docWidth, + height : docHeight + }); + } + self.autoPos(self.div.width(), self.div.height()); + self.footerDiv = footerDiv; + self.bodyDiv = bodyDiv; + self.headerDiv = headerDiv; + self.isLoading = false; + }, + setMaskIndex : function(z) { + var self = this; + self.mask.div.css('z-index', z); + }, + showLoading : function(msg) { + msg = _undef(msg, ''); + var self = this, body = self.bodyDiv; + self.loading = K('
      ' + msg + '
      ') + .width(body.width()).height(body.height()) + .css('top', self.headerDiv.height() + 'px'); + body.css('visibility', 'hidden').after(self.loading); + self.isLoading = true; + return self; + }, + hideLoading : function() { + this.loading && this.loading.remove(); + this.bodyDiv.css('visibility', 'visible'); + this.isLoading = false; + return this; + }, + remove : function() { + var self = this; + if (self.options.beforeRemove) { + self.options.beforeRemove.call(self); + } + self.mask && self.mask.remove(); + self.iframeMask && self.iframeMask.remove(); + self.closeIcon.unbind(); + K('input', self.div).unbind(); + K('button', self.div).unbind(); + self.footerDiv.unbind(); + self.bodyDiv.unbind(); + self.headerDiv.unbind(); + K('iframe', self.div).each(function() { + K(this).remove(); + }); + KDialog.parent.remove.call(self); + return self; + } +}); +function _dialog(options) { + return new KDialog(options); +} +K.DialogClass = KDialog; +K.dialog = _dialog; +function _tabs(options) { + var self = _widget(options), + remove = self.remove, + afterSelect = options.afterSelect, + div = self.div, + liList = []; + div.addClass('ke-tabs') + .bind('contextmenu,mousedown,mousemove', function(e) { + e.preventDefault(); + }); + var ul = K('
        '); + div.append(ul); + self.add = function(tab) { + var li = K('
      • ' + tab.title + '
      • '); + li.data('tab', tab); + liList.push(li); + ul.append(li); + }; + self.selectedIndex = 0; + self.select = function(index) { + self.selectedIndex = index; + _each(liList, function(i, li) { + li.unbind(); + if (i === index) { + li.addClass('ke-tabs-li-selected'); + K(li.data('tab').panel).show(''); + } else { + li.removeClass('ke-tabs-li-selected').removeClass('ke-tabs-li-on') + .mouseover(function() { + K(this).addClass('ke-tabs-li-on'); + }) + .mouseout(function() { + K(this).removeClass('ke-tabs-li-on'); + }) + .click(function() { + self.select(i); + }); + K(li.data('tab').panel).hide(); + } + }); + if (afterSelect) { + afterSelect.call(self, index); + } + }; + self.remove = function() { + _each(liList, function() { + this.remove(); + }); + ul.remove(); + remove.call(self); + }; + return self; +} +K.tabs = _tabs; +function _loadScript(url, fn) { + var head = document.getElementsByTagName('head')[0] || (_QUIRKS ? document.body : document.documentElement), + script = document.createElement('script'); + head.appendChild(script); + script.src = url; + script.charset = 'utf-8'; + script.onload = script.onreadystatechange = function() { + if (!this.readyState || this.readyState === 'loaded') { + if (fn) { + fn(); + } + script.onload = script.onreadystatechange = null; + head.removeChild(script); + } + }; +} +function _chopQuery(url) { + var index = url.indexOf('?'); + return index > 0 ? url.substr(0, index) : url; +} +function _loadStyle(url) { + var head = document.getElementsByTagName('head')[0] || (_QUIRKS ? document.body : document.documentElement), + link = document.createElement('link'), + absoluteUrl = _chopQuery(_formatUrl(url, 'absolute')); + var links = K('link[rel="stylesheet"]', head); + for (var i = 0, len = links.length; i < len; i++) { + if (_chopQuery(_formatUrl(links[i].href, 'absolute')) === absoluteUrl) { + return; + } + } + head.appendChild(link); + link.href = url; + link.rel = 'stylesheet'; +} +function _ajax(url, fn, method, param, dataType) { + method = method || 'GET'; + dataType = dataType || 'json'; + var xhr = window.XMLHttpRequest ? new window.XMLHttpRequest() : new ActiveXObject('Microsoft.XMLHTTP'); + xhr.open(method, url, true); + xhr.onreadystatechange = function () { + if (xhr.readyState == 4 && xhr.status == 200) { + if (fn) { + var data = _trim(xhr.responseText); + if (dataType == 'json') { + data = _json(data); + } + fn(data); + } + } + }; + if (method == 'POST') { + var params = []; + _each(param, function(key, val) { + params.push(encodeURIComponent(key) + '=' + encodeURIComponent(val)); + }); + try { + xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); + } catch (e) {} + xhr.send(params.join('&')); + } else { + xhr.send(null); + } +} +K.loadScript = _loadScript; +K.loadStyle = _loadStyle; +K.ajax = _ajax; +var _plugins = {}; +function _plugin(name, fn) { + if (name === undefined) { + return _plugins; + } + if (!fn) { + return _plugins[name]; + } + _plugins[name] = fn; +} +var _language = {}; +function _parseLangKey(key) { + var match, ns = 'core'; + if ((match = /^(\w+)\.(\w+)$/.exec(key))) { + ns = match[1]; + key = match[2]; + } + return { ns : ns, key : key }; +} +function _lang(mixed, langType) { + langType = langType === undefined ? K.options.langType : langType; + if (typeof mixed === 'string') { + if (!_language[langType]) { + return 'no language'; + } + var pos = mixed.length - 1; + if (mixed.substr(pos) === '.') { + return _language[langType][mixed.substr(0, pos)]; + } + var obj = _parseLangKey(mixed); + return _language[langType][obj.ns][obj.key]; + } + _each(mixed, function(key, val) { + var obj = _parseLangKey(key); + if (!_language[langType]) { + _language[langType] = {}; + } + if (!_language[langType][obj.ns]) { + _language[langType][obj.ns] = {}; + } + _language[langType][obj.ns][obj.key] = val; + }); +} +function _getImageFromRange(range, fn) { + if (range.collapsed) { + return; + } + range = range.cloneRange().up(); + var sc = range.startContainer, so = range.startOffset; + if (!_WEBKIT && !range.isControl()) { + return; + } + var img = K(sc.childNodes[so]); + if (!img || img.name != 'img') { + return; + } + if (fn(img)) { + return img; + } +} +function _bindContextmenuEvent() { + var self = this, doc = self.edit.doc; + K(doc).contextmenu(function(e) { + if (self.menu) { + self.hideMenu(); + } + if (!self.useContextmenu) { + e.preventDefault(); + return; + } + if (self._contextmenus.length === 0) { + return; + } + var maxWidth = 0, items = []; + _each(self._contextmenus, function() { + if (this.title == '-') { + items.push(this); + return; + } + if (this.cond && this.cond()) { + items.push(this); + if (this.width && this.width > maxWidth) { + maxWidth = this.width; + } + } + }); + while (items.length > 0 && items[0].title == '-') { + items.shift(); + } + while (items.length > 0 && items[items.length - 1].title == '-') { + items.pop(); + } + var prevItem = null; + _each(items, function(i) { + if (this.title == '-' && prevItem.title == '-') { + delete items[i]; + } + prevItem = this; + }); + if (items.length > 0) { + e.preventDefault(); + var pos = K(self.edit.iframe).pos(), + menu = _menu({ + x : pos.x + e.clientX, + y : pos.y + e.clientY, + width : maxWidth, + css : { visibility: 'hidden' }, + shadowMode : self.shadowMode + }); + _each(items, function() { + if (this.title) { + menu.addItem(this); + } + }); + var docEl = _docElement(menu.doc), + menuHeight = menu.div.height(); + if (e.clientY + menuHeight >= docEl.clientHeight - 100) { + menu.pos(menu.x, _removeUnit(menu.y) - menuHeight); + } + menu.div.css('visibility', 'visible'); + self.menu = menu; + } + }); +} +function _bindNewlineEvent() { + var self = this, doc = self.edit.doc, newlineTag = self.newlineTag; + if (_IE && newlineTag !== 'br') { + return; + } + if (_GECKO && _V < 3 && newlineTag !== 'p') { + return; + } + if (_OPERA && _V < 9) { + return; + } + var brSkipTagMap = _toMap('h1,h2,h3,h4,h5,h6,pre,li'), + pSkipTagMap = _toMap('p,h1,h2,h3,h4,h5,h6,pre,li,blockquote'); + function getAncestorTagName(range) { + var ancestor = K(range.commonAncestor()); + while (ancestor) { + if (ancestor.type == 1 && !ancestor.isStyle()) { + break; + } + ancestor = ancestor.parent(); + } + return ancestor.name; + } + K(doc).keydown(function(e) { + if (e.which != 13 || e.shiftKey || e.ctrlKey || e.altKey) { + return; + } + self.cmd.selection(); + var tagName = getAncestorTagName(self.cmd.range); + if (tagName == 'marquee' || tagName == 'select') { + return; + } + if (newlineTag === 'br' && !brSkipTagMap[tagName]) { + e.preventDefault(); + self.insertHtml('
        ' + (_IE && _V < 9 ? '' : '\u200B')); + return; + } + if (!pSkipTagMap[tagName]) { + _nativeCommand(doc, 'formatblock', '

        '); + } + }); + K(doc).keyup(function(e) { + if (e.which != 13 || e.shiftKey || e.ctrlKey || e.altKey) { + return; + } + if (newlineTag == 'br') { + return; + } + if (_GECKO) { + var root = self.cmd.commonAncestor('p'); + var a = self.cmd.commonAncestor('a'); + if (a && a.text() == '') { + a.remove(true); + self.cmd.range.selectNodeContents(root[0]).collapse(true); + self.cmd.select(); + } + return; + } + self.cmd.selection(); + var tagName = getAncestorTagName(self.cmd.range); + if (tagName == 'marquee' || tagName == 'select') { + return; + } + if (!pSkipTagMap[tagName]) { + _nativeCommand(doc, 'formatblock', '

        '); + } + var div = self.cmd.commonAncestor('div'); + if (div) { + var p = K('

        '), + child = div[0].firstChild; + while (child) { + var next = child.nextSibling; + p.append(child); + child = next; + } + div.before(p); + div.remove(); + self.cmd.range.selectNodeContents(p[0]); + self.cmd.select(); + } + }); +} +function _bindTabEvent() { + var self = this, doc = self.edit.doc; + K(doc).keydown(function(e) { + if (e.which == 9) { + e.preventDefault(); + if (self.afterTab) { + self.afterTab.call(self, e); + return; + } + var cmd = self.cmd, range = cmd.range; + range.shrink(); + if (range.collapsed && range.startContainer.nodeType == 1) { + range.insertNode(K('@ ', doc)[0]); + cmd.select(); + } + self.insertHtml('    '); + } + }); +} +function _bindFocusEvent() { + var self = this; + K(self.edit.textarea[0], self.edit.win).focus(function(e) { + if (self.afterFocus) { + self.afterFocus.call(self, e); + } + }).blur(function(e) { + if (self.afterBlur) { + self.afterBlur.call(self, e); + } + }); +} +function _removeBookmarkTag(html) { + return _trim(html.replace(/]*id="?__kindeditor_bookmark_\w+_\d+__"?[^>]*><\/span>/ig, '')); +} +function _removeTempTag(html) { + return html.replace(/]+class="?__kindeditor_paste__"?[^>]*>[\s\S]*?<\/div>/ig, ''); +} +function _addBookmarkToStack(stack, bookmark) { + if (stack.length === 0) { + stack.push(bookmark); + return; + } + var prev = stack[stack.length - 1]; + if (_removeBookmarkTag(bookmark.html) !== _removeBookmarkTag(prev.html)) { + stack.push(bookmark); + } +} +function _undoToRedo(fromStack, toStack) { + var self = this, edit = self.edit, + body = edit.doc.body, + range, bookmark; + if (fromStack.length === 0) { + return self; + } + if (edit.designMode) { + range = self.cmd.range; + bookmark = range.createBookmark(true); + bookmark.html = body.innerHTML; + } else { + bookmark = { + html : body.innerHTML + }; + } + _addBookmarkToStack(toStack, bookmark); + var prev = fromStack.pop(); + if (_removeBookmarkTag(bookmark.html) === _removeBookmarkTag(prev.html) && fromStack.length > 0) { + prev = fromStack.pop(); + } + if (edit.designMode) { + edit.html(prev.html); + if (prev.start) { + range.moveToBookmark(prev); + self.select(); + } + } else { + K(body).html(_removeBookmarkTag(prev.html)); + } + return self; +} +function KEditor(options) { + var self = this; + self.options = {}; + function setOption(key, val) { + if (KEditor.prototype[key] === undefined) { + self[key] = val; + } + self.options[key] = val; + } + _each(options, function(key, val) { + setOption(key, options[key]); + }); + _each(K.options, function(key, val) { + if (self[key] === undefined) { + setOption(key, val); + } + }); + var se = K(self.srcElement || '