redmine_dmsf/app/models/dmsf_file.rb
COLA@Redminetest 9e328c39ab dmsf_file_revisions are ordered by id instead of major_version, minor_version and updated_at.
Previously they were ordered by major_version DESC, minor_version DESC and updated_at DESC.
This caused problems after restoring deleted files as the updated_at field were set to the same for all revisions,
and renamed files have the same major and minor version and could therefore be sorted in the wrong order.
Why would it not be ordered by id from the start?
2016-11-24 22:23:46 +01:00

458 lines
15 KiB
Ruby

# encoding: utf-8
#
# Redmine plugin for Document Management System "Features"
#
# Copyright (C) 2011 Vít Jonáš <vit.jonas@gmail.com>
# Copyright (C) 2011-16 Karel Pičman <karel.picman@kontron.com>
#
# 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.
begin
require 'xapian'
$xapian_bindings_available = true
rescue LoadError
Rails.logger.info 'REDMAIN_XAPIAN ERROR: No Ruby bindings for Xapian installed !!. PLEASE install Xapian search engine interface for Ruby.'
$xapian_bindings_available = false
end
class DmsfFile < ActiveRecord::Base
unloadable
include RedmineDmsf::Lockable
belongs_to :project
belongs_to :dmsf_folder
belongs_to :deleted_by_user, :class_name => 'User', :foreign_key => 'deleted_by_user_id'
has_many :dmsf_file_revisions, -> { order("#{DmsfFileRevision.table_name}.id DESC") },
:dependent => :destroy
has_many :locks, -> { where(entity_type: 0).order("#{DmsfLock.table_name}.updated_at DESC") },
:class_name => 'DmsfLock', :foreign_key => 'entity_id', :dependent => :destroy
has_many :referenced_links, -> { where target_type: DmsfFile.model_name.to_s},
:class_name => 'DmsfLink', :foreign_key => 'target_id', :dependent => :destroy
STATUS_DELETED = 1
STATUS_ACTIVE = 0
scope :visible, -> { where(:deleted => STATUS_ACTIVE) }
scope :deleted, -> { where(:deleted => STATUS_DELETED) }
validates :name, :presence => true
validates_format_of :name, :with => DmsfFolder.invalid_characters,
:message => l(:error_contains_invalid_character)
validate :validates_name_uniqueness
def validates_name_uniqueness
existing_file = DmsfFile.visible.find_file_by_name(self.project, self.dmsf_folder, self.name)
errors.add(:name, l('activerecord.errors.messages.taken')) unless
existing_file.nil? || existing_file.id == self.id
end
acts_as_event :title => Proc.new { |o| o.name },
:description => Proc.new { |o|
desc = Redmine::Search.cache_store.fetch("DmsfFile-#{o.id}")
if desc
Redmine::Search.cache_store.delete("DmsfFile-#{o.id}")
else
desc = o.description
desc += ' / ' if o.description.present? && o.last_revision.comment.present?
desc += o.last_revision.comment if o.last_revision.comment.present?
end
desc
},
:url => Proc.new { |o| {:controller => 'dmsf_files', :action => 'view', :id => o} },
:datetime => Proc.new { |o| o.updated_at },
:author => Proc.new { |o| o.last_revision.user }
acts_as_searchable :columns => ["#{table_name}.name", "#{DmsfFileRevision.table_name}.title", "#{DmsfFileRevision.table_name}.description", "#{DmsfFileRevision.table_name}.comment"],
:project_key => 'project_id',
:date_column => "#{table_name}.updated_at"
before_create :default_values
def default_values
@notifications = Setting.plugin_redmine_dmsf['dmsf_default_notifications']
if @notifications == '1'
self.notification = true
else
self.notification = nil
end
end
@@storage_path = nil
def self.storage_path
return @@storage_path if @@storage_path.present?
path = Setting.plugin_redmine_dmsf['dmsf_storage_directory']
path = Pathname(Redmine::Configuration['attachments_storage_path']).join('dmsf') if path.blank? && Redmine::Configuration['attachments_storage_path'].present?
path = Rails.root.join('files/dmsf').to_s if path.blank?
path.strip if path
path
end
# Lets introduce a write for storage path, that way we can also
# better interact from test-cases etc
def self.storage_path=(path)
begin
FileUtils.mkdir_p(path) unless File.exist?(path)
rescue Exception => e
Rails.logger.error e.message
end
@@storage_path = path
end
def self.find_file_by_name(project, folder, name)
where(
:project_id => project,
:dmsf_folder_id => folder ? folder.id : nil,
:name => name).visible.first
end
def last_revision
unless defined?(@last_revision)
@last_revision = self.deleted? ? self.dmsf_file_revisions.first : self.dmsf_file_revisions.visible.first
end
@last_revision
end
def set_last_revision(new_revision)
@last_revision = new_revision
end
def deleted?
self.deleted == STATUS_DELETED
end
def delete(commit)
if locked_for_user?
Rails.logger.info l(:error_file_is_locked)
errors[:base] << l(:error_file_is_locked)
return false
end
begin
# Revisions and links of a deleted file SHOULD be deleted too
self.dmsf_file_revisions.each { |r| r.delete(commit, true) }
if commit
self.destroy
else
self.deleted = STATUS_DELETED
self.deleted_by_user = User.current
save
end
rescue Exception => e
Rails.logger.error e.message
errors[:base] << e.message
return false
end
end
def restore
if self.dmsf_folder_id && (self.dmsf_folder.nil? || self.dmsf_folder.deleted?)
errors[:base] << l(:error_parent_folder)
return false
end
self.dmsf_file_revisions.each { |r| r.restore }
self.deleted = STATUS_ACTIVE
self.deleted_by_user = nil
save
end
def title
self.last_revision ? self.last_revision.title : self.name
end
def description
self.last_revision ? self.last_revision.description : ''
end
def version
self.last_revision ? self.last_revision.version : '0'
end
def workflow
self.last_revision ? self.last_revision.workflow : nil
end
def size
self.last_revision ? self.last_revision.size : 0
end
def dmsf_path
path = self.dmsf_folder ? self.dmsf_folder.dmsf_path : []
path.push(self)
path
end
def dmsf_path_str
self.dmsf_path.map { |element| element.title }.join('/')
end
def notify?
return true if self.notification
return true if self.dmsf_folder && self.dmsf_folder.notify?
return true if !self.dmsf_folder && self.project.dmsf_notification
return false
end
def notify_deactivate
self.notification = nil
self.save!
end
def notify_activate
self.notification = true
self.save!
end
# Returns an array of projects that current user can copy file to
def self.allowed_target_projects_on_copy
projects = []
if User.current.admin?
projects = Project.visible.has_module('dmsf').all
elsif User.current.logged?
User.current.memberships.each {|m| projects << m.project if m.roles.detect {|r| r.allowed_to?(:file_manipulation)} && m.project.module_enabled?('dmsf')}
end
projects
end
def move_to(project, folder)
if self.locked_for_user?
errors[:base] << l(:error_file_is_locked)
return false
end
# If the target project differs from the source project we must physically move the disk files
if self.project != project
self.dmsf_file_revisions.all.each do |rev|
if File.exist? rev.disk_file(self.project)
FileUtils.mv rev.disk_file(self.project), rev.disk_file(project)
end
end
end
self.project = project
self.dmsf_folder = folder
new_revision = self.last_revision.clone
new_revision.dmsf_file = self
new_revision.comment = l(:comment_moved_from, :source => "#{self.project.identifier}:#{self.dmsf_path_str}")
new_revision.custom_values = []
self.last_revision.custom_values.each do |cv|
new_revision.custom_values << CustomValue.new({:custom_field => cv.custom_field, :value => cv.value})
end
self.set_last_revision(new_revision)
self.save && new_revision.save
end
def copy_to(project, folder)
# If the target project differs from the source project we must physically move the disk files
if self.project != project
self.dmsf_file_revisions.all.each do |rev|
if File.exist? rev.disk_file(self.project)
FileUtils.cp rev.disk_file(self.project), rev.disk_file(project)
end
end
end
file = DmsfFile.new
file.dmsf_folder = folder
file.project = project
file.name = self.name
file.notification = Setting.plugin_redmine_dmsf['dmsf_default_notifications'].present?
if file.save && self.last_revision
new_revision = self.last_revision.clone
new_revision.dmsf_file = file
new_revision.comment = l(:comment_copied_from, :source => "#{self.project.identifier}: #{self.dmsf_path_str}")
new_revision.custom_values = []
self.last_revision.custom_values.each do |cv|
new_revision.custom_values << CustomValue.new({:custom_field => cv.custom_field, :value => cv.value})
end
file.delete(true) unless new_revision.save
end
return file
end
# To fulfill searchable module expectations
def self.search(tokens, projects = nil, options = {}, user = User.current)
tokens = [] << tokens unless tokens.is_a?(Array)
projects = [] << projects if projects.is_a?(Project)
project_ids = projects.collect(&:id) if projects
if options[:offset]
limit_options = ["dmsf_files.updated_at #{options[:before] ? '<' : '>'} ?", options[:offset]]
end
if options[:titles_only]
columns = [searchable_options[:columns][1]]
else
columns = searchable_options[:columns]
end
token_clauses = columns.collect{ |column| "(LOWER(#{column}) LIKE ?)" }
sql = (['(' + token_clauses.join(' OR ') + ')'] * tokens.size).join(options[:all_words] ? ' AND ' : ' OR ')
find_options = [sql, * (tokens.collect {|w| "%#{w.downcase}%"} * token_clauses.size).sort]
project_conditions = []
project_conditions << Project.allowed_to_condition(user, :view_dmsf_files)
project_conditions << "#{DmsfFile.table_name}.project_id IN (#{project_ids.join(',')})" if project_ids.present?
results = []
scope = self.visible.joins(:project, :dmsf_file_revisions)
scope = scope.limit(options[:limit]) unless options[:limit].blank?
scope = scope.where(limit_options) unless limit_options.blank?
scope = scope.where(project_conditions.join(' AND '))
results = scope.where(find_options).uniq.to_a
if !options[:titles_only] && $xapian_bindings_available
database = nil
begin
lang = Setting.plugin_redmine_dmsf['dmsf_stemming_lang'].strip
databasepath = File.join(
Setting.plugin_redmine_dmsf['dmsf_index_database'].strip, lang)
database = Xapian::Database.new(databasepath)
rescue Exception => e
Rails.logger.warn "REDMAIN_XAPIAN ERROR: Xapian database is not properly set, initiated or it's corrupted."
Rails.logger.warn e.message
end
if database
enquire = Xapian::Enquire.new(database)
query_string = tokens.join(' ')
qp = Xapian::QueryParser.new()
stemmer = Xapian::Stem.new(lang)
qp.stemmer = stemmer
qp.database = database
case Setting.plugin_redmine_dmsf['dmsf_stemming_strategy'].strip
when 'STEM_NONE'
qp.stemming_strategy = Xapian::QueryParser::STEM_NONE
when 'STEM_SOME'
qp.stemming_strategy = Xapian::QueryParser::STEM_SOME
when 'STEM_ALL'
qp.stemming_strategy = Xapian::QueryParser::STEM_ALL
end
if options[:all_words]
qp.default_op = Xapian::Query::OP_AND
else
qp.default_op = Xapian::Query::OP_OR
end
query = qp.parse_query(query_string)
enquire.query = query
matchset = enquire.mset(0, 1000)
if matchset
matchset.matches.each { |m|
docdata = m.document.data{url}
dochash = Hash[*docdata.scan(/(url|sample|modtime|author|type|size)=\/?([^\n\]]+)/).flatten]
filename = dochash['url']
if filename
dmsf_attrs = filename.scan(/^([^\/]+\/[^_]+)_([\d]+)_(.*)$/)
id_attribute = 0
id_attribute = dmsf_attrs[0][1] if dmsf_attrs.length > 0
next if dmsf_attrs.length == 0 || id_attribute == 0
next unless results.select{|f| f.id.to_s == id_attribute}.empty?
dmsf_file = DmsfFile.visible.where(limit_options).where(:id => id_attribute).first
if dmsf_file
if user.allowed_to?(:view_dmsf_files, dmsf_file.project) &&
(project_ids.blank? || (project_ids.include?(dmsf_file.project.id)))
Redmine::Search.cache_store.write("DmsfFile-#{dmsf_file.id}",
dochash['sample'].force_encoding('UTF-8')) if dochash['sample']
break if(!options[:limit].blank? && results.count >= options[:limit])
results << dmsf_file
end
end
end
}
end
end
end
[results, results.count]
end
def self.search_result_ranks_and_ids(tokens, user = User.current, projects = nil, options = {})
r = self.search(tokens, projects, options, user)[0]
r.map{ |f| [f.updated_at.to_i, f.id]}
end
def display_name
if self.name.length > 50
return "#{self.name[0, 25]}...#{self.name[-25, 25]}"
end
self.name
end
def image?
self.last_revision && !!(self.last_revision.disk_filename =~ /\.(bmp|gif|jpg|jpe|jpeg|png|svg)$/i)
end
def preview(limit)
result = 'No preview available'
if (self.last_revision.disk_filename =~ /\.(txt|ini|diff|c|cpp|php|csv|rb|h|erb|html|css|py)$/i)
begin
f = File.new(self.last_revision.disk_file)
f.each_line do |line|
case f.lineno
when 1
result = line
when limit.to_i + 1
break
else
result << line
end
end
rescue Exception => e
result = e.message
end
end
result
end
def formatted_name(format)
if self.last_revision
self.last_revision.formatted_name(format)
else
self.name
end
end
def owner?(user)
self.last_revision && (self.last_revision.user == user)
end
def involved?(user)
self.dmsf_file_revisions.each do |file_revision|
return true if file_revision.user == user
end
false
end
end