Refactoring the avatar helpers, and adding avatar display to teams.
[gitorious:yousource.git] / app / helpers / application_helper.rb
1 # encoding: utf-8
2 #--
3 #   Copyright (C) 2009 Nokia Corporation and/or its subsidiary(-ies)
4 #   Copyright (C) 2007, 2008 Johan Sørensen <johan@johansorensen.com>
5 #   Copyright (C) 2008 August Lilleaas <augustlilleaas@gmail.com>
6 #   Copyright (C) 2008 David A. Cuadrado <krawek@gmail.com>
7 #   Copyright (C) 2008 Tor Arne Vestbø <tavestbo@trolltech.com>
8 #   Copyright (C) 2009 Fabio Akita <fabio.akita@gmail.com>
9 #   Copyright (C) 2009 Bill Marquette <bill.marquette@gmail.com>
10 #
11 #   This program is free software: you can redistribute it and/or modify
12 #   it under the terms of the GNU Affero General Public License as published by
13 #   the Free Software Foundation, either version 3 of the License, or
14 #   (at your option) any later version.
15 #
16 #   This program is distributed in the hope that it will be useful,
17 #   but WITHOUT ANY WARRANTY; without even the implied warranty of
18 #   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
19 #   GNU Affero General Public License for more details.
20 #
21 #   You should have received a copy of the GNU Affero General Public License
22 #   along with this program.  If not, see <http://www.gnu.org/licenses/>.
23 #++
24
25 # Methods added to this helper will be available to all templates in the application.
26 module ApplicationHelper
27   include TagsHelper
28   include UsersHelper
29   include BreadcrumbsHelper
30   include EventRenderingHelper
31   
32   def help_box(style = :side, icon = :help, &block)
33     out = %Q{<div class="help-box #{style} #{icon} round-5"><div class="icon #{icon}"></div>}
34     out << capture(&block)
35     out << "</div>"
36     concat(out)
37   end
38   
39   def pull_box(title, options = {}, &block)
40     css_class = options.delete(:class)
41     out = %Q{<div class="pull-box #{css_class}">}
42     out << %Q{<h3 class="round-top-5 pull-box-header">#{title}</h3>} if title
43     out << %Q{<div class="pull-box-content">}
44     out << capture(&block)
45     out << "</div></div>"
46     concat(out)
47   end
48   
49   def dialog_box(title, options = {}, &block)
50     css_class = options.delete(:class)
51     out = %Q{<div class="dialog-box #{css_class}">}
52     out << %Q{<h3 class="round-top-5 dialog-box-header">#{title}</h3>} if title
53     out << %Q{<div class="dialog-box-content">}
54     out << capture(&block)
55     out << "</div></div>"
56     concat(out)
57   end
58   
59   def markdown(text, options = [:smart])
60     rd = RDiscount.new(text.to_s, *options)
61     force_utf8(rd.to_html)
62   end
63   
64   def feed_icon(url, alt_title = "Atom feed", size = :small)
65     link_to image_tag("silk/feed.png", :class => "feed_icon"), url,
66       :alt => alt_title, :title => alt_title
67   end
68   
69   def default_css_tag_sizes
70     %w(tag_size_1 tag_size_2 tag_size_3 tag_size_4)
71   end
72   
73   def linked_tag_list_as_sentence(tags)
74     tags.map do |tag|
75       link_to(h(tag.name), search_path(:q => "category:#{h(tag.name)}"))
76     end.to_sentence
77   end
78   
79   def build_notice_for(object)
80     out =  %Q{<div class="being_constructed round-10">}
81     out <<  %Q{<div class="being_constructed_content round-10">}
82     out << %Q{  <p>#{I18n.t( "application_helper.notice_for").call(object.class.name.humanize.downcase)}</p>}
83     out << %Q{  <p class="spin">#{image_tag("spinner.gif")}</p>}
84     out << %Q{  <p class="hint">If this message persist beyond what's reasonable, feel free to #{link_to("contact us", contact_path)}</p>}
85     out << %Q{</div></div>}
86     out
87   end
88   
89   def render_if_ready(object)
90     if object.respond_to?(:ready?) && object.ready?
91       yield
92     else
93       concat(build_notice_for(object))
94     end
95   end  
96   
97   def selected_if_current_page(url_options, slack = false)
98     if slack
99       if controller.request.request_uri.index(CGI.escapeHTML(url_for(url_options))) == 0
100         "selected"
101       end
102     else
103       "selected" if current_page?(url_options)
104     end
105   end
106   
107   def submenu_selected_class_if_current?(section)
108     case section
109     when :overview
110      if %w[projects].include?(controller.controller_name )
111        return "selected"
112      end
113     when :repositories
114       if %w[repositories trees logs commits comitters comments merge_requests 
115             blobs committers].include?(controller.controller_name )
116         return "selected"
117       end
118     when :pages
119       if %w[pages].include?(controller.controller_name )
120         return "selected"
121       end
122     end
123   end
124   
125   def link_to_with_selected(name, options = {}, html_options = nil)
126     html_options = current_page?(options) ? {:class => "selected"} : nil
127     link_to(name, options = {}, html_options)
128   end
129   
130   def syntax_themes_css
131     out = []
132     if @load_syntax_themes
133       # %w[ active4d all_hallows_eve amy blackboard brilliance_black brilliance_dull 
134       #     cobalt dawn eiffel espresso_libre idle iplastic lazy mac_classic 
135       #     magicwb_amiga pastels_on_dark slush_poppies spacecadet sunburst 
136       #     twilight zenburnesque 
137       # ].each do |syntax|
138       #   out << stylesheet_link_tag("syntax_themes/#{syntax}")
139       # end
140       return stylesheet_link_tag("syntax_themes/idle")
141     end
142     out.join("\n")
143   end
144   
145   def base_url(full_url)
146     URI.parse(full_url).host
147   end
148   
149   def gravatar_url_for(email, options = {})
150     options.reverse_merge!(:default => "images/default_face.gif")
151     "http://www.gravatar.com/avatar.php?gravatar_id=" << 
152     (email.nil? ? "" : Digest::MD5.hexdigest(email)) <<
153     "&amp;default=" <<
154     u("http://#{GitoriousConfig['gitorious_host']}:#{request.port}/#{options.delete(:default)}") <<
155     options.map { |k,v| "&amp;#{k}=#{v}" }.join
156   end
157   
158   # For a User object, return either his/her avatar or the gravatar for her email address
159   # Options
160   # - Pass on :size for the height+width of the image in pixels
161   # - Pass on :version for a named version/style of the avatar
162   def avatar(user, options={})
163     if user.avatar?
164       avatar_style = options.delete(:version) || :thumb
165       image_options = { :alt => 'avatar'}.merge(:width => options[:size], :height => options[:size])
166       image_tag(user.avatar.url(avatar_style), image_options)
167     else
168       gravatar(user.email, options)
169     end
170   end
171   
172   # Returns an avatar from an email address (for instance from a commit) where we don't have an actual User object
173   def avatar_from_email(email, options={})
174     return if email.blank?
175     avatar_style = options.delete(:version) || :thumb
176     image = User.find_avatar_for_email(email, avatar_style)
177     if image == :nil
178       gravatar(email, options)
179     else
180       image_options = { :alt => 'avatar'}.merge(:width => options[:size], :height => options[:size])
181       image_tag(image, image_options)
182     end
183   end
184   
185   def gravatar(email, options = {})
186     size = options[:size]
187     image_options = { :alt => "avatar" }
188     if size
189       image_options.merge!(:width => size, :height => size)
190     end
191     image_tag(gravatar_url_for(email, options), image_options)
192   end
193   
194   def gravatar_frame(email, options = {})
195     extra_css_class = options[:style] ? " gravatar_#{options[:style]}" : ""
196     %{<div class="gravatar#{extra_css_class}">#{gravatar(email, options)}</div>}
197   end
198   
199   def flashes
200     flash.map do |type, content| 
201       content_tag(:div, content_tag(:p, content), :class => "flash_message #{type}")
202     end.join("\n")
203   end
204   
205   def commit_graph_tag(repository, ref = "master")
206     filename = Gitorious::Graphs::CommitsBuilder.filename(repository, ref)
207     if File.exist?(File.join(Gitorious::Graphs::Builder.graph_dir, filename))
208       image_tag("graphs/#{filename}")
209     end
210   end
211   
212   def commit_graph_by_author_tag(repository, ref = "master")    
213     filename = Gitorious::Graphs::CommitsByAuthorBuilder.filename(repository, ref)
214     if File.exist?(File.join(Gitorious::Graphs::Builder.graph_dir, filename))
215       image_tag("graphs/#{filename}")
216     end
217   end
218   
219   def action_and_body_for_event(event)
220     target = event.target
221     if target.nil?
222       return ["", "", ""]
223     end
224     # These are defined in event_rendering_helper.rb:
225     action, body, category = self.send("render_event_#{Action::css_class(event.action)}", event)
226     
227     body = sanitize(body, :tags => %w[a em i strong b])
228     [action, body, category]
229   end
230   
231   def link_to_remote_if(condition, name, options)
232     if condition
233       link_to_remote(name, options)
234     else
235       content_tag(:span, name)
236     end
237   end
238   
239   def sidebar_content?
240     !@content_for_sidebar.blank?
241   end
242   
243   def render_readme(repository)
244     possibilities = []
245     repository.git.git.ls_tree({:name_only => true}, "master").each do |line|
246       possibilities << line[0, line.length-1] if line =~ /README.*/
247     end
248     
249     return "" if possibilities.empty?
250     text = repository.git.git.show({}, "master:#{possibilities.first}")
251     markdown(text) rescue simple_format(sanitize(text))
252   end
253   
254   def render_markdown_help
255     render :partial => '/site/markdown_help'
256   end
257   
258   def file_path(repository, filename, head = "master")
259     project_repository_blob_path(repository.project, repository, branch_with_tree(head, filename))
260   end
261   
262   def link_to_help_toggle(dom_id, style = :image)
263     if style == :image
264       link_to_function(image_tag("help_grey.png", {
265         :alt => t("application_helper.more_info")
266       }), "$('#{dom_id}').toggle()", :class => "more_info")
267     else
268       %Q{<span class="hint">(} +
269       link_to_function("?", "$('#{dom_id}').toggle()", :class => "more_info") +
270       ")</span>"
271     end
272     
273   end
274   
275   FILE_EXTN_MAPPINGS = {
276     '.cpp' => 'cplusplus-file', 
277     '.c' => 'c-file',
278     '.h' => 'header-file',
279     '.java' => 'java-file',
280     '.sh' => 'exec-file',
281     '.exe'  => 'exec-file',
282     '.rb' => 'ruby-file',
283     '.png' => 'image-file',
284     '.jpg' => 'image-file', 
285     '.gif' => 'image-file',
286     'jpeg' => 'image-file',
287     '.zip' => 'compressed-file',
288     '.gz' => 'compressed-file'}
289   
290   def class_for_filename(filename)
291     return FILE_EXTN_MAPPINGS[File.extname(filename)] || 'file'
292   end
293   
294   def render_download_links(project, repository, head, options={})
295     links = []
296     exceptions = Array(options[:except])
297     unless exceptions.include?(:source_tree)
298       links << content_tag(:li, link_to("View source tree for #{desplat_path(head)}", 
299                   tree_path(head)), :class => "tree")
300     end
301
302     head = desplat_path(head) if head.is_a?(Array)
303     
304     if head =~ /^[a-z0-9]{40}$/ # it looks like a SHA1
305       head = head[0..7]
306     end
307     
308     {
309       'tar.gz' => 'tar',
310       # 'zip' => 'zip',
311     }.each do |extension, url_key|
312       archive_path = self.send("project_repository_archive_#{url_key}_path", project, repository, head)
313       link_html = link_to("Download #{head} as #{extension}", archive_path, 
314                                   :onclick => "Gitorious.DownloadChecker.checkURL('#{archive_path}?format=js', 'archive-box-#{head}');return false",
315                                   :class => "download-link")
316       link_callback_box = content_tag(:div, "", :class => "archive-download-box round-5 shadow-2", 
317         :id => "archive-box-#{head}", :style => "display:none;")
318       links << content_tag(:li, link_html+link_callback_box, :class => extension.split('.').last)
319     end
320     
321     if options.delete(:only_list_items)
322       links.join("\n")
323     else
324       css_classes = options[:class] || "meta"
325       content_tag(:ul, links.join("\n"), :class => "links #{css_classes}")
326     end
327   end
328   
329   def paragraphs_with_more(text)
330     return if text.blank?
331     first, rest = text.split("</p>", 2)
332     if rest.blank?
333       first + "</p>"
334     else
335       %Q{#{first} 
336         <a href="#more" onclick="$('description-rest').toggle(); this.hide()">more&hellip;</a></p>
337         <div id="description-rest" style="display:none;">#{rest}</div>}
338     end
339   end
340   
341   def markdown_hint
342     t("views.common.format_using_markdown", 
343       :markdown => %(<a href="http://daringfireball.net/projects/markdown/">Markdown</a>))
344   end
345   
346   def current_site
347     @controller.current_site
348   end
349   
350   def new_polymorphic_comment_path(parent, comment)
351     if parent
352       repo_owner_path(@repository, [@project, @repository, parent, comment])
353     else
354       repo_owner_path(@repository, [@project, @repository, comment])
355     end
356   end
357   
358   def force_utf8(str)
359     if str.respond_to?(:force_encoding)
360       str.force_encoding("utf-8")
361     else
362       str.mb_chars
363     end
364       
365   end
366   
367   # Creates a CSS styled <button>.
368   #
369   #  <%= styled_button :big, "Create user" %>
370   #  <%= styled_button :medium, "Do something!", :class => "foo", :id => "bar" %>
371   def styled_button(size_identifier, label, options = {})
372     options.reverse_merge!(:type => "submit", :class => size_identifier.to_s)
373     content_tag(:button, %{<span>#{label}</span>}, options)
374   end
375   
376   # Similar to styled_button, but creates a link_to <a>, not a <button>.
377   #
378   #  <%= button_link :big, "Sign up", new_user_path %>
379   def button_link(size_identifier, label, url, options = {})
380     options[:class] = "#{size_identifier} button_link"
381     link_to(%{<span>#{label}</span>}, url, options)
382   end
383   
384   # Array => HTML list. The option hash is applied to the <ul> tag.
385   #
386   #  <%= list(items) {|i| i.title } %>
387   #  <%= list(items, :class => "foo") {|i| link_to i, foo_path }
388   def list(items, options = {})
389     list_items = items.map {|i| %{<li>#{block_given? ? yield(i) : i}</li>} }.join("\n")
390     content_tag(:ul, list_items, options)
391   end
392   
393   def summary_box(title, content, image)
394     %{
395       <div class="summary_box">
396         <div class="summary_box_image">
397           #{image}
398         </div>
399         
400         <div class="summary_box_content">
401           <strong>#{title}</strong>
402           #{content}
403         </div>
404         
405         <div class="clear"></div>
406       </div>
407     }
408   end
409   
410   def project_summary_box(project)
411     summary_box link_to(project.title, project),
412       truncate(project.descriptions_first_paragraph, 80),
413       avatar_wrapper(default_avatar)
414   end
415   
416   def team_summary_box(team)
417     text = list([
418       "Created: #{team.created_at.strftime("%B #{team.created_at.strftime("%d").to_i.ordinalize} %Y")}",
419       "Total activities: #{team.event_count}"
420     ], :class => "simple")
421     
422     summary_box link_to(team.name, group_path(team)),
423       text,
424       glossy_homepage_avatar(team.avatar? ? image_tag(team.avatar.url(:thumb), :width => 30, :height => 30) : default_avatar)
425       
426   end
427   
428   def user_summary_box(user)
429     text = text = list([
430       "Projects: #{user.projects.count}",
431       "Total activities: #{user.events.count}"
432     ], :class => "simple")
433     
434     summary_box link_to(user.login, user),
435       text,
436       glossy_homepage_avatar_for_user(user)
437   end
438   
439   def glossy_homepage_avatar(avatar)
440     avatar_wrapper(avatar + "<span></span>")
441   end
442   
443   def glossy_homepage_avatar_for_user(user)
444     glossy_homepage_avatar(avatar(user, :size => 30, :default => "images/icon_default.png"))
445   end
446   
447   def default_avatar
448     image_tag("icon_default.png", :width => 30, :height => 30)
449   end
450   
451   # This is pretty ugly, the 'avatar' helper should do this. But we need backwards compability.
452   def avatar_wrapper(avatar)
453     content_tag(:div, avatar, :class => "glossy_avatar_wrapper")
454   end
455 end