From 16f70437d185dd3d0864c35d84801366c201b7f6 Mon Sep 17 00:00:00 2001 From: fanqiang <316257774@qq.com> Date: Fri, 22 Nov 2013 10:24:15 +0800 Subject: [PATCH 01/22] forum init. --- app/assets/javascripts/forums.js | 2 + app/assets/stylesheets/forums.css | 4 ++ app/controllers/forums_controller.rb | 83 ++++++++++++++++++++++ app/helpers/forums_helper.rb | 2 + app/models/forum.rb | 3 + app/views/forums/_form.html.erb | 17 +++++ app/views/forums/edit.html.erb | 6 ++ app/views/forums/index.html.erb | 21 ++++++ app/views/forums/new.html.erb | 5 ++ app/views/forums/show.html.erb | 5 ++ config/routes.rb | 25 +++++++ db/migrate/20131122020026_create_forums.rb | 8 +++ 12 files changed, 181 insertions(+) create mode 100644 app/assets/javascripts/forums.js create mode 100644 app/assets/stylesheets/forums.css create mode 100644 app/controllers/forums_controller.rb create mode 100644 app/helpers/forums_helper.rb create mode 100644 app/models/forum.rb create mode 100644 app/views/forums/_form.html.erb create mode 100644 app/views/forums/edit.html.erb create mode 100644 app/views/forums/index.html.erb create mode 100644 app/views/forums/new.html.erb create mode 100644 app/views/forums/show.html.erb create mode 100644 db/migrate/20131122020026_create_forums.rb diff --git a/app/assets/javascripts/forums.js b/app/assets/javascripts/forums.js new file mode 100644 index 00000000..dee720fa --- /dev/null +++ b/app/assets/javascripts/forums.js @@ -0,0 +1,2 @@ +// Place all the behaviors and hooks related to the matching controller here. +// All this logic will automatically be available in application.js. diff --git a/app/assets/stylesheets/forums.css b/app/assets/stylesheets/forums.css new file mode 100644 index 00000000..afad32db --- /dev/null +++ b/app/assets/stylesheets/forums.css @@ -0,0 +1,4 @@ +/* + Place all the styles related to the matching controller here. + They will automatically be included in application.css. +*/ diff --git a/app/controllers/forums_controller.rb b/app/controllers/forums_controller.rb new file mode 100644 index 00000000..806ba257 --- /dev/null +++ b/app/controllers/forums_controller.rb @@ -0,0 +1,83 @@ +class ForumsController < ApplicationController + # GET /forums + # GET /forums.json + def index + @forums = Forum.all + + respond_to do |format| + format.html # index.html.erb + format.json { render json: @forums } + end + end + + # GET /forums/1 + # GET /forums/1.json + def show + @forum = Forum.find(params[:id]) + + respond_to do |format| + format.html # show.html.erb + format.json { render json: @forum } + end + end + + # GET /forums/new + # GET /forums/new.json + def new + @forum = Forum.new + + respond_to do |format| + format.html # new.html.erb + format.json { render json: @forum } + end + end + + # GET /forums/1/edit + def edit + @forum = Forum.find(params[:id]) + end + + # POST /forums + # POST /forums.json + def create + @forum = Forum.new(params[:forum]) + + respond_to do |format| + if @forum.save + format.html { redirect_to @forum, notice: 'Forum was successfully created.' } + format.json { render json: @forum, status: :created, location: @forum } + else + format.html { render action: "new" } + format.json { render json: @forum.errors, status: :unprocessable_entity } + end + end + end + + # PUT /forums/1 + # PUT /forums/1.json + def update + @forum = Forum.find(params[:id]) + + respond_to do |format| + if @forum.update_attributes(params[:forum]) + format.html { redirect_to @forum, notice: 'Forum was successfully updated.' } + format.json { head :no_content } + else + format.html { render action: "edit" } + format.json { render json: @forum.errors, status: :unprocessable_entity } + end + end + end + + # DELETE /forums/1 + # DELETE /forums/1.json + def destroy + @forum = Forum.find(params[:id]) + @forum.destroy + + respond_to do |format| + format.html { redirect_to forums_url } + format.json { head :no_content } + end + end +end diff --git a/app/helpers/forums_helper.rb b/app/helpers/forums_helper.rb new file mode 100644 index 00000000..2e531fd4 --- /dev/null +++ b/app/helpers/forums_helper.rb @@ -0,0 +1,2 @@ +module ForumsHelper +end diff --git a/app/models/forum.rb b/app/models/forum.rb new file mode 100644 index 00000000..e87540e7 --- /dev/null +++ b/app/models/forum.rb @@ -0,0 +1,3 @@ +class Forum < ActiveRecord::Base + # attr_accessible :title, :body +end diff --git a/app/views/forums/_form.html.erb b/app/views/forums/_form.html.erb new file mode 100644 index 00000000..2eaf28e3 --- /dev/null +++ b/app/views/forums/_form.html.erb @@ -0,0 +1,17 @@ +<%= form_for(@forum) do |f| %> + <% if @forum.errors.any? %> +
+ | + | + |
---|---|---|
<%= link_to 'Show', forum %> | +<%= link_to 'Edit', edit_forum_path(forum) %> | +<%= link_to 'Destroy', forum, method: :delete, data: { confirm: 'Are you sure?' } %> | +
<%= notice %>
+ + +<%= link_to 'Edit', edit_forum_path(@forum) %> | +<%= link_to 'Back', forums_path %> diff --git a/config/routes.rb b/config/routes.rb index 8d09fbca..b2b90e7f 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -16,6 +16,31 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. RedmineApp::Application.routes.draw do + resources :forums do + + member do + get 'settings(/:tab)', :action => 'settings', :as => 'settings' + post 'modules' + post 'archive' + post 'unarchive' + post 'close' + post 'reopen' + match 'copy', :via => [:get, :post] + end + + shallow do + resources :memberships, :controller => 'members', :only => [:index, :show, :new, :create, :update, :destroy] do + collection do + get 'autocomplete' + end + end + end + + resources :boards + + end + + resources :shares #added by william diff --git a/db/migrate/20131122020026_create_forums.rb b/db/migrate/20131122020026_create_forums.rb new file mode 100644 index 00000000..b6b06237 --- /dev/null +++ b/db/migrate/20131122020026_create_forums.rb @@ -0,0 +1,8 @@ +class CreateForums < ActiveRecord::Migration + def change + create_table :forums do |t| + + t.timestamps + end + end +end From 4a4d1159f8315c3ba5277730a1beb4c7b9d549c9 Mon Sep 17 00:00:00 2001 From: fanqiang <316257774@qq.com> Date: Fri, 22 Nov 2013 11:19:08 +0800 Subject: [PATCH 02/22] =?UTF-8?q?=E5=B0=8F=E6=94=B9=E6=80=A1=E6=83=85?= =?UTF-8?q?=EF=BC=8C=E5=A4=A7=E6=94=B9=E4=BC=A4=E8=BA=AB=EF=BC=8C=E5=BC=BA?= =?UTF-8?q?=E6=94=B9=E7=81=B0=E9=A3=9E=E7=83=9F=E7=81=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/controllers/forums_controller.rb | 131 ++++-- app/models/forum.rb | 645 +++++++++++++++++++++++++++ 2 files changed, 751 insertions(+), 25 deletions(-) diff --git a/app/controllers/forums_controller.rb b/app/controllers/forums_controller.rb index 806ba257..41f71d8d 100644 --- a/app/controllers/forums_controller.rb +++ b/app/controllers/forums_controller.rb @@ -1,12 +1,29 @@ class ForumsController < ApplicationController + + + # GET /forums # GET /forums.json def index - @forums = Forum.all + # @forums = Forum.all respond_to do |format| - format.html # index.html.erb - format.json { render json: @forums } + format.html { + scope = Forum + unless params[:closed] + scope = scope.active + end + @forums = scope.visible.order('lft').all + } + format.api { + @offset, @limit = api_offset_and_limit + @project_count = Forum.visible.count + @projects = Forum.visible.offset(@offset).limit(@limit).order('lft').all + } + format.atom { + projects = Forum.visible.order('created_on DESC').limit(Setting.feeds_limit.to_i).all + render_feed(projects, :title => "#{Setting.app_title}: #{l(:label_project_latest)}") + } end end @@ -15,9 +32,30 @@ class ForumsController < ApplicationController def show @forum = Forum.find(params[:id]) + # # try to redirect to the requested menu item + # if params[:jump] && redirect_to_project_menu_item(@project, params[:jump]) + # return + # end + + @users_by_role = @forum.users_by_role + # @subprojects = @project.children.visible.all + # @news = @project.news.limit(5).includes(:author, :project).reorder("#{News.table_name}.created_on DESC").all + # @trackers = @project.rolled_up_trackers + + # cond = @project.project_condition(Setting.display_subprojects_issues?) + + # @open_issues_by_tracker = Issue.visible.open.where(cond).count(:group => :tracker) + # @total_issues_by_tracker = Issue.visible.where(cond).count(:group => :tracker) + + # if User.current.allowed_to?(:view_time_entries, @project) + # @total_hours = TimeEntry.visible.sum(:hours, :include => :project, :conditions => cond).to_f + # end + + @key = User.current.rss_key + respond_to do |format| - format.html # show.html.erb - format.json { render json: @forum } + format.html + format.api end end @@ -26,6 +64,11 @@ class ForumsController < ApplicationController def new @forum = Forum.new + # @issue_custom_fields = IssueCustomField.sorted.all + # @trackers = Tracker.sorted.all + # @project = Project.new + @forum.safe_attributes = params[:forum] + respond_to do |format| format.html # new.html.erb format.json { render json: @forum } @@ -40,15 +83,37 @@ class ForumsController < ApplicationController # POST /forums # POST /forums.json def create - @forum = Forum.new(params[:forum]) + # @forum = Forum.new(params[:forum]) - respond_to do |format| - if @forum.save - format.html { redirect_to @forum, notice: 'Forum was successfully created.' } - format.json { render json: @forum, status: :created, location: @forum } - else - format.html { render action: "new" } - format.json { render json: @forum.errors, status: :unprocessable_entity } + # @issue_custom_fields = IssueCustomField.sorted.all + # @trackers = Tracker.sorted.all + @forum = Forum.new + @project.safe_attributes = params[:project] + + if @forum.save + # @project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id') + # Add current user as a project member if he is not admin + unless User.current.admin? + r = Role.givable.find_by_id(Setting.new_project_user_role_id.to_i) || Role.givable.first + m = Member.new(:user => User.current, :roles => [r]) + @forum.members << m + end + respond_to do |format| + format.html { + flash[:notice] = l(:notice_successful_create) + # if params[:continue] + # attrs = {:parent_id => @forum.parent_id}.reject {|k,v| v.nil?} + # redirect_to new_project_path(attrs) + # else + redirect_to settings_project_path(@forum) + # end + } + format.api { render :action => 'show', :status => :created, :location => url_for(:controller => 'forums', :action => 'show', :id => @forum.id) } + end + else + respond_to do |format| + format.html { render :action => 'new' } + format.api { render_validation_errors(@forum) } end end end @@ -58,13 +123,23 @@ class ForumsController < ApplicationController def update @forum = Forum.find(params[:id]) - respond_to do |format| - if @forum.update_attributes(params[:forum]) - format.html { redirect_to @forum, notice: 'Forum was successfully updated.' } - format.json { head :no_content } - else - format.html { render action: "edit" } - format.json { render json: @forum.errors, status: :unprocessable_entity } + @forum.safe_attributes = params[:project] + if @forum.save + # @project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id') + respond_to do |format| + format.html { + flash[:notice] = l(:notice_successful_update) + redirect_to settings_project_path(@forum) + } + format.api { render_api_ok } + end + else + respond_to do |format| + format.html { + settings + render :action => 'settings' + } + format.api { render_validation_errors(@forum) } end end end @@ -73,11 +148,17 @@ class ForumsController < ApplicationController # DELETE /forums/1.json def destroy @forum = Forum.find(params[:id]) - @forum.destroy - - respond_to do |format| - format.html { redirect_to forums_url } - format.json { head :no_content } + # @forum.destroy + + @project_to_destroy = @forum + if api_request? || params[:confirm] + @project_to_destroy.destroy + respond_to do |format| + format.html { redirect_to admin_projects_path } + format.api { render_api_ok } + end end + # hide project in layout + @project = nil end end diff --git a/app/models/forum.rb b/app/models/forum.rb index e87540e7..872a6b4a 100644 --- a/app/models/forum.rb +++ b/app/models/forum.rb @@ -1,3 +1,648 @@ class Forum < ActiveRecord::Base # attr_accessible :title, :body + include Redmine::SafeAttributes + + # Project statuses + STATUS_ACTIVE = 1 + STATUS_CLOSED = 5 + STATUS_ARCHIVED = 9 + + # Maximum length for project identifiers + IDENTIFIER_MAX_LENGTH = 100 + + # Specific overidden Activities + + has_many :members, :include => [:principal, :roles], :conditions => "#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{Principal::STATUS_ACTIVE}" + has_many :memberships, :class_name => 'Member' + has_many :member_principals, :class_name => 'Member', + :include => :principal, + :conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{Principal::STATUS_ACTIVE})" + has_many :users, :through => :members + has_many :principals, :through => :member_principals, :source => :principal + + has_many :enabled_modules, :dependent => :delete_all + + + + has_many :boards, :dependent => :destroy, :order => "position ASC" + # Custom field for the project issues + # has_and_belongs_to_many :issue_custom_fields, + # :class_name => 'IssueCustomField', + # :order => "#{CustomField.table_name}.position", + # :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}", + # :association_foreign_key => 'custom_field_id' + + acts_as_nested_set :order => 'name', :dependent => :destroy + acts_as_attachable :view_permission => :view_files, + :delete_permission => :manage_files + + acts_as_customizable + acts_as_searchable :columns => ['name', 'identifier', 'description'], :project_key => 'id', :permission => nil + acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"}, + :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o}}, + :author => nil + + attr_protected :status + + validates_presence_of :name, :identifier + validates_uniqueness_of :identifier + validates_associated :repository, :wiki + validates_length_of :name, :maximum => 255 + validates_length_of :homepage, :maximum => 255 + validates_length_of :identifier, :in => 1..IDENTIFIER_MAX_LENGTH + # donwcase letters, digits, dashes but not digits only + validates_format_of :identifier, :with => /\A(?!\d+$)[a-z0-9\-_]*\z/, :if => Proc.new { |p| p.identifier_changed? } + # reserved words + validates_exclusion_of :identifier, :in => %w( new ) + + # after_save :update_position_under_parent, :if => Proc.new {|project| project.name_changed?} + # after_save :update_inherited_members, :if => Proc.new {|project| project.inherit_members_changed?} + before_destroy :delete_all_members + + # TODO: 可能要加表 这部分用于改变项目的模块 + scope :has_module, lambda {|mod| + where("#{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name=?)", mod.to_s) + } + scope :active, lambda { where(:status => STATUS_ACTIVE) } + scope :status, lambda {|arg| where(arg.blank? ? nil : {:status => arg.to_i}) } + scope :all_public, lambda { where(:is_public => true) } + scope :visible, lambda {|*args| where(Project.visible_condition(args.shift || User.current, *args)) } + scope :allowed_to, lambda {|*args| + user = User.current + permission = nil + if args.first.is_a?(Symbol) + permission = args.shift + else + user = args.shift + permission = args.shift + end + where(Project.allowed_to_condition(user, permission, *args)) + } + scope :like, lambda {|arg| + if arg.blank? + where(nil) + else + pattern = "%#{arg.to_s.strip.downcase}%" + where("LOWER(identifier) LIKE :p OR LOWER(name) LIKE :p", :p => pattern) + end + } + + def initialize(attributes=nil, *args) + super + + initialized = (attributes || {}).stringify_keys + if !initialized.key?('identifier') && Setting.sequential_project_identifiers? + self.identifier = Project.next_identifier + end + if !initialized.key?('is_public') + self.is_public = Setting.default_projects_public? + end + if !initialized.key?('enabled_module_names') + self.enabled_module_names = Setting.default_projects_modules + end + end + + def identifier=(identifier) + super unless identifier_frozen? + end + + def identifier_frozen? + errors[:identifier].blank? && !(new_record? || identifier.blank?) + end + + # returns latest created projects + # non public projects will be returned only if user is a member of those + def self.latest(user=nil, count=5) + visible(user).limit(count).order("created_on DESC").all + end + + # Returns true if the project is visible to +user+ or to the current user. + def visible?(user=User.current) + user.allowed_to?(:view_project, self) + end + + # Returns a SQL conditions string used to find all projects visible by the specified user. + # + # Examples: + # Project.visible_condition(admin) => "projects.status = 1" + # Project.visible_condition(normal_user) => "((projects.status = 1) AND (projects.is_public = 1 OR projects.id IN (1,3,4)))" + # Project.visible_condition(anonymous) => "((projects.status = 1) AND (projects.is_public = 1))" + def self.visible_condition(user, options={}) + allowed_to_condition(user, :view_project, options) + end + + # Returns a SQL conditions string used to find all projects for which +user+ has the given +permission+ + # + # Valid options: + # * :project => limit the condition to project + # * :with_subprojects => limit the condition to project and its subprojects + # * :member => limit the condition to the user projects + def self.allowed_to_condition(user, permission, options={}) + perm = Redmine::AccessControl.permission(permission) + base_statement = (perm && perm.read? ? "#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED}" : "#{Project.table_name}.status = #{Project::STATUS_ACTIVE}") + if perm && perm.project_module + # If the permission belongs to a project module, make sure the module is enabled + base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')" + end + if options[:project] + project_statement = "#{Project.table_name}.id = #{options[:project].id}" + project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects] + base_statement = "(#{project_statement}) AND (#{base_statement})" + end + + if user.admin? + base_statement + else + statement_by_role = {} + unless options[:member] + role = user.logged? ? Role.non_member : Role.anonymous + if role.allowed_to?(permission) + statement_by_role[role] = "#{Project.table_name}.is_public = #{connection.quoted_true}" + end + end + if user.logged? + user.projects_by_role.each do |role, projects| + if role.allowed_to?(permission) && projects.any? + statement_by_role[role] = "#{Project.table_name}.id IN (#{projects.collect(&:id).join(',')})" + end + end + end + if statement_by_role.empty? + "1=0" + else + if block_given? + statement_by_role.each do |role, statement| + if s = yield(role, user) + statement_by_role[role] = "(#{statement} AND (#{s}))" + end + end + end + "((#{base_statement}) AND (#{statement_by_role.values.join(' OR ')}))" + end + end + end + + # Returns the Systemwide and project specific activities + def activities(include_inactive=false) + if include_inactive + return all_activities + else + return active_activities + end + end + + + # Returns a :conditions SQL string that can be used to find the issues associated with this project. + # + # Examples: + # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))" + # project.project_condition(false) => "projects.id = 1" + def project_condition(with_subprojects) + cond = "#{Project.table_name}.id = #{id}" + cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects + cond + end + + def self.find(*args) + if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/) + project = find_by_identifier(*args) + raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil? + project + else + super + end + end + + def self.find_by_param(*args) + self.find(*args) + end + + alias :base_reload :reload + def reload(*args) + @shared_versions = nil + @rolled_up_versions = nil + @rolled_up_trackers = nil + @all_issue_custom_fields = nil + @all_time_entry_custom_fields = nil + @to_param = nil + @allowed_parents = nil + @allowed_permissions = nil + @actions_allowed = nil + @start_date = nil + @due_date = nil + base_reload(*args) + end + + def to_param + # id is used for projects with a numeric identifier (compatibility) + @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id.to_s : identifier) + end + + def active? + self.status == STATUS_ACTIVE + end + + def archived? + self.status == STATUS_ARCHIVED + end + + # Archives the project and its descendants + def archive + # Check that there is no issue of a non descendant project that is assigned + # to one of the project or descendant versions + v_ids = self_and_descendants.collect {|p| p.version_ids}.flatten + if v_ids.any? && + Issue. + includes(:project). + where("#{Project.table_name}.lft < ? OR #{Project.table_name}.rgt > ?", lft, rgt). + where("#{Issue.table_name}.fixed_version_id IN (?)", v_ids). + exists? + return false + end + Project.transaction do + archive! + end + true + end + + # Unarchives the project + # All its ancestors must be active + def unarchive + return false if ancestors.detect {|a| !a.active?} + update_attribute :status, STATUS_ACTIVE + end + + def close + self_and_descendants.status(STATUS_ACTIVE).update_all :status => STATUS_CLOSED + end + + def reopen + self_and_descendants.status(STATUS_CLOSED).update_all :status => STATUS_ACTIVE + end + + # Recalculates all lft and rgt values based on project names + # Unlike Project.rebuild!, these values are recalculated even if the tree "looks" valid + # Used in BuildProjectsTree migration + def self.rebuild_tree! + transaction do + update_all "lft = NULL, rgt = NULL" + rebuild!(false) + end + end + + # Returns an array of the trackers used by the project and its active sub projects + def rolled_up_trackers + @rolled_up_trackers ||= + Tracker. + joins(:projects). + joins("JOIN #{EnabledModule.table_name} ON #{EnabledModule.table_name}.project_id = #{Project.table_name}.id AND #{EnabledModule.table_name}.name = 'issue_tracking'"). + select("DISTINCT #{Tracker.table_name}.*"). + where("#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status <> #{STATUS_ARCHIVED}", lft, rgt). + sorted. + all + end + + # Returns a hash of project users grouped by role + def users_by_role + members.includes(:user, :roles).all.inject({}) do |h, m| + m.roles.each do |r| + h[r] ||= [] + h[r] << m.user + end + h + end + end + + # Deletes all project's members + def delete_all_members + me, mr = Member.table_name, MemberRole.table_name + connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})") + Member.delete_all(['project_id = ?', id]) + end + + # Users/groups issues can be assigned to + def assignable_users + assignable = Setting.issue_group_assignment? ? member_principals : members + assignable.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.principal}.sort + end + + # Returns the mail adresses of users that should be always notified on project events + def recipients + notified_users.collect {|user| user.mail} + end + + # Returns the users that should be notified on project events + def notified_users + # TODO: User part should be extracted to User#notify_about? + members.select {|m| m.principal.present? && (m.mail_notification? || m.principal.mail_notification == 'all')}.collect {|m| m.principal} + end + + def project + self + end + + def <=>(project) + name.downcase <=> project.name.downcase + end + + def to_s + name + end + + # Returns a short description of the projects (first lines) + def short_description(length = 255) + description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description + end + + def css_classes + s = 'project' + s << ' root' if root? + s << ' child' if child? + s << (leaf? ? ' leaf' : ' parent') + unless active? + if archived? + s << ' archived' + else + s << ' closed' + end + end + s + end + + # The earliest start date of a project, based on it's issues and versions + def start_date + @start_date ||= [ + issues.minimum('start_date'), + shared_versions.minimum('effective_date'), + Issue.fixed_version(shared_versions).minimum('start_date') + ].compact.min + end + + # The latest due date of an issue or version + def due_date + @due_date ||= [ + issues.maximum('due_date'), + shared_versions.maximum('effective_date'), + Issue.fixed_version(shared_versions).maximum('due_date') + ].compact.max + end + + def overdue? + active? && !due_date.nil? && (due_date < Date.today) + end + + # Return true if this project allows to do the specified action. + # action can be: + # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit') + # * a permission Symbol (eg. :edit_project) + def allows_to?(action) + if archived? + # No action allowed on archived projects + return false + end + unless active? || Redmine::AccessControl.read_action?(action) + # No write action allowed on closed projects + return false + end + # No action allowed on disabled modules + if action.is_a? Hash + allowed_actions.include? "#{action[:controller]}/#{action[:action]}" + else + allowed_permissions.include? action + end + end + + def module_enabled?(module_name) + module_name = module_name.to_s + enabled_modules.detect {|m| m.name == module_name} + end + + def enabled_module_names=(module_names) + if module_names && module_names.is_a?(Array) + module_names = module_names.collect(&:to_s).reject(&:blank?) + self.enabled_modules = module_names.collect {|name| enabled_modules.detect {|mod| mod.name == name} || EnabledModule.new(:name => name)} + else + enabled_modules.clear + end + end + + # Returns an array of the enabled modules names + def enabled_module_names + enabled_modules.collect(&:name) + end + + # Enable a specific module + # + # Examples: + # project.enable_module!(:issue_tracking) + # project.enable_module!("issue_tracking") + def enable_module!(name) + enabled_modules << EnabledModule.new(:name => name.to_s) unless module_enabled?(name) + end + + # Disable a module if it exists + # + # Examples: + # project.disable_module!(:issue_tracking) + # project.disable_module!("issue_tracking") + # project.disable_module!(project.enabled_modules.first) + def disable_module!(target) + target = enabled_modules.detect{|mod| target.to_s == mod.name} unless enabled_modules.include?(target) + target.destroy unless target.blank? + end + + safe_attributes 'name', + 'description', + 'homepage', + 'is_public', + 'identifier', + 'custom_field_values', + 'custom_fields', + 'tracker_ids', + 'issue_custom_field_ids' + + safe_attributes 'enabled_module_names', + :if => lambda {|project, user| project.new_record? || user.allowed_to?(:select_project_modules, project) } + + safe_attributes 'inherit_members', + :if => lambda {|project, user| project.parent.nil? || project.parent.visible?(user)} + + + # Returns an auto-generated project identifier based on the last identifier used + def self.next_identifier + p = Project.order('id DESC').first + p.nil? ? nil : p.identifier.to_s.succ + end + + # Copies and saves the Project instance based on the +project+. + # Duplicates the source project's: + # * Wiki + # * Versions + # * Categories + # * Issues + # * Members + # * Queries + # + # Accepts an +options+ argument to specify what to copy + # + # Examples: + # project.copy(1) # => copies everything + # project.copy(1, :only => 'members') # => copies members only + # project.copy(1, :only => ['members', 'versions']) # => copies members and versions + def copy(project, options={}) + project = project.is_a?(Project) ? project : Project.find(project) + + to_be_copied = %w(wiki versions issue_categories issues members queries boards) + to_be_copied = to_be_copied & options[:only].to_a unless options[:only].nil? + + Project.transaction do + if save + reload + to_be_copied.each do |name| + send "copy_#{name}", project + end + Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self) + save + end + end + end + + # Returns a new unsaved Project instance with attributes copied from +project+ + def self.copy_from(project) + project = project.is_a?(Project) ? project : Project.find(project) + # clear unique attributes + attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt') + copy = Project.new(attributes) + copy.enabled_modules = project.enabled_modules + copy.trackers = project.trackers + copy.custom_values = project.custom_values.collect {|v| v.clone} + copy.issue_custom_fields = project.issue_custom_fields + copy + end + + # Yields the given block for each project with its level in the tree + def self.project_tree(projects, &block) + ancestors = [] + projects.sort_by(&:lft).each do |project| + while (ancestors.any? && !project.is_descendant_of?(ancestors.last)) + ancestors.pop + end + yield project, ancestors.size + ancestors << project + end + end + + private + # Copies members from +project+ + def copy_members(project) + # Copy users first, then groups to handle members with inherited and given roles + members_to_copy = [] + members_to_copy += project.memberships.select {|m| m.principal.is_a?(User)} + members_to_copy += project.memberships.select {|m| !m.principal.is_a?(User)} + + members_to_copy.each do |member| + new_member = Member.new + new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on") + # only copy non inherited roles + # inherited roles will be added when copying the group membership + role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id) + next if role_ids.empty? + new_member.role_ids = role_ids + new_member.project = self + self.members << new_member + end + end + + # Copies boards from +project+ + def copy_boards(project) + project.boards.each do |board| + new_board = Board.new + new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id") + new_board.project = self + self.boards << new_board + end + end + + def allowed_permissions + @allowed_permissions ||= begin + module_names = enabled_modules.all(:select => :name).collect {|m| m.name} + Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name} + end + end + + def allowed_actions + @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten + end + + # Returns all the active Systemwide and project specific activities + def active_activities + overridden_activity_ids = self.time_entry_activities.collect(&:parent_id) + + if overridden_activity_ids.empty? + return TimeEntryActivity.shared.active + else + return system_activities_and_project_overrides + end + end + + # Returns all the Systemwide and project specific activities + # (inactive and active) + def all_activities + overridden_activity_ids = self.time_entry_activities.collect(&:parent_id) + + if overridden_activity_ids.empty? + return TimeEntryActivity.shared + else + return system_activities_and_project_overrides(true) + end + end + + # Returns the systemwide active activities merged with the project specific overrides + def system_activities_and_project_overrides(include_inactive=false) + if include_inactive + return TimeEntryActivity.shared. + where("id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)).all + + self.time_entry_activities + else + return TimeEntryActivity.shared.active. + where("id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)).all + + self.time_entry_activities.active + end + end + + # Archives subprojects recursively + def archive! + children.each do |subproject| + subproject.send :archive! + end + update_attribute :status, STATUS_ARCHIVED + end + + def update_position_under_parent + set_or_update_position_under(parent) + end + + # Inserts/moves the project so that target's children or root projects stay alphabetically sorted + def set_or_update_position_under(target_parent) + parent_was = parent + sibs = (target_parent.nil? ? self.class.roots : target_parent.children) + to_be_inserted_before = sibs.sort_by {|c| c.name.to_s.downcase}.detect {|c| c.name.to_s.downcase > name.to_s.downcase } + + if to_be_inserted_before + move_to_left_of(to_be_inserted_before) + elsif target_parent.nil? + if sibs.empty? + # move_to_root adds the project in first (ie. left) position + move_to_root + else + move_to_right_of(sibs.last) unless self == sibs.last + end + else + # move_to_child_of adds the project in last (ie.right) position + move_to_child_of(target_parent) + end + if parent_was != target_parent + after_parent_changed(parent_was) + end + end end From c03f17e3b4dd8616071b36be2e06afb3eb71a1ec Mon Sep 17 00:00:00 2001 From: yanxd<%= f.text_field :name, :required => true %>
+<%= f.text_field :description, :required => true, :size => 80 %>
<%= f.submit %>name | +description | +creator | @@ -9,6 +12,9 @@ <% @forums.each do |forum| %> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
<%= forum.name %> | +<%= forum.description %> | +<%= forum.creator.show_name %> | <%= link_to 'Show', forum %> | <%= link_to 'Edit', edit_forum_path(forum) %> | <%= link_to 'Destroy', forum, method: :delete, data: { confirm: 'Are you sure?' } %> | diff --git a/app/views/forums/show.html.erb b/app/views/forums/show.html.erb index b7cea417..63ac98bb 100644 --- a/app/views/forums/show.html.erb +++ b/app/views/forums/show.html.erb @@ -1,5 +1,6 @@
subject | +content | +author | +
---|---|---|
<%= link_to memo.subject, forum_memo_path(memo) %> | +<%= memo.content %> | +<%= memo.author %> | +
+ <%= notice %> +
++ <%= %> +
++ <%= %> +
+subject | +content | +author | +
---|---|---|
<%= link_to @memo.subject, forum_memo_path(@memo) %> | +<%= @memo.content %> | +<%= @memo.author %> | +
subject | +content | +author | +
---|---|---|
<%= link_to reply.subject, forum_memo_path(reply) %> | +<%= reply.content %> | +<%= reply.author %> | +
<%= l(:label_homeworks_form_new_description) %>
+ +<%= f.text_field :content, :required => true, :size => 60, :style => "width:150px;" %>
+<%= hidden_field_tag 'subject', ||=@memo.subject %> \ No newline at end of file diff --git a/app/views/memos/show.html.erb b/app/views/memos/show.html.erb index e0fc7cbb..05da1db1 100644 --- a/app/views/memos/show.html.erb +++ b/app/views/memos/show.html.erb @@ -1,36 +1,90 @@ -
- <%= notice %> -
-- <%= %> -
-- <%= %> -
-subject | -content | -author | -
---|---|---|
<%= link_to @memo.subject, forum_memo_path(@memo) %> | -<%= @memo.content %> | -<%= @memo.author %> | -
subject | -content | -author | -
---|---|---|
<%= link_to reply.subject, forum_memo_path(reply) %> | -<%= reply.content %> | -<%= reply.author %> | -
+ <%= link_to image_tag(url_to_avatar(reply.author), :class => "avatar"), user_path(reply.author) %> | +
+ <%= textilizable reply, :content %>
+
+ |
+
<%= authoring reply.created_at, reply.author %> | +
<%= link_to image_tag(url_to_avatar(topic.author), :class => "avatar"), user_path(topic.author) %> | +
+
|
+
+
<%= l(:label_no_data) %>
+<% end %> +软件项目托管社区 | +<%= l(:label_user_location) %> : | +
+
+ <%= form_tag(:controller => 'projects', :action => "search", :method => :get) do %>
+ <%= text_field_tag 'name', params[:name], :size => 20 %>
+ <%= hidden_field_tag 'project_type', params[:project_type] %>
+ <%= submit_tag l(:label_search), :class => "enterprise", :name => nil %>
+ <% end %>
+
+ |
+
<%= link_to "forge.trustie.net/projects", :controller => 'projects', :action => 'index', :project_type => 0 %> | +<%=link_to l(:label_home),home_path %> > <%=link_to '讨论区', :controller => 'forums', :action => 'index' %> > <%=link_to @forum.name, forum_path(@forum) %> |
+
subject | -content | -author | -
---|---|---|
<%= link_to memo.subject, forum_memo_path(memo) %> | -<%= memo.content %> | -<%= memo.author %> | -
<%= f.text_field :subject, :required => true %>
+<%= f.text_field :content, :required => true, :size => 80 %>
+ <%= f.submit %> +<%= link_to image_tag(url_to_avatar(topic.author), :class => "avatar"), user_path(topic.author) %> |
-
<%= pagination_links_full @topic_pages, @topic_count %>
<% else %>
<%= l(:label_no_data) %> <% end %> diff --git a/app/views/forums/edit.html.erb b/app/views/forums/edit.html.erb index 483e3b5f..e8111d61 100644 --- a/app/views/forums/edit.html.erb +++ b/app/views/forums/edit.html.erb @@ -1,3 +1,4 @@ +Editing forum<%= render 'form' %> diff --git a/app/views/forums/index.html.erb b/app/views/forums/index.html.erb index d9ad7b90..687a9b17 100644 --- a/app/views/forums/index.html.erb +++ b/app/views/forums/index.html.erb @@ -1,4 +1,29 @@ -Listing forums+ +
+
+
+<%= render :partial => 'forums/forum_list', :locals => {:forums => @forums} %>
+
+
+
-<%= link_to 'New Forum', new_forum_path %>
diff --git a/app/views/forums/new.html.erb b/app/views/forums/new.html.erb
index 703217a1..565816a1 100644
--- a/app/views/forums/new.html.erb
+++ b/app/views/forums/new.html.erb
@@ -1,3 +1,4 @@
+
New forum<%= render 'form' %> diff --git a/app/views/forums/show.html.erb b/app/views/forums/show.html.erb index 1d12ab6d..fcb4769d 100644 --- a/app/views/forums/show.html.erb +++ b/app/views/forums/show.html.erb @@ -1,3 +1,4 @@ +<%= notice %> From d882dde164eb9107691e10bf62b85a0af8895431 Mon Sep 17 00:00:00 2001 From: fanqiang <316257774@qq.com> Date: Sun, 24 Nov 2013 20:03:19 +0800 Subject: [PATCH 13/22] one more time --- app/views/forums/_forum_list.html.erb | 42 +++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 app/views/forums/_forum_list.html.erb diff --git a/app/views/forums/_forum_list.html.erb b/app/views/forums/_forum_list.html.erb new file mode 100644 index 00000000..e5bd3b14 --- /dev/null +++ b/app/views/forums/_forum_list.html.erb @@ -0,0 +1,42 @@ + +
+<% if forums.any? %>
+ <% forums.each do |forum| %>
+
\ No newline at end of file
From 54c6ed72c161c186b98fe343a131eef0bc4f4666 Mon Sep 17 00:00:00 2001
From: yanxd
<%= pagination_links_full @forums_pages, @forums_count %>
+<% else %>
+<% end %>
+
-
- <%= render :partial => 'layouts/base_header'%>
-
-
-
- <%= call_hook :view_layouts_base_body_bottom %>
+
-
\ No newline at end of file
diff --git a/app/views/memos/show.html.erb b/app/views/memos/show.html.erb
index 71d8dc10..b8abb9d9 100644
--- a/app/views/memos/show.html.erb
+++ b/app/views/memos/show.html.erb
@@ -1,90 +1,121 @@
-
-
-
- <%= render_flash_messages %>
- <%= yield %>
- <%= call_hook :view_layouts_base_content %>
-
- <%= render_flash_messages %>
+
+ <%= call_hook :view_layouts_base_body_bottom %>
+ <%=render :partial => 'layouts/base_header'%>
+
-
-
-
-
- <%= debug(params) if Rails.env.development? %>
+
+
+
-
+
+
+
-
-
- <%= link_to image_tag(url_to_avatar(@memo.author), :class => "avatar", :style => "width: 24px; height: 24px"), user_path(@memo.author) %>
- <%=h @memo.subject %>
-
- <%= textilizable(@memo, :content) %>
- <%= authoring @memo.created_at, @memo.author %>
+
+
+
+
+ <%= link_to image_tag(url_to_avatar(@memo.author), :class => "avatar"), user_path(@memo.author) %>
+ <%=h @memo.author %> +
+
<%= label_tag l(:field_subject) %>: <%=h @memo.subject %>
+ <%= textilizable(@memo, :content) %>
+
- - - - <%= l(:label_reply_plural) %> (<%= @replies.nil? ? 0 : @replies.size %>)+
+
-<%= l(:label_reply_plural) %> (<%= @replies.nil? ? 0 : @replies.size %>)+ <% reply_count = 0%> <% @replies.each do |reply| %> +<%= reply_count += 1 %>L : ">
-
<% end %>
- <%= link_to(
- image_tag('comment.png'),
- {:action => 'quote', :id => reply},
- :remote => true,
- :method => 'get',
- :title => l(:button_quote)) if !@memo.locked? && authorize_for('messages', 'reply') %>
- <%= link_to(
- image_tag('edit.png'),
- {:action => 'edit', :id => reply},
- :title => l(:button_edit)
- ) if reply.destroyable_by?(User.current) %>
- <%= link_to(
- image_tag('delete.png'),
- {:action => 'destroy', :id => reply},
- :method => :post,
- :data => {:confirm => l(:text_are_you_sure)},
- :title => l(:button_delete)
- ) if reply.destroyable_by?(User.current) %>
-
-
-
+ <%= link_to(
+ image_tag('comment.png'),
+ {:action => 'quote', :id => reply},
+ :remote => true,
+ :method => 'get',
+ :title => l(:button_quote)) if !@memo.locked? && authorize_for('messages', 'reply') %>
+ <%= link_to(
+ image_tag('edit.png'),
+ {:action => 'edit', :id => reply},
+ :title => l(:button_edit)
+ ) if reply.destroyable_by?(User.current) %>
+ <%= link_to(
+ image_tag('delete.png'),
+ {:action => 'destroy', :id => reply},
+ :method => :post,
+ :data => {:confirm => l(:text_are_you_sure)},
+ :title => l(:button_delete)
+ ) if reply.destroyable_by?(User.current) %>
+
-
+
- <%= labelled_form_for(@mome_new, url: forum_memos_path) do |f| %>
- reply
-
<%= forum.description%> |
标签~~~~~~~~~~ |
+
+
+ |
<%= authoring forum.created_at, forum.creator %>
diff --git a/app/views/forums/index.html.erb b/app/views/forums/index.html.erb
index 687a9b17..80c379ed 100644
--- a/app/views/forums/index.html.erb
+++ b/app/views/forums/index.html.erb
@@ -2,48 +2,45 @@
|
- <%=render :partial => 'layouts/base_header'%>
+ <%=render :partial => 'layouts/base_header'%>
-
diff --git a/app/views/tags/_tag.html.erb b/app/views/tags/_tag.html.erb
index c3d0cb4b..9597afc0 100644
--- a/app/views/tags/_tag.html.erb
+++ b/app/views/tags/_tag.html.erb
@@ -1,51 +1,51 @@
<%= image_tag("/images/sidebar/tags.png") %>
-<%= l(:label_tag) %>:
+ <% else %>
-<% if User.current.logged? %>
-<%= toggle_link (image_tag "/images/sidebar/add.png"), 'put-tag-form', {:focus => 'name'} %>
+ <%= image_tag("/images/sidebar/tags.png") %>
+ <%= l(:label_tag) %>:
+
+ <% if User.current.logged? %>
+ <%= toggle_link (image_tag "/images/sidebar/add.png"), 'put-tag-form', {:focus => 'name'} %>
+ <% end %>
+
+
+
<% end %>
-
-
-
-
-<% end %>
From 8218fde5098e5e7264affddaeab627f41a6ab6ad Mon Sep 17 00:00:00 2001
From: yanxd
+
+
-<% else %>
-<%=h @board.name %>
-<%=h @board.description %>
+
共有 <%=link_to @topics.count %> 个贴子
-
+
<% if @topics.any? %>
<% end %>
+
<%= pagination_links_full @topic_pages, @topic_count %>
diff --git a/app/views/messages/show.html.erb b/app/views/messages/show.html.erb
index 0529de84..cfbc31f2 100644
--- a/app/views/messages/show.html.erb
+++ b/app/views/messages/show.html.erb
@@ -23,13 +23,11 @@
) if @message.destroyable_by?(User.current) %>
<%= avatar(@topic.author, :size => "24") %><%=h @topic.subject %>+<%= avatar(@topic.author, :size => "24") %><%=h @topic.subject %>
-
diff --git a/app/views/welcome/index.html.erb b/app/views/welcome/index.html.erb
index da406ad6..7d26a72b 100644
--- a/app/views/welcome/index.html.erb
+++ b/app/views/welcome/index.html.erb
@@ -106,7 +106,7 @@
<%= l :label_hot_project%>
<% find_all_hot_project.map do |project| break if(project == find_all_hot_project[5]) %>
- <%=link_to( project.name, project_path(project), :class => "nowrap" )%>
+ <%=link_to( project.name, project_path(project.project_id), :class => "nowrap" )%>
diff --git a/config/locales/zh.yml b/config/locales/zh.yml
index 9f7dfcff..5bb60833 100644
--- a/config/locales/zh.yml
+++ b/config/locales/zh.yml
@@ -1727,3 +1727,6 @@ zh:
label_newbie_faq: '新手指引 & 问答'
label_hot_project: '热门项目'
label_memo_create_succ: 回复成功
+ label_memo_create: 发布
+ label_memo_new: 新建主题
+ label_memo_edit: 修改主题
diff --git a/config/routes.rb b/config/routes.rb
index b44dcf02..8eaa1a74 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -17,7 +17,11 @@
RedmineApp::Application.routes.draw do
resources :forums do
- resources :memos
+ resources :memos do
+ collection do
+ get "quote"
+ end
+ end
end
diff --git a/public/javascripts/ckeditor/CHANGES.md b/public/javascripts/ckeditor/CHANGES.md
new file mode 100644
index 00000000..94a66056
--- /dev/null
+++ b/public/javascripts/ckeditor/CHANGES.md
@@ -0,0 +1,342 @@
+CKEditor 4 Changelog
+====================
+
+## CKEditor 4.3
+
+New Features:
+
+* [#10612](http://dev.ckeditor.com/ticket/10612): Internet Explorer 11 support.
+* [#10869](http://dev.ckeditor.com/ticket/10869): Widgets: Added better integration with the [Elements Path](http://ckeditor.com/addon/elementspath) plugin.
+* [#10886](http://dev.ckeditor.com/ticket/10886): Widgets: Added tooltip to the drag handle.
+* [#10933](http://dev.ckeditor.com/ticket/10933): Widgets: Introduced drag and drop of block widgets with the [Line Utilities](http://ckeditor.com/addon/lineutils) plugin.
+* [#10936](http://dev.ckeditor.com/ticket/10936): Widget System changes for easier integration with other dialog systems.
+* [#10895](http://dev.ckeditor.com/ticket/10895): [Enhanced Image](http://ckeditor.com/addon/image2): Added file browser integration.
+* [#11002](http://dev.ckeditor.com/ticket/11002): Added the [`draggable`](http://docs.ckeditor.com/#!/api/CKEDITOR.plugins.widget.definition-property-draggable) option to disable drag and drop support for widgets.
+* [#10937](http://dev.ckeditor.com/ticket/10937): [Mathematical Formulas](http://ckeditor.com/addon/mathjax) widget improvements:
+ * loading indicator ([#10948](http://dev.ckeditor.com/ticket/10948)),
+ * applying paragraph changes (like font color change) to iframe ([#10841](http://dev.ckeditor.com/ticket/10841)),
+ * Firefox and IE9 clipboard fixes ([#10857](http://dev.ckeditor.com/ticket/10857)),
+ * fixing same origin policy issue ([#10840](http://dev.ckeditor.com/ticket/10840)),
+ * fixing undo bugs ([#10842](http://dev.ckeditor.com/ticket/10842), [#10930](http://dev.ckeditor.com/ticket/10930)),
+ * fixing other minor bugs.
+* [#10862](http://dev.ckeditor.com/ticket/10862): [Placeholder](http://ckeditor.com/addon/placeholder) plugin was rewritten as a widget.
+* [#10822](http://dev.ckeditor.com/ticket/10822): Added styles system integration with non-editable elements (for example widgets) and their nested editables. Styles cannot change non-editable content and are applied in nested editable only if allowed by its type and content filter.
+* [#10856](http://dev.ckeditor.com/ticket/10856): Menu buttons will now toggle the visibility of their panels when clicked multiple times. [Language](http://ckeditor.com/addon/language) plugin fixes: Added active language highlighting, added an option to remove the language.
+* [#10028](http://dev.ckeditor.com/ticket/10028): New [`config.dialog_noConfirmCancel`](http://docs.ckeditor.com/#!/api/CKEDITOR.config-cfg-dialog_noConfirmCancel) configuration option that eliminates the need to confirm closing of a dialog window when the user changed any of its fields.
+* [#10848](http://dev.ckeditor.com/ticket/10848): Integrate remaining plugins ([Styles](http://ckeditor.com/addon/stylescombo), [Format](http://ckeditor.com/addon/format), [Font](http://ckeditor.com/addon/font), [Color Button](http://ckeditor.com/addon/colorbutton), [Language](http://ckeditor.com/addon/language) and [Indent](http://ckeditor.com/addon/indent)) with [active filter](http://docs.ckeditor.com/#!/api/CKEDITOR.editor-property-activeFilter).
+* [#10855](http://dev.ckeditor.com/ticket/10855): Change the extension of emoticons in the [BBCode](http://ckeditor.com/addon/bbcode) sample from GIF to PNG.
+
+Fixed Issues:
+
+* [#10831](http://dev.ckeditor.com/ticket/10831): [Enhanced Image](http://ckeditor.com/addon/image2): Merged `image2inline` and `image2block` into one `image2` widget.
+* [#10835](http://dev.ckeditor.com/ticket/10835): [Enhanced Image](http://ckeditor.com/addon/image2): Improved visibility of the resize handle.
+* [#10836](http://dev.ckeditor.com/ticket/10836): [Enhanced Image](http://ckeditor.com/addon/image2): Preserve custom mouse cursor while resizing the image.
+* [#10939](http://dev.ckeditor.com/ticket/10939): [Firefox] [Enhanced Image](http://ckeditor.com/addon/image2): hovering the image causes it to change.
+* [#10866](http://dev.ckeditor.com/ticket/10866): Fixed: Broken *Tab* key navigation in the [Enhanced Image](http://ckeditor.com/addon/image2) dialog window.
+* [#10833](http://dev.ckeditor.com/ticket/10833): Fixed: *Lock ratio* option should be on by default in the [Enhanced Image](http://ckeditor.com/addon/image2) dialog window.
+* [#10881](http://dev.ckeditor.com/ticket/10881): Various improvements to *Enter* key behavior in nested editables.
+* [#10879](http://dev.ckeditor.com/ticket/10879): [Remove Format](http://ckeditor.com/addon/removeformat) should not leak from a nested editable.
+* [#10877](http://dev.ckeditor.com/ticket/10877): Fixed: [WebSpellChecker](http://ckeditor.com/addon/wsc) fails to apply changes if a nested editable was focused.
+* [#10877](http://dev.ckeditor.com/ticket/10877): Fixed: [SCAYT](http://ckeditor.com/addon/wsc) blocks typing in nested editables.
+* [#11079](http://dev.ckeditor.com/ticket/11079): Add button icons to the [Placeholder](http://ckeditor.com/addon/placeholder) sample.
+* [#10870](http://dev.ckeditor.com/ticket/10870): The `paste` command is no longer being disabled when the clipboard is empty.
+* [#10854](http://dev.ckeditor.com/ticket/10854): Fixed: Firefox prepends `<%= project.description %> ` to ``, so it is stripped by the HTML data processor. +* [#10823](http://dev.ckeditor.com/ticket/10823): Fixed: [Link](http://ckeditor.com/addon/link) plugin does not work with non-editable content. +* [#10828](http://dev.ckeditor.com/ticket/10828): [Magic Line](http://ckeditor.com/addon/magicline) integration with the Widget System. +* [#10865](http://dev.ckeditor.com/ticket/10865): Improved hiding copybin, so copying widgets works smoothly. +* [#11066](http://dev.ckeditor.com/ticket/11066): Widget's private parts use CSS reset. +* [#11027](http://dev.ckeditor.com/ticket/11027): Fixed: Block commands break on widgets; added the [`contentDomInvalidated`](http://docs.ckeditor.com/#!/api/CKEDITOR.editor-event-contentDomInvalidated) event. +* [#10430](http://dev.ckeditor.com/ticket/10430): Resolve dependence of the [Image](http://ckeditor.com/addon/image) plugin on the [Form Elements](http://ckeditor.com/addon/forms) plugin. +* [#10911](http://dev.ckeditor.com/ticket/10911): Fixed: Browser *Alt* hotkeys will no longer be blocked while a widget is focused. +* [#11082](http://dev.ckeditor.com/ticket/11082): Fixed: Selected widget is not copied or cut when using toolbar buttons or context menu. +* [#11083](http://dev.ckeditor.com/ticket/11083): Fixed list and div element application to block widgets. +* [#10887](http://dev.ckeditor.com/ticket/10887): Internet Explorer 8 compatibility issues related to the Widget System. +* [#11074](http://dev.ckeditor.com/ticket/11074): Temporarily disabled inline widget drag and drop, because of seriously buggy native `range#moveToPoint` method. +* [#11098](http://dev.ckeditor.com/ticket/11098): Fixed: Wrong selection position after undoing widget drag and drop. +* [#11110](http://dev.ckeditor.com/ticket/11110): Fixed: IFrame and Flash objects are being incorrectly pasted in certain conditions. +* [#11129](http://dev.ckeditor.com/ticket/11129): Page break is lost when loading data. +* [#11123](http://dev.ckeditor.com/ticket/11123): [Firefox] Widget is destroyed after being dragged outside of ``. +* [#11124](http://dev.ckeditor.com/ticket/11124): Fixed the [Elements Path](http://ckeditor.com/addon/elementspath) in an editor using the [Div Editing Area](http://ckeditor.com/addon/divarea). + +## CKEditor 4.3 Beta + +New Features: + +* [#9764](http://dev.ckeditor.com/ticket/9764): Widget System. + * [Widget plugin](http://ckeditor.com/addon/widget) introducing the [Widget API](http://docs.ckeditor.com/#!/api/CKEDITOR.plugins.widget). + * New [`editor.enterMode`](http://docs.ckeditor.com/#!/api/CKEDITOR.editor-property-enterMode) and [`editor.shiftEnterMode`](http://docs.ckeditor.com/#!/api/CKEDITOR.editor-property-shiftEnterMode) properties – normalized versions of [`config.enterMode`](http://docs.ckeditor.com/#!/api/CKEDITOR.config-cfg-enterMode) and [`config.shiftEnterMode`](http://docs.ckeditor.com/#!/api/CKEDITOR.config-cfg-shiftEnterMode). + * Dynamic editor settings. Starting from CKEditor 4.3 Beta, *Enter* mode values and [content filter](http://docs.ckeditor.com/#!/guide/dev_advanced_content_filter) instances may be changed dynamically (for example when the caret was placed in an element in which editor features should be adjusted). When you are implementing a new editor feature, you should base its behavior on [dynamic](http://docs.ckeditor.com/#!/api/CKEDITOR.editor-property-activeEnterMode) or [static](http://docs.ckeditor.com/#!/api/CKEDITOR.editor-property-enterMode) *Enter* mode values depending on whether this feature works in selection context or globally on editor content. + * Dynamic *Enter* mode values – [`editor.setActiveEnterMode`](http://docs.ckeditor.com/#!/api/CKEDITOR.editor-method-setActiveEnterMode) method, [`editor.activeEnterModeChange`](http://docs.ckeditor.com/#!/api/CKEDITOR.editor-event-activeEnterModeChange) event, and two properties: [`editor.activeEnterMode`](http://docs.ckeditor.com/#!/api/CKEDITOR.editor-property-activeEnterMode) and [`editor.activeShiftEnterMode`](http://docs.ckeditor.com/#!/api/CKEDITOR.editor-property-activeShiftEnterMode). + * Dynamic content filter instances – [`editor.setActiveFilter`](http://docs.ckeditor.com/#!/api/CKEDITOR.editor-method-setActiveFilter) method, [`editor.activeFilterChange`](http://docs.ckeditor.com/#!/api/CKEDITOR.editor-event-activeFilterChange) event, and [`editor.activeFilter`](http://docs.ckeditor.com/#!/api/CKEDITOR.editor-property-activeFilter) property. + * "Fake" selection was introduced. It makes it possible to virtually select any element when the real selection remains hidden. See the [`selection.fake`](http://docs.ckeditor.com/#!/api/CKEDITOR.dom.selection-method-fake) method. + * Default [`htmlParser.filter`](http://docs.ckeditor.com/#!/api/CKEDITOR.htmlParser.filter) rules are not applied to non-editable elements (elements with `contenteditable` attribute set to `false` and their descendants) anymore. To add a rule which will be applied to all elements you need to pass an additional argument to the [`filter.addRules`](http://docs.ckeditor.com/#!/api/CKEDITOR.htmlParser.filter-method-addRules) method. + * Dozens of new methods were introduced – most interesting ones: + * [`document.find`](http://docs.ckeditor.com/#!/api/CKEDITOR.dom.document-method-find), + * [`document.findOne`](http://docs.ckeditor.com/#!/api/CKEDITOR.dom.document-method-findOne), + * [`editable.insertElementIntoRange`](http://docs.ckeditor.com/#!/api/CKEDITOR.editable-method-insertElementIntoRange), + * [`range.moveToClosestEditablePosition`](http://docs.ckeditor.com/#!/api/CKEDITOR.dom.range-method-moveToClosestEditablePosition), + * New methods for [`htmlParser.node`](http://docs.ckeditor.com/#!/api/CKEDITOR.htmlParser.node) and [`htmlParser.element`](http://docs.ckeditor.com/#!/api/CKEDITOR.htmlParser.element). +* [#10659](http://dev.ckeditor.com/ticket/10659): New [Enhanced Image](http://ckeditor.com/addon/image2) plugin that introduces a widget with integrated image captions, an option to center images, and dynamic "click and drag" resizing. +* [#10664](http://dev.ckeditor.com/ticket/10664): New [Mathematical Formulas](http://ckeditor.com/addon/mathjax) plugin that introduces the MathJax widget. +* [#7987](https://dev.ckeditor.com/ticket/7987): New [Language](http://ckeditor.com/addon/language) plugin that implements Language toolbar button to support [WCAG 3.1.2 Language of Parts](http://www.w3.org/TR/UNDERSTANDING-WCAG20/meaning-other-lang-id.html). +* [#10708](http://dev.ckeditor.com/ticket/10708): New [smileys](http://ckeditor.com/addon/smiley). + +## CKEditor 4.2.3 + +Fixed Issues: + +* [#10994](http://dev.ckeditor.com/ticket/10994): Fixed: Loading external jQuery library when opening the [jQuery Adapter](http://docs.ckeditor.com/#!/guide/dev_jquery) sample directly from file. +* [#10975](http://dev.ckeditor.com/ticket/10975): [IE] Fixed: Error thrown while opening the color palette. +* [#9929](http://dev.ckeditor.com/ticket/9929): [Blink/WebKit] Fixed: A non-breaking space is created once a character is deleted and a regular space is typed. +* [#10963](http://dev.ckeditor.com/ticket/10963): Fixed: JAWS issue with the keyboard shortcut for [Magic Line](http://ckeditor.com/addon/magicline). +* [#11096](http://dev.ckeditor.com/ticket/11096): Fixed: TypeError: Object has no method 'is'. + +## CKEditor 4.2.2 + +Fixed Issues: + +* [#9314](http://dev.ckeditor.com/ticket/9314): Fixed: Incorrect error message on closing a dialog window without saving changs. +* [#10308](http://dev.ckeditor.com/ticket/10308): [IE10] Fixed: Unspecified error when deleting a row. +* [#10945](http://dev.ckeditor.com/ticket/10945): [Chrome] Fixed: Clicking with a mouse inside the editor does not show the caret. +* [#10912](http://dev.ckeditor.com/ticket/10912): Prevent default action when content of a non-editable link is clicked. +* [#10913](http://dev.ckeditor.com/ticket/10913): Fixed [`CKEDITOR.plugins.addExternal`](http://docs.ckeditor.com/#!/api/CKEDITOR.resourceManager-method-addExternal) not handling paths including file name specified. +* [#10666](http://dev.ckeditor.com/ticket/10666): Fixed [`CKEDITOR.tools.isArray`](http://docs.ckeditor.com/#!/api/CKEDITOR.tools-method-isArray) not working cross frame. +* [#10910](http://dev.ckeditor.com/ticket/10910): [IE9] Fixed JavaScript error thrown in Compatibility Mode when clicking and/or typing in the editing area. +* [#10868](http://dev.ckeditor.com/ticket/10868): [IE8] Prevent the browser from crashing when applying the Inline Quotation style. +* [#10915](http://dev.ckeditor.com/ticket/10915): Fixed: Invalid CSS filter in the Kama skin. +* [#10914](http://dev.ckeditor.com/ticket/10914): Plugins [Indent List](http://ckeditor.com/addon/indentlist) and [Indent Block](http://ckeditor.com/addon/indentblock) are now included in the build configuration. +* [#10812](http://dev.ckeditor.com/ticket/10812): Fixed [`range#createBookmark2`](http://docs.ckeditor.com/#!/api/CKEDITOR.dom.range-method-createBookmark2) incorrectly normalizing offsets. This bug was causing many issues: [#10850](http://dev.ckeditor.com/ticket/10850), [#10842](http://dev.ckeditor.com/ticket/10842). +* [#10951](http://dev.ckeditor.com/ticket/10951): Reviewed and optimized focus handling on panels (combo, menu buttons, color buttons, and context menu) to enhance accessibility. Fixed [#10705](http://dev.ckeditor.com/ticket/10705), [#10706](http://dev.ckeditor.com/ticket/10706) and [#10707](http://dev.ckeditor.com/ticket/10707). +* [#10704](http://dev.ckeditor.com/ticket/10704): Fixed a JAWS issue with the Select Color dialog window title not being announced. +* [#10753](http://dev.ckeditor.com/ticket/10753): The floating toolbar in inline instances now has a dedicated accessibility label. + +## CKEditor 4.2.1 + +Fixed Issues: + +* [#10301](http://dev.ckeditor.com/ticket/10301): [IE9-10] Undo fails after 3+ consecutive paste actions with a JavaScript error. +* [#10689](http://dev.ckeditor.com/ticket/10689): Save toolbar button saves only the first editor instance. +* [#10368](http://dev.ckeditor.com/ticket/10368): Move language reading direction definition (`dir`) from main language file to core. +* [#9330](http://dev.ckeditor.com/ticket/9330): Fixed pasting anchors from MS Word. +* [#8103](http://dev.ckeditor.com/ticket/8103): Fixed pasting nested lists from MS Word. +* [#9958](http://dev.ckeditor.com/ticket/9958): [IE9] Pressing the "OK" button will trigger the `onbeforeunload` event in the popup dialog. +* [#10662](http://dev.ckeditor.com/ticket/10662): Fixed styles from the Styles drop-down list not registering to the ACF in case when the [Shared Spaces plugin](http://ckeditor.com/addon/sharedspace) is used. +* [#9654](http://dev.ckeditor.com/ticket/9654): Problems with Internet Explorer 10 Quirks Mode. +* [#9816](http://dev.ckeditor.com/ticket/9816): Floating toolbar does not reposition vertically in several cases. +* [#10646](http://dev.ckeditor.com/ticket/10646): Removing a selected sublist or nested table with *Backspace/Delete* removes the parent element. +* [#10623](http://dev.ckeditor.com/ticket/10623): [WebKit] Page is scrolled when opening a drop-down list. +* [#10004](http://dev.ckeditor.com/ticket/10004): [ChromeVox] Button names are not announced. +* [#10731](http://dev.ckeditor.com/ticket/10731): [WebSpellChecker](http://ckeditor.com/addon/wsc) plugin breaks cloning of editor configuration. +* It is now possible to set per instance [WebSpellChecker](http://ckeditor.com/addon/wsc) plugin configuration instead of setting the configuration globally. + +## CKEditor 4.2 + +**Important Notes:** + +* Dropped compatibility support for Internet Explorer 7 and Firefox 3.6. + +* Both the Basic and the Standard distribution packages will not contain the new [Indent Block](http://ckeditor.com/addon/indentblock) plugin. Because of this the [Advanced Content Filter](http://docs.ckeditor.com/#!/guide/dev_advanced_content_filter) might remove block indentations from existing contents. If you want to prevent this, either [add an appropriate ACF rule to your filter](http://docs.ckeditor.com/#!/guide/dev_allowed_content_rules) or create a custom build based on the Basic/Standard package and add the Indent Block plugin in [CKBuilder](http://ckeditor.com/builder). + +New Features: + +* [#10027](http://dev.ckeditor.com/ticket/10027): Separated list and block indentation into two plugins: [Indent List](http://ckeditor.com/addon/indentlist) and [Indent Block](http://ckeditor.com/addon/indentblock). +* [#8244](http://dev.ckeditor.com/ticket/8244): Use *(Shift+)Tab* to indent and outdent lists. +* [#10281](http://dev.ckeditor.com/ticket/10281): The [jQuery Adapter](http://docs.ckeditor.com/#!/guide/dev_jquery) is now available. Several jQuery-related issues fixed: [#8261](http://dev.ckeditor.com/ticket/8261), [#9077](http://dev.ckeditor.com/ticket/9077), [#8710](http://dev.ckeditor.com/ticket/8710), [#8530](http://dev.ckeditor.com/ticket/8530), [#9019](http://dev.ckeditor.com/ticket/9019), [#6181](http://dev.ckeditor.com/ticket/6181), [#7876](http://dev.ckeditor.com/ticket/7876), [#6906](http://dev.ckeditor.com/ticket/6906). +* [#10042](http://dev.ckeditor.com/ticket/10042): Introduced [`config.title`](http://docs.ckeditor.com/#!/api/CKEDITOR.config-cfg-title) setting to change the human-readable title of the editor. +* [#9794](http://dev.ckeditor.com/ticket/9794): Added [`editor.onChange`](http://docs.ckeditor.com/#!/api/CKEDITOR.editor-event-change) event. +* [#9923](http://dev.ckeditor.com/ticket/9923): HiDPI support in the editor UI. HiDPI icons for [Moono skin](http://ckeditor.com/addon/moono) added. +* [#8031](http://dev.ckeditor.com/ticket/8031): Handle `required` attributes on ` |