Merge remote-tracking branch 'origin/master' into saltation
[gitorious:taladars-gitorious-saltation.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 #   Copyright (C) 2010 Christian Johansen <christian@shortcut.no>
11 #
12 #   This program is free software: you can redistribute it and/or modify
13 #   it under the terms of the GNU Affero General Public License as published by
14 #   the Free Software Foundation, either version 3 of the License, or
15 #   (at your option) any later version.
16 #
17 #   This program is distributed in the hope that it will be useful,
18 #   but WITHOUT ANY WARRANTY; without even the implied warranty of
19 #   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
20 #   GNU Affero General Public License for more details.
21 #
22 #   You should have received a copy of the GNU Affero General Public License
23 #   along with this program.  If not, see <http://www.gnu.org/licenses/>.
24 #++
25
26 # Methods added to this helper will be available to all templates in the application.
27 module ApplicationHelper
28   include ActsAsTaggableOn::TagsHelper
29   include UsersHelper
30   include BreadcrumbsHelper
31   include EventRenderingHelper
32   include RoutingHelper
33
34   GREETINGS = ["Hello", "Hi", "Greetings", "Howdy", "Heya", "G'day"]
35
36   def random_greeting
37     GREETINGS[rand(GREETINGS.length)]
38   end
39
40   def private_abbr(object)
41     return "" if !object.respond_to?('visibility_publics?') || object.visibility_publics?
42     I18n.t("visibility.private_abbr")
43   end
44
45   def help_box(style = :side, icon = :help, options = {}, &block)
46     out = %Q{<div id="#{options.delete(:id)}" style="#{options.delete(:style)}"
47                   class="help-box #{style} #{icon} round-5">
48                <div class="icon #{icon}"></div>}
49     out << capture(&block)
50     out << "</div>"
51     concat(out)
52   end
53
54   def pull_box(title, options = {}, &block)
55     css_class = options.delete(:class)
56     out = %Q{<div class="pull-box-container #{css_class}">}
57     out << %Q{<div class="pull-box-header"><h3>#{title}</h3></div>} if title
58     out << %Q{<div class="pull-box-content">}
59     out << capture(&block)
60     out << "</div></div>"
61     concat(out)
62   end
63
64   def dialog_box(title, options = {}, &block)
65     css_class = options.delete(:class)
66     out = %Q{<div class="dialog-box #{css_class}">}
67     out << %Q{<h3 class="round-top-5 dialog-box-header">#{title}</h3>} if title
68     out << %Q{<div class="dialog-box-content">}
69     out << capture(&block)
70     out << "</div></div>"
71     concat(out)
72   end
73
74   def markdown(text, options = [:smart])
75     renderer = MarkupRenderer.new(text, :markdown => options)
76     renderer.to_html
77   end
78
79   def render_markdown(text, *options)
80     # RDiscount < 1.4 doesn't support the :auto_link, use Rails' instead
81     auto_link = options.delete(:auto_link)
82     markdown_options = [:smart] + options
83     markdownized_text = markdown(text, markdown_options)
84     if auto_link
85       markdownized_text = auto_link(markdownized_text, :urls)
86     end
87     sanitize(markdownized_text)
88   end
89
90   def feed_icon(url, alt_title = "Atom feed", size = :small)
91     link_to image_tag("silk/feed.png", :class => "feed_icon"), url,
92       :alt => alt_title, :title => alt_title
93   end
94
95   def default_css_tag_sizes
96     %w(tag_size_1 tag_size_2 tag_size_3 tag_size_4)
97   end
98
99   def linked_tag_list_as_sentence(tags)
100     tags.map do |tag|
101       link_to(h(tag.name), search_path(:q => "category:#{h(tag.name)}"))
102     end.to_sentence
103   end
104
105   def build_notice_for(object, options = {})
106     out =  %Q{<div class="being_constructed round-10">}
107     out <<  %Q{<div class="being_constructed_content round-10">}
108     out << %Q{  <p>#{I18n.t( "application_helper.notice_for").call(object.class.name.humanize.downcase)}</p>}
109     if options.delete(:include_refresh_link)
110       out << %Q{<p class="spin hint"><a href="#{url_for()}">Click to refresh</a></p>}
111     else
112       out << %Q{<p class="spin">#{image_tag("spinner.gif")}</p>}
113     end
114     out << %Q{  <p class="hint">If this message persist beyond what is reasonable, feel free to #{link_to("contact us", contact_path)}</p>}
115     out << %Q{</div></div>}
116     out
117   end
118
119   def render_if_ready(object, options = {})
120     if object.respond_to?(:ready?) && object.ready?
121       yield
122     else
123       concat(build_notice_for(object, options))
124     end
125   end
126
127   def selected_if_current_page(url_options, slack = false)
128     if slack
129       if controller.request.request_uri.index(CGI.escapeHTML(url_for(url_options))) == 0
130         "selected"
131       end
132     else
133       "selected" if current_page?(url_options)
134     end
135   end
136
137   def submenu_selected_class_if_current?(section)
138     case section
139     when :overview
140      if %w[projects].include?(controller.controller_name )
141        return "selected"
142      end
143     when :repositories
144       if %w[repositories trees logs commits comitters comments merge_requests
145             blobs committers].include?(controller.controller_name )
146         return "selected"
147       end
148     when :pages
149       if %w[pages].include?(controller.controller_name )
150         return "selected"
151       end
152     end
153   end
154
155   def link_to_with_selected(name, options = {}, html_options = nil)
156     html_options = current_page?(options) ? {:class => "selected"} : nil
157     link_to(name, options = {}, html_options)
158   end
159
160   def syntax_themes_css
161     out = []
162     if @load_syntax_themes
163       # %w[ active4d all_hallows_eve amy blackboard brilliance_black brilliance_dull
164       #     cobalt dawn eiffel espresso_libre idle iplastic lazy mac_classic
165       #     magicwb_amiga pastels_on_dark slush_poppies spacecadet sunburst
166       #     twilight zenburnesque
167       # ].each do |syntax|
168       #   out << stylesheet_link_tag("syntax_themes/#{syntax}")
169       # end
170       return stylesheet_link_tag("syntax_themes/idle")
171     end
172     out.join("\n")
173   end
174
175   def base_url(full_url)
176     URI.parse(full_url).host
177   end
178
179   def gravatar_url_for(email, options = {})
180     prefix = request.ssl? ? "https://secure" : "http://www"
181     scheme = request.ssl? ? "https" : "http"
182     options.reverse_merge!(:default => "images/default_face.gif")
183     port_string = [443, 80].include?(request.port) ? "" : ":#{request.port}"
184     "#{prefix}.gravatar.com/avatar.php?gravatar_id=" +
185     (email.nil? ? "" : Digest::MD5.hexdigest(email.downcase)) + "&amp;default=" +
186       u("#{scheme}://#{GitoriousConfig['gitorious_host']}#{port_string}" +
187       "/#{options.delete(:default)}") +
188     options.map { |k,v| "&amp;#{k}=#{v}" }.join
189   end
190
191   # For a User object, return either his/her avatar or the gravatar for her email address
192   # Options
193   # - Pass on :size for the height+width of the image in pixels
194   # - Pass on :version for a named version/style of the avatar
195   def avatar(user, options={})
196     if user.avatar?
197       avatar_style = options.delete(:version) || :thumb
198       image_options = { :alt => 'avatar'}.merge(:width => options[:size], :height => options[:size])
199       image_tag(user.avatar.url(avatar_style), image_options)
200     else
201       gravatar(user.email, options)
202     end
203   end
204
205   # Returns an avatar from an email address (for instance from a commit) where we don't have an actual User object
206   def avatar_from_email(email, options={})
207     return if email.blank?
208     avatar_style = options.delete(:version) || :thumb
209     image = User.find_avatar_for_email(email, avatar_style)
210     if image == :nil
211       gravatar(email, options)
212     else
213       image_options = { :alt => 'avatar'}.merge(:width => options[:size], :height => options[:size])
214       image_tag(image, image_options)
215     end
216   end
217
218   def gravatar(email, options = {})
219     size = options[:size]
220     image_options = { :alt => "avatar" }
221     if size
222       image_options.merge!(:width => size, :height => size)
223     end
224     image_tag(gravatar_url_for(email, options), image_options)
225   end
226
227   def gravatar_frame(email, options = {})
228     extra_css_class = options[:style] ? " gravatar_#{options[:style]}" : ""
229     %{<div class="gravatar#{extra_css_class}">#{gravatar(email, options)}</div>}
230   end
231
232   def flashes
233     flash.map do |type, content|
234       content_tag(:div, content_tag(:p, content), :class => "flash_message #{type}")
235     end.join("\n")
236   end
237
238   def commit_graph_tag(repository, ref = "master")
239   end
240
241   def commit_graph_by_author_tag(repository, ref = "master")
242   end
243
244   def action_and_body_for_event(event)
245     target = event.target
246     if target.nil?
247       return ["", "", ""]
248     end
249     # These are defined in event_rendering_helper.rb:
250     begin
251       action, body, category = self.send("render_event_#{Action::css_class(event.action)}", event)
252     rescue ActiveRecord::RecordNotFound
253       return ["","",""]
254     end
255     body = sanitize(body, :tags => %w[a em i strong b])
256     [action, body, category]
257   end
258
259   def link_to_remote_if(condition, name, options, html_options = {})
260     if condition
261       link_to_remote(name, options, html_options)
262     else
263       content_tag(:span, name)
264     end
265   end
266
267   def sidebar_content?
268     !@content_for_sidebar.blank?
269   end
270
271   def render_readme(repository)
272     possibilities = []
273     repository.git.git.ls_tree({:name_only => true}, "master").each do |line|
274       possibilities << line[0, line.length-1] if line =~ /README.*/
275     end
276
277     return "" if possibilities.empty?
278     text = repository.git.git.show({}, "master:#{possibilities.first}")
279     markdown(text) rescue simple_format(sanitize(text))
280   end
281
282   def render_markdown_help
283     render :partial => '/site/markdown_help'
284   end
285
286   def file_path(repository, filename, head = "master")
287     project_repository_blob_path(repository.project, repository, branch_with_tree(head, filename))
288   end
289
290   def link_to_help_toggle(dom_id, style = :image)
291     if style == :image
292       link_to_function(image_tag("help_grey.png", {
293         :alt => t("application_helper.more_info")
294       }), "$('##{dom_id}').toggle()", :class => "more_info")
295     else
296       %Q{<span class="hint">(} +
297       link_to_function("?", "$('##{dom_id}').toggle()", :class => "more_info") +
298       ")</span>"
299     end
300
301   end
302
303   FILE_EXTN_MAPPINGS = {
304     '.cpp' => 'cplusplus-file',
305     '.c' => 'c-file',
306     '.h' => 'header-file',
307     '.java' => 'java-file',
308     '.sh' => 'exec-file',
309     '.exe'  => 'exec-file',
310     '.rb' => 'ruby-file',
311     '.png' => 'image-file',
312     '.jpg' => 'image-file',
313     '.gif' => 'image-file',
314     'jpeg' => 'image-file',
315     '.zip' => 'compressed-file',
316     '.gz' => 'compressed-file'}
317
318   def class_for_filename(filename)
319     return FILE_EXTN_MAPPINGS[File.extname(filename)] || 'file'
320   end
321
322   def render_download_links(project, repository, head, options={})
323     head = desplat_path(head) if head.is_a?(Array)
324
325     links = []
326     exceptions = Array(options[:except])
327     unless exceptions.include?(:source_tree)
328       links << content_tag(:li, link_to("Source tree",
329                   tree_path(head)), :class => "tree")
330     end
331
332     if head =~ /^[a-z0-9]{40}$/ # it looks like a SHA1
333       head = head[0..7]
334     end
335
336     {
337       'tar.gz' => 'tar',
338       # 'zip' => 'zip',
339     }.each do |extension, url_key|
340       archive_path = self.send("project_repository_archive_#{url_key}_path", project, repository, ensplat_path(head))
341       link_html = link_to("Download #{head} as #{extension}", archive_path,
342                                   :onclick => "Gitorious.DownloadChecker.checkURL('#{archive_path}?format=js', 'archive-box-#{head.gsub("/", "_")}');return false",
343                                   :class => "download-link")
344       link_callback_box = content_tag(:div, "", :class => "archive-download-box round-5 shadow-2",
345         :id => "archive-box-#{head.gsub("/", "_")}", :style => "display:none;")
346       links << content_tag(:li, link_html+link_callback_box, :class => extension.split('.').last)
347     end
348
349     if options.delete(:only_list_items)
350       links.join("\n")
351     else
352       css_classes = options[:class] || "meta"
353       content_tag(:ul, links.join("\n"), :class => "links #{css_classes}")
354     end
355   end
356
357   def paragraphs_with_more(text, identifier)
358     return if text.blank?
359     first, rest = text.split("</p>", 2)
360     if rest.blank?
361       first + "</p>"
362     else
363       %Q{#{first}
364         <a href="#more"
365            onclick="$('#description-rest-#{identifier}').toggle(); $(this).hide()">more&hellip;</a></p>
366         <div id="description-rest-#{identifier}" style="display:none;">#{rest}</div>}
367     end
368   end
369
370   def markdown_hint
371     t("views.common.format_using_markdown",
372       :markdown => %(<a href="http://daringfireball.net/projects/markdown/">Markdown</a>))
373   end
374
375   def current_site
376     @controller.current_site
377   end
378
379   def new_polymorphic_comment_path(parent, comment)
380     if parent
381       repo_owner_path(@repository, [@project, @repository, parent, comment])
382     else
383       repo_owner_path(@repository, [@project, @repository, comment])
384     end
385   end
386
387   def force_utf8(str)
388     if str.respond_to?(:force_encoding)
389       str.force_encoding("UTF-8")
390       if str.valid_encoding?
391         str
392       else
393         str.encode("binary", :invalid => :replace, :undef => :replace).encode("utf-8")
394       end
395     else
396       str.mb_chars
397     end
398
399   end
400
401   # Creates a CSS styled <button>.
402   #
403   #  <%= styled_button :big, "Create user" %>
404   #  <%= styled_button :medium, "Do something!", :class => "foo", :id => "bar" %>
405   def styled_button(size_identifier, label, options = {})
406     options.reverse_merge!(:type => "submit", :class => size_identifier.to_s)
407     content_tag(:button, %{<span>#{label}</span>}, options)
408   end
409
410   # Similar to styled_button, but creates a link_to <a>, not a <button>.
411   #
412   #  <%= button_link :big, "Sign up", new_user_path %>
413   def button_link(size_identifier, label, url, options = {})
414     options[:class] = "#{size_identifier} button_link"
415     link_to(%{<span>#{label}</span>}, url, options)
416   end
417
418   # Array => HTML list. The option hash is applied to the <ul> tag.
419   #
420   #  <%= list(items) {|i| i.title } %>
421   #  <%= list(items, :class => "foo") {|i| link_to i, foo_path }
422   def list(items, options = {})
423     list_items = items.map {|i| %{<li>#{block_given? ? yield(i) : i}</li>} }.join("\n")
424     content_tag(:ul, list_items, options)
425   end
426
427   def summary_box(title, content, image)
428     %{
429       <div class="summary_box">
430         <div class="summary_box_image">
431           #{image}
432         </div>
433
434         <div class="summary_box_content">
435           <strong>#{title}</strong>
436           #{content}
437         </div>
438
439         <div class="clear"></div>
440       </div>
441     }
442   end
443
444   def project_summary_box(project)
445     summary_box link_to(project.title, project),
446       truncate(project.descriptions_first_paragraph, 80),
447       glossy_homepage_avatar(default_avatar)
448   end
449
450   def team_summary_box(team)
451     text = list([
452       "Created: #{team.created_at.strftime("%B #{team.created_at.strftime("%d").to_i.ordinalize} %Y")}",
453       "Total activities: #{team.event_count}"
454     ], :class => "simple")
455
456     summary_box link_to(team.name, group_path(team)),
457       text,
458       glossy_homepage_avatar(team.avatar? ? image_tag(team.avatar.url(:thumb), :width => 30, :height => 30) : default_avatar)
459
460   end
461
462   def user_summary_box(user)
463     text = text = list([
464       "Projects: #{user.projects.count}",
465       "Total activities: #{user.events.count}"
466     ], :class => "simple")
467
468     summary_box link_to(user.login, user),
469       text,
470       glossy_homepage_avatar_for_user(user)
471   end
472
473   def glossy_homepage_avatar(avatar)
474     content_tag(:div, avatar + "<span></span>", :class => "glossy_avatar_wrapper")
475   end
476
477   def glossy_homepage_avatar_for_user(user)
478     glossy_homepage_avatar(avatar(user, :size => 30, :default => "images/icon_default.png"))
479   end
480
481   def default_avatar
482     image_tag("icon_default.png", :width => 30, :height => 30)
483   end
484
485   def secure_login_url
486     if SslRequirement.disable_ssl_check?
487       sessions_path
488     else
489       sessions_url(:protocol => "https", :host => SslRequirement.ssl_host)
490     end
491   end
492
493   def comment_applies_to_merge_request?(parent)
494     MergeRequest === parent && (logged_in? && parent.resolvable_by?(current_user))
495   end
496
497   def statuses_for_merge_request_for_select(merge_request)
498     merge_request.target_repository.project.merge_request_statuses.map do |status|
499       if status.description.blank?
500         [h(status.name), h(status.name)]
501       else
502         [h("#{status.name} - #{status.description}"), h(status.name)]
503       end
504     end
505   end
506
507   # The javascripts to be included in all layouts
508   def include_javascripts
509     jquery = ["", "/autocomplete", "/cookie", "/color_picker", "/cycle.all.min",
510               "/ui", "/ui/selectable", "/scrollto", "/expander",
511               "/timeago"].collect { |f| "lib/jquery#{f}" }
512
513     gitorious = ["", "/observable", "/application", "/resource_toggler", "/jquery",
514                  "/merge_requests", "/diff_browser", "/messages", "/live_search",
515                  "/repository_search"].collect { |f| "gitorious#{f}" }
516
517     scripts = jquery + ["core_extensions"] + gitorious + ["application"]
518
519     javascript_include_tag(scripts, :cache => true)
520   end
521
522   # inserts a <wbr> tag somewhere in the middle of +str+
523   def wbr_middle(str)
524     half_size = str.length / 2
525     str.to_s[0..half_size-1] + "<wbr />" + str[half_size..-1]
526   end
527
528   def time_ago(time, options = {})
529     return unless time
530     options[:class] ||= "timeago"
531     content_tag(:abbr, time.to_s, options.merge(:title => time.getutc.iso8601))
532   end
533
534   def white_button_link_to(label, url, options = {})
535     size = options.delete(:size) || "small"
536     css_classes = ["white-button", "#{size}-button"]
537     if extra_class = options.delete(:class)
538       css_classes << extra_class
539     end
540     content_tag(:div, link_to(label, url, :class => "round-10"),
541         :id => options.delete(:id), :class => css_classes.flatten.join(" "))
542   end
543
544   def link_button_link_to(label, url, options = {})
545     size = options.delete(:size) || "small"
546     css_classes = ["button", "#{size}-button"]
547     if extra_class = options.delete(:class)
548       css_classes << extra_class
549     end
550     content_tag(:div, link_to(label, url, :class => ""),
551         :id => options.delete(:id), :class => css_classes.flatten.join(" "))
552   end
553   
554   def render_pagination_links(collection, options = {})
555     default_options = {
556       :previous_label => "Previous",
557       :next_label => "Next",
558       :container => "True"
559     }
560     will_paginate(collection, options.merge(default_options))
561   end
562
563   def dashboard_path
564     root_url(:host => GitoriousConfig["gitorious_host"], :protocol => GitoriousConfig["scheme"])
565   end
566
567   def git_or_ssh_url_checked(repository, currently)
568     if logged_in?
569       if currently == :git && !repository.writable_by?(current_user)
570         return 'checked="checked"'
571       elsif currently == :ssh && repository.writable_by?(current_user)
572         return 'checked="checked"'
573       else
574         return ""
575       end
576     else
577       return currently == :git ? 'checked="checked"' : ""
578     end
579   end
580 end