#-- # Copyright (C) 2007 Dimitrij Denissenko # Please read LICENSE document for more information. #++ class Ticket < ActiveRecord::Base belongs_to :milestone belongs_to :priority belongs_to :status belongs_to :project belongs_to :assigned_user, :class_name => 'User', :foreign_key => 'assigned_user_id' belongs_to :user has_many :ticket_changes, :order => 'created_at', :dependent => :destroy has_one :attachment, :as => :attachable, :dependent => :destroy has_and_belongs_to_many :subscribers, :class_name => 'User', :join_table => 'ticket_subscribers', :uniq => true has_and_belongs_to_many :ticket_properties, :include => [:ticket_property_type], :uniq => true validates_presence_of :author, :summary, :content, :status_id, :priority_id, :project_id validates_as_email :email, :allow_nil => true attr_accessible :assigned_user_login, :status_id, :priority_id, :milestone_id, :property_ids attr_accessor :subscribe_current_user, :original_attributes acts_as_spam_protectable define_previewable( :title => Proc.new{|o| _('Ticket #%s (%s) reported by %s - %s', o.id, o.status.name, o.author, o.summary)}, :content => [:content, 2], :url => { :controller => 'tickets', :action => 'show', :params => Proc.new{|o| {:id => o.id}} }, :date_column => :created_at, :find_options => { :include => [:status] }, :searchable_columns => ['tickets.summary', 'tickets.content', 'tickets.author', 'tickets.id'], :feed => :tickets ) cattr_accessor :attributes_to_monitor self.attributes_to_monitor = ['assigned_user_id', 'status_id', 'priority_id', 'milestone_id', 'property_ids'] cattr_accessor :static_property_klasses self.static_property_klasses = ['Status', 'Priority', 'Milestone'] protected :ticket_property_ids, :ticket_property_ids= protected def before_validation_on_create unless User.current.public? self.user = User.current end if self.user && !self.user.public? self.author = self.user.name self.email = self.user.email end true end def before_validation_on_update last_change = self.ticket_changes.last if last_change && last_change.new_record? last_change.changes = self.changed_attributes end true end def before_validation self.email = nil if self.email.blank? true end def validate_on_create self.attachment.errors.each_full do |msg| self.errors.add(:attachment, msg) end unless self.attachment.blank? || self.attachment.valid? true end def validate unless self.project.milestones_at(self.created_at).find{|m| self.milestone_id == m.id } self.errors.add(:milestone_id, ActiveRecord::Errors.default_error_messages[:inclusion]) end if self.milestone_id && self.project true end def before_save self.status ||= Status.default self.priority ||= Priority.default true end def after_save unless self.property_ids.blank? self.ticket_property_ids = self.property_ids end true end def captions_for_changed_ticket_properties(property_ids) options = { :include => [:ticket_property_type] } Project.current.ticket_properties.find(property_ids, options).inject({}) do |result, property| result[property.ticket_property_type.name] = property.name result end end def attribute_change(class_name, old_id, new_id, name_method = :name) klass = class_name.constantize old_value = klass.find(old_id).send(name_method) rescue nil if old_id new_value = klass.find(new_id).send(name_method) rescue nil if new_id attribute_change_hash(old_value, new_value) end def attribute_change_hash(old_value, new_value) (old_value || new_value) && old_value != new_value ? {:old => old_value, :new => new_value} : nil end public # Returns a hash of attributes being modified since last save def changed_attributes return {} if self.original_attributes.blank? @changed_attributes ||= self.class.attributes_to_monitor.inject({}) do |result, attr_name| old_value, new_value = self.original_attributes[attr_name], send(attr_name) case attr_name when 'property_ids' old_props = captions_for_changed_ticket_properties(old_value) new_props = captions_for_changed_ticket_properties(new_value) old_props.each do |(k, v)| change = attribute_change_hash(v, new_props.delete(k)) result[k] = change if change end new_props.each do |(k, v)| change = attribute_change_hash(old_props[k], v) result[k] = change if change end when 'assigned_user_id' change = attribute_change('User', old_value, new_value, :login) result['Assigned user'] = change if change when 'status_id' change = attribute_change('Status', old_value, new_value) result['Status'] = change if change when 'priority_id' change = attribute_change('Priority', old_value, new_value) result['Priority'] = change if change when 'milestone_id' change = attribute_change('Milestone', old_value, new_value) result['Milestone'] = change if change else result[attr_name.humanize] = attribute_change_hash(old_value, new_value) end if old_value != new_value result end end def protected_attributes=(params) self.author = params[:author] if params[:author] self.email = params[:email] if params[:email] self.summary = params[:summary] if params[:summary] self.content = params[:content] if params[:content] self.attachment = Attachment.parse(params[:attachment]) end def property_ids read_attribute(:property_ids) || (self.property_ids = self.ticket_property_ids) end def property_ids=(values) values = values.is_a?(Array) ? values.map { |id| Kernel.Float(id).to_i rescue nil }.compact : nil write_attribute(:property_ids, values) end def has_attachment? !self.attachment.blank? end def assigned_user_login=(user_login) self.project ||= Project.current user_obj = self.project ? self.project.find_in_users_by_login(user_login) : nil if user_obj && !user_obj.public? && user_obj.has_permission?(self.project, :create_and_work_on_tickets) self.assigned_user = user_obj else self.assigned_user = nil end end # Returns the user name of the assigned user, blank string if none assigned def assigned_user_login read_attribute(:assigned_user_login) || (self.assigned_user ? self.assigned_user.login : nil) end def status_id read_attribute(:status_id) || write_attribute(:status_id, Status.default.id) end def priority_id read_attribute(:priority_id) || write_attribute(:priority_id, Priority.default.id) end def property_map @property_map ||= ticket_properties.index_by { |prop| prop.ticket_property_type.id } end def state self.status.id == self.status_id ? self.status.state : self.status.reload.state end def permitted_subscribers subscribers.select do |user| !user.public? && user.has_permission?(self.project, :view_tickets) && user.has_permission?(self.project, :subscribe_to_tickets) end end def add_subscriber(user) if !user.public? && user.has_permission?(self.project, :subscribe_to_tickets) self.subscribers << user end end def remove_subscriber(user) if !user.public? && user.has_permission?(self.project, :subscribe_to_tickets) self.subscribers.delete(user) end end def toggle_subscription(user) subscriber?(user) ? remove_subscriber(user) : add_subscriber(user) subscriber?(user) end def subscriber?(user) self.subscribers.include?(user) end def clear_association_cache #:nodoc: super @property_map = @changed_attributes = nil write_attribute(:property_ids, nil) end def monitor_attribute_changes! self.original_attributes = Ticket.attributes_to_monitor.inject({}) do |result, attr_name| result[attr_name] = send(attr_name) result end end def previous_ticket(filter_set, report = nil) options = self.class.options_for_pagination(filter_set, report) options.merge!(:order => 'tickets.updated_at ASC') options[:conditions].first << ' AND tickets.updated_at > ?' options[:conditions] << self.updated_at self.class.find(:first, options) end def next_ticket(filter_set, report = nil) options = self.class.options_for_pagination(filter_set, report) options.merge!(:order => 'tickets.updated_at DESC') options[:conditions].first << ' AND tickets.updated_at < ?' options[:conditions] << self.updated_at self.class.find(:first, options) end class << self # Required for AKismet def content_type 'ticket' end def options_for_pagination(filter_set, report = nil) opts = { :include => [ :user, :assigned_user, :subscribers, :status, :priority, :milestone, :ticket_changes, { :ticket_properties => :ticket_property_type } ], :conditions => conditions_option_for_pagination(filter_set, report), :joins => joins_option_for_pagination(filter_set, report) } end protected def joins_option_for_pagination(filter_set, report = nil) joins = filter_set.dynamic_filters(true).map do |filter| mt_name = "ticket_properties_tickets_#{filter.type_id}" sanitize_sql([ "INNER JOIN ticket_properties_tickets AS #{mt_name} " + "ON #{mt_name}.ticket_property_id IN (?) " + "AND #{mt_name}.ticket_id = tickets.id", filter.selected ]) end joins.blank? ? nil : joins.join(' ') end def conditions_option_for_pagination(filter_set, report = nil) conditions = ['tickets.project_id = ?', Project.current.id] unless User.current.public? if filter_set[:my_tickets].include?(1) conditions.first << ' AND tickets.assigned_user_id = ?' conditions << User.current.id end if filter_set[:my_tickets].include?(2) conditions.first << ' AND tickets.user_id = ?' conditions << User.current.id end if filter_set[:my_tickets].include?(3) conditions.first << ' AND ticket_subscribers.user_id = ?' conditions << User.current.id end end filter_set.static_filters(true).each do |filter| conditions.first << " AND #{filter.name.tableize}.id IN (?)" conditions << filter.selected end if report && !report.since_date.blank? conditions.first << ' AND tickets.updated_at > ?' conditions << report.since_date end if filter_set.search_term fields = ['tickets.author', 'tickets.summary', 'tickets.content', 'ticket_changes.author', 'ticket_changes.content'] expression = fields.map do |field| "#{field} LIKE ?" end.join(' OR ') conditions.first << " AND ( #{expression} )" conditions += [filter_set.search_term] * fields.size end conditions end end end