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