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