Repository activities page in Gitorious 3 UI
[gitorious:mainline.git] / app / helpers / application_helper.rb
1 # encoding: utf-8
2 #--
3 #   Copyright (C) 2011-2013 Gitorious AS
4 #   Copyright (C) 2009 Nokia Corporation and/or its subsidiary(-ies)
5 #   Copyright (C) 2007, 2008 Johan Sørensen <johan@johansorensen.com>
6 #   Copyright (C) 2008 August Lilleaas <augustlilleaas@gmail.com>
7 #   Copyright (C) 2008 David A. Cuadrado <krawek@gmail.com>
8 #   Copyright (C) 2008 Tor Arne Vestbø <tavestbo@trolltech.com>
9 #   Copyright (C) 2009 Fabio Akita <fabio.akita@gmail.com>
10 #   Copyright (C) 2009 Bill Marquette <bill.marquette@gmail.com>
11 #   Copyright (C) 2010 Christian Johansen <christian@shortcut.no>
12 #
13 #   This program is free software: you can redistribute it and/or modify
14 #   it under the terms of the GNU Affero General Public License as published by
15 #   the Free Software Foundation, either version 3 of the License, or
16 #   (at your option) any later version.
17 #
18 #   This program is distributed in the hope that it will be useful,
19 #   but WITHOUT ANY WARRANTY; without even the implied warranty of
20 #   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
21 #   GNU Affero General Public License for more details.
22 #
23 #   You should have received a copy of the GNU Affero General Public License
24 #   along with this program.  If not, see <http://www.gnu.org/licenses/>.
25 #++
26 require "libdolt"
27 require "gitorious"
28 require "gitorious/view/dolt_url_helper"
29 require "gitorious/view/repository_helper"
30
31 # Methods added to this helper will be available to all templates in the application.
32 module ApplicationHelper
33   include ActsAsTaggableOn::TagsHelper
34   include UsersHelper
35   include BreadcrumbsHelper
36   include EventRenderingHelper
37   include RoutingHelper
38   include SiteWikiPagesHelper
39   include Gitorious::Authorization
40   include GroupRoutingHelper
41   include Gitorious::CacheInPrivateHelper
42   include DoltViewHelpers
43
44   GREETINGS = ["Hello", "Hi", "Greetings", "Howdy", "Heya", "G'day"]
45
46   STYLESHEETS = {
47     :common => ["content", "sidebar", "forms", "buttons", "base"],
48     :external => ["external"]
49   }
50
51   def random_greeting
52     GREETINGS[rand(GREETINGS.length)]
53   end
54
55   def help_box(style = :side, icon = :help, options = {}, &block)
56     raw <<-HTML
57       <div id="#{options.delete(:id)}" style="#{options.delete(:style)}"
58            class="help-box #{style} #{icon} round-5">
59         <div class="icon #{icon}"></div>
60         #{capture(&block)}
61       </div>
62     HTML
63   end
64
65   def pull_box(title, options = {}, &block)
66     title_html = title.nil? ? "" : "<div class=\"pull-box-header\"><h3>#{title}</h3></div>"
67     raw <<-HTML
68       <div class="pull-box-container #{options.delete(:class)}">
69         #{title_html}
70         <div class="pull-box-content">
71           #{capture(&block)}
72         </div>
73       </div>
74     HTML
75   end
76
77   def dialog_box(title, options = {}, &block)
78     title_html = title.nil? ? "" : "<h3 class=\"round-top-5 dialog-box-header\">#{title}</h3>"
79     raw <<-HTML
80       <div class="dialog-box #{options.delete(:class)}">
81         #{title_html}
82         <div class="dialog-box-content">
83           #{capture(&block)}
84         </div>
85       </div>
86     HTML
87   end
88
89   def markdown(text, options = [:smart])
90     renderer = MarkupRenderer.new(text, :markdown => options)
91     renderer.to_html.html_safe
92   end
93
94   def render_markdown(text, *options)
95     # RDiscount < 1.4 doesn't support the :auto_link, use Rails' instead
96     auto_link = options.delete(:auto_link)
97     markdown_options = [:smart] + options
98     markdownized_text = markdown(text, markdown_options)
99     if auto_link
100       markdownized_text = auto_link(markdownized_text, :urls)
101     end
102     sanitize(markdownized_text).html_safe
103   end
104
105   def feed_icon(url, alt_title = "Atom feed", size = :small)
106     link_to(image_tag("silk/feed.png", :class => "feed_icon"), url,
107             :alt => alt_title, :title => alt_title)
108   end
109
110   def default_css_tag_sizes
111     %w(tag_size_1 tag_size_2 tag_size_3 tag_size_4)
112   end
113
114   def linked_tag_list_as_sentence(tags)
115     tags.map do |tag|
116       link_to(h(tag.name), search_path(:q => "@category #{h(tag.name)}"))
117     end.to_sentence.html_safe
118   end
119
120   def build_notice_for(object, options = {})
121     out =  %Q{<div class="being_constructed round-10">}
122     out <<  %Q{<div class="being_constructed_content round-10">}
123     out << %Q{  <p>#{I18n.t( "application_helper.notice_for", :class_name => object.class.name.humanize.downcase)}</p>}
124     if options.delete(:include_refresh_link)
125       out << %Q{<p class="spin hint"><a href="#{url_for()}">Click to refresh</a></p>}
126     else
127       out << %Q{<p class="spin">#{image_tag("spinner.gif")}</p>}
128     end
129     out << %Q{  <p class="hint">If this message persists beyond what is reasonable, feel free to #{link_to("contact us", contact_path)}</p>}
130     out << %Q{</div></div>}
131     out.html_safe
132   end
133
134   def render_if_ready(object, options = {})
135     if object.respond_to?(:ready?) && object.ready?
136       yield
137     else
138       raw build_notice_for(object, options)
139     end
140   end
141
142   def selected_if_current_page(url_options, slack = false)
143     if slack
144       if controller.request.fullpath.index(CGI.escapeHTML(url_for(url_options))) == 0
145         "selected"
146       end
147     else
148       "selected" if current_page?(url_options)
149     end
150   end
151
152   def submenu_selected_class_if_current?(section)
153     case section
154     when :overview
155      if %w[projects].include?(controller.controller_name )
156        return "selected"
157      end
158     when :repositories
159       if %w[repositories trees logs commits comitters comments merge_requests
160             blobs committers].include?(controller.controller_name )
161         return "selected"
162       end
163     when :pages
164       if %w[pages].include?(controller.controller_name )
165         return "selected"
166       end
167     end
168   end
169
170   def link_to_with_selected(name, options = {}, html_options = nil)
171     html_options = current_page?(options) ? {:class => "selected"} : nil
172     link_to(name, options = {}, html_options)
173   end
174
175   def syntax_themes_css
176     out = []
177     if @load_syntax_themes
178       # %w[ active4d all_hallows_eve amy blackboard brilliance_black brilliance_dull
179       #     cobalt dawn eiffel espresso_libre idle iplastic lazy mac_classic
180       #     magicwb_amiga pastels_on_dark slush_poppies spacecadet sunburst
181       #     twilight zenburnesque
182       # ].each do |syntax|
183       #   out << stylesheet_link_tag("syntax_themes/#{syntax}")
184       # end
185       return stylesheet_link_tag("syntax_themes/idle")
186     end
187     out.join("\n").html_safe
188   end
189
190   def gravatar_url_for(email, options = {})
191     prefix = request.ssl? ? "https://secure" : "http://www"
192     scheme = request.ssl? ? "https" : "http"
193     options.reverse_merge!(:default => "images/default_face.gif")
194     port_string = [443, 80].include?(request.port) ? "" : ":#{request.port}"
195     "#{prefix}.gravatar.com/avatar/" +
196     (email.nil? ? "" : Digest::MD5.hexdigest(email.downcase)) + "&amp;default=" +
197       u("#{scheme}://#{Gitorious.host}#{port_string}" +
198       "/#{options.delete(:default)}") +
199     options.map { |k,v| "&amp;#{k}=#{v}" }.join
200   end
201
202   # For a User object, return either his/her avatar or the gravatar for her email address
203   # Options
204   # - Pass on :size for the height+width of the image in pixels
205   # - Pass on :version for a named version/style of the avatar
206   def avatar(user, options={})
207     if user.avatar?
208       avatar_style = options.delete(:version) || :thumb
209       image_options = { :alt => 'avatar'}.merge(:width => options[:size], :height => options[:size])
210       image_tag(user.avatar.url(avatar_style), image_options)
211     else
212       gravatar(user.email, options)
213     end
214   end
215
216   # Returns an avatar from an email address (for instance from a commit) where we don't have an actual User object
217   def avatar_from_email(email, options={})
218     return if email.blank?
219     avatar_style = options.delete(:version) || :thumb
220     image = User.find_avatar_for_email(email, avatar_style)
221     if image == :nil
222       gravatar(email, options)
223     else
224       image_options = { :alt => 'avatar'}.merge(:width => options[:size], :height => options[:size])
225       image_tag(image, image_options)
226     end
227   end
228
229   def gravatar(email, options = {}, image_options = {})
230     size = options[:size]
231     image_options = image_options.merge({ :alt => "avatar" })
232     if size
233       image_options.merge!(:width => size, :height => size)
234     end
235     image_tag(gravatar_url_for(email, options), image_options)
236   end
237
238   def gravatar_frame(email, options = {})
239     extra_css_class = options[:style] ? " gravatar_#{options[:style]}" : ""
240     %{<div class="gravatar#{extra_css_class}">#{gravatar(email, options)}</div>}.html_safe
241   end
242
243   def flashes
244     flash.map do |type, content|
245       content_tag(:div, content_tag(:p, content), :class => "flash_message #{type}")
246     end.join("\n").html_safe
247   end
248
249   def action_and_body_for_event(event)
250     target = event.target
251     if target.nil?
252       return ["", "", ""]
253     end
254     # These are defined in event_rendering_helper.rb:
255     begin
256       action, body, category = self.send("render_event_#{Action::css_class(event.action)}", event)
257     rescue ActiveRecord::RecordNotFound
258       return ["","",""]
259     end
260     body = sanitize(body, :tags => %w[a em i strong b])
261     [action, body, category]
262   end
263
264   def link_to_remote_if(condition, name, options, html_options = {})
265     if condition
266       link_to_remote(name, options, html_options)
267     else
268       content_tag(:span, name)
269     end
270   end
271
272   def render_readme(repository)
273     possibilities = []
274     repository.git.git.ls_tree({:name_only => true}, "master").each do |line|
275       possibilities << line[0, line.length-1] if line =~ /README.*/
276     end
277
278     return "" if possibilities.empty?
279     text = repository.git.git.show({}, "master:#{possibilities.first}")
280     markdown(text) rescue simple_format(sanitize(text))
281   end
282
283   def render_markdown_help
284     render(:partial => "/site/markdown_help")
285   end
286
287   def link_to_help_toggle(dom_id, style = :image)
288     if style == :image
289       link_to_function(image_tag("help_grey.png", {
290         :alt => t("application_helper.more_info")
291       }), "$('##{dom_id}').toggle()", :class => "more_info")
292     else
293       %Q{<span class="hint">(} +
294       link_to_function("?", "$('##{dom_id}').toggle()", :class => "more_info") +
295       ")</span>"
296     end
297   end
298
299   FILE_EXTN_MAPPINGS = {
300     ".cpp" => "cplusplus-file",
301     ".c" => "c-file",
302     ".h" => "header-file",
303     ".java" => "java-file",
304     ".sh" => "exec-file",
305     ".exe"  => "exec-file",
306     ".rb" => "ruby-file",
307     ".png" => "image-file",
308     ".jpg" => "image-file",
309     ".gif" => "image-file",
310     "jpeg" => "image-file",
311     ".zip" => "compressed-file",
312     ".gz" => "compressed-file"}
313
314   def class_for_filename(filename)
315     return FILE_EXTN_MAPPINGS[File.extname(filename)] || "file"
316   end
317
318   def render_download_links(project, repository, head, options={})
319     head = desplat_path(head) if head.is_a?(Array)
320
321     (["tar.gz", "zip"].map do |extension|
322       link_html = link_to("Download #{refname(head)} as #{extension}",
323                           archive_url(repository.path_segment, head, extension),
324                           :title => "Download #{refname(head)} as #{extension}",
325                           :class => "download-link")
326       content_tag(:li, link_html, :class => extension.split('.').last)
327     end).join("\n").html_safe
328   end
329
330   def paragraphs_with_more(text, identifier)
331     return if text.blank?
332     first, rest = text.split("</p>", 2)
333     if rest.blank?
334       (first + "</p>").html_safe
335     else
336       (<<-HTML).html_safe
337         #{first}
338         <a href="#more"
339            onclick="$('#description-rest-#{identifier}').toggle(); $(this).hide()">more&hellip;</a></p>
340         <div id="description-rest-#{identifier}" style="display:none;">#{rest}</div>
341       HTML
342     end
343   end
344
345   def markdown_hint
346     t("views.common.format_using_markdown",
347       :markdown => %(<a href="http://daringfireball.net/projects/markdown/">Markdown</a>)).html_safe
348   end
349
350   def current_site
351     controller.current_site
352   end
353
354   def force_utf8(str)
355     if str.respond_to?(:force_encoding)
356       str.force_encoding("UTF-8")
357       if str.valid_encoding?
358         str
359       else
360         str.encode("binary", :invalid => :replace, :undef => :replace).encode("utf-8")
361       end
362     else
363       str.mb_chars
364     end
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, content_tag(:span, label), 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     }.html_safe
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       glossy_homepage_avatar(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   end
426
427   def user_summary_box(user)
428     text = text = list([
429       "Projects: #{user.projects.count}",
430       "Total activities: #{user.events.count}"
431     ], :class => "simple")
432
433     summary_box link_to(user.login, user),
434       text,
435       glossy_homepage_avatar_for_user(user)
436   end
437
438   def glossy_homepage_avatar(avatar)
439     content_tag(:div, avatar + "<span></span>", :class => "glossy_avatar_wrapper")
440   end
441
442   def glossy_homepage_avatar_for_user(user)
443     glossy_homepage_avatar(avatar(user, :size => 30, :default => "images/icon_default.png"))
444   end
445
446   def default_avatar
447     image_tag("icon_default.png", :width => 30, :height => 30)
448   end
449
450   def comment_applies_to_merge_request?(parent)
451     MergeRequest === parent && (logged_in? && can_resolve_merge_request?(current_user, parent))
452   end
453
454   def statuses_for_merge_request_for_select(merge_request)
455     merge_request.target_repository.project.merge_request_statuses.map do |status|
456       if status.description.blank?
457         [h(status.name), h(status.name)]
458       else
459         [h("#{status.name} - #{status.description}"), h(status.name)]
460       end
461     end
462   end
463
464   def include_stylesheets(group)
465     stylesheets = STYLESHEETS[group]
466     cache_name = "gts-#{group}"
467     additional = Gitorious::Configuration.get("#{group}_stylesheets")
468
469     unless additional.nil?
470       additional = [additional] unless Array === additional
471       stylesheets.concat(additional)
472       cache_name << "-#{additional.join('-').gsub(/[^a-z0-9_\-]/, '-')}"
473       cache_name = cache_name.gsub(/-+/, '-')
474     end
475
476     stylesheet_link_tag stylesheets, :cache => cache_name
477   end
478
479   # The javascripts to be included in all layouts
480   def include_javascripts
481     jquery = ["", "/autocomplete", "/cookie", "/color_picker", "/cycle.all.min",
482               "/ui", "/ui/selectable", "/scrollto", "/expander",
483               "/timeago","/pjax"].collect { |f| "lib/jquery#{f}" }
484
485     gitorious = ["", "/observable", "/application", "/resource_toggler", "/jquery",
486                  "/merge_requests", "/diff_browser", "/messages", "/live_search",
487                  "/repository_search"].collect { |f| "gitorious#{f}" }
488
489     scripts = jquery + ["core_extensions"] + gitorious + ["rails.js", "lib/spin.js/spin.js", "application"]
490
491     javascript_include_tag(scripts, :cache => true)
492   end
493
494   def favicon_link_tag
495     url = Gitorious::Configuration.get("favicon_url", "/favicon.ico")
496     "<link rel=\"shortcut icon\" href=\"#{url}\" type=\"image/x-icon\">".html_safe
497   end
498
499   def logo_link
500     logo = Gitorious::Configuration.get("logo_url", "/img/logo.png")
501     link_to(logo.blank? ? "Gitorious" : image_tag(logo), root_path)
502   end
503
504   # inserts a <wbr> tag somewhere in the middle of +str+
505   def wbr_middle(str)
506     half_size = str.length / 2
507     (str.to_s[0..half_size-1] + "<wbr />" + str[half_size..-1]).html_safe
508   end
509
510   def time_ago(time, options = {})
511     return unless time
512     options[:class] ||= "timeago"
513     content_tag(:abbr, time.to_s, options.merge(:title => time.getutc.iso8601))
514   end
515
516   def white_button_link_to(label, url, options = {})
517     size = options.delete(:size) || "small"
518     css_classes = ["white-button", "#{size}-button"]
519     if extra_class = options.delete(:class)
520       css_classes << extra_class
521     end
522     content_tag(:div, link_to(label, url, :class => "round-10"),
523         :id => options.delete(:id), :class => css_classes.flatten.join(" "))
524   end
525
526   def link_button_link_to(label, url, options = {})
527     size = options.delete(:size) || "small"
528     css_classes = ["button", "#{size}-button"]
529     if extra_class = options.delete(:class)
530       css_classes << extra_class
531     end
532     content_tag(:div, link_to(label, url, :class => "", :confirm => options[:confirm]),
533         :id => options.delete(:id), :class => css_classes.flatten.join(" "))
534   end
535
536   def render_pagination_links(collection, options = {})
537     default_options = {
538       :previous_label => "Previous",
539       :next_label => "Next",
540       :container => "True"
541     }
542     (will_paginate(collection, options.merge(default_options)) || "").html_safe
543   end
544
545   def dashboard_path
546     root_url(:host => Gitorious.host, :protocol => Gitorious.scheme)
547   end
548
549   def site_domain
550     host = Gitorious.host
551     port = Gitorious.port
552     port = port.to_i != 80 ? ":#{port}" : ""
553     "#{host}#{port}"
554   end
555
556   def fq_root_link
557     Gitorious.url("/")
558   end
559
560   def url?(setting)
561     !Gitorious::View.send(:"#{setting}_url").blank?
562   end
563
564   def footer_link(setting, html_options={})
565     url = Gitorious::View.send(:"#{setting}_url")
566     text = t("views.layout.#{setting}")
567     "<li>#{link_to text, url, html_options}</li>".html_safe
568   end
569
570   def namespaced_atom_feed(options={}, &block)
571     options["xmlns:gts"] = "http://gitorious.org/schema"
572     atom_feed(options, &block)
573   end
574
575   # Temporary - Rails 3 removed error_messages_for
576   def error_messages(model)
577     errors = model.is_a?(Array) ? model : model.errors.full_messages
578     return "" if !errors.any?
579     result = errors.inject("") { |memo, obj| memo << content_tag(:li, obj) }
580     header = content_tag(:h2, pluralize(errors.size, "error"))
581     "<div class=\"errorExplanation\" id=\"errorExplanation\">#{header}<ul>#{result}</ul></div>".html_safe
582   end
583
584   def vcs_link_tag(options)
585     content_for :extra_head do
586       (<<-HTML).html_safe
587         <link rel="vcs-git" href="#{h(options[:href])}" title="#{h(options[:title])}">
588       HTML
589     end
590   end
591
592   def long_ordinal(date)
593     date.strftime("%B #{date.day.ordinalize}, %Y")
594   end
595
596   # Used for compatibility with Dolt views
597   def partial(template, locals = {})
598     render(:template => File.join("ui3", template), :locals => locals).html_safe
599   end
600 end