Merge remote-tracking branch 'official/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 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 => "http")
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