Commit e6e057d1 authored by Holger Just's avatar Holger Just

Merge branch 'release-v3.2.0' into stable

parents d61ad013 bd132c56
...@@ -31,3 +31,4 @@ doc/app ...@@ -31,3 +31,4 @@ doc/app
/.rvmrc* /.rvmrc*
/*.iml /*.iml
/.idea /.idea
.rbx
language: ruby
rvm:
- 1.8.7
- 1.9.2
- 1.9.3
- rbx-18mode
env:
- "RAILS_ENV=test DB=mysql BUNDLE_WITHOUT=rmagick:mysql2:postgres:sqlite"
- "RAILS_ENV=test DB=mysql2 BUNDLE_WITHOUT=rmagick:mysql:postgres:sqlite"
- "RAILS_ENV=test DB=postgres BUNDLE_WITHOUT=rmagick:mysql:mysql2:sqlite"
- "RAILS_ENV=test DB=sqlite BUNDLE_WITHOUT=rmagick:mysql:mysql2:postgres"
matrix:
exclude:
- rvm: 1.9.2
env: "RAILS_ENV=test DB=mysql BUNDLE_WITHOUT=rmagick:mysql2:postgres:sqlite"
- rvm: 1.9.3
env: "RAILS_ENV=test DB=mysql BUNDLE_WITHOUT=rmagick:mysql2:postgres:sqlite"
- rvm: rbx-18mode
env: "RAILS_ENV=test DB=mysql BUNDLE_WITHOUT=rmagick:mysql2:postgres:sqlite"
allow_failures:
- rvm: rbx-18mode
before_script:
- "sudo apt-get --no-install-recommends install bzr cvs darcs git mercurial subversion"
- "rake ci:travis:prepare"
branches:
only:
- unstable
- master
- stable
notifications:
email: false
irc: "irc.freenode.org#chiliproject"
...@@ -9,12 +9,15 @@ gem "rubytree", "~> 0.5.2", :require => 'tree' ...@@ -9,12 +9,15 @@ gem "rubytree", "~> 0.5.2", :require => 'tree'
gem "rdoc", ">= 2.4.2" gem "rdoc", ">= 2.4.2"
gem "liquid", "~> 2.3.0" gem "liquid", "~> 2.3.0"
gem "acts-as-taggable-on", "= 2.1.0" gem "acts-as-taggable-on", "= 2.1.0"
gem 'gravatarify', '~> 3.0.0'
# Needed only on RUBY_VERSION = 1.8, ruby 1.9+ compatible interpreters should bring their csv # Needed only on RUBY_VERSION = 1.8, ruby 1.9+ compatible interpreters should bring their csv
gem "fastercsv", "~> 1.5.0", :platforms => [:ruby_18, :jruby, :mingw_18] gem "fastercsv", "~> 1.5.0", :platforms => [:ruby_18, :jruby, :mingw_18]
gem "tzinfo", "~> 0.3.31" # Fixes #903. Not required for Rails >= 3.2 gem "tzinfo", "~> 0.3.31" # Fixes #903. Not required for Rails >= 3.2
group :test do group :test do
gem 'shoulda', '~> 2.10.3' gem 'shoulda', '~> 2.10.3'
# Shoulda doesn't work nice on 1.9.3 and seems to need test-unit explicitely…
gem 'test-unit', :platforms => [:mri_19]
gem 'edavis10-object_daddy', :require => 'object_daddy' gem 'edavis10-object_daddy', :require => 'object_daddy'
gem 'mocha' gem 'mocha'
gem 'capybara' gem 'capybara'
...@@ -52,7 +55,7 @@ end ...@@ -52,7 +55,7 @@ end
# orders of magnitude compared to their native counterparts. You have been # orders of magnitude compared to their native counterparts. You have been
# warned. # warned.
platforms :mri, :mingw do platforms :mri, :mingw, :rbx do
group :mysql2 do group :mysql2 do
gem "mysql2", "~> 0.2.7" gem "mysql2", "~> 0.2.7"
end end
...@@ -74,7 +77,7 @@ platforms :mri_18, :mingw_18 do ...@@ -74,7 +77,7 @@ platforms :mri_18, :mingw_18 do
end end
end end
platforms :mri_19, :mingw_19 do platforms :mri_19, :mingw_19, :rbx do
group :sqlite do group :sqlite do
gem "sqlite3" gem "sqlite3"
end end
......
...@@ -77,7 +77,7 @@ class AdminController < ApplicationController ...@@ -77,7 +77,7 @@ class AdminController < ApplicationController
def info def info
@db_adapter_name = ActiveRecord::Base.connection.adapter_name @db_adapter_name = ActiveRecord::Base.connection.adapter_name
@checklist = [ @checklist = [
[:text_default_administrator_account_changed, User.find(:first, :conditions => ["login=? and hashed_password=?", 'admin', User.hash_password('admin')]).nil?], [:text_default_administrator_account_changed, !User.find_by_login("admin").try(:check_password?, "admin")],
[:text_file_repository_writable, File.writable?(Attachment.storage_path)], [:text_file_repository_writable, File.writable?(Attachment.storage_path)],
[:text_plugin_assets_writable, File.writable?(Engines.public_directory)], [:text_plugin_assets_writable, File.writable?(Engines.public_directory)],
[:text_rmagick_available, Object.const_defined?(:Magick)] [:text_rmagick_available, Object.const_defined?(:Magick)]
......
...@@ -31,18 +31,6 @@ class ApplicationController < ActionController::Base ...@@ -31,18 +31,6 @@ class ApplicationController < ActionController::Base
cookies.delete(:autologin) cookies.delete(:autologin)
end end
# Remove broken cookie after upgrade from 0.8.x (#4292)
# See https://rails.lighthouseapp.com/projects/8994/tickets/3360
# TODO: remove it when Rails is fixed
before_filter :delete_broken_cookies
def delete_broken_cookies
if cookies['_chiliproject_session'] && cookies['_chiliproject_session'] !~ /--/
cookies.delete '_chiliproject_session'
redirect_to home_path
return false
end
end
# FIXME: Remove this when all of Rack and Rails have learned how to # FIXME: Remove this when all of Rack and Rails have learned how to
# properly use encodings # properly use encodings
before_filter :params_filter before_filter :params_filter
......
...@@ -85,7 +85,7 @@ class DocumentsController < ApplicationController ...@@ -85,7 +85,7 @@ class DocumentsController < ApplicationController
if attachments.present? && attachments[:files].present? && Setting.notified_events.include?('document_added') if attachments.present? && attachments[:files].present? && Setting.notified_events.include?('document_added')
# TODO: refactor # TODO: refactor
attachments.first.container.recipients.each do |recipient| @document.recipients.each do |recipient|
Mailer.deliver_attachments_added(attachments[:files], recipient) Mailer.deliver_attachments_added(attachments[:files], recipient)
end end
end end
......
...@@ -17,7 +17,7 @@ require 'cgi' ...@@ -17,7 +17,7 @@ require 'cgi'
module ApplicationHelper module ApplicationHelper
include Redmine::I18n include Redmine::I18n
include GravatarHelper::PublicMethods include Gravatarify::Helper
extend Forwardable extend Forwardable
def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
...@@ -954,6 +954,16 @@ module ApplicationHelper ...@@ -954,6 +954,16 @@ module ApplicationHelper
(@has_content && @has_content[name]) || false (@has_content && @has_content[name]) || false
end end
# Returns the gravatar image tag for the given email
# +email+ is a string with an email address
def gravatar(email, options={})
gravatarify_options = {}
gravatarify_options[:secure] = options.delete :ssl
[:default, :size, :rating, :filetype].each {|key| gravatarify_options[:key] = options.delete :key}
gravatarify_options[:html] = options
gravatar_tag email, gravatarify_options
end
# Returns the avatar image tag for the given +user+ if avatars are enabled # Returns the avatar image tag for the given +user+ if avatars are enabled
# +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>') # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
def avatar(user, options = { }) def avatar(user, options = { })
......
...@@ -712,7 +712,7 @@ class Issue < ActiveRecord::Base ...@@ -712,7 +712,7 @@ class Issue < ActiveRecord::Base
# The default assumption is that journals have the same permissions # The default assumption is that journals have the same permissions
# as the journaled object, issue notes have separate permissions though # as the journaled object, issue notes have separate permissions though
def journal_editable_by?(journal, user) def journal_editable_by?(journal, user)
return true if journal.author == user && user.allowed_to?(:edit_own_issue_notes, project) return true if journal.user == user && user.allowed_to?(:edit_own_issue_notes, project)
user.allowed_to? :edit_issue_notes, project user.allowed_to? :edit_issue_notes, project
end end
......
...@@ -41,7 +41,6 @@ class Message < ActiveRecord::Base ...@@ -41,7 +41,6 @@ class Message < ActiveRecord::Base
acts_as_watchable acts_as_watchable
attr_protected :locked, :sticky
validates_presence_of :board, :subject, :content validates_presence_of :board, :subject, :content
validates_length_of :subject, :maximum => 255 validates_length_of :subject, :maximum => 255
...@@ -51,7 +50,7 @@ class Message < ActiveRecord::Base ...@@ -51,7 +50,7 @@ class Message < ActiveRecord::Base
:conditions => Project.allowed_to_condition(args.first || User.current, :view_messages) } } :conditions => Project.allowed_to_condition(args.first || User.current, :view_messages) } }
safe_attributes 'subject', 'content' safe_attributes 'subject', 'content'
safe_attributes 'locked', 'sticky', safe_attributes 'locked', 'sticky', 'board_id',
:if => lambda {|message, user| :if => lambda {|message, user|
user.allowed_to?(:edit_messages, message.project) user.allowed_to?(:edit_messages, message.project)
} }
...@@ -81,9 +80,15 @@ class Message < ActiveRecord::Base ...@@ -81,9 +80,15 @@ class Message < ActiveRecord::Base
end end
def after_destroy def after_destroy
parent.reset_last_reply_id! if parent
board.reset_counters! board.reset_counters!
end end
def reset_last_reply_id!
clid = children.present? ? children.last.id : nil
self.update_attribute(:last_reply_id, clid)
end
def sticky=(arg) def sticky=(arg)
write_attribute :sticky, (arg == true || arg.to_s == '1' ? 1 : 0) write_attribute :sticky, (arg == true || arg.to_s == '1' ? 1 : 0)
end end
......
...@@ -27,7 +27,7 @@ class Version < ActiveRecord::Base ...@@ -27,7 +27,7 @@ class Version < ActiveRecord::Base
validates_presence_of :name validates_presence_of :name
validates_uniqueness_of :name, :scope => [:project_id] validates_uniqueness_of :name, :scope => [:project_id]
validates_length_of :name, :maximum => 60 validates_length_of :name, :maximum => 60
validates_format_of :effective_date, :with => /^\d{4}-\d{2}-\d{2}$/, :message => :not_a_date, :allow_nil => true validates_format_of :start_date, :effective_date, :with => /^\d{4}-\d{2}-\d{2}$/, :message => :not_a_date, :allow_nil => true
validates_inclusion_of :status, :in => VERSION_STATUSES validates_inclusion_of :status, :in => VERSION_STATUSES
validates_inclusion_of :sharing, :in => VERSION_SHARINGS validates_inclusion_of :sharing, :in => VERSION_SHARINGS
...@@ -37,6 +37,7 @@ class Version < ActiveRecord::Base ...@@ -37,6 +37,7 @@ class Version < ActiveRecord::Base
safe_attributes 'name', safe_attributes 'name',
'description', 'description',
'start_date',
'effective_date', 'effective_date',
'due_date', 'due_date',
'wiki_page_title', 'wiki_page_title',
......
<h2><%=l(:label_register)%> <%=link_to l(:label_login_with_open_id_option), signin_url if Setting.openid? %></h2> <h2><%=l(:label_register)%> <%=link_to l(:label_login_with_open_id_option), signin_url if Setting.openid? %></h2>
<% form_tag({:action => 'register'}, :class => "tabular") do %> <% form_tag({:action => 'register'}, :class => "tabular", :autocomplete => :off) do %>
<%= error_messages_for 'user' %> <%= error_messages_for 'user' %>
<div class="box"> <div class="box">
......
...@@ -3,8 +3,7 @@ ...@@ -3,8 +3,7 @@
<div class="autoscroll"> <div class="autoscroll">
<table class="list issues"> <table class="list issues">
<thead><tr> <thead><tr>
<th class="checkbox hide-when-print"><%= link_to image_tag('toggle_check.png'), {}, :onclick => 'toggleIssuesSelection(Element.up(this, "form")); return false;', <th class="checkbox hide-when-print"><%= link_to image_tag('toggle_check.png'), {}, :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}" %>
:title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}" %>
</th> </th>
<%= sort_header_tag('id', :caption => '#', :default_order => 'desc') %> <%= sort_header_tag('id', :caption => '#', :default_order => 'desc') %>
<% query.columns.each do |column| %> <% query.columns.each do |column| %>
......
...@@ -3,5 +3,4 @@ ...@@ -3,5 +3,4 @@
<%= call_hook(:view_issues_sidebar_planning_bottom) %> <%= call_hook(:view_issues_sidebar_planning_bottom) %>
<%= render_sidebar_queries %>
<%= call_hook(:view_issues_sidebar_queries_bottom) %> <%= call_hook(:view_issues_sidebar_queries_bottom) %>
<p><%= l(:mail_body_wiki_content_added, :id => link_to(h(@wiki_content.page.pretty_title), @wiki_content_url), <p><%= l(:mail_body_wiki_content_added, :id => link_to(h(@wiki_content.page.pretty_title), @wiki_content_url),
:author => h(@wiki_content.author)) %><br /> :author => h(@wiki_content.author)) %><br />
<em><%=h @wiki_content.comments %></em></p> <em><%=h @wiki_content.last_journal.notes %></em></p>
<%= l(:mail_body_wiki_content_added, :id => h(@wiki_content.page.pretty_title), <%= l(:mail_body_wiki_content_added, :id => h(@wiki_content.page.pretty_title),
:author => h(@wiki_content.author)) %> :author => h(@wiki_content.author)) %>
<%= @wiki_content.comments %> <%= @wiki_content.last_journal.notes %>
<%= @wiki_content_url %> <%= @wiki_content_url %>
<p><%= l(:mail_body_wiki_content_updated, :id => link_to(h(@wiki_content.page.pretty_title), @wiki_content_url), <p><%= l(:mail_body_wiki_content_updated, :id => link_to(h(@wiki_content.page.pretty_title), @wiki_content_url),
:author => h(@wiki_content.author)) %><br /> :author => h(@wiki_content.author)) %><br />
<em><%=h @wiki_content.comments %></em></p> <em><%=h @wiki_content.last_journal.notes %></em></p>
<p><%= l(:label_view_diff) %>:<br /> <p><%= l(:label_view_diff) %>:<br />
<%= link_to h(@wiki_diff_url), @wiki_diff_url %></p> <%= link_to h(@wiki_diff_url), @wiki_diff_url %></p>
<%= l(:mail_body_wiki_content_updated, :id => h(@wiki_content.page.pretty_title), <%= l(:mail_body_wiki_content_updated, :id => h(@wiki_content.page.pretty_title),
:author => h(@wiki_content.author)) %> :author => h(@wiki_content.author)) %>
<%= @wiki_content.comments %> <%= @wiki_content.last_journal.notes %>
<%= @wiki_content.page.pretty_title %>: <%= @wiki_content.page.pretty_title %>:
<%= @wiki_content_url %> <%= @wiki_content_url %>
......
...@@ -115,4 +115,32 @@ module ActionController ...@@ -115,4 +115,32 @@ module ActionController
end end
end end
end end
# Backported fix for CVE-2012-2660
# https://groups.google.com/group/rubyonrails-security/browse_thread/thread/f1203e3376acec0f
# TODO: Remove this once we are on Rails >= 3.2.4
require 'action_controller/request'
class Request
protected
# Remove nils from the params hash
def deep_munge(hash)
hash.each_value do |v|
case v
when Array
v.grep(Hash) { |x| deep_munge(x) }
when Hash
deep_munge(v)
end
end
keys = hash.keys.find_all { |k| hash[k] == [nil] }
keys.each { |k| hash[k] = nil }
hash
end
def parse_query(qs)
deep_munge(super)
end
end
end end
...@@ -983,22 +983,22 @@ bg: ...@@ -983,22 +983,22 @@ bg:
description_choose_project: Проекти description_choose_project: Проекти
description_date_from: Въведете начална дата description_date_from: Въведете начална дата
label_deleted_custom_field: (изтрито потребителско поле) label_deleted_custom_field: (изтрито потребителско поле)
field_custom_filter: Custom LDAP filter field_custom_filter: Потребителски LDAP филтър
text_display_subprojects: Display subprojects text_display_subprojects: Показване на подпроекти
text_current_project: Current project text_current_project: Текущ проект
label_toc: Contents label_toc: Съдържание
search_input_placeholder: search ... search_input_placeholder: търсене ...
setting_mail_handler_confirmation_on_success: Send confirmation email on successful incoming email setting_mail_handler_confirmation_on_success: Изпращане на е-мейл за потвърждение при успешен входен е-мейл
label_mail_handler_confirmation: "Confirmation of email submission: %{subject}" label_mail_handler_confirmation: "Потвърждение на изпратено с е-мейл: %{subject}"
label_mail_handler_errors_with_submission: "There were errors with your email submission:" label_mail_handler_errors_with_submission: "Има грешки във вашия е-мейл:"
label_document_watchers: Watchers label_document_watchers: Наблюдатели
setting_mail_handler_confirmation_on_failure: Send confirmation email on failed incoming email setting_mail_handler_confirmation_on_failure: Изпращане на е-мейл за потвърждение при неуспешен входен е-мейл
label_between: between label_between: between
label_mail_handler_failure: "Failed email submission: %{subject}" label_mail_handler_failure: "Пропаднал е-мейл: %{subject}"
notice_not_authorized_action: You are not authorized to perform this action. notice_not_authorized_action: Вие нямате разрешение да изпълните това действие.
text_mail_handler_confirmation_successful: Your email has been successful added at the following url text_mail_handler_confirmation_successful: Вашият е-мейл беше успешно добавен на следващия адрес
field_issue_summary: Issue summary field_issue_summary: Заглавие на задачата
field_new_saved_query: New saved query field_new_saved_query: Нова записана заявка
field_issue_view_all_open: View all open issues field_issue_view_all_open: Разглеждане на всички отворени задачи
label_subtask_add: Add a subtask label_subtask_add: Добавяне на подзадача
label_issue_hierarchy: Issue hierarchy label_issue_hierarchy: Йерархия на задачите
This diff is collapsed.
...@@ -963,28 +963,28 @@ de: ...@@ -963,28 +963,28 @@ de:
notice_gantt_chart_truncated: Die Grafik ist unvollständig, da das Maximum der anzeigbaren Aufgaben überschritten wurde (%{max}) notice_gantt_chart_truncated: Die Grafik ist unvollständig, da das Maximum der anzeigbaren Aufgaben überschritten wurde (%{max})
setting_gantt_items_limit: Maximale Anzahl von Aufgaben die im Gantt-Chart angezeigt werden. setting_gantt_items_limit: Maximale Anzahl von Aufgaben die im Gantt-Chart angezeigt werden.
text_powered_by: Powered by %{link} text_powered_by: Powered by %{link}
label_cvs_module: Module label_cvs_module: Modul
label_filesystem_path: Root directory label_filesystem_path: Wurzelverzeichnis
label_darcs_path: Root directory label_darcs_path: Wurzelverzeichnis
label_bazaar_path: Root directory label_bazaar_path: Wurzelverzeichnis
label_cvs_path: CVSROOT label_cvs_path: CVSROOT
label_git_path: Path to .git directory label_git_path: Pfad zum .git Verzeichnis
label_mercurial_path: Root directory label_mercurial_path: Wurzelverzeichnis
label_my_queries: My custom queries label_my_queries: Meine benutzerdefinierten Abfragen
label_additional_workflow_transitions_for_assignee: Additional transitions allowed when the user is the assignee label_additional_workflow_transitions_for_assignee: Zusätzliche Workflow-Übergänge wenn das Ticket an den Benutzer zugewiesen ist
text_journal_changed_no_detail: "%{label} updated" text_journal_changed_no_detail: "%{label} aktualisiert"
button_expand_all: Expand all button_expand_all: Alles ausklappen
button_collapse_all: Collapse all button_collapse_all: Alles einklappen
label_additional_workflow_transitions_for_author: Additional transitions allowed when the user is the author label_additional_workflow_transitions_for_author: Zusätzliche Workflow-Übergänge wenn der Benutzer der Autor ist
field_effective_date: Due date field_effective_date: Abshlussdatum
label_news_comment_added: Comment added to a news label_news_comment_added: "Kommentar erfolgreich hinzugefügt"
field_warn_on_leaving_unsaved: Warn me when leaving a page with unsaved text field_warn_on_leaving_unsaved: "Warnen wenn eine Seite mit ungespeichertem Text verlassen wird"
text_warn_on_leaving_unsaved: The current page contains unsaved text that will be lost if you leave this page. text_warn_on_leaving_unsaved: "Die aktuelle Seite enthält ungespeicherten Text er verloren geht wenn Sie diese Seite verlassen."
text_default_encoding: "Default: UTF-8" text_default_encoding: "Standard: UTF-8"
text_git_repo_example: a bare and local repository (e.g. /gitrepo, c:\gitrepo) text_git_repo_example: Ein lokales "bare Repository" (z.B. /gitrepo, c:\gitrepo)
label_notify_member_plural: Email issue updates label_notify_member_plural: Benachrichtigungen verschicken
label_path_encoding: Path encoding label_path_encoding: Pfadkodierung
text_mercurial_repo_example: local repository (e.g. /hgrepo, c:\hgrepo) text_mercurial_repo_example: Lokales Projektarchiv (z.B. /hgrepo, c:\hgrepo)
label_diff: diff label_diff: diff
description_filter: Filter description_filter: Filter
description_search: Suchfeld description_search: Suchfeld
...@@ -1003,19 +1003,19 @@ de: ...@@ -1003,19 +1003,19 @@ de:
description_date_range_interval: Zeitraum durch Start- und Enddatum festlegen description_date_range_interval: Zeitraum durch Start- und Enddatum festlegen
description_date_from: Startdatum eintragen description_date_from: Startdatum eintragen
description_date_to: Enddatum eintragen description_date_to: Enddatum eintragen
field_custom_filter: Custom LDAP filter field_custom_filter: Benutzerdefinierter LDAP-Filter
label_toc: "Inhaltsverzeichnis" label_toc: "Inhaltsverzeichnis"
text_display_subprojects: Display subprojects text_display_subprojects: Unterprojekte anzeigen
text_current_project: Current project text_current_project: Aktuelles Project
setting_mail_handler_confirmation_on_success: Send confirmation email on successful incoming email setting_mail_handler_confirmation_on_success: Bestätigungs-E-Mail bei erfolgreich eingegangenen E-Mails versenden
label_mail_handler_confirmation: "Confirmation of email submission: %{subject}" label_mail_handler_confirmation: "Bestätigung der E-Mail-Verarbeitung: %{subject}"
label_mail_handler_errors_with_submission: "There were errors with your email submission:" label_mail_handler_errors_with_submission: "Es traten Fehler bei der E-Mail verarbeitung auf:"
label_document_watchers: Watchers label_document_watchers: Beobachter
setting_mail_handler_confirmation_on_failure: Send confirmation email on failed incoming email setting_mail_handler_confirmation_on_failure: Bestätigungs-E-Mail bei fehlgeschlagenen eingehenden E-Mails versenden
label_between: between label_between: zwischen
label_mail_handler_failure: "Failed email submission: %{subject}" label_mail_handler_failure: "E-Mail-Versand fehlgeschlagen: %{subject}"
notice_not_authorized_action: You are not authorized to perform this action. notice_not_authorized_action: Sie sind für diese Aktion nicht autorisiert.
text_mail_handler_confirmation_successful: Your email has been successful added at the following url text_mail_handler_confirmation_successful: Ihre E-Mil wurde erfolgreich zu folgender URL hinzugefügt
field_issue_summary: Issue summary field_issue_summary: Ticketübersicht
field_new_saved_query: New saved query field_new_saved_query: Neue gespeicherte Abfrage
field_issue_view_all_open: View all open issues field_issue_view_all_open: Alle offen Tickets
This diff is collapsed.
...@@ -154,7 +154,7 @@ pt-BR: ...@@ -154,7 +154,7 @@ pt-BR:
general_text_Yes: 'Sim' general_text_Yes: 'Sim'
general_text_no: 'não' general_text_no: 'não'
general_text_yes: 'sim' general_text_yes: 'sim'
general_lang_name: 'Português(Brasil)' general_lang_name: 'Português (Brasil)'
general_csv_separator: ';' general_csv_separator: ';'
general_csv_decimal_separator: ',' general_csv_decimal_separator: ','
general_csv_encoding: ISO-8859-1 general_csv_encoding: ISO-8859-1
...@@ -245,7 +245,7 @@ pt-BR: ...@@ -245,7 +245,7 @@ pt-BR:
field_role: Cargo field_role: Cargo
field_homepage: Página do projeto field_homepage: Página do projeto
field_is_public: Público field_is_public: Público
field_parent: Sub-projeto de field_parent: Subprojeto de
field_is_in_roadmap: Exibir no planejamento field_is_in_roadmap: Exibir no planejamento
field_login: Usuário field_login: Usuário
field_mail_notification: Notificações por e-mail field_mail_notification: Notificações por e-mail
...@@ -273,7 +273,7 @@ pt-BR: ...@@ -273,7 +273,7 @@ pt-BR:
field_comments: Comentário field_comments: Comentário
field_url: URL field_url: URL
field_start_page: Página inicial field_start_page: Página inicial
field_subproject: Sub-projeto field_subproject: Subprojeto
field_hours: Horas field_hours: Horas
field_activity: Atividade field_activity: Atividade
field_spent_on: Data field_spent_on: Data
...@@ -411,8 +411,8 @@ pt-BR: ...@@ -411,8 +411,8 @@ pt-BR:
label_auth_source: Modo de autenticação label_auth_source: Modo de autenticação
label_auth_source_new: Novo modo de autenticação label_auth_source_new: Novo modo de autenticação
label_auth_source_plural: Modos de autenticação label_auth_source_plural: Modos de autenticação
label_subproject_plural: Sub-projetos label_subproject_plural: Subprojetos
label_and_its_subprojects: "%{value} e seus sub-projetos" label_and_its_subprojects: "%{value} e seus subprojetos"
label_min_max_length: Tamanho mín-máx label_min_max_length: Tamanho mín-máx
label_list: Lista label_list: Lista
label_date: Data label_date: Data
...@@ -879,7 +879,7 @@ pt-BR: ...@@ -879,7 +879,7 @@ pt-BR:
field_sharing: Compartilhamento field_sharing: Compartilhamento
label_version_sharing_hierarchy: Com a hierarquia do projeto label_version_sharing_hierarchy: Com a hierarquia do projeto
label_version_sharing_system: Com todos os projetos label_version_sharing_system: Com todos os projetos
label_version_sharing_descendants: Com sub-projetos label_version_sharing_descendants: Com subprojetos
label_version_sharing_tree: Com a árvore do projeto label_version_sharing_tree: Com a árvore do projeto
label_version_sharing_none: Sem compartilhamento label_version_sharing_none: Sem compartilhamento
error_can_not_archive_project: Este projeto não pode ser arquivado error_can_not_archive_project: Este projeto não pode ser arquivado
...@@ -989,7 +989,7 @@ pt-BR: ...@@ -989,7 +989,7 @@ pt-BR:
label_mercurial_path: Diretório raiz label_mercurial_path: Diretório raiz
label_diff: diff label_diff: diff
description_search: Searchfield description_search: Campo de pesquisa
description_user_mail_notification: Configuração de notificações por e-mail description_user_mail_notification: Configuração de notificações por e-mail
description_date_range_list: Escolha um período a partira da lista description_date_range_list: Escolha um período a partira da lista
description_date_to: Digite a data final description_date_to: Digite a data final
...@@ -1008,21 +1008,21 @@ pt-BR: ...@@ -1008,21 +1008,21 @@ pt-BR:
description_date_from: Digita a data inicial description_date_from: Digita a data inicial
label_deleted_custom_field: (campo personalizado excluído) label_deleted_custom_field: (campo personalizado excluído)
field_custom_filter: Custom LDAP filter field_custom_filter: Custom LDAP filter
text_display_subprojects: Display subprojects text_display_subprojects: Exibir subprojetos
text_current_project: Current project text_current_project: Projeto atual
label_toc: Contents label_toc: Conteúdo
search_input_placeholder: search ... search_input_placeholder: pesquisar ...
setting_mail_handler_confirmation_on_success: Send confirmation email on successful incoming email setting_mail_handler_confirmation_on_success: Enviar confirmação ao receber um e-mail
label_mail_handler_confirmation: "Confirmation of email submission: %{subject}" label_mail_handler_confirmation: "Confirmação de envio de e-mail: %{subject}"
label_mail_handler_errors_with_submission: "There were errors with your email submission:" label_mail_handler_errors_with_submission: "Seu envio de email falhou:"
label_document_watchers: Watchers label_document_watchers: Observadores
setting_mail_handler_confirmation_on_failure: Send confirmation email on failed incoming email setting_mail_handler_confirmation_on_failure: Enviar confirmação ao falhar o recebimento de um e-mail
label_between: between label_between: between
label_mail_handler_failure: "Failed email submission: %{subject}" label_mail_handler_failure: "Envio de e-mail falhou: %{subject}"
notice_not_authorized_action: You are not authorized to perform this action. notice_not_authorized_action: Você não tem permissão para fazer isto.
text_mail_handler_confirmation_successful: Your email has been successful added at the following url text_mail_handler_confirmation_successful: Seu e-mail foi adicionado com sucesso a URL a seguir
field_issue_summary: Issue summary field_issue_summary: Relatório
field_new_saved_query: New saved query field_new_saved_query: Nova consulta
field_issue_view_all_open: View all open issues field_issue_view_all_open: Ver tarefas abertas
label_subtask_add: Add a subtask label_subtask_add: Adicionar
label_issue_hierarchy: Issue hierarchy label_issue_hierarchy: Subtarefas
This diff is collapsed.
...@@ -12,6 +12,8 @@ ...@@ -12,6 +12,8 @@
# See doc/COPYRIGHT.rdoc for more details. # See doc/COPYRIGHT.rdoc for more details.
#++ #++
require 'wiki_content'
class MergeWikiVersionsWithJournals < ActiveRecord::Migration class MergeWikiVersionsWithJournals < ActiveRecord::Migration
# This is provided here for migrating up after the WikiContent::Version class has been removed # This is provided here for migrating up after the WikiContent::Version class has been removed
class WikiContent < ActiveRecord::Base class WikiContent < ActiveRecord::Base
......
= ChiliProject Changelog = ChiliProject Changelog
== 2012-06-09 v3.2.0
* Bug #844: Set autocomplete=off for some fields in Registration form
* Bug #863: missing journals fixture at test/unit/issue_test.rb
* Bug #950: jQuery AJAX requests don't include CSRF token
* Bug #966: "edit own notes" fails since 3.1.0
* Bug #967: Menu - Missing translations (French)
* Bug #968: Forum threads aren't always displaying "Last Message"
* Bug #969: Forum URLs in the menu are missing the project_id
* Bug #970: Long version titles extend outside the group menu when expanding Roadmap
* Bug #974: menu link broken in duplicate issue mode
* Bug #975: Start Date is not saved for Versions
* Bug #984: Cannot unlock a forum thread
* Bug #986: Notification Mail for Wiki-Changes doesn't contain change comment
* Bug #994: select all in issue list isn't working
* Bug #1007: Right clicking on item in roadmap displays menu at incorrect position
* Bug #1008: error 500 when uploading a new file to an existing document
* Bug #1024: Select multiple issues with shift key in issue list
* Bug #1025: Rails: Unsafe Query Generation Risk in Ruby on Rails (CVE-2012-2660)
* Bug #1033: Replace vendored gravatar lib by gem
* Feature #749: Git Integration: Property Main Branch
* Feature #947: Reformat the CSS files to use a standard
* Feature #983: Bulgarian translation of several strings
* Feature #988: Swedish translation of several strings
* Feature #1016: Limit height of project drop down menu
* Task #982: Updated czech localization for chiliproject 3.1
== 2012-04-04 v3.1.0 == 2012-04-04 v3.1.0
* Bug #739: Relative textile links not converted to full URLs in emails * Bug #739: Relative textile links not converted to full URLs in emails
......
.icon-example-works { background-image: url(../images/it_works.png); } .icon-example-works {
background-image: url(../images/it_works.png);
}
...@@ -18,7 +18,7 @@ module ChiliProject ...@@ -18,7 +18,7 @@ module ChiliProject
module VERSION #:nodoc: module VERSION #:nodoc:
MAJOR = 3 MAJOR = 3
MINOR = 1 MINOR = 2
PATCH = 0 PATCH = 0
TINY = PATCH # Redmine compat TINY = PATCH # Redmine compat
......
...@@ -259,7 +259,7 @@ Redmine::MenuManager.map :project_menu do |menu| ...@@ -259,7 +259,7 @@ Redmine::MenuManager.map :project_menu do |menu|
:caption => :label_issue_plural, :caption => :label_issue_plural,
:children => issue_query_proc :children => issue_query_proc
}) })
menu.push(:new_issue, { :controller => 'issues', :action => 'new' }, { menu.push(:new_issue, { :controller => 'issues', :action => 'new', :copy_from => nil }, {
:param => :project_id, :param => :project_id,
:caption => :label_issue_new, :caption => :label_issue_new,
:parent => :issues, :parent => :issues,
...@@ -345,7 +345,7 @@ Redmine::MenuManager.map :project_menu do |menu| ...@@ -345,7 +345,7 @@ Redmine::MenuManager.map :project_menu do |menu|
project.boards.collect do |board| project.boards.collect do |board|
Redmine::MenuManager::MenuItem.new( Redmine::MenuManager::MenuItem.new(
"board-#{board.id}".to_sym, "board-#{board.id}".to_sym,
{ :controller => 'boards', :action => 'show', :id => board }, { :controller => 'boards', :action => 'show', :project_id => project, :id => board },
{ {
:caption => board.name # is h() in menu_helper.rb :caption => board.name # is h() in menu_helper.rb
}) })
......
...@@ -100,6 +100,13 @@ module Redmine ...@@ -100,6 +100,13 @@ module Redmine
def default_branch def default_branch
bras = self.branches bras = self.branches
return nil if bras.nil? return nil if bras.nil?
head = nil
scm_cmd('symbolic-ref', 'HEAD') do |io|
head = io.readline
end
/^refs\/heads\/(.*)$/.match(head)
bras.include?($1) ? $1 : bras.first
rescue ScmCommandAborted, EOFError
bras.include?('master') ? 'master' : bras.first bras.include?('master') ? 'master' : bras.first
end end
......
...@@ -271,7 +271,8 @@ module Redmine ...@@ -271,7 +271,8 @@ module Redmine
end end
blame blame
rescue HgCommandAborted rescue HgCommandAborted
nil # means not found or cannot be annotated # means not found or cannot be annotated
Annotate.new
end end
class Revision < Redmine::Scm::Adapters::Revision class Revision < Redmine::Scm::Adapters::Revision
......
...@@ -24,6 +24,41 @@ end ...@@ -24,6 +24,41 @@ end
# Tasks can be hooked into by redefining them in a plugin # Tasks can be hooked into by redefining them in a plugin
namespace :ci do namespace :ci do
namespace :travis do
desc "Prepare a TRAVIS run"
task :prepare do
ENV['RAILS_ENV'] = 'test'
RAILS_ENV = 'test'
database_yml = {"database" => "chiliproject_test"}
database_yml.merge! case ENV['DB']
when 'mysql'
{"adapter" => "mysql", "username" => "root"}
when 'mysql2'
{"adapter" => "mysql2", "username" => "root"}
when 'postgres'
{"adapter" => "postgresql", "username" => "postgres"}
when 'sqlite'
{"adapter" => "sqlite3", "database" => "db/test.sqlite3"}
end
File.open("config/database.yml", 'w') do |f|
YAML.dump({"test" => database_yml}, f)
end
Rake::Task["generate_session_store"].invoke
# Create and migrate the database
Rake::Task["db:create"].invoke
Rake::Task["db:migrate"].invoke
Rake::Task["db:migrate:plugins"].invoke
Rake::Task["db:schema:dump"].invoke
# Create test repositories
Rake::Task["test:scm:setup:all"].invoke
end
end
desc "Setup Redmine for a new build." desc "Setup Redmine for a new build."
task :setup do task :setup do
Rake::Task["ci:dump_environment"].invoke Rake::Task["ci:dump_environment"].invoke
......
...@@ -467,7 +467,15 @@ jQuery.viewportHeight = function() { ...@@ -467,7 +467,15 @@ jQuery.viewportHeight = function() {
// Automatically use format.js for jQuery Ajax // Automatically use format.js for jQuery Ajax
jQuery.ajaxSetup({ jQuery.ajaxSetup({
'beforeSend': function(xhr) {xhr.setRequestHeader("Accept", "text/javascript")} 'beforeSend': function(xhr) {
xhr.setRequestHeader("Accept", "text/javascript");
// TODO: Remove once jquery-rails (Rails 3) has been added a dependency
var csrf_meta_tag = jQuery('meta[name="csrf-token"]');
if (csrf_meta_tag) {
xhr.setRequestHeader('X-CSRF-Token', csrf_meta_tag.attr('content'));
}
}
}) })
/* TODO: integrate with existing code and/or refactor */ /* TODO: integrate with existing code and/or refactor */
...@@ -563,6 +571,13 @@ jQuery(document).ready(function($) { ...@@ -563,6 +571,13 @@ jQuery(document).ready(function($) {
// Click on the menu header with a dropdown menu // Click on the menu header with a dropdown menu
$('#account-nav .drop-down').live('click', function(event) { $('#account-nav .drop-down').live('click', function(event) {
var menuItem = $(this); var menuItem = $(this);
var menuUl = menuItem.find('> ul');
menuUl.css('height', 'auto');
if(menuUl.height() > $.viewportHeight()) {
var windowHeight = $.viewportHeight() - 150;
menuUl.css({'height': windowHeight});
}
toggleTopMenu(menuItem); toggleTopMenu(menuItem);
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
var element = el; var element = el;
var opts = options; var opts = options;
var observingContextMenuClick; var observingContextMenuClick;
var observingToggleAllClick;
var lastSelected = null; var lastSelected = null;
var menu; var menu;
var menuId = 'context-menu'; var menuId = 'context-menu';
...@@ -68,18 +69,17 @@ ...@@ -68,18 +69,17 @@
if (lastSelected !== null) if (lastSelected !== null)
{ {
var toggling = false; var toggling = false;
var rows = $(selectorName); var rows = $('.' + selectorName);
for (i = 0; i < rows.length; i++) rows.each(function() {
{ var self = $(this);
if (toggling || rows[i] == tr) if(toggling || (self.get(0) == tr.get(0))) {
{ methods.addSelection(self);
methods.addSelection(rows[i]);
} }
if (rows[i] == tr || rows[i] == lastSelected) if(((self.get(0) == tr.get(0)) || (self.get(0) == lastSelected.get(0)))
{ && (tr.get(0) !== lastSelected.get(0))) {
toggling = !toggling; toggling = !toggling;
} }
} });
} else { } else {
methods.addSelection(tr); methods.addSelection(tr);
} }
...@@ -153,7 +153,7 @@ ...@@ -153,7 +153,7 @@
} }
if(maxHeight > $(window).height()) { if(maxHeight > $(window).height()) {
renderY =+ menu.height(); renderY -= menu.height();
menu.addClass(reverseYClass); menu.addClass(reverseYClass);
} else { } else {
menu.removeClass(reverseYClass); menu.removeClass(reverseYClass);
...@@ -174,6 +174,7 @@ ...@@ -174,6 +174,7 @@
addSelection: function(element) { addSelection: function(element) {
element.addClass(contextMenuSelectionClass); element.addClass(contextMenuSelectionClass);
methods.checkSelectionBox(element, true); methods.checkSelectionBox(element, true);
methods.clearDocumentSelection();
}, },
isSelected: function(element) { isSelected: function(element) {
return element.hasClass(contextMenuSelectionClass); return element.hasClass(contextMenuSelectionClass);
...@@ -194,6 +195,27 @@ ...@@ -194,6 +195,27 @@
inputs.each(function() { inputs.each(function() {
inputs.attr('checked', checked ? 'checked' : false); inputs.attr('checked', checked ? 'checked' : false);
}); });
},
toggleIssuesSelection: function(e) {
e.preventDefault();
e.stopPropagation();
var issues = $(this).parents('form').find('tr.issue');
var checked = methods.isSelected(issues.eq(0));
issues.each(function() {
var self = $(this);
if(checked) {
methods.removeSelection(self)
} else {
methods.addSelection(self);
}
});
},
clearDocumentSelection: function() {
if(document.selection) {
document.selection.clear();
} else {
window.getSelection().removeAllRanges();
}
} }
}; };
...@@ -205,6 +227,11 @@ ...@@ -205,6 +227,11 @@
observingContextMenuClick = true; observingContextMenuClick = true;
} }
if(!observingToggleAllClick) {
element.find('.issues img[alt="Toggle_check"]').bind('click', methods.toggleIssuesSelection);
observingToggleAllClick = true;
}
methods.unselectAll(); methods.unselectAll();
}; };
......
This diff is collapsed.
/* The main calendar widget. DIV containing a table. */ /* The main calendar widget. DIV containing a table. */
img.calendar-trigger { img.calendar-trigger {
cursor: pointer; cursor: pointer;
vertical-align: middle; vertical-align: middle;
margin-left: 4px; margin-left: 4px;
} }
div.calendar { position: relative; z-index: 30;} div.calendar {
position: relative;
z-index: 30;
}
div.calendar, div.calendar table { div.calendar,
div.calendar table {
border: 1px solid #556; border: 1px solid #556;
font-size: 11px; font-size: 11px;
color: #000; color: #000;
...@@ -18,10 +21,9 @@ div.calendar, div.calendar table { ...@@ -18,10 +21,9 @@ div.calendar, div.calendar table {
} }
/* Header part -- contains navigation buttons and day names. */ /* Header part -- contains navigation buttons and day names. */
div.calendar .button { /* "<<", "<", ">", ">>" buttons have this class */ div.calendar .button { /* "<<", "<", ">", ">>" buttons have this class */
text-align: center; /* They are the navigation buttons */ text-align: center; /* They are the navigation buttons */
padding: 2px; /* Make the buttons seem like they're pressing */ padding: 2px; /* Make the buttons seem like they're pressing */
} }
div.calendar .nav { div.calendar .nav {
...@@ -29,7 +31,7 @@ div.calendar .nav { ...@@ -29,7 +31,7 @@ div.calendar .nav {
} }
div.calendar thead .title { /* This holds the current "month, year" */ div.calendar thead .title { /* This holds the current "month, year" */
font-weight: bold; /* Pressing it will take you to the current date */ font-weight: bold; /* Pressing it will take you to the current date */
text-align: center; text-align: center;
background: #fff; background: #fff;
color: #000; color: #000;
...@@ -68,17 +70,18 @@ div.calendar thead .active { /* Active (pressed) buttons in header */ ...@@ -68,17 +70,18 @@ div.calendar thead .active { /* Active (pressed) buttons in header */
} }
/* The body part -- contains all the days in month. */ /* The body part -- contains all the days in month. */
div.calendar tbody .day { /* Cells <TD> containing month days dates */ div.calendar tbody .day { /* Cells <TD> containing month days dates */
width: 2em; width: 2em;
color: #456; color: #456;
text-align: right; text-align: right;
padding: 2px 4px 2px 2px; padding: 2px 4px 2px 2px;
} }
div.calendar tbody .day.othermonth { div.calendar tbody .day.othermonth {
font-size: 80%; font-size: 80%;
color: #bbb; color: #bbb;
} }
div.calendar tbody .day.othermonth.oweekend { div.calendar tbody .day.othermonth.oweekend {
color: #fbb; color: #fbb;
} }
...@@ -125,7 +128,9 @@ div.calendar tbody td.today { /* Cell showing selected date */ ...@@ -125,7 +128,9 @@ div.calendar tbody td.today { /* Cell showing selected date */
color: #f00; color: #f00;
} }
div.calendar tbody .disabled { color: #999; } div.calendar tbody .disabled {
color: #999;
}
div.calendar tbody .emptycell { /* Empty cells (the best is to hide them) */ div.calendar tbody .emptycell { /* Empty cells (the best is to hide them) */
visibility: hidden; visibility: hidden;
...@@ -136,7 +141,6 @@ div.calendar tbody .emptyrow { /* Empty row (some months need less than 6 rows) ...@@ -136,7 +141,6 @@ div.calendar tbody .emptyrow { /* Empty row (some months need less than 6 rows)
} }
/* The footer part -- status bar and "Close" button */ /* The footer part -- status bar and "Close" button */
div.calendar tfoot .footrow { /* The <TR> in footer (only one right now) */ div.calendar tfoot .footrow { /* The <TR> in footer (only one right now) */
text-align: center; text-align: center;
background: #556; background: #556;
...@@ -163,7 +167,6 @@ div.calendar tfoot .active { /* Active (pressed) style for buttons in footer */ ...@@ -163,7 +167,6 @@ div.calendar tfoot .active { /* Active (pressed) style for buttons in footer */
} }
/* Combo boxes (menus that display months/years for direct selection) */ /* Combo boxes (menus that display months/years for direct selection) */
div.calendar .combo { div.calendar .combo {
position: absolute; position: absolute;
display: none; display: none;
......
#context-menu { position: absolute; z-index: 40; } #context-menu {
position: absolute;
z-index: 40;
}
#context-menu ul, #context-menu li, #context-menu a { #context-menu ul,
display:block; #context-menu li,
margin:0; #context-menu a {
padding:0; display: block;
border:0; margin: 0;
padding: 0;
border: 0;
} }
#context-menu ul { #context-menu ul {
width:150px; width: 150px;
border-top:1px solid #ddd; border-top: 1px solid #ddd;
border-left:1px solid #ddd; border-left: 1px solid #ddd;
border-bottom:1px solid #777; border-bottom: 1px solid #777;
border-right:1px solid #777; border-right: 1px solid #777;
background:white; background: white;
list-style:none; list-style: none;
} }
#context-menu li { #context-menu li {
position:relative; position: relative;
padding:1px; padding: 1px;
z-index:39; z-index: 39;
border:1px solid white; border: 1px solid white;
}
#context-menu li.folder ul {
position: absolute;
left: 168px; /* ie6 */
top: -2px;
max-height: 300px;
overflow: hidden;
overflow-y: auto;
}
#context-menu li.folder > ul {
left: 148px;
} }
#context-menu li.folder ul { position:absolute; left:168px; /* IE6 */ top:-2px; max-height:300px; overflow:hidden; overflow-y: auto; }
#context-menu li.folder>ul { left:148px; }
#context-menu.reverse-y li.folder>ul { top:auto; bottom:0; } #context-menu.reverse-y li.folder > ul {
#context-menu.reverse-x li.folder ul { left:auto; right:168px; /* IE6 */ } top: auto;
#context-menu.reverse-x li.folder>ul { right:148px; } bottom: 0;
}
#context-menu.reverse-x li.folder ul {
left: auto;
right: 168px; /* IE6 */
}
#context-menu.reverse-x li.folder > ul {
right: 148px;
}
#context-menu a { #context-menu a {
text-decoration:none !important; text-decoration: none !important;
background-repeat: no-repeat; background-repeat: no-repeat;
background-position: 1px 50%; background-position: 1px 50%;
padding: 1px 0px 1px 20px; padding: 1px 0px 1px 20px;
width:100%; /* IE */ width: 100%; /* IE */
} }
#context-menu li>a { width:auto; } /* others */
#context-menu a.disabled, #context-menu a.disabled:hover {color: #ccc;} #context-menu li > a {
#context-menu li:hover { border:1px solid gray; background-color:#eee; } width: auto;
#context-menu a:hover {color:#2A5685;} }
#context-menu li.folder:hover { z-index:40; }
#context-menu ul ul, #context-menu li:hover ul ul { display:none; } /* others */
#context-menu li:hover ul, #context-menu li:hover li:hover ul { display:block; } #context-menu a.disabled,
#context-menu a.disabled:hover {
color: #ccc;
}
#context-menu li:hover {
border: 1px solid gray;
background-color: #eee;
}
#context-menu a:hover {
color: #2A5685;
}
#context-menu li.folder:hover {
z-index: 40;
}
#context-menu ul ul,
#context-menu li:hover ul ul {
display: none;
}
#context-menu li:hover ul,
#context-menu li:hover li:hover ul {
display: block;
}
/* selected element */ /* selected element */
.context-menu-selection { background-color:#507AAA !important; color:#000 !important; } .context-menu-selection {
.context-menu-selection:hover { background-color:#507AAA !important; color:#000 !important; } background-color: #507AAA !important;
color: #000 !important;
}
.context-menu-selection:hover {
background-color: #507AAA !important;
color: #000 !important;
}
#context-menu .submenu { #context-menu .submenu {
border: transparent solid 5px; border: transparent solid 5px;
......
#context-menu li.folder ul { left:auto; right:168px; } #context-menu li.folder ul {
#context-menu li.folder>ul { left:auto; right:148px; } left: auto;
#context-menu li a.submenu { background:url("../images/bullet_arrow_left.png") left no-repeat; } right: 168px;
}
#context-menu li.folder > ul {
left: auto;
right: 148px;
}
#context-menu li a.submenu {
background: url("../images/bullet_arrow_left.png") left no-repeat;
}
#context-menu a { #context-menu a {
background-position: 100% 40%; background-position: 100% 40%;
......
/* IE6 how i love to hate thee */ /* IE6 how i love to hate thee */
#account-nav li a { #account-nav li a {
width:45px; width: 45px;
} }
#account-nav li li a { #account-nav li li a {
width:150px; width: 150px;
} }
.title-bar { .title-bar {
zoom:1; zoom: 1;
} }
.title-bar-extras label { .title-bar-extras label {
float:none; float: none;
display:inline; display: inline;
padding-right:10px; padding-right: 10px;
} }
.issue-dropdown li.hover { .issue-dropdown li.hover {
background-color:#fff; background-color: #fff;
} }
.issue-dropdown li.hover ul { .issue-dropdown li.hover ul {
display:block; display: block;
left:112px; left: 112px;
} }
body .file-thumbs a { body .file-thumbs a {
width:150px; width: 150px;
} }
#history .journal { #history .journal {
zoom:1; zoom: 1;
} }
body #history .wiki { body #history .wiki {
overflow:hidden; overflow: hidden;
zoom:1; zoom: 1;
} }
#main-menu li li { #main-menu li li {
height:30px; height: 30px;
} }
#main-menu li li li { #main-menu li li li {
height:auto; height: auto;
} }
a.has-thumb.active { a.has-thumb.active {
background:none; background: none;
} }
.title-bar-extras ul { .title-bar-extras ul {
background-image:none; background-image: none;
border-top:1px solid #154E5D; border-top: 1px solid #154E5D;
} }
\ No newline at end of file
/* These will be included for IE6 & IE7 */ /* These will be included for IE6 & IE7 */
.title-bar h2 { .title-bar h2 {
height:21px; height: 21px;
} }
td.dropdown { td.dropdown {
z-index:50; z-index: 50;
position:relative; position: relative;
} }
body .title-bar-extras { body .title-bar-extras {
overflow:hidden; overflow: hidden;
} }
#main-menu, .title-bar { #main-menu,
z-index:4; .title-bar {
z-index: 4;
} }
.title-bar .button-large ul { .title-bar .button-large ul {
z-index:15; z-index: 15;
} }
form.tooltip-active { form.tooltip-active {
z-index:14; z-index: 14;
} }
#main-menu li li a span { #main-menu li li a span {
position:absolute; position: absolute;
right:0; right: 0;
top:0; top: 0;
display:block; display: block;
} }
#main-menu li li li a span { #main-menu li li li a span {
right:10px; right: 10px;
} }
body .file-thumbs a { body .file-thumbs a {
max-width:150px; max-width: 150px;
} }
#watchers { #watchers {
position:relative; position: relative;
z-index:5; z-index: 5;
} }
div.attachments { div.attachments {
position:relative; position: relative;
z-index:4; z-index: 4;
} }
fieldset.collapsible.header_collapsible legend { fieldset.collapsible.header_collapsible legend {
margin-left:-15px; margin-left: -15px;
} }
.jstEditor { .jstEditor {
padding-left: 0px; padding-left: 0px;
} }
.jstEditor textarea, .jstEditor iframe { .jstEditor textarea, .jstEditor iframe {
margin: 0; margin: 0;
} }
.jstHandle { .jstHandle {
height: 10px; height: 10px;
font-size: 0.1em; font-size: 0.1em;
cursor: s-resize; cursor: s-resize;
/*background: transparent url(img/resizer.png) no-repeat 45% 50%;*/ /*background: transparent url(img/resizer.png) no-repeat 45% 50%;*/
} }
.jstElements { .jstElements {
padding: 3px 3px; padding: 3px 3px;
} }
.jstElements button { .jstElements button {
margin-right : 6px; margin-right: 6px;
width : 24px; width: 24px;
height: 24px; height: 24px;
padding: 4px; padding: 4px;
border-style: solid; border-style: solid;
border-width: 1px; border-width: 1px;
border-color: #ddd; border-color: #ddd;
background-color : #f7f7f7; background-color: #f7f7f7;
background-position : 50% 50%; background-position: 50% 50%;
background-repeat: no-repeat; background-repeat: no-repeat;
} }
.jstElements button:hover { .jstElements button:hover {
border-color : #000; border-color: #000;
} }
.jstElements button span { .jstElements button span {
display : none; display: none;
} }
.jstElements span { .jstElements span {
display : inline; display: inline;
} }
.jstSpacer { .jstSpacer {
width : 0px; width: 0px;
font-size: 1px; font-size: 1px;
margin-right: 4px; margin-right: 4px;
} }
.jstElements .help { float: right; margin-right: 0.5em; padding-top: 8px; font-size: 0.9em; } .jstElements .help {
.jstElements .help a {padding: 2px 0 2px 20px; background: url(../images/help.png) no-repeat 0 50%;} float: right;
margin-right: 0.5em;
padding-top: 8px;
font-size: 0.9em;
}
.jstElements .help a {
padding: 2px 0 2px 20px;
background: url(../images/help.png) no-repeat 0 50%;
}
/* Buttons /* Buttons
-------------------------------------------------------- */ -------------------------------------------------------- */
.jstb_strong { .jstb_strong {
background-image: url(../images/jstoolbar/bt_strong.png); background-image: url(../images/jstoolbar/bt_strong.png);
} }
.jstb_em { .jstb_em {
background-image: url(../images/jstoolbar/bt_em.png); background-image: url(../images/jstoolbar/bt_em.png);
} }
.jstb_ins { .jstb_ins {
background-image: url(../images/jstoolbar/bt_ins.png); background-image: url(../images/jstoolbar/bt_ins.png);
} }
.jstb_del { .jstb_del {
background-image: url(../images/jstoolbar/bt_del.png); background-image: url(../images/jstoolbar/bt_del.png);
} }
.jstb_code { .jstb_code {
background-image: url(../images/jstoolbar/bt_code.png); background-image: url(../images/jstoolbar/bt_code.png);
} }
.jstb_h1 { .jstb_h1 {
background-image: url(../images/jstoolbar/bt_h1.png); background-image: url(../images/jstoolbar/bt_h1.png);
} }
.jstb_h2 { .jstb_h2 {
background-image: url(../images/jstoolbar/bt_h2.png); background-image: url(../images/jstoolbar/bt_h2.png);
} }
.jstb_h3 { .jstb_h3 {
background-image: url(../images/jstoolbar/bt_h3.png); background-image: url(../images/jstoolbar/bt_h3.png);
} }
.jstb_ul { .jstb_ul {
background-image: url(../images/jstoolbar/bt_ul.png); background-image: url(../images/jstoolbar/bt_ul.png);
} }
.jstb_ol { .jstb_ol {
background-image: url(../images/jstoolbar/bt_ol.png); background-image: url(../images/jstoolbar/bt_ol.png);
} }
.jstb_bq { .jstb_bq {
background-image: url(../images/jstoolbar/bt_bq.png); background-image: url(../images/jstoolbar/bt_bq.png);
} }
.jstb_unbq { .jstb_unbq {
background-image: url(../images/jstoolbar/bt_bq_remove.png); background-image: url(../images/jstoolbar/bt_bq_remove.png);
} }
.jstb_pre { .jstb_pre {
background-image: url(../images/jstoolbar/bt_pre.png); background-image: url(../images/jstoolbar/bt_pre.png);
} }
.jstb_link { .jstb_link {
background-image: url(../images/jstoolbar/bt_link.png); background-image: url(../images/jstoolbar/bt_link.png);
} }
.jstb_img { .jstb_img {
background-image: url(../images/jstoolbar/bt_img.png); background-image: url(../images/jstoolbar/bt_img.png);
} }
/***** Media print specific styles *****/ /***** Media print specific styles *****/
@media print { @media print {
#top-menu, #header, #main-menu, #sidebar, #footer, .contextual, .other-formats { display:none; } #top-menu,
#main { background: #fff; } #header,
#content { width: 99%; margin: 0; padding: 0; border: 0; background: #fff; overflow: visible !important;} #main-menu,
#wiki_add_attachment { display:none; } #sidebar,
.hide-when-print { display: none; } #footer,
.autoscroll {overflow-x: visible;} .contextual,
table.list {margin-top:0.5em;} .other-formats {
table.list th, table.list td {border: 1px solid #aaa;} display:none;
}
#main {
background: #fff;
}
#content {
width: 99%;
margin: 0;
padding: 0;
border: 0;
background: #fff;
overflow: visible !important;
}
#wiki_add_attachment {
display:none;
}
.hide-when-print {
display: none;
}
.autoscroll {
overflow-x: visible;
}
table.list {
margin-top:0.5em;
}
table.list th,
table.list td {
border: 1px solid #aaa;
}
} }
...@@ -2,37 +2,101 @@ ...@@ -2,37 +2,101 @@
* CSS Reset * CSS Reset
* Based on http://meyerweb.com/eric/tools/css/reset/ * Based on http://meyerweb.com/eric/tools/css/reset/
*/ */
html,
html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, font, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, b, u, i, center, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td { body,
margin: 0; div,
padding: 0; span,
border: 0; applet,
outline: 0; object,
font-size: 100%; iframe,
vertical-align: baseline; h1,
background: transparent; h2,
h3,
h4,
h5,
h6,
p,
blockquote,
pre,
a,
abbr,
acronym,
address,
big,
cite,
code,
del,
dfn,
em,
font,
img,
ins,
kbd,
q,
s,
samp,
small,
strike,
strong,
sub,
sup,
tt,
var,
b,
u,
i,
center,
dl,
dt,
dd,
ol,
ul,
li,
fieldset,
form,
label,
legend,
table,
caption,
tbody,
tfoot,
thead,
tr,
th,
td {
margin: 0;
padding: 0;
border: 0;
outline: 0;
font-size: 100%;
vertical-align: baseline;
background: transparent;
} }
body { body {
line-height: 1; line-height: 1;
} }
ol, ul {
list-style: none; ol,
ul {
list-style: none;
} }
ins { ins {
text-decoration: underline; text-decoration: underline;
} }
del { del {
text-decoration: line-through; text-decoration: line-through;
} }
/* tables still need 'cellspacing="0"' in the markup */ /* tables still need 'cellspacing="0"' in the markup */
table { table {
border-collapse: collapse; border-collapse: collapse;
border-spacing: 0; border-spacing: 0;
} }
/* Helper classes */ /* Helper classes */
.clearfix { .clearfix {
clear:both; clear: both;
} }
\ No newline at end of file
body, #wrapper { direction: rtl;} body,
#wrapper {
#quick-search { float: left; } direction: rtl;
#main-menu { margin-left: -500px; left: auto; right: 6px; margin-right: 0px;} }
#main-menu li { float: right; }
#top-menu ul { float: right; } #quick-search {
#account { float: left; } float: left;
#top-menu #loggedas { float: left; } }
#top-menu li { float: right; }
.tabular label.floating #main-menu {
{ margin-left: -500px;
margin-right: 0; left: auto;
margin-left: auto; right: 6px;
text-align: right; margin-right: 0px;
} }
.tabular label
{ #main-menu li {
float: right; float: right;
margin-left: auto; }
}
.tabular p #top-menu ul {
{ float: right;
clear: right; }
}
.tabular label.block { text-align: right; } #account {
.icon float: left;
{ }
background-position: 100% 40%;
padding-right: 20px; #top-menu #loggedas {
padding-left: 0px; float: left;
} }
div#activity dt, #search-results dt
{ #top-menu li {
background-position: 100% 50%; float: right;
padding-right: 20px; }
padding-left: 0px;
} .tabular label.floating {
#content .tabs ul li { float: right; } margin-right: 0;
#content .tabs ul { padding-left: auto; padding-right: 1em; } margin-left: auto;
table.progress { float: right; } text-align: right;
.contextual { float: left; } }
.icon22 { background-position: 100% 40%; padding-right: 26px; padding-left: auto; }
h3, .wiki h2 { padding: 10px 2px 1px 0; } .tabular label {
.tooltip span.tip { text-align: right; } float: right;
tr.issue td.subject { text-align: right; } margin-left: auto;
tr.time-entry td.subject, tr.time-entry td.comments { text-align: right; } }
#sidebar { float: left; }
#main.nosidebar #content { border-width: 1px; border-style: solid; border-color: #D7D7D7 #BBBBBB #BBBBBB #D7D7D7;} .tabular p {
.tabular.settings label { margin-left: auto; } clear: right;
.splitcontentleft { float: right; } }
.splitcontentright { float: left; }
p.progress-info { clear: right; } .tabular label.block {
table.list td.buttons a { padding-right: 20px; } text-align: right;
.filecontent { direction: ltr; } }
.entries { direction: ltr; }
.changeset-changes { direction: ltr; padding-left: 2em } .icon {
.changesets { direction: ltr; } background-position: 100% 40%;
div#issue-changesets { float: left; margin-right: 1em; margin-left: 0 } padding-right: 20px;
div#issue-changesets div.wiki { direction: ltr; padding-left: 2em } padding-left: 0px;
#activity dt, .journal { clear: right; } }
.journal-link { float: left; }
div.wiki pre { direction: ltr; } div#activity dt,
#search-results dt {
background-position: 100% 50%;
padding-right: 20px;
padding-left: 0px;
}
#content .tabs ul li {
float: right;
}
#content .tabs ul {
padding-left: auto;
padding-right: 1em;
}
table.progress {
float: right;
}
.contextual {
float: left;
}
.icon22 {
background-position: 100% 40%;
padding-right: 26px;
padding-left: auto;
}
h3,
.wiki h2 {
padding: 10px 2px 1px 0;
}
.tooltip span.tip {
text-align: right;
}
tr.issue td.subject {
text-align: right;
}
tr.time-entry td.subject,
tr.time-entry td.comments {
text-align: right;
}
#sidebar {
float: left;
}
#main.nosidebar #content {
border-width: 1px;
border-style: solid;
border-color: #D7D7D7 #BBBBBB #BBBBBB #D7D7D7;
}
.tabular.settings label {
margin-left: auto;
}
.splitcontentleft {
float: right;
}
.splitcontentright {
float: left;
}
p.progress-info {
clear: right;
}
table.list td.buttons a {
padding-right: 20px;
}
.filecontent {
direction: ltr;
}
.entries {
direction: ltr;
}
.changeset-changes {
direction: ltr;
padding-left: 2em;
}
.changesets {
direction: ltr;
}
div#issue-changesets {
float: left;
margin-right: 1em;
margin-left: 0;
}
div#issue-changesets div.wiki {
direction: ltr;
padding-left: 2em;
}
#activity dt,
.journal {
clear: right;
}
.journal-link {
float: left;
}
div.wiki pre {
direction: ltr;
}
This diff is collapsed.
...@@ -147,6 +147,28 @@ LOREM ...@@ -147,6 +147,28 @@ LOREM
end end
end end
context "#add_attachment" do
setup do
@request.session[:user_id] = 2
set_tmp_attachments_directory
@document = Document.generate!(:project => Project.find('ecookbook'),
:title => 'Test')
end
should "send a notification mail" do
ActionMailer::Base.deliveries.clear
Setting.notified_events = Setting.notified_events.dup << 'document_added'
post :add_attachment,
:id => @document.id,
:attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}}
@document.reload
assert_not_nil @document
assert_equal 2, ActionMailer::Base.deliveries.size
end
end
def test_destroy def test_destroy
@request.session[:user_id] = 2 @request.session[:user_id] = 2
post :destroy, :id => 1 post :destroy, :id => 1
......
...@@ -121,6 +121,30 @@ class MessagesControllerTest < ActionController::TestCase ...@@ -121,6 +121,30 @@ class MessagesControllerTest < ActionController::TestCase
assert_equal 'New body', message.content assert_equal 'New body', message.content
end end
def test_post_edit_sticky_and_locked
@request.session[:user_id] = 2
post :edit, :board_id => 1, :id => 1,
:message => { :subject => 'New subject',
:content => 'New body',
:locked => '1',
:sticky => '1'}
assert_redirected_to '/boards/1/topics/1'
message = Message.find(1)
assert_equal true, message.sticky?
assert_equal true, message.locked?
end
def test_post_edit_should_allow_to_change_board
@request.session[:user_id] = 2
post :edit, :board_id => 1, :id => 1,
:message => { :subject => 'New subject',
:content => 'New body',
:board_id => 2}
assert_redirected_to '/boards/2/topics/1'
message = Message.find(1)
assert_equal Board.find(2), message.board
end
def test_reply def test_reply
@request.session[:user_id] = 2 @request.session[:user_id] = 2
post :reply, :board_id => 1, :id => 1, :reply => { :content => 'This is a test reply', :subject => 'Test reply' } post :reply, :board_id => 1, :id => 1, :reply => { :content => 'This is a test reply', :subject => 'Test reply' }
......
...@@ -131,6 +131,10 @@ class ProjectEnumerationsControllerTest < ActionController::TestCase ...@@ -131,6 +131,10 @@ class ProjectEnumerationsControllerTest < ActionController::TestCase
end end
def test_update_when_creating_new_activities_will_not_convert_existing_data_if_an_exception_is_raised def test_update_when_creating_new_activities_will_not_convert_existing_data_if_an_exception_is_raised
# SQLite doesn't support nested transactions, thus we can't test transaction-
# based features in a test wrapped in a transaction.
return if ChiliProject::Database.sqlite?
# TODO: Need to cause an exception on create but these tests # TODO: Need to cause an exception on create but these tests
# aren't setup for mocking. Just create a record now so the # aren't setup for mocking. Just create a record now so the
# second one is a dupicate # second one is a dupicate
......
...@@ -112,13 +112,18 @@ class VersionsControllerTest < ActionController::TestCase ...@@ -112,13 +112,18 @@ class VersionsControllerTest < ActionController::TestCase
def test_post_update def test_post_update
@request.session[:user_id] = 2 @request.session[:user_id] = 2
today = Date.today
put :update, :id => 2, put :update, :id => 2,
:version => { :name => 'New version name', :version => { :name => 'New version name',
:effective_date => Date.today.strftime("%Y-%m-%d")} :start_date => today.yesterday.strftime("%Y-%m-%d"),
:effective_date => today.strftime("%Y-%m-%d"),
}
assert_redirected_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => 'ecookbook' assert_redirected_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => 'ecookbook'
version = Version.find(2) version = Version.find(2)
assert_equal 'New version name', version.name assert_equal 'New version name', version.name
assert_equal Date.today, version.effective_date assert_equal today.yesterday, version.start_date
assert_equal today, version.effective_date
end end
def test_post_update_with_validation_failure def test_post_update_with_validation_failure
......
...@@ -21,6 +21,7 @@ class IssueTest < ActiveSupport::TestCase ...@@ -21,6 +21,7 @@ class IssueTest < ActiveSupport::TestCase
:issue_statuses, :issue_categories, :issue_relations, :workflows, :issue_statuses, :issue_categories, :issue_relations, :workflows,
:enumerations, :enumerations,
:issues, :issues,
:journals,
:custom_fields, :custom_fields_projects, :custom_fields_trackers, :custom_values, :custom_fields, :custom_fields_projects, :custom_fields_trackers, :custom_values,
:time_entries :time_entries
......
...@@ -14,7 +14,9 @@ ...@@ -14,7 +14,9 @@
require File.expand_path('../../test_helper', __FILE__) require File.expand_path('../../test_helper', __FILE__)
class JournalTest < ActiveSupport::TestCase class JournalTest < ActiveSupport::TestCase
fixtures :issues, :issue_statuses, :journals, :enumerations fixtures :issues, :issue_statuses, :journals, :enumerations,\
:users, :trackers, :projects, :members, :member_roles, :roles,\
:enabled_modules
def setup def setup
@journal = IssueJournal.find(1) @journal = IssueJournal.find(1)
...@@ -35,23 +37,17 @@ class JournalTest < ActiveSupport::TestCase ...@@ -35,23 +37,17 @@ class JournalTest < ActiveSupport::TestCase
def test_create_should_send_email_notification def test_create_should_send_email_notification
ActionMailer::Base.deliveries.clear ActionMailer::Base.deliveries.clear
issue = Issue.find(:first) issue = issues :issues_001
if issue.journals.empty?
issue.init_journal(User.current, "This journal represents the creationa of journal version 1")
issue.save
end
user = User.find(:first)
assert_equal 0, ActionMailer::Base.deliveries.size assert_equal 0, ActionMailer::Base.deliveries.size
issue.reload
issue.update_attribute(:subject, "New subject to trigger automatic journal entry") issue.update_attribute(:subject, "New subject to trigger automatic journal entry")
assert_equal 2, ActionMailer::Base.deliveries.size assert_equal 2, ActionMailer::Base.deliveries.size
end end
def test_create_should_not_send_email_notification_if_told_not_to def test_create_should_not_send_email_notification_if_told_not_to
ActionMailer::Base.deliveries.clear ActionMailer::Base.deliveries.clear
issue = Issue.find(:first) issue = issues :issues_001
user = User.find(:first) user = users :users_001
journal = issue.init_journal(user, "A note") journal = issue.init_journal(user, "A note")
JournalObserver.instance.send_notification = false JournalObserver.instance.send_notification = false
......
...@@ -231,6 +231,13 @@ begin ...@@ -231,6 +231,13 @@ begin
end end
end end
def test_default_branch
@adapter.send :scm_cmd, 'branch', '-m', 'master', 'non-master-default-branch'
assert_equal 'non-master-default-branch', @adapter.default_branch
ensure
@adapter.send :scm_cmd, 'branch', '-m', 'non-master-default-branch', 'master'
end
private private
def test_scm_version_for(scm_command_version, version) def test_scm_version_for(scm_command_version, version)
......
...@@ -101,6 +101,18 @@ class MessageTest < ActiveSupport::TestCase ...@@ -101,6 +101,18 @@ class MessageTest < ActiveSupport::TestCase
# Watchers removed # Watchers removed
end end
def test_destroy_last_reply
message = Message.find(4)
last_reply = message.last_reply
penultimate_reply = message.children[-2]
assert last_reply.destroy
message.reload
assert_equal penultimate_reply, message.last_reply
end
def test_destroy_reply def test_destroy_reply
message = Message.find(5) message = Message.find(5)
board = message.board board = message.board
......
...@@ -17,7 +17,7 @@ class NewsTest < ActiveSupport::TestCase ...@@ -17,7 +17,7 @@ class NewsTest < ActiveSupport::TestCase
fixtures :projects, :users, :roles, :members, :member_roles, :enabled_modules, :news fixtures :projects, :users, :roles, :members, :member_roles, :enabled_modules, :news
def valid_news def valid_news
{ :title => 'Test news', :description => 'Lorem ipsum etc', :author => User.find(:first) } { :title => 'Test news', :description => 'Lorem ipsum etc', :author => users(:users_001) }
end end
...@@ -28,7 +28,7 @@ class NewsTest < ActiveSupport::TestCase ...@@ -28,7 +28,7 @@ class NewsTest < ActiveSupport::TestCase
def test_create_should_send_email_notification def test_create_should_send_email_notification
ActionMailer::Base.deliveries.clear ActionMailer::Base.deliveries.clear
Setting.notified_events = Setting.notified_events.dup << 'news_added' Setting.notified_events = Setting.notified_events.dup << 'news_added'
news = Project.find(:first).news.new(valid_news) news = projects(:projects_001).news.new(valid_news)
assert news.save assert news.save
assert_equal 2, ActionMailer::Base.deliveries.size assert_equal 2, ActionMailer::Base.deliveries.size
......
...@@ -82,7 +82,7 @@ class ProjectTest < ActiveSupport::TestCase ...@@ -82,7 +82,7 @@ class ProjectTest < ActiveSupport::TestCase
end end
assert_equal Tracker.all, Project.new.trackers assert_equal Tracker.all, Project.new.trackers
assert_equal Tracker.find(1, 3), Project.new(:tracker_ids => [1, 3]).trackers assert_equal Tracker.find(1, 3).sort_by(&:id), Project.new(:tracker_ids => [1, 3]).trackers.sort_by(&:id)
end end
def test_update def test_update
......
Copyright (c) 2007 West Arete Computing, Inc.
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
\ No newline at end of file
== Gravatar Plugin
This plugin provides a handful of view helpers for displaying gravatars
(globally-recognized avatars).
Gravatars allow users to configure an avatar to go with their email address at
a central location: http://gravatar.com. Gravatar-aware websites (such
as yours) can then look up and display each user's preferred avatar, without
having to handle avatar management. The user gets the benefit of not having to
set up an avatar for each site that they post on.
== Installation
cd ~/myapp
ruby script/plugin install git://github.com/woods/gravatar-plugin.git
or, if you're using piston[http://piston.rubyforge.org] (worth it!):
cd ~/myapp/vendor/plugins
piston import git://github.com/woods/gravatar-plugin.git
== Example
If you represent your users with a model that has an +email+ method (typical
for most rails authentication setups), then you can simply use this method
in your views:
<%= gravatar_for @user %>
This will be replaced with the full HTML +img+ tag necessary for displaying
that user's gravatar.
Other helpers are documented under GravatarHelper::PublicMethods.
== Acknowledgments
Thanks to Magnus Bergmark (http://github.com/Mange), who contributed the SSL
support in this plugin, as well as a few minor fixes.
The following people have also written gravatar-related Ruby libraries:
* Seth Rasmussen created the gravatar gem[http://gravatar.rubyforge.org]
* Matt McCray has also created a gravatar
plugin[http://mattmccray.com/svn/rails/plugins/gravatar_helper]
== Author
Scott A. Woods
West Arete Computing, Inc.
http://westarete.com
scott at westarete dot com
== TODO
* Add specs for ssl support
* Finish rdoc documentation
\ No newline at end of file
require 'spec/rake/spectask'
require 'rake/rdoctask'
desc 'Default: run all specs'
task :default => :spec
desc 'Run all application-specific specs'
Spec::Rake::SpecTask.new(:spec) do |t|
# t.rcov = true
end
desc "Report code statistics (KLOCs, etc) from the application"
task :stats do
RAILS_ROOT = File.dirname(__FILE__)
STATS_DIRECTORIES = [
%w(Libraries lib/),
%w(Specs spec/),
].collect { |name, dir| [ name, "#{RAILS_ROOT}/#{dir}" ] }.select { |name, dir| File.directory?(dir) }
require 'code_statistics'
CodeStatistics.new(*STATS_DIRECTORIES).to_s
end
namespace :doc do
desc 'Generate documentation for the assert_request plugin.'
Rake::RDocTask.new(:plugin) do |rdoc|
rdoc.rdoc_dir = 'rdoc'
rdoc.title = 'Gravatar Rails Plugin'
rdoc.options << '--line-numbers' << '--inline-source' << '--accessor' << 'cattr_accessor=rw'
rdoc.rdoc_files.include('README')
rdoc.rdoc_files.include('lib/**/*.rb')
end
end
author: Scott Woods, West Arete Computing
summary: View helpers for displaying gravatars.
homepage: http://github.com/woods/gravatar-plugin/
plugin: git://github.com/woods/gravatar-plugin.git
license: MIT
version: 0.1
rails_version: 1.0+
#-- encoding: UTF-8
require 'gravatar'
ActionView::Base.send :include, GravatarHelper::PublicMethods
#-- encoding: UTF-8
require 'digest/md5'
require 'cgi'
module GravatarHelper
# These are the options that control the default behavior of the public
# methods. They can be overridden during the actual call to the helper,
# or you can set them in your environment.rb as such:
#
# # Allow racier gravatars
# GravatarHelper::DEFAULT_OPTIONS[:rating] = 'R'
#
DEFAULT_OPTIONS = {
# The URL of a default image to display if the given email address does
# not have a gravatar.
:default => nil,
# The default size in pixels for the gravatar image (they're square).
:size => 50,
# The maximum allowed MPAA rating for gravatars. This allows you to
# exclude gravatars that may be out of character for your site.
:rating => 'PG',
# The alt text to use in the img tag for the gravatar. Since it's a
# decorational picture, the alt text should be empty according to the
# XHTML specs.
:alt => '',
# The title text to use for the img tag for the gravatar.
:title => '',
# The class to assign to the img tag for the gravatar.
:class => 'gravatar',
# Whether or not to display the gravatars using HTTPS instead of HTTP
:ssl => false,
}
# The methods that will be made available to your views.
module PublicMethods
# Return the HTML img tag for the given user's gravatar. Presumes that
# the given user object will respond_to "email", and return the user's
# email address.
def gravatar_for(user, options={})
gravatar(user.email, options)
end
# Return the HTML img tag for the given email address's gravatar.
def gravatar(email, options={})
src = h(gravatar_url(email, options))
options = DEFAULT_OPTIONS.merge(options)
[:class, :alt, :size, :title].each { |opt| options[opt] = h(options[opt]) }
"<img class=\"#{options[:class]}\" alt=\"#{options[:alt]}\" title=\"#{options[:title]}\" width=\"#{options[:size]}\" height=\"#{options[:size]}\" src=\"#{src}\" />"
end
# Returns the base Gravatar URL for the given email hash. If ssl evaluates to true,
# a secure URL will be used instead. This is required when the gravatar is to be
# displayed on a HTTPS site.
def gravatar_api_url(hash, ssl=false)
if ssl
"https://secure.gravatar.com/avatar/#{hash}"
else
"http://www.gravatar.com/avatar/#{hash}"
end
end
# Return the gravatar URL for the given email address.
def gravatar_url(email, options={})
email_hash = Digest::MD5.hexdigest(email)
options = DEFAULT_OPTIONS.merge(options)
options[:default] = CGI::escape(options[:default]) unless options[:default].nil?
gravatar_api_url(email_hash, options.delete(:ssl)).tap do |url|
opts = []
[:rating, :size, :default].each do |opt|
unless options[opt].nil?
value = h(options[opt])
opts << [opt, value].join('=')
end
end
url << "?#{opts.join('&')}" unless opts.empty?
end
end
end
end
#-- encoding: UTF-8
require 'rubygems'
require 'erb' # to get "h"
require 'active_support' # to get "returning"
require File.dirname(__FILE__) + '/../lib/gravatar'
include GravatarHelper, GravatarHelper::PublicMethods, ERB::Util
describe "gravatar_url with a custom default URL" do
before(:each) do
@original_options = DEFAULT_OPTIONS.dup
DEFAULT_OPTIONS[:default] = "no_avatar.png"
@url = gravatar_url("somewhere")
end
it "should include the \"default\" argument in the result" do
@url.should match(/&default=no_avatar.png/)
end
after(:each) do
DEFAULT_OPTIONS.merge!(@original_options)
end
end
describe "gravatar_url with default settings" do
before(:each) do
@url = gravatar_url("somewhere")
end
it "should have a nil default URL" do
DEFAULT_OPTIONS[:default].should be_nil
end
it "should not include the \"default\" argument in the result" do
@url.should_not match(/&default=/)
end
end
describe "gravatar with a custom title option" do
it "should include the title in the result" do
gravatar('example@example.com', :title => "This is a title attribute").should match(/This is a title attribute/)
end
end
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment