##// END OF EJS Templates
Unified diff viewer for attached files with .patch or .diff extension (#1403)....
Jean-Philippe Lang -
r1502:d77c1d2829f9
parent child
Show More
@@ -0,0 +1,15
1 <h2><%=h @attachment.filename %></h2>
2
3 <div class="attachments">
4 <p><%= h("#{@attachment.description} - ") unless @attachment.description.blank? %>
5 <span class="author"><%= @attachment.author %>, <%= format_time(@attachment.created_on) %></span></p>
6 <p><%= link_to l(:button_download), {:controller => 'attachments', :action => 'download', :id => @attachment } -%>
7 <span class="size">(<%= number_to_human_size @attachment.filesize %>)</span></p>
8
9 </div>
10 &nbsp;
11 <%= render :partial => 'common/diff', :locals => {:diff => @diff, :diff_type => @diff_type} %>
12
13 <% content_for :header_tags do -%>
14 <%= stylesheet_link_tag "scm" -%>
15 <% end -%>
@@ -1,39 +1,46
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class AttachmentsController < ApplicationController
19 19 layout 'base'
20 20 before_filter :find_project, :check_project_privacy
21 21
22 def show
23 if @attachment.is_diff?
24 @diff = File.new(@attachment.diskfile, "rb").read
25 render :action => 'diff'
26 else
27 download
28 end
29 end
30
22 31 def download
23 32 # images are sent inline
24 33 send_file @attachment.diskfile, :filename => filename_for_content_disposition(@attachment.filename),
25 34 :type => @attachment.content_type,
26 35 :disposition => (@attachment.image? ? 'inline' : 'attachment')
27 rescue
28 # in case the disk file was deleted
29 render_404
30 36 end
31 37
32 38 private
33 39 def find_project
34 40 @attachment = Attachment.find(params[:id])
41 render_404 and return false unless File.readable?(@attachment.diskfile)
35 42 @project = @attachment.project
36 43 rescue
37 44 render_404
38 45 end
39 46 end
@@ -1,25 +1,29
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 module AttachmentsHelper
19 19 # displays the links to a collection of attachments
20 20 def link_to_attachments(attachments, options = {})
21 21 if attachments.any?
22 22 render :partial => 'attachments/links', :locals => {:attachments => attachments, :options => options}
23 23 end
24 24 end
25
26 def to_utf8(str)
27 str
28 end
25 29 end
@@ -1,177 +1,177
1 1 # redMine - project management software
2 2 # Copyright (C) 2006 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require 'csv'
19 19
20 20 module IssuesHelper
21 21 include ApplicationHelper
22 22
23 23 def render_issue_tooltip(issue)
24 24 @cached_label_start_date ||= l(:field_start_date)
25 25 @cached_label_due_date ||= l(:field_due_date)
26 26 @cached_label_assigned_to ||= l(:field_assigned_to)
27 27 @cached_label_priority ||= l(:field_priority)
28 28
29 29 link_to_issue(issue) + ": #{h(issue.subject)}<br /><br />" +
30 30 "<strong>#{@cached_label_start_date}</strong>: #{format_date(issue.start_date)}<br />" +
31 31 "<strong>#{@cached_label_due_date}</strong>: #{format_date(issue.due_date)}<br />" +
32 32 "<strong>#{@cached_label_assigned_to}</strong>: #{issue.assigned_to}<br />" +
33 33 "<strong>#{@cached_label_priority}</strong>: #{issue.priority.name}"
34 34 end
35 35
36 36 def sidebar_queries
37 37 unless @sidebar_queries
38 38 # User can see public queries and his own queries
39 39 visible = ARCondition.new(["is_public = ? OR user_id = ?", true, (User.current.logged? ? User.current.id : 0)])
40 40 # Project specific queries and global queries
41 41 visible << (@project.nil? ? ["project_id IS NULL"] : ["project_id IS NULL OR project_id = ?", @project.id])
42 42 @sidebar_queries = Query.find(:all,
43 43 :order => "name ASC",
44 44 :conditions => visible.conditions)
45 45 end
46 46 @sidebar_queries
47 47 end
48 48
49 49 def show_detail(detail, no_html=false)
50 50 case detail.property
51 51 when 'attr'
52 52 label = l(("field_" + detail.prop_key.to_s.gsub(/\_id$/, "")).to_sym)
53 53 case detail.prop_key
54 54 when 'due_date', 'start_date'
55 55 value = format_date(detail.value.to_date) if detail.value
56 56 old_value = format_date(detail.old_value.to_date) if detail.old_value
57 57 when 'status_id'
58 58 s = IssueStatus.find_by_id(detail.value) and value = s.name if detail.value
59 59 s = IssueStatus.find_by_id(detail.old_value) and old_value = s.name if detail.old_value
60 60 when 'assigned_to_id'
61 61 u = User.find_by_id(detail.value) and value = u.name if detail.value
62 62 u = User.find_by_id(detail.old_value) and old_value = u.name if detail.old_value
63 63 when 'priority_id'
64 64 e = Enumeration.find_by_id(detail.value) and value = e.name if detail.value
65 65 e = Enumeration.find_by_id(detail.old_value) and old_value = e.name if detail.old_value
66 66 when 'category_id'
67 67 c = IssueCategory.find_by_id(detail.value) and value = c.name if detail.value
68 68 c = IssueCategory.find_by_id(detail.old_value) and old_value = c.name if detail.old_value
69 69 when 'fixed_version_id'
70 70 v = Version.find_by_id(detail.value) and value = v.name if detail.value
71 71 v = Version.find_by_id(detail.old_value) and old_value = v.name if detail.old_value
72 72 end
73 73 when 'cf'
74 74 custom_field = CustomField.find_by_id(detail.prop_key)
75 75 if custom_field
76 76 label = custom_field.name
77 77 value = format_value(detail.value, custom_field.field_format) if detail.value
78 78 old_value = format_value(detail.old_value, custom_field.field_format) if detail.old_value
79 79 end
80 80 when 'attachment'
81 81 label = l(:label_attachment)
82 82 end
83 83
84 84 label ||= detail.prop_key
85 85 value ||= detail.value
86 86 old_value ||= detail.old_value
87 87
88 88 unless no_html
89 89 label = content_tag('strong', label)
90 90 old_value = content_tag("i", h(old_value)) if detail.old_value
91 91 old_value = content_tag("strike", old_value) if detail.old_value and (!detail.value or detail.value.empty?)
92 92 if detail.property == 'attachment' && !value.blank? && Attachment.find_by_id(detail.prop_key)
93 93 # Link to the attachment if it has not been removed
94 value = link_to(value, :controller => 'attachments', :action => 'download', :id => detail.prop_key)
94 value = link_to(value, :controller => 'attachments', :action => 'show', :id => detail.prop_key)
95 95 else
96 96 value = content_tag("i", h(value)) if value
97 97 end
98 98 end
99 99
100 100 if !detail.value.blank?
101 101 case detail.property
102 102 when 'attr', 'cf'
103 103 if !detail.old_value.blank?
104 104 label + " " + l(:text_journal_changed, old_value, value)
105 105 else
106 106 label + " " + l(:text_journal_set_to, value)
107 107 end
108 108 when 'attachment'
109 109 "#{label} #{value} #{l(:label_added)}"
110 110 end
111 111 else
112 112 case detail.property
113 113 when 'attr', 'cf'
114 114 label + " " + l(:text_journal_deleted) + " (#{old_value})"
115 115 when 'attachment'
116 116 "#{label} #{old_value} #{l(:label_deleted)}"
117 117 end
118 118 end
119 119 end
120 120
121 121 def issues_to_csv(issues, project = nil)
122 122 ic = Iconv.new(l(:general_csv_encoding), 'UTF-8')
123 123 export = StringIO.new
124 124 CSV::Writer.generate(export, l(:general_csv_separator)) do |csv|
125 125 # csv header fields
126 126 headers = [ "#",
127 127 l(:field_status),
128 128 l(:field_project),
129 129 l(:field_tracker),
130 130 l(:field_priority),
131 131 l(:field_subject),
132 132 l(:field_assigned_to),
133 133 l(:field_category),
134 134 l(:field_fixed_version),
135 135 l(:field_author),
136 136 l(:field_start_date),
137 137 l(:field_due_date),
138 138 l(:field_done_ratio),
139 139 l(:field_estimated_hours),
140 140 l(:field_created_on),
141 141 l(:field_updated_on)
142 142 ]
143 143 # Export project custom fields if project is given
144 144 # otherwise export custom fields marked as "For all projects"
145 145 custom_fields = project.nil? ? IssueCustomField.for_all : project.all_custom_fields
146 146 custom_fields.each {|f| headers << f.name}
147 147 # Description in the last column
148 148 headers << l(:field_description)
149 149 csv << headers.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end }
150 150 # csv lines
151 151 issues.each do |issue|
152 152 fields = [issue.id,
153 153 issue.status.name,
154 154 issue.project.name,
155 155 issue.tracker.name,
156 156 issue.priority.name,
157 157 issue.subject,
158 158 issue.assigned_to,
159 159 issue.category,
160 160 issue.fixed_version,
161 161 issue.author.name,
162 162 format_date(issue.start_date),
163 163 format_date(issue.due_date),
164 164 issue.done_ratio,
165 165 issue.estimated_hours,
166 166 format_time(issue.created_on),
167 167 format_time(issue.updated_on)
168 168 ]
169 169 custom_fields.each {|f| fields << show_value(issue.custom_value_for(f)) }
170 170 fields << issue.description
171 171 csv << fields.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end }
172 172 end
173 173 end
174 174 export.rewind
175 175 export
176 176 end
177 177 end
@@ -1,114 +1,118
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require "digest/md5"
19 19
20 20 class Attachment < ActiveRecord::Base
21 21 belongs_to :container, :polymorphic => true
22 22 belongs_to :author, :class_name => "User", :foreign_key => "author_id"
23 23
24 24 validates_presence_of :container, :filename, :author
25 25 validates_length_of :filename, :maximum => 255
26 26 validates_length_of :disk_filename, :maximum => 255
27 27
28 28 acts_as_event :title => :filename,
29 29 :url => Proc.new {|o| {:controller => 'attachments', :action => 'download', :id => o.id}}
30 30
31 31 cattr_accessor :storage_path
32 32 @@storage_path = "#{RAILS_ROOT}/files"
33 33
34 34 def validate
35 35 errors.add_to_base :too_long if self.filesize > Setting.attachment_max_size.to_i.kilobytes
36 36 end
37 37
38 38 def file=(incoming_file)
39 39 unless incoming_file.nil?
40 40 @temp_file = incoming_file
41 41 if @temp_file.size > 0
42 42 self.filename = sanitize_filename(@temp_file.original_filename)
43 43 self.disk_filename = Attachment.disk_filename(filename)
44 44 self.content_type = @temp_file.content_type.to_s.chomp
45 45 self.filesize = @temp_file.size
46 46 end
47 47 end
48 48 end
49 49
50 50 def file
51 51 nil
52 52 end
53 53
54 54 # Copy temp file to its final location
55 55 def before_save
56 56 if @temp_file && (@temp_file.size > 0)
57 57 logger.debug("saving '#{self.diskfile}'")
58 58 File.open(diskfile, "wb") do |f|
59 59 f.write(@temp_file.read)
60 60 end
61 61 self.digest = Digest::MD5.hexdigest(File.read(diskfile))
62 62 end
63 63 # Don't save the content type if it's longer than the authorized length
64 64 if self.content_type && self.content_type.length > 255
65 65 self.content_type = nil
66 66 end
67 67 end
68 68
69 69 # Deletes file on the disk
70 70 def after_destroy
71 71 File.delete(diskfile) if !filename.blank? && File.exist?(diskfile)
72 72 end
73 73
74 74 # Returns file's location on disk
75 75 def diskfile
76 76 "#{@@storage_path}/#{self.disk_filename}"
77 77 end
78 78
79 79 def increment_download
80 80 increment!(:downloads)
81 81 end
82 82
83 83 def project
84 84 container.project
85 85 end
86 86
87 87 def image?
88 88 self.filename =~ /\.(jpe?g|gif|png)$/i
89 89 end
90 90
91 def is_diff?
92 self.filename =~ /\.(patch|diff)$/i
93 end
94
91 95 private
92 96 def sanitize_filename(value)
93 97 # get only the filename, not the whole path
94 98 just_filename = value.gsub(/^.*(\\|\/)/, '')
95 99 # NOTE: File.basename doesn't work right with Windows paths on Unix
96 100 # INCORRECT: just_filename = File.basename(value.gsub('\\\\', '/'))
97 101
98 102 # Finally, replace all non alphanumeric, hyphens or periods with underscore
99 103 @filename = just_filename.gsub(/[^\w\.\-]/,'_')
100 104 end
101 105
102 106 # Returns an ASCII or hashed filename
103 107 def self.disk_filename(filename)
104 108 df = DateTime.now.strftime("%y%m%d%H%M%S") + "_"
105 109 if filename =~ %r{^[a-zA-Z0-9_\.\-]*$}
106 110 df << filename
107 111 else
108 112 df << Digest::MD5.hexdigest(filename)
109 113 # keep the extension if any
110 114 df << $1 if filename =~ %r{(\.[a-zA-Z0-9]+)$}
111 115 end
112 116 df
113 117 end
114 118 end
@@ -1,18 +1,18
1 1 <div class="attachments">
2 2 <% for attachment in attachments %>
3 <p><%= link_to attachment.filename, {:controller => 'attachments', :action => 'download', :id => attachment }, :class => 'icon icon-attachment' -%>
3 <p><%= link_to attachment.filename, {:controller => 'attachments', :action => 'show', :id => attachment }, :class => 'icon icon-attachment' -%>
4 4 <%= h(" - #{attachment.description}") unless attachment.description.blank? %>
5 5 <span class="size">(<%= number_to_human_size attachment.filesize %>)</span>
6 6 <% if options[:delete_url] %>
7 7 <%= link_to image_tag('delete.png'), options[:delete_url].update({:attachment_id => attachment}),
8 8 :confirm => l(:text_are_you_sure),
9 9 :method => :post,
10 10 :class => 'delete',
11 11 :title => l(:button_delete) %>
12 12 <% end %>
13 13 <% unless options[:no_author] %>
14 14 <span class="author"><%= attachment.author %>, <%= format_time(attachment.created_on) %></span>
15 15 <% end %>
16 16 </p>
17 17 <% end %>
18 18 </div>
@@ -1,41 +1,43
1 1 ActionController::Routing::Routes.draw do |map|
2 2 # Add your own custom routes here.
3 3 # The priority is based upon order of creation: first created -> highest priority.
4 4
5 5 # Here's a sample route:
6 6 # map.connect 'products/:id', :controller => 'catalog', :action => 'view'
7 7 # Keep in mind you can assign values other than :controller and :action
8 8
9 9 map.home '', :controller => 'welcome'
10 10 map.signin 'login', :controller => 'account', :action => 'login'
11 11 map.signout 'logout', :controller => 'account', :action => 'logout'
12 12
13 13 map.connect 'wiki/:id/:page/:action', :controller => 'wiki', :page => nil
14 14 map.connect 'roles/workflow/:id/:role_id/:tracker_id', :controller => 'roles', :action => 'workflow'
15 15 map.connect 'help/:ctrl/:page', :controller => 'help'
16 16 #map.connect ':controller/:action/:id/:sort_key/:sort_order'
17 17
18 18 map.connect 'issues/:issue_id/relations/:action/:id', :controller => 'issue_relations'
19 19 map.connect 'projects/:project_id/issues/:action', :controller => 'issues'
20 20 map.connect 'projects/:project_id/news/:action', :controller => 'news'
21 21 map.connect 'projects/:project_id/documents/:action', :controller => 'documents'
22 22 map.connect 'projects/:project_id/boards/:action/:id', :controller => 'boards'
23 23 map.connect 'projects/:project_id/timelog/:action/:id', :controller => 'timelog'
24 24 map.connect 'boards/:board_id/topics/:action/:id', :controller => 'messages'
25 25
26 26 map.with_options :controller => 'repositories' do |omap|
27 27 omap.repositories_show 'repositories/browse/:id/*path', :action => 'browse'
28 28 omap.repositories_changes 'repositories/changes/:id/*path', :action => 'changes'
29 29 omap.repositories_diff 'repositories/diff/:id/*path', :action => 'diff'
30 30 omap.repositories_entry 'repositories/entry/:id/*path', :action => 'entry'
31 31 omap.repositories_entry 'repositories/annotate/:id/*path', :action => 'annotate'
32 32 end
33 33
34 map.connect 'attachments/:id', :controller => 'attachments', :action => 'show'
35
34 36 # Allow downloading Web Service WSDL as a file with an extension
35 37 # instead of a file named 'wsdl'
36 38 map.connect ':controller/service.wsdl', :action => 'wsdl'
37 39
38 40
39 41 # Install the default route as the lowest priority.
40 42 map.connect ':controller/:action/:id'
41 43 end
General Comments 0
You need to be logged in to leave comments. Login now