Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Merge branch 'release-v3.0.0' into stable

  • Loading branch information...
commit 11e93ff36a126bd0f7ce3022bc9aa8777628237c 2 parents ad996d7 + 3ecace9
@meineerde meineerde authored
Showing with 3,201 additions and 704 deletions.
  1. +1 −0  .gitignore
  2. +4 −1 Gemfile
  3. +4 −0 app/controllers/admin_controller.rb
  4. +3 −2 app/controllers/application_controller.rb
  5. +34 −1 app/controllers/auto_completes_controller.rb
  6. +27 −9 app/controllers/documents_controller.rb
  7. +5 −1 app/controllers/files_controller.rb
  8. +11 −8 app/controllers/groups_controller.rb
  9. +23 −0 app/controllers/journals_controller.rb
  10. +1 −0  app/controllers/ldap_auth_sources_controller.rb
  11. +1 −1  app/controllers/messages_controller.rb
  12. +1 −0  app/controllers/queries_controller.rb
  13. +4 −2 app/controllers/time_entry_reports_controller.rb
  14. +10 −2 app/controllers/users_controller.rb
  15. +28 −4 app/controllers/watchers_controller.rb
  16. +37 −0 app/drops/base_drop.rb
  17. +79 −0 app/drops/issue_drop.rb
  18. +17 −0 app/drops/issue_status_drop.rb
  19. +17 −0 app/drops/principal_drop.rb
  20. +17 −0 app/drops/project_drop.rb
  21. +17 −0 app/drops/tracker_drop.rb
  22. +17 −0 app/drops/wiki_page_drop.rb
  23. +156 −24 app/helpers/application_helper.rb
  24. +3 −2 app/helpers/issues_helper.rb
  25. +20 −10 app/helpers/journals_helper.rb
  26. +3 −2 app/helpers/queries_helper.rb
  27. +7 −0 app/models/document.rb
  28. +5 −1 app/models/document_observer.rb
  29. +5 −0 app/models/group.rb
  30. +4 −0 app/models/issue.rb
  31. +3 −1 app/models/issue_observer.rb
  32. +4 −0 app/models/issue_status.rb
  33. +10 −3 app/models/journal_observer.rb
  34. +7 −0 app/models/mail_handler.rb
  35. +62 −39 app/models/mailer.rb
  36. +8 −1 app/models/message_observer.rb
  37. +5 −1 app/models/news_observer.rb
  38. +80 −0 app/models/principal.rb
  39. +16 −1 app/models/project.rb
  40. +73 −95 app/models/query.rb
  41. +16 −0 app/models/query/statement_invalid.rb
  42. +42 −0 app/models/query_column.rb
  43. +40 −0 app/models/query_custom_field_column.rb
  44. +41 −14 app/models/setting.rb
  45. +4 −0 app/models/tracker.rb
  46. +0 −68 app/models/user.rb
  47. +1 −1  app/models/watcher.rb
  48. +4 −0 app/models/wiki_page.rb
  49. +34 −0 app/views/account/_login.rhtml
  50. +1 −1  app/views/account/login.rhtml
  51. +1 −5 app/views/admin/_menu.rhtml
  52. +0 −2  app/views/admin/index.rhtml
  53. +1 −0  app/views/auto_completes/projects.html.erb
  54. 0  app/views/{groups/autocomplete_for_user.html.erb → auto_completes/users.html.erb}
  55. +10 −0 app/views/boards/show.rhtml
  56. +36 −26 app/views/context_menus/issues.html.erb
  57. +9 −0 app/views/documents/_form.rhtml
  58. +10 −0 app/views/documents/show.rhtml
  59. +1 −13 app/views/groups/_memberships.html.erb
  60. +1 −1  app/views/groups/_users.html.erb
  61. +23 −22 app/views/help/wiki_syntax_detailed.html.erb
  62. +1 −1  app/views/issues/_edit.rhtml
  63. +6 −7 app/views/issues/_relations.rhtml
  64. +0 −10 app/views/issues/_sidebar.rhtml
  65. +27 −4 app/views/issues/index.rhtml
  66. +78 −59 app/views/issues/show.rhtml
  67. +9 −0 app/views/journals/diff.html.erb
  68. +3 −5 app/views/layouts/admin.rhtml
  69. +123 −50 app/views/layouts/base.rhtml
  70. +2 −0  app/views/mailer/mail_handler_confirmation.text.html.rhtml
  71. +2 −0  app/views/mailer/mail_handler_confirmation.text.plain.rhtml
  72. +3 −0  app/views/mailer/mail_handler_missing_information.text.html.rhtml
  73. +3 −0  app/views/mailer/mail_handler_missing_information.text.plain.rhtml
  74. +1 −0  app/views/mailer/mail_handler_unauthorized_action.text.html.rhtml
  75. +1 −0  app/views/mailer/mail_handler_unauthorized_action.text.plain.rhtml
  76. +23 −0 app/views/members/_membership_assignment.html.erb
  77. +10 −0 app/views/messages/show.rhtml
  78. +5 −1 app/views/projects/index.rhtml
  79. +1 −1  app/views/projects/settings/_issue_categories.rhtml
  80. +50 −19 app/views/queries/_filters.rhtml
  81. +12 −0 app/views/queries/_form.rhtml
  82. +5 −0 app/views/queries/_new_issue_button.html.erb
  83. +5 −0 app/views/search/_quick_search.html.erb
  84. +3 −0  app/views/settings/_mail_handler.rhtml
  85. +3 −2 app/views/users/_mail_notifications.html.erb
  86. +1 −12 app/views/users/_memberships.rhtml
  87. +1 −0  app/views/versions/index.html.erb
  88. +16 −9 app/views/watchers/_watchers.rhtml
  89. +8 −0 app/views/wiki/show.rhtml
  90. +3 −0  config/environment.rb
  91. +13 −0 config/locales/bg.yml
  92. +13 −0 config/locales/bs.yml
  93. +13 −0 config/locales/ca.yml
  94. +13 −0 config/locales/cs.yml
  95. +13 −0 config/locales/da.yml
  96. +13 −0 config/locales/de.yml
  97. +13 −0 config/locales/el.yml
  98. +13 −0 config/locales/en-GB.yml
  99. +13 −0 config/locales/en.yml
  100. +13 −0 config/locales/es.yml
  101. +13 −0 config/locales/eu.yml
  102. +13 −0 config/locales/fa.yml
  103. +13 −0 config/locales/fi.yml
  104. +13 −0 config/locales/fr.yml
  105. +13 −0 config/locales/gl.yml
  106. +13 −0 config/locales/he.yml
  107. +13 −0 config/locales/hr.yml
  108. +13 −0 config/locales/hu.yml
  109. +13 −0 config/locales/id.yml
  110. +13 −0 config/locales/it.yml
  111. +13 −0 config/locales/ja.yml
  112. +13 −0 config/locales/ko.yml
  113. +13 −0 config/locales/lt.yml
  114. +13 −0 config/locales/lv.yml
  115. +13 −0 config/locales/mk.yml
  116. +13 −0 config/locales/mn.yml
  117. +13 −0 config/locales/nl.yml
  118. +13 −0 config/locales/no.yml
  119. +13 −0 config/locales/pl.yml
  120. +13 −0 config/locales/pt-BR.yml
  121. +13 −0 config/locales/pt.yml
  122. +13 −0 config/locales/ro.yml
  123. +13 −0 config/locales/ru.yml
  124. +13 −0 config/locales/sk.yml
  125. +13 −0 config/locales/sl.yml
  126. +13 −0 config/locales/sr-YU.yml
  127. +13 −0 config/locales/sr.yml
  128. +13 −0 config/locales/sv.yml
  129. +13 −0 config/locales/th.yml
  130. +13 −0 config/locales/tr.yml
  131. +13 −0 config/locales/uk.yml
  132. +13 −0 config/locales/vi.yml
  133. +13 −0 config/locales/zh-TW.yml
  134. +13 −0 config/locales/zh.yml
  135. +2 −0  config/routes.rb
  136. +5 −1 config/settings.yml
  137. +23 −0 db/migrate/20111025231354_add_display_subprojects_to_query.rb
  138. +43 −0 db/migrate/20111125191432_acts_as_taggable_on_migration.rb
  139. +49 −1 doc/CHANGELOG.rdoc
  140. +8 −1 lib/chili_project/compatibility.rb
  141. +31 −0 lib/chili_project/info.rb
  142. +23 −0 lib/chili_project/liquid.rb
  143. +32 −0 lib/chili_project/liquid/file_system.rb
  144. +99 −0 lib/chili_project/liquid/filters.rb
  145. +87 −0 lib/chili_project/liquid/legacy.rb
  146. +24 −0 lib/chili_project/liquid/liquid_ext.rb
  147. +40 −0 lib/chili_project/liquid/liquid_ext/block.rb
  148. +62 −0 lib/chili_project/liquid/liquid_ext/context.rb
  149. +44 −0 lib/chili_project/liquid/liquid_ext/strainer.rb
  150. +54 −0 lib/chili_project/liquid/tags.rb
  151. +74 −0 lib/chili_project/liquid/tags/child_pages.rb
  152. +25 −0 lib/chili_project/liquid/tags/hello_world.rb
  153. +28 −0 lib/chili_project/liquid/tags/identity.rb
  154. +83 −0 lib/chili_project/liquid/tags/include.rb
  155. +18 −0 lib/chili_project/liquid/tags/tag.rb
  156. +137 −0 lib/chili_project/liquid/template.rb
  157. +61 −0 lib/chili_project/liquid/variables.rb
  158. +2 −2 lib/chili_project/version.rb
  159. +15 −3 lib/redmine.rb
  160. +2 −15 lib/redmine/info.rb
  161. +13 −1 lib/redmine/pop3.rb
  162. +1 −1  lib/redmine/syntax_highlighting.rb
  163. +1 −52 lib/redmine/wiki_formatting.rb
  164. +32 −83 lib/redmine/wiki_formatting/macros.rb
  165. +2 −0  lib/tasks/email.rake
  166. BIN  public/images/add.png
  167. BIN  public/images/arrow-bottom-right.png
  168. BIN  public/images/arrow-down-2.png
  169. BIN  public/images/arrow-down-3.png
  170. BIN  public/images/arrow-down-grey.png
  171. BIN  public/images/arrow-down-white.png
  172. BIN  public/images/arrow-down.png
  173. BIN  public/images/arrow-right.png
  174. BIN  public/images/arrow-up-white.png
  175. BIN  public/images/arrow_breadcrumb.png
  176. BIN  public/images/arrow_grey_top_navigation.png
  177. BIN  public/images/arrow_white_top_navigation.png
  178. BIN  public/images/background_active_button.png
  179. BIN  public/images/background_breadcrumb.png
  180. BIN  public/images/background_header.png
  181. BIN  public/images/background_search.png
  182. BIN  public/images/background_top_navigation.png
  183. BIN  public/images/background_widgets.png
  184. BIN  public/images/blockquote-bg.png
  185. BIN  public/images/calendar.png
  186. BIN  public/images/check.png
  187. BIN  public/images/clock.png
  188. BIN  public/images/comment.png
  189. BIN  public/images/copy.png
  190. BIN  public/images/delete.png
  191. BIN  public/images/delete.png.oxygen
  192. BIN  public/images/disk.png
  193. BIN  public/images/dot-blue.png
  194. BIN  public/images/double_arrow_toggle_down.png
  195. BIN  public/images/double_arrow_toggle_down_white.png
  196. BIN  public/images/double_arrow_toggle_up.png
  197. BIN  public/images/double_arrow_toggle_up_white.png
  198. BIN  public/images/edit.png
  199. BIN  public/images/files-showhide.png
  200. 0  public/{themes/chiliproject → }/images/fugue/arrow.png
  201. 0  public/{themes/chiliproject → }/images/fugue/balloons.png
  202. 0  public/{themes/chiliproject → }/images/fugue/books-stack.png
  203. 0  public/{themes/chiliproject → }/images/fugue/burn.png
  204. 0  public/{themes/chiliproject → }/images/fugue/calendar-month.png
  205. 0  public/{themes/chiliproject → }/images/fugue/clock--plus.png
  206. 0  public/{themes/chiliproject → }/images/fugue/clock.png
  207. 0  public/{themes/chiliproject → }/images/fugue/dashboard--pencil.png
  208. 0  public/{themes/chiliproject → }/images/fugue/disk-black.png
  209. 0  public/{themes/chiliproject → }/images/fugue/document-horizontal-text.png
  210. 0  public/{themes/chiliproject → }/images/fugue/document-text-image.png
  211. 0  public/{themes/chiliproject → }/images/fugue/document-zipper.png
  212. 0  public/{themes/chiliproject → }/images/fugue/documents-text.png
  213. 0  public/{themes/chiliproject → }/images/fugue/documents.png
  214. 0  public/{themes/chiliproject → }/images/fugue/equalizer.png
  215. 0  public/{themes/chiliproject → }/images/fugue/hammer--arrow.png
  216. 0  public/{themes/chiliproject → }/images/fugue/history.txt
  217. 0  public/{themes/chiliproject → }/images/fugue/layout-2.png
  218. 0  public/{themes/chiliproject → }/images/fugue/layout-select-content.png
  219. 0  public/{themes/chiliproject → }/images/fugue/lightning.png
  220. 0  public/{themes/chiliproject → }/images/fugue/magnifier-left.png
  221. 0  public/{themes/chiliproject → }/images/fugue/map-pin.png
  222. 0  public/{themes/chiliproject → }/images/fugue/money--pencil.png
  223. 0  public/{themes/chiliproject → }/images/fugue/monitor.png
  224. 0  public/{themes/chiliproject → }/images/fugue/newspaper.png
  225. 0  public/{themes/chiliproject → }/images/fugue/notebooks--pencil.png
  226. 0  public/{themes/chiliproject → }/images/fugue/pencil-small.png
  227. 0  public/{themes/chiliproject → }/images/fugue/pill--exclamation.png
  228. 0  public/{themes/chiliproject → }/images/fugue/plus-small.png
  229. 0  public/{themes/chiliproject → }/images/fugue/projection-screen--pencil.png
  230. 0  public/{themes/chiliproject → }/images/fugue/question-balloon.png
  231. 0  public/{themes/chiliproject → }/images/fugue/readme.txt
  232. 0  public/{themes/chiliproject → }/images/fugue/report--exclamation.png
  233. 0  public/{themes/chiliproject → }/images/fugue/ruler--pencil.png
  234. 0  public/{themes/chiliproject → }/images/fugue/safe.png
  235. 0  public/{themes/chiliproject → }/images/fugue/star-empty.png
  236. 0  public/{themes/chiliproject → }/images/fugue/star.png
  237. 0  public/{themes/chiliproject → }/images/fugue/sticky-note.png
  238. 0  public/{themes/chiliproject → }/images/fugue/tags-label.png
  239. 0  public/{themes/chiliproject → }/images/fugue/tick-shield.png
  240. 0  public/{themes/chiliproject → }/images/fugue/ticket--arrow.png
  241. 0  public/{themes/chiliproject → }/images/fugue/ticket--minus.png
  242. 0  public/{themes/chiliproject → }/images/fugue/ticket--plus.png
  243. 0  public/{themes/chiliproject → }/images/fugue/ticket.png
  244. 0  public/{themes/chiliproject → }/images/fugue/trophy.png
  245. 0  public/{themes/chiliproject → }/images/fugue/ui-progress-bar.png
  246. 0  public/{themes/chiliproject → }/images/fugue/user-business.png
  247. BIN  public/images/gradient-down.png
  248. BIN  public/images/gradient-up.png
  249. BIN  public/images/icon_help.png
  250. BIN  public/images/icon_help_grey.png
  251. BIN  public/images/icon_home.png
  252. BIN  public/images/icon_home_grey.png
  253. BIN  public/images/loadingAnimation.gif
  254. BIN  public/images/macFFBgHack.png
  255. BIN  public/images/mimetypes/applix.png
  256. BIN  public/images/mimetypes/ascii.png
  257. BIN  public/images/mimetypes/binary.png
  258. BIN  public/images/mimetypes/cdbo_list.png
  259. BIN  public/images/mimetypes/cdimage.png
  260. BIN  public/images/mimetypes/cdr.png
  261. BIN  public/images/mimetypes/cdtrack.png
  262. BIN  public/images/mimetypes/colorscm.png
  263. BIN  public/images/mimetypes/core.png
  264. BIN  public/images/mimetypes/deb.png
  265. BIN  public/images/mimetypes/document.png
  266. BIN  public/images/mimetypes/document2.png
  267. BIN  public/images/mimetypes/dvi.png
  268. BIN  public/images/mimetypes/empty.png
  269. BIN  public/images/mimetypes/encrypted.png
  270. BIN  public/images/mimetypes/exec_wine.png
  271. BIN  public/images/mimetypes/file_locked.png
  272. BIN  public/images/mimetypes/file_temporary.png
  273. BIN  public/images/mimetypes/font.png
  274. BIN  public/images/mimetypes/font_bitmap.png
  275. BIN  public/images/mimetypes/font_truetype.png
  276. BIN  public/images/mimetypes/font_type1.png
  277. BIN  public/images/mimetypes/gf.png
  278. BIN  public/images/mimetypes/html.png
  279. BIN  public/images/mimetypes/image.png
  280. BIN  public/images/mimetypes/image2.png
  281. BIN  public/images/mimetypes/info.png
  282. BIN  public/images/mimetypes/karbon.png
  283. BIN  public/images/mimetypes/karbon_karbon.png
  284. BIN  public/images/mimetypes/kchart_chrt.png
  285. BIN  public/images/mimetypes/kformula_kfo.png
  286. BIN  public/images/mimetypes/kivio_flw.png
  287. BIN  public/images/mimetypes/kmultiple.png
  288. BIN  public/images/mimetypes/koffice.png
  289. BIN  public/images/mimetypes/kpresenter_kpr.png
  290. BIN  public/images/mimetypes/krita_kra.png
  291. BIN  public/images/mimetypes/kspread_ksp.png
  292. BIN  public/images/mimetypes/kugar_kud.png
  293. BIN  public/images/mimetypes/kugardata.png
  294. BIN  public/images/mimetypes/kword_kwd.png
  295. BIN  public/images/mimetypes/log.png
  296. BIN  public/images/mimetypes/make.png
  297. BIN  public/images/mimetypes/man.png
  298. BIN  public/images/mimetypes/message.png
  299. BIN  public/images/mimetypes/message2.png
  300. BIN  public/images/mimetypes/metafont.png
Sorry, we could not display the entire diff because too many files (525) changed.
View
1  .gitignore
@@ -4,6 +4,7 @@
/config/configuration.yml
/config/database.yml
/config/email.yml
+/config/setup_load_paths.rb
/config/initializers/session_store.rb
/coverage
/db/*.db
View
5 Gemfile
@@ -1,11 +1,14 @@
+# -*- coding: utf-8 -*-
source :rubygems
gem "rails", "2.3.14"
-gem "coderay", "~> 0.9.7"
+gem "coderay", "~> 1.0.0"
gem "i18n", "~> 0.4.2"
gem "rubytree", "~> 0.5.2", :require => 'tree'
gem "rdoc", ">= 2.4.2"
+gem "liquid", "~> 2.3.0"
+gem "acts-as-taggable-on", "= 2.1.0"
# 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]
View
4 app/controllers/admin_controller.rb
@@ -19,6 +19,10 @@ class AdminController < ApplicationController
include SortHelper
+ menu_item :projects, :only => [:projects]
+ menu_item :plugins, :only => [:plugins]
+ menu_item :info, :only => [:info]
+
def index
@no_configuration_data = Redmine::DefaultData::Loader::no_data?
end
View
5 app/controllers/application_controller.rb
@@ -65,6 +65,9 @@ def utf8nize!(obj)
filter_parameter_logging :password
rescue_from ActionController::InvalidAuthenticityToken, :with => :invalid_authenticity_token
+ # FIXME: This doesn't work with Rails >= 3.0 anymore
+ # Possible workaround: https://github.com/rails/rails/issues/671#issuecomment-1780159
+ rescue_from ActionController::RoutingError, :with => proc{render_404}
include Redmine::Search::Controller
include Redmine::MenuManager::MenuController
@@ -75,8 +78,6 @@ def utf8nize!(obj)
end
def user_setup
- # Check the settings cache for each request
- Setting.check_cache
# Find the current user
User.current = find_current_user
end
View
35 app/controllers/auto_completes_controller.rb
@@ -13,7 +13,8 @@
#++
class AutoCompletesController < ApplicationController
- before_filter :find_project
+ before_filter :find_project, :only => :issues
+ before_filter :require_admin, :only => :projects
def issues
@issues = []
@@ -33,6 +34,38 @@ def issues
render :layout => false
end
+ def users
+ if params[:remove_group_members].present?
+ @group = Group.find(params[:remove_group_members])
+ @removed_users = @group.users
+ end
+
+ if params[:remove_watchers].present? && params[:klass].present?
+ watcher_class = params[:klass].constantize
+ if watcher_class.included_modules.include?(Redmine::Acts::Watchable) # check class is a watching class
+ @object = watcher_class.find(params[:remove_watchers])
+ @removed_users = @object.watcher_users
+ end
+ end
+
+ @removed_users ||= []
+
+ if params[:include_groups]
+ user_finder = Principal
+ else
+ user_finder = User
+ end
+
+ @users = user_finder.active.like(params[:q]).find(:all, :limit => 100) - @removed_users
+ render :layout => false
+ end
+
+ def projects
+ @principal = Principal.find(params[:id])
+ @projects = Project.active.like(params[:q]).find(:all, :limit => 100) - @principal.projects
+ render :layout => false
+ end
+
private
def find_project
View
36 app/controllers/documents_controller.rb
@@ -44,19 +44,32 @@ def show
def new
@document = @project.documents.build(params[:document])
- if request.post? and @document.save
- attachments = Attachment.attach_files(@document, params[:attachments])
- render_attachment_warning_if_needed(@document)
- flash[:notice] = l(:notice_successful_create)
- redirect_to :action => 'index', :project_id => @project
+ if request.post?
+ if User.current.allowed_to?(:add_document_watchers, @project) && params[:document]['watcher_user_ids'].present?
+ @document.watcher_user_ids = params[:document]['watcher_user_ids']
+ end
+
+ if @document.save
+ attachments = Attachment.attach_files(@document, params[:attachments])
+ render_attachment_warning_if_needed(@document)
+ flash[:notice] = l(:notice_successful_create)
+ redirect_to :action => 'index', :project_id => @project
+ end
end
end
def edit
@categories = DocumentCategory.all
- if request.post? and @document.update_attributes(params[:document])
- flash[:notice] = l(:notice_successful_update)
- redirect_to :action => 'show', :id => @document
+
+ if request.post?
+ if User.current.allowed_to?(:add_document_watchers, @project) && params[:document]['watcher_user_ids'].present?
+ @document.watcher_user_ids = params[:document]['watcher_user_ids']
+ end
+
+ if @document.update_attributes(params[:document])
+ flash[:notice] = l(:notice_successful_update)
+ redirect_to :action => 'show', :id => @document
+ end
end
end
@@ -69,7 +82,12 @@ def add_attachment
attachments = Attachment.attach_files(@document, params[:attachments])
render_attachment_warning_if_needed(@document)
- Mailer.deliver_attachments_added(attachments[:files]) 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
+ attachments.first.container.recipients.each do |recipient|
+ Mailer.deliver_attachments_added(attachments[:files], recipient)
+ end
+ end
redirect_to :action => 'show', :id => @document
end
View
6 app/controllers/files_controller.rb
@@ -42,7 +42,11 @@ def create
render_attachment_warning_if_needed(container)
if !attachments.empty? && !attachments[:files].blank? && Setting.notified_events.include?('file_added')
- Mailer.deliver_attachments_added(attachments[:files])
+ # TODO: refactor
+ recipients = attachments[:files].first.container.project.notified_users.select {|user| user.allowed_to?(:view_files, container.project)}.collect {|u| u.mail}
+ recipients.each do |recipient|
+ Mailer.deliver_attachments_added(attachments[:files], recipient)
+ end
end
redirect_to project_files_path(@project)
end
View
19 app/controllers/groups_controller.rb
@@ -126,16 +126,19 @@ def remove_user
end
end
- def autocomplete_for_user
- @group = Group.find(params[:id])
- @users = User.active.not_in_group(@group).like(params[:q]).all(:limit => 100)
- render :layout => false
- end
-
def edit_membership
@group = Group.find(params[:id])
- @membership = Member.edit_membership(params[:membership_id], params[:membership], @group)
- @membership.save if request.post?
+
+ if params[:project_ids] # Multiple memberships, one per project
+ params[:project_ids].each do |project_id|
+ @membership = Member.edit_membership(params[:membership_id], (params[:membership]|| {}).merge(:project_id => project_id), @group)
+ @membership.save if request.post?
+ end
+ else # Single membership
+ @membership = Member.edit_membership(params[:membership_id], params[:membership], @group)
+ @membership.save if request.post?
+ end
+
respond_to do |format|
if @membership.valid?
format.html { redirect_to :controller => 'groups', :action => 'edit', :id => @group, :tab => 'memberships' }
View
23 app/controllers/journals_controller.rb
@@ -12,6 +12,8 @@
# See doc/COPYRIGHT.rdoc for more details.
#++
+require 'diff'
+
class JournalsController < ApplicationController
before_filter :find_journal, :only => [:edit, :diff]
before_filter :find_issue, :only => [:new]
@@ -84,6 +86,22 @@ def edit
end
end
+ def diff
+ if valid_field?(params[:field])
+ from = @journal.changes[params[:field]][0]
+ to = @journal.changes[params[:field]][1]
+
+ @diff = Redmine::Helpers::Diff.new(to, from)
+ @issue = @journal.journaled
+ respond_to do |format|
+ format.html { }
+ format.js { render :layout => false }
+ end
+ else
+ render_404
+ end
+ end
+
private
def find_journal
@@ -100,4 +118,9 @@ def find_issue
rescue ActiveRecord::RecordNotFound
render_404
end
+
+ # Is this a valid field for diff'ing?
+ def valid_field?(field)
+ field.to_s.strip == "description"
+ end
end
View
1  app/controllers/ldap_auth_sources_controller.rb
@@ -14,6 +14,7 @@
class LdapAuthSourcesController < AuthSourcesController
+ menu_item :ldap_authentication, :only => [:index]
protected
def auth_source_class
View
2  app/controllers/messages_controller.rb
@@ -110,7 +110,7 @@ def quote
content = "#{ll(Setting.default_language, :text_user_wrote, user)}\\n> "
content << text.to_s.strip.gsub(%r{<pre>((.|\s)*?)</pre>}m, '[...]').gsub('"', '\"').gsub(/(\r?\n|\r\n?)/, "\\n> ") + "\\n\\n"
render(:update) { |page|
- page << "$('reply_subject').value = \"#{subject}\";"
+ page << "$('message_subject').value = \"#{subject}\";"
page.<< "$('message_content').value = \"#{content}\";"
page.show 'reply'
page << "Form.Element.focus('message_content');"
View
1  app/controllers/queries_controller.rb
@@ -20,6 +20,7 @@ class QueriesController < ApplicationController
def new
@query = Query.new(params[:query])
@query.project = params[:query_is_for_all] ? nil : @project
+ @query.display_subprojects = params[:display_subprojects] if params[:display_subprojects].present?
@query.user = User.current
@query.is_public = false unless User.current.allowed_to?(:manage_public_queries, @project) || User.current.admin?
View
6 app/controllers/time_entry_reports_controller.rb
@@ -72,8 +72,7 @@ def report
@periods = []
# Date#at_beginning_of_ not supported in Rails 1.2.x
date_from = @from.to_time
- # 100 columns max
- while date_from <= @to.to_time && @periods.length < 100
+ while date_from <= @to.to_time
case @columns
when 'year'
@periods << "#{date_from.year}"
@@ -161,6 +160,9 @@ def load_available_criterias
@available_criterias = { 'project' => {:sql => "#{TimeEntry.table_name}.project_id",
:klass => Project,
:label => :label_project},
+ 'status' => {:sql => "#{Issue.table_name}.status_id",
+ :klass => IssueStatus,
+ :label => :field_status},
'version' => {:sql => "#{Issue.table_name}.fixed_version_id",
:klass => Version,
:label => :label_version},
View
12 app/controllers/users_controller.rb
@@ -197,8 +197,16 @@ def destroy
def edit_membership
- @membership = Member.edit_membership(params[:membership_id], params[:membership], @user)
- @membership.save if request.post?
+ if params[:project_ids] # Multiple memberships, one per project
+ params[:project_ids].each do |project_id|
+ @membership = Member.edit_membership(params[:membership_id], (params[:membership] || {}).merge(:project_id => project_id), @user)
+ @membership.save if request.post?
+ end
+ else # Single membership
+ @membership = Member.edit_membership(params[:membership_id], params[:membership], @user)
+ @membership.save if request.post?
+ end
+
respond_to do |format|
if @membership.valid?
format.html { redirect_to :controller => 'users', :action => 'edit', :id => @user, :tab => 'memberships' }
View
32 app/controllers/watchers_controller.rb
@@ -16,6 +16,7 @@ class WatchersController < ApplicationController
before_filter :find_project
before_filter :require_login, :check_project_privacy, :only => [:watch, :unwatch]
before_filter :authorize, :only => [:new, :destroy]
+ before_filter :authorize_access_to_object, :only => [:new, :destroy]
verify :method => :post,
:only => [ :watch, :unwatch ],
@@ -34,9 +35,12 @@ def unwatch
end
def new
- @watcher = Watcher.new(params[:watcher])
- @watcher.watchable = @watched
- @watcher.save if request.post?
+ params[:user_ids].each do |user_id|
+ @watcher = Watcher.new((params[:watcher] || {}).merge({:user_id => user_id}))
+ @watcher.watchable = @watched
+ @watcher.save if request.post?
+ end if params[:user_ids].present?
+
respond_to do |format|
format.html { redirect_to :back }
format.js do
@@ -50,7 +54,7 @@ def new
end
def destroy
- @watched.set_watcher(User.find(params[:user_id]), false) if request.post?
+ @watched.set_watcher(Principal.find(params[:user_id]), false) if request.post?
respond_to do |format|
format.html { redirect_to :back }
format.js do
@@ -94,4 +98,24 @@ def set_watcher(user, watching)
rescue ::ActionController::RedirectBackError
render :text => (watching ? 'Watcher added.' : 'Watcher removed.'), :layout => true
end
+
+ def authorize_access_to_object
+ permission = ''
+ case params[:action]
+ when 'new'
+ permission << 'add_'
+ when 'destroy'
+ permission << 'delete_'
+ end
+
+ # Ends up like: :delete_wiki_page_watchers
+ permission << "#{@watched.class.name.underscore}_watchers"
+
+ if User.current.allowed_to?(permission.to_sym, @project)
+ return true
+ else
+ deny_access
+ end
+ end
+
end
View
37 app/drops/base_drop.rb
@@ -0,0 +1,37 @@
+#-- encoding: UTF-8
+#-- copyright
+# ChiliProject is a project management system.
+#
+# Copyright (C) 2010-2012 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+class BaseDrop < Liquid::Drop
+ def initialize(object)
+ @object = object unless object.respond_to?(:visible?) && !object.visible?
+ end
+
+ # Defines a Liquid method on the drop that is allowed to call the
+ # Ruby method directly. Best used for attributes.
+ #
+ # Based on Module#liquid_methods
+ def self.allowed_methods(*allowed_methods)
+ class_eval do
+ allowed_methods.each do |sym|
+ define_method sym do
+ if @object.respond_to?(:public_send)
+ @object.public_send(sym) rescue nil
+ else
+ @object.send(sym) rescue nil
+ end
+ end
+ end
+ end
+ end
+end
View
79 app/drops/issue_drop.rb
@@ -0,0 +1,79 @@
+#-- encoding: UTF-8
+#-- copyright
+# ChiliProject is a project management system.
+#
+# Copyright (C) 2010-2012 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+class IssueDrop < BaseDrop
+ allowed_methods :id
+ allowed_methods :subject
+ allowed_methods :description
+ allowed_methods :project
+ allowed_methods :tracker
+ allowed_methods :status
+ allowed_methods :due_date
+ allowed_methods :category
+ allowed_methods :assigned_to
+ allowed_methods :priority
+ allowed_methods :fixed_version
+ allowed_methods :author
+ allowed_methods :created_on
+ allowed_methods :updated_on
+ allowed_methods :start_date
+ allowed_methods :done_ratio
+ allowed_methods :estimated_hours
+ allowed_methods :parent
+
+ def custom_field(name)
+ return '' unless name.present?
+ custom_field = IssueCustomField.find_by_name(name.strip)
+ return '' unless custom_field.present?
+ custom_value = @object.custom_value_for(custom_field)
+ if custom_value.present?
+ return custom_value.value
+ else
+ return ''
+ end
+ end
+
+ # TODO: both required, method_missing for Ruby and before_method for Liquid
+
+ # Allows accessing custom fields by their name:
+ #
+ # - issue.the_name_of_player => CustomField(:name => "The name Of Player")
+ #
+ def before_method(method_sym)
+ if custom_field_with_matching_name = has_custom_field_with_matching_name?(method_sym)
+ custom_field(custom_field_with_matching_name.name)
+ else
+ super
+ end
+ end
+
+ # Allows accessing custom fields by their name:
+ #
+ # - issue.the_name_of_player => CustomField(:name => "The name Of Player")
+ #
+ def method_missing(method_sym, *arguments, &block)
+ if custom_field_with_matching_name = has_custom_field_with_matching_name?(method_sym)
+ custom_field(custom_field_with_matching_name.name)
+ else
+ super
+ end
+ end
+
+private
+ def has_custom_field_with_matching_name?(method_sym)
+ custom_field_with_matching_name = @object.available_custom_fields.detect {|custom_field|
+ custom_field.name.downcase.underscore.gsub(' ','_') == method_sym.to_s
+ }
+ end
+end
View
17 app/drops/issue_status_drop.rb
@@ -0,0 +1,17 @@
+#-- encoding: UTF-8
+#-- copyright
+# ChiliProject is a project management system.
+#
+# Copyright (C) 2010-2012 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+class IssueStatusDrop < BaseDrop
+ allowed_methods :name
+end
View
17 app/drops/principal_drop.rb
@@ -0,0 +1,17 @@
+#-- encoding: UTF-8
+#-- copyright
+# ChiliProject is a project management system.
+#
+# Copyright (C) 2010-2012 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+class PrincipalDrop < BaseDrop
+ allowed_methods :name
+end
View
17 app/drops/project_drop.rb
@@ -0,0 +1,17 @@
+#-- encoding: UTF-8
+#-- copyright
+# ChiliProject is a project management system.
+#
+# Copyright (C) 2010-2012 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+class ProjectDrop < BaseDrop
+ allowed_methods :name, :identifier
+end
View
17 app/drops/tracker_drop.rb
@@ -0,0 +1,17 @@
+#-- encoding: UTF-8
+#-- copyright
+# ChiliProject is a project management system.
+#
+# Copyright (C) 2010-2012 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+class TrackerDrop < BaseDrop
+ allowed_methods :name
+end
View
17 app/drops/wiki_page_drop.rb
@@ -0,0 +1,17 @@
+#-- encoding: UTF-8
+#-- copyright
+# ChiliProject is a project management system.
+#
+# Copyright (C) 2010-2012 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+class WikiPageDrop < BaseDrop
+ allowed_methods :title
+end
View
180 app/helpers/application_helper.rb
@@ -16,7 +16,6 @@
require 'cgi'
module ApplicationHelper
- include Redmine::WikiFormatting::Macros::Definitions
include Redmine::I18n
include GravatarHelper::PublicMethods
@@ -224,17 +223,15 @@ def render_tabs(tabs)
end
# Renders the project quick-jump box
- def render_project_jump_box
- projects = User.current.memberships.collect(&:project).compact.uniq
+ def render_project_jump_box(projects = [], html_options = {})
+ projects ||= User.current.memberships.collect(&:project).compact.uniq
if projects.any?
- s = '<select onchange="if (this.value != \'\') { window.location = this.value; }">' +
- "<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
- '<option value="" disabled="disabled">---</option>'
- s << project_tree_options_for_select(projects, :selected => @project) do |p|
- { :value => url_for(:controller => 'projects', :action => 'show', :id => p, :jump => current_menu_item) }
- end
- s << '</select>'
- s
+ # option_tags = content_tag :option, l(:label_jump_to_a_project), :value => ""
+ option_tags = (content_tag :option, "", :value => "" )
+ option_tags << project_tree_options_for_select(projects, :selected => @project) do |p|
+ { :value => url_for(:controller => 'projects', :action => 'show', :id => p, :jump => current_menu_item) }
+ end
+ select_tag "", option_tags, html_options.merge({ :onchange => "if (this.value != \'\') { window.location = this.value; }" })
end
end
@@ -288,7 +285,15 @@ def project_nested_ul(projects, &block)
def principals_check_box_tags(name, principals)
s = ''
principals.sort.each do |principal|
- s << "<label>#{ check_box_tag name, principal.id, false } #{h principal}</label>\n"
+ s << "<label style='display:block;'>#{ check_box_tag name, principal.id, false } #{h principal}</label>\n"
+ end
+ s
+ end
+
+ def projects_check_box_tags(name, projects)
+ s = ''
+ projects.each do |project|
+ s << "<label>#{ check_box_tag name, project.id, false } #{h project}</label>\n"
end
s
end
@@ -389,7 +394,9 @@ def other_formats_links(&block)
end
def page_header_title
- if @project.nil? || @project.new_record?
+ if @page_header_title.present?
+ h(@page_header_title)
+ elsif @project.nil? || @project.new_record?
h(Setting.app_title)
else
b = []
@@ -429,8 +436,8 @@ def body_css_classes
css << 'theme-' + theme.name
end
- css << 'controller-' + params[:controller]
- css << 'action-' + params[:action]
+ css << 'controller-' + params[:controller] if params[:controller]
+ css << 'action-' + params[:action] if params[:action]
css.join(' ')
end
@@ -447,19 +454,51 @@ def textilizable(*args)
case args.size
when 1
obj = options[:object]
- text = args.shift
+ input_text = args.shift
when 2
obj = args.shift
attr = args.shift
- text = obj.send(attr).to_s
+ input_text = obj.send(attr).to_s
else
raise ArgumentError, 'invalid arguments to textilizable'
end
- return '' if text.blank?
+ return '' if input_text.blank?
project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
only_path = options.delete(:only_path) == false ? false : true
- text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr) { |macro, args| exec_macro(macro, obj, args) }
+ begin
+ text = ChiliProject::Liquid::Legacy.run_macros(input_text)
+ liquid_template = ChiliProject::Liquid::Template.parse(text)
+ liquid_variables = get_view_instance_variables_for_liquid
+ liquid_variables.merge!({'current_user' => User.current})
+ liquid_variables.merge!({'toc' => '{{toc}}'}) # Pass toc through to replace later
+ liquid_variables.merge!(ChiliProject::Liquid::Variables.all)
+
+ # Pass :view in a register so this view (with helpers) can be used inside of a tag
+ text = liquid_template.render(liquid_variables, :registers => {:view => self, :object => obj, :attribute => attr})
+
+ # Add Liquid errors to the log
+ if Rails.logger && Rails.logger.debug?
+ msg = ""
+ liquid_template.errors.each do |exception|
+ msg << "[Liquid Error] #{exception.message}\n:\n#{exception.backtrace.join("\n")}"
+ msg << "\n\n"
+ end
+ Rails.logger.debug msg
+ end
+ rescue Liquid::SyntaxError => exception
+ msg = "[Liquid Syntax Error] #{exception.message}"
+ if Rails.logger && Rails.logger.debug?
+ log_msg = "#{msg}\n"
+ log_msg << exception.backtrace.collect{ |str| " #{str}" }.join("\n")
+ log_msg << "\n\n"
+ Rails.logger.debug log_msg
+ end
+
+ # Skip Liquid if there is a syntax error
+ text = content_tag(:div, msg, :class => "flash error")
+ text << h(input_text)
+ end
@parsed_headings = []
text = parse_non_pre_blocks(text) do |text|
@@ -712,7 +751,7 @@ def parse_headings(text, project, obj, attr, only_path, options)
end
end
- TOC_RE = /<p>\{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
+ TOC_RE = /<p>\{%\s*toc(_right|_left)?\s*%\}<\/p>/i unless const_defined?(:TOC_RE)
# Renders the TOC with given headings
def replace_toc(text, headings)
@@ -720,10 +759,14 @@ def replace_toc(text, headings)
if headings.empty?
''
else
- div_class = 'toc'
- div_class << ' right' if $1 == '>'
- div_class << ' left' if $1 == '<'
- out = "<ul class=\"#{div_class}\"><li>"
+ toc_class = 'toc'
+ toc_class << ' right' if $1 == '_right'
+ toc_class << ' left' if $1 == '_left'
+
+ out = "<fieldset class=\"header_collapsible collapsible #{toc_class}\">"
+ out << "<legend onclick=\"toggleFieldset(this);\"><span>#{l(:label_toc)}</span></legend>"
+ out << "<div>"
+ out << "<ul class=\"toc\"><li>"
root = headings.map(&:first).min
current = root
started = false
@@ -741,6 +784,7 @@ def replace_toc(text, headings)
end
out << '</li></ul>' * (current - root)
out << '</li></ul>'
+ out << '</div></fieldset>'
end
end
end
@@ -879,6 +923,12 @@ def has_content?(name)
# +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 = { })
if Setting.gravatar_enabled?
+ if user.is_a?(Group)
+ size = options[:size] || 50
+ size = "#{size}x#{size}" # image_tag uses WxH
+ options[:class] ||= 'gravatar'
+ return image_tag("group.png", options.merge(:size => size))
+ end
options.merge!({:ssl => (defined?(request) && request.ssl?), :default => Setting.gravatar_default})
email = nil
if user.respond_to?(:mail)
@@ -935,6 +985,72 @@ def api_meta(options)
end
end
+ # Expands the current menu item using JavaScript based on the params
+ def expand_current_menu
+ current_menu_class =
+ case
+ when params[:controller] == "timelog"
+ "reports"
+ when params[:controller] == 'projects' && params[:action] == 'changelog'
+ "reports"
+ when params[:controller] == 'issues' && ['calendar','gantt'].include?(params[:action])
+ "reports"
+ when params[:controller] == 'projects' && params[:action] == 'roadmap'
+ 'roadmap'
+ when params[:controller] == 'versions' && params[:action] == 'show'
+ 'roadmap'
+ when params[:controller] == 'projects' && params[:action] == 'settings'
+ 'settings'
+ when params[:controller] == 'contracts' || params[:controller] == 'deliverables'
+ 'contracts'
+ else
+ params[:controller]
+ end
+
+
+ javascript_tag("jQuery.menu_expand({ menuItem: '.#{current_menu_class}' });")
+ end
+
+ # Menu items for the main top menu
+ def main_top_menu_items
+ split_top_menu_into_main_or_more_menus[:main]
+ end
+
+ # Menu items for the more top menu
+ def more_top_menu_items
+ split_top_menu_into_main_or_more_menus[:more]
+ end
+
+ def help_menu_item
+ split_top_menu_into_main_or_more_menus[:help]
+ end
+
+ # Split the :top_menu into separate :main and :more items
+ def split_top_menu_into_main_or_more_menus
+ unless @top_menu_split
+ items_for_main_level = []
+ items_for_more_level = []
+ help_menu = nil
+ menu_items_for(:top_menu) do |item|
+ if item.name == :home || item.name == :my_page
+ items_for_main_level << item
+ elsif item.name == :help
+ help_menu = item
+ elsif item.name == :projects
+ # Remove, present in layout
+ else
+ items_for_more_level << item
+ end
+ end
+ @top_menu_split = {
+ :main => items_for_main_level,
+ :more => items_for_more_level,
+ :help => help_menu
+ }
+ end
+ @top_menu_split
+ end
+
private
def wiki_helper
@@ -946,4 +1062,20 @@ def wiki_helper
def link_to_content_update(text, url_params = {}, html_options = {})
link_to(text, url_params, html_options)
end
+
+ def get_view_instance_variables_for_liquid
+ internal_variables = %w{
+ @output_buffer @cookies @helpers @real_format @assigns_added @assigns
+ @view_paths @controller
+ }
+ self.instance_variables.collect(&:to_s).reject do |ivar|
+ ivar.match(/^@_/) || # Rails "internal" variables: @_foo
+ ivar.match(/^@template/) ||
+ internal_variables.include?(ivar)
+ end.inject({}) do |acc,ivar|
+ acc[ivar.sub('@','')] = instance_variable_get(ivar)
+ acc
+ end
+ end
+
end
View
5 app/helpers/issues_helper.rb
@@ -52,13 +52,14 @@ def render_issue_tooltip(issue)
"<strong>#{@cached_label_priority}</strong>: #{h(issue.priority.name)}"
end
+ # TODO: deprecate and/or remove
def render_issue_subject_with_tree(issue)
s = ''
ancestors = issue.root? ? [] : issue.ancestors.all
ancestors.each do |ancestor|
- s << '<div>' + content_tag('p', link_to_issue(ancestor))
+ s << '<div>' + content_tag('h2', link_to_issue(ancestor))
end
- s << '<div>' + content_tag('h3', h(issue.subject))
+ s << '<div class="subject">' + content_tag('h2', h(issue.subject))
s << '</div>' * (ancestors.size + 1)
s
end
View
30 app/helpers/journals_helper.rb
@@ -27,24 +27,34 @@ def self.included(base)
def render_journal(model, journal, options = {})
return "" if journal.initial?
- journal_content = render_journal_details(journal, :label_updated_time_by)
- journal_content += render_notes(model, journal, options) unless journal.notes.blank?
- content_tag "div", journal_content, { :id => "change-#{journal.id}", :class => journal.css_classes }
+
+ journal_classes = journal.css_classes
+ journal_content = render_journal_details(journal, :label_updated_time_by, model, options)
+
+ avatar = avatar(journal.user, :size => "40")
+ unless avatar.blank?
+ profile_wrap = content_tag("div", avatar, {:class => "profile-wrap"}, false)
+ journal_content = profile_wrap + journal_content
+ journal_classes << " has-avatar"
+ end
+
+ content_tag("div", journal_content, :id => "change-#{journal.id}", :class => journal_classes)
end
- # This renders a journal entry wiht a header and details
- def render_journal_details(journal, header_label = :label_updated_time_by)
+ # This renders a journal entry with a header and details
+ def render_journal_details(journal, header_label = :label_updated_time_by, model=nil, options={})
header = <<-HTML
<h4>
- <div style="float:right;">#{link_to "##{journal.anchor}", :anchor => "note-#{journal.anchor}"}</div>
- #{avatar(journal.user, :size => "24")}
- #{content_tag('a', '', :name => "note-#{journal.anchor}")}
+ <div class="journal-link" style="float:right;">#{link_to "##{journal.anchor}", :anchor => "note-#{journal.anchor}"}</div>
#{authoring journal.created_at, journal.user, :label => header_label}
+ #{content_tag('a', '', :name => "note-#{journal.anchor}")}
</h4>
HTML
+ header << render_notes(model, journal, options) unless journal.notes.blank?
+
if journal.details.any?
- details = content_tag "ul", :class => "details" do
+ details = content_tag "ul", :class => "journal-attributes details" do
journal.details.collect do |detail|
if d = journal.render_detail(detail)
content_tag("li", d)
@@ -53,7 +63,7 @@ def render_journal_details(journal, header_label = :label_updated_time_by)
end
end
- content_tag("div", "#{header}#{details}", :id => "change-#{journal.id}", :class => "journal")
+ content_tag "div", "#{header}#{details}", :class => "journal-details"
end
def render_notes(model, journal, options={})
View
5 app/helpers/queries_helper.rb
@@ -84,11 +84,12 @@ def retrieve_query
end
end
@query.group_by = params[:group_by]
+ @query.display_subprojects = params[:display_subprojects] if params[:display_subprojects]
@query.column_names = params[:c] || (params[:query] && params[:query][:column_names])
- session[:query] = {:project_id => @query.project_id, :filters => @query.filters, :group_by => @query.group_by, :column_names => @query.column_names}
+ session[:query] = {:project_id => @query.project_id, :filters => @query.filters, :group_by => @query.group_by, :column_names => @query.column_names, :display_subprojects => @query.display_subprojects}
else
@query = Query.find_by_id(session[:query][:id]) if session[:query][:id]
- @query ||= Query.new(:name => "_", :project => @project, :filters => session[:query][:filters], :group_by => session[:query][:group_by], :column_names => session[:query][:column_names])
+ @query ||= Query.new(:name => "_", :project => @project, :filters => session[:query][:filters], :group_by => session[:query][:group_by], :column_names => session[:query][:column_names], :display_subprojects => session[:query][:display_subprojects])
@query.project = @project
end
end
View
7 app/models/document.rb
@@ -24,6 +24,7 @@ class Document < ActiveRecord::Base
end)
acts_as_searchable :columns => ['title', "#{table_name}.description"], :include => :project
+ acts_as_watchable
validates_presence_of :project, :title, :category
validates_length_of :title, :maximum => 60
@@ -48,4 +49,10 @@ def updated_on
end
@updated_on
end
+
+ def recipients
+ mails = super # from acts_as_event
+ mails += watcher_recipients
+ mails.uniq
+ end
end
View
6 app/models/document_observer.rb
@@ -14,6 +14,10 @@
class DocumentObserver < ActiveRecord::Observer
def after_create(document)
- Mailer.deliver_document_added(document) if Setting.notified_events.include?('document_added')
+ if Setting.notified_events.include?('document_added')
+ document.recipients.each do |recipient|
+ Mailer.deliver_document_added(document, recipient)
+ end
+ end
end
end
View
5 app/models/group.rb
@@ -22,6 +22,11 @@ class Group < Principal
validates_uniqueness_of :lastname, :case_sensitive => false
validates_length_of :lastname, :maximum => 30
+ # Returns an array of all of the email addresses of the group's users
+ def mails
+ users.collect(&:mail)
+ end
+
def to_s
lastname.to_s
end
View
4 app/models/issue.rb
@@ -103,6 +103,10 @@ def visible?(usr=nil)
(usr || User.current).allowed_to?(:view_issues, self.project)
end
+ def to_liquid
+ IssueDrop.new(self)
+ end
+
def after_initialize
if new_record?
# set default values for new records only
View
4 app/models/issue_observer.rb
@@ -17,7 +17,9 @@ class IssueObserver < ActiveRecord::Observer
def after_create(issue)
if self.send_notification
- Mailer.deliver_issue_add(issue) if Setting.notified_events.include?('issue_added')
+ (issue.recipients + issue.watcher_recipients).uniq.each do |recipient|
+ Mailer.deliver_issue_add(issue, recipient)
+ end
end
clear_notification
end
View
4 app/models/issue_status.rb
@@ -28,6 +28,10 @@ def after_save
IssueStatus.update_all("is_default=#{connection.quoted_false}", ['id <> ?', id]) if self.is_default?
end
+ def to_liquid
+ IssueStatusDrop.new(self)
+ end
+
# Returns the default status for new issues
def self.default
find(:first, :conditions =>["is_default=?", true])
View
13 app/models/journal_observer.rb
@@ -27,11 +27,15 @@ def after_create(journal)
if journal.initial?
if Setting.notified_events.include?('wiki_content_added')
- Mailer.deliver_wiki_content_added(wiki_content)
+ (wiki_content.recipients + wiki_page.wiki.watcher_recipients).uniq.each do |recipient|
+ Mailer.deliver_wiki_content_added(wiki_content, recipient)
+ end
end
else
if Setting.notified_events.include?('wiki_content_updated')
- Mailer.deliver_wiki_content_updated(wiki_content)
+ (wiki_content.recipients + wiki_page.wiki.watcher_recipients + wiki_page.watcher_recipients).uniq.each do |recipient|
+ Mailer.deliver_wiki_content_updated(wiki_content, recipient)
+ end
end
end
end
@@ -43,7 +47,10 @@ def after_create_issue_journal(journal)
(Setting.notified_events.include?('issue_note_added') && journal.notes.present?) ||
(Setting.notified_events.include?('issue_status_updated') && journal.new_status.present?) ||
(Setting.notified_events.include?('issue_priority_updated') && journal.new_value_for('priority_id').present?)
- Mailer.deliver_issue_edit(journal)
+ issue = journal.issue
+ (issue.recipients + issue.watcher_recipients).uniq.each do |recipient|
+ Mailer.deliver_issue_edit(journal, recipient)
+ end
end
end
View
7 app/models/mail_handler.rb
@@ -69,6 +69,7 @@ def receive(email)
else
# Default behaviour, emails from unknown users are ignored
logger.info "MailHandler: ignoring email from unknown user [#{sender_email}]" if logger && logger.info
+ Mailer.deliver_mail_handler_unauthorized_action(user, email.subject.to_s, :to => sender_email) if Setting.mail_handler_confirmation_on_failure
return false
end
end
@@ -102,12 +103,15 @@ def dispatch
rescue ActiveRecord::RecordInvalid => e
# TODO: send a email to the user
logger.error e.message if logger
+ Mailer.deliver_mail_handler_missing_information(user, email.subject.to_s, e.message) if Setting.mail_handler_confirmation_on_failure
false
rescue MissingInformation => e
logger.error "MailHandler: missing information from #{user}: #{e.message}" if logger
+ Mailer.deliver_mail_handler_missing_information(user, email.subject.to_s, e.message) if Setting.mail_handler_confirmation_on_failure
false
rescue UnauthorizedAction => e
logger.error "MailHandler: unauthorized attempt from #{user}" if logger
+ Mailer.deliver_mail_handler_unauthorized_action(user, email.subject.to_s) if Setting.mail_handler_confirmation_on_failure
false
end
@@ -141,6 +145,7 @@ def receive_issue
issue.save!
add_attachments(issue)
logger.info "MailHandler: issue ##{issue.id} created by #{user}" if logger && logger.info
+ Mailer.deliver_mail_handler_confirmation(issue, user, issue.subject) if Setting.mail_handler_confirmation_on_success
issue
end
@@ -162,6 +167,7 @@ def receive_issue_reply(issue_id)
add_attachments(issue)
issue.save!
logger.info "MailHandler: issue ##{issue.id} updated by #{user}" if logger && logger.info
+ Mailer.deliver_mail_handler_confirmation(issue.last_journal, user, email.subject) if Setting.mail_handler_confirmation_on_success
issue.last_journal
end
@@ -190,6 +196,7 @@ def receive_message_reply(message_id)
reply.board = message.board
message.children << reply
add_attachments(reply)
+ Mailer.deliver_mail_handler_confirmation(message, user, reply.subject) if Setting.mail_handler_confirmation_on_success
reply
else
logger.info "MailHandler: ignoring reply from [#{sender_email}] to a locked topic" if logger && logger.info
View
101 app/models/mailer.rb
@@ -30,20 +30,19 @@ def self.default_url_options
{ :host => h, :protocol => Setting.protocol }
end
- # Builds a tmail object used to email recipients of the added issue.
+ # Builds a tmail object used to email a recipient of the added issue.
#
# Example:
- # issue_add(issue) => tmail object
- # Mailer.deliver_issue_add(issue) => sends an email to issue recipients
- def issue_add(issue)
+ # issue_add(issue, 'user@example.com') => tmail object
+ # Mailer.deliver_issue_add(issue, 'user@example.com') => sends an email to 'user@example.com'
+ def issue_add(issue, recipient)
redmine_headers 'Project' => issue.project.identifier,
'Issue-Id' => issue.id,
'Issue-Author' => issue.author.login,
'Type' => "Issue"
redmine_headers 'Issue-Assignee' => issue.assigned_to.login if issue.assigned_to
message_id issue
- recipients issue.recipients
- cc(issue.watcher_recipients - @recipients)
+ recipients [recipient]
subject "[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}] (#{issue.status.name}) #{issue.subject}"
body :issue => issue,
:issue_url => url_for(:controller => 'issues', :action => 'show', :id => issue)
@@ -53,9 +52,9 @@ def issue_add(issue)
# Builds a tmail object used to email recipients of the edited issue.
#
# Example:
- # issue_edit(journal) => tmail object
- # Mailer.deliver_issue_edit(journal) => sends an email to issue recipients
- def issue_edit(journal)
+ # issue_edit(journal, 'user@example.com') => tmail object
+ # Mailer.deliver_issue_edit(journal, 'user@example.com') => sends an email to issue recipients
+ def issue_edit(journal, recipient)
issue = journal.journaled.reload
redmine_headers 'Project' => issue.project.identifier,
'Issue-Id' => issue.id,
@@ -65,9 +64,7 @@ def issue_edit(journal)
message_id journal
references issue
@author = journal.user
- recipients issue.recipients
- # Watchers in cc
- cc(issue.watcher_recipients - @recipients)
+ recipients [recipient]
s = "[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}] "
s << "(#{issue.status.name}) " if journal.details['status_id']
s << issue.subject
@@ -93,12 +90,12 @@ def reminder(user, issues, days)
# Builds a tmail object used to email users belonging to the added document's project.
#
# Example:
- # document_added(document) => tmail object
- # Mailer.deliver_document_added(document) => sends an email to the document's project recipients
- def document_added(document)
+ # document_added(document, 'test@example.com') => tmail object
+ # Mailer.deliver_document_added(document, 'test@example.com') => sends an email to the document's project recipients
+ def document_added(document, recipient)
redmine_headers 'Project' => document.project.identifier,
'Type' => "Document"
- recipients document.recipients
+ recipients [recipient]
subject "[#{document.project.name}] #{l(:label_document_new)}: #{document.title}"
body :document => document,
:document_url => url_for(:controller => 'documents', :action => 'show', :id => document)
@@ -110,7 +107,7 @@ def document_added(document)
# Example:
# attachments_added(attachments) => tmail object
# Mailer.deliver_attachments_added(attachments) => sends an email to the project's recipients
- def attachments_added(attachments)
+ def attachments_added(attachments, recipient)
container = attachments.first.container
added_to = ''
added_to_url = ''
@@ -118,16 +115,14 @@ def attachments_added(attachments)
when 'Project'
added_to_url = url_for(:controller => 'files', :action => 'index', :project_id => container)
added_to = "#{l(:label_project)}: #{container}"
- recipients container.project.notified_users.select {|user| user.allowed_to?(:view_files, container.project)}.collect {|u| u.mail}
when 'Version'
added_to_url = url_for(:controller => 'files', :action => 'index', :project_id => container.project)
added_to = "#{l(:label_version)}: #{container.name}"
- recipients container.project.notified_users.select {|user| user.allowed_to?(:view_files, container.project)}.collect {|u| u.mail}
when 'Document'
added_to_url = url_for(:controller => 'documents', :action => 'show', :id => container.id)
added_to = "#{l(:label_document)}: #{container.title}"
- recipients container.recipients
end
+ recipients [recipient]
redmine_headers 'Project' => container.project.identifier,
'Type' => "Attachment"
subject "[#{container.project.name}] #{l(:label_attachment_new)}"
@@ -142,11 +137,11 @@ def attachments_added(attachments)
# Example:
# news_added(news) => tmail object
# Mailer.deliver_news_added(news) => sends an email to the news' project recipients
- def news_added(news)
+ def news_added(news, recipient)
redmine_headers 'Project' => news.project.identifier,
'Type' => "News"
message_id news
- recipients news.recipients
+ recipients [recipient]
subject "[#{news.project.name}] #{l(:label_news)}: #{news.title}"
body :news => news,
:news_url => url_for(:controller => 'news', :action => 'show', :id => news)
@@ -176,14 +171,13 @@ def news_comment_added(comment)
# Example:
# message_posted(message) => tmail object
# Mailer.deliver_message_posted(message) => sends an email to the recipients
- def message_posted(message)
+ def message_posted(message, recipient)
redmine_headers 'Project' => message.project.identifier,
'Topic-Id' => (message.parent_id || message.id),
'Type' => "Forum"
message_id message
references message.parent unless message.parent.nil?
- recipients(message.recipients)
- cc((message.root.watcher_recipients + message.board.watcher_recipients).uniq - @recipients)
+ recipients [recipient]
subject "[#{message.board.project.name} - #{message.board.name} - msg#{message.root.id}] #{message.subject}"
body :message => message,
:message_url => url_for({ :controller => 'messages', :action => 'show', :board_id => message.board, :id => message.root, :r => message, :anchor => "message-#{message.id}" })
@@ -195,13 +189,12 @@ def message_posted(message)
# Example:
# wiki_content_added(wiki_content) => tmail object
# Mailer.deliver_wiki_content_added(wiki_content) => sends an email to the project's recipients
- def wiki_content_added(wiki_content)
+ def wiki_content_added(wiki_content, recipient)
redmine_headers 'Project' => wiki_content.project.identifier,
'Wiki-Page-Id' => wiki_content.page.id,
'Type' => "Wiki"
message_id wiki_content
- recipients wiki_content.recipients
- cc(wiki_content.page.wiki.watcher_recipients - recipients)
+ recipients [recipient]
subject "[#{wiki_content.project.name}] #{l(:mail_subject_wiki_content_added, :id => wiki_content.page.pretty_title)}"
body :wiki_content => wiki_content,
:wiki_content_url => url_for(:controller => 'wiki', :action => 'show', :project_id => wiki_content.project, :id => wiki_content.page.title)
@@ -213,13 +206,12 @@ def wiki_content_added(wiki_content)
# Example:
# wiki_content_updated(wiki_content) => tmail object
# Mailer.deliver_wiki_content_updated(wiki_content) => sends an email to the project's recipients
- def wiki_content_updated(wiki_content)
+ def wiki_content_updated(wiki_content, recipient)
redmine_headers 'Project' => wiki_content.project.identifier,
'Wiki-Page-Id' => wiki_content.page.id,
'Type' => "Wiki"
message_id wiki_content
- recipients wiki_content.recipients
- cc(wiki_content.page.wiki.watcher_recipients + wiki_content.page.watcher_recipients - recipients)
+ recipients [recipient]
subject "[#{wiki_content.project.name}] #{l(:mail_subject_wiki_content_updated, :id => wiki_content.page.pretty_title)}"
body :wiki_content => wiki_content,
:wiki_content_url => url_for(:controller => 'wiki', :action => 'show', :project_id => wiki_content.project, :id => wiki_content.page.title),
@@ -293,6 +285,44 @@ def register(token)
render_multipart('register', body)
end
+ def mail_handler_confirmation(object, user, email_subject)
+ recipients user.mail
+
+ case
+ when object.is_a?(Issue)
+ project = object.project.name
+ url = url_for(:controller => 'issues', :action => 'show', :id => object.id)
+ when object.is_a?(Journal)
+ project = object.project.name
+ url = url_for(:controller => 'issues', :action => 'show', :id => object.issue.id)
+ when object.class == Message
+ project = object.project.name
+ url = url_for(object.event_url)
+ else
+ project = ''
+ url = ''
+ end
+
+ subject "[#{project}] #{l(:label_mail_handler_confirmation, :subject => email_subject)}"
+ body(:object => object,
+ :url => url)
+ render_multipart('mail_handler_confirmation', body)
+ end
+
+ def mail_handler_unauthorized_action(user, email_subject, options={})
+ recipients options[:to] || user.mail
+ subject l(:label_mail_handler_failure, :subject => email_subject)
+ body({})
+ render_multipart('mail_handler_unauthorized_action', body)
+ end
+
+ def mail_handler_missing_information(user, email_subject, error_message)
+ recipients user.mail
+ subject l(:label_mail_handler_failure, :subject => email_subject)
+ body({:errors => error_message.to_s})
+ render_multipart('mail_handler_missing_information', body)
+ end
+
def test(user)
redmine_headers 'Type' => "Test"
set_language_if_valid(user.language)
@@ -395,7 +425,7 @@ def create_mail
# Removes the current user from the recipients and cc
# if he doesn't want to receive notifications about what he does
@author ||= User.current
- if @author.pref[:no_self_notified]
+ if @author && @author.mail && @author.pref[:no_self_notified]
recipients((recipients.is_a?(Array) ? recipients : [recipients]) - [@author.mail]) if recipients.present?
cc((cc.is_a?(Array) ? cc : [cc]) - [@author.mail]) if cc.present?
end
@@ -403,13 +433,6 @@ def create_mail
notified_users = [recipients, cc].flatten.compact.uniq
# Rails would log recipients only, not cc and bcc
mylogger.info "Sending email notification to: #{notified_users.join(', ')}" if mylogger
-
- # Blind carbon copy recipients
- if Setting.bcc_recipients?
- bcc(notified_users)
- recipients []
- cc []
- end
super
end
View
9 app/models/message_observer.rb
@@ -14,6 +14,13 @@
class MessageObserver < ActiveRecord::Observer
def after_create(message)
- Mailer.deliver_message_posted(message) if Setting.notified_events.include?('message_posted')
+ if Setting.notified_events.include?('message_posted')
+ recipients = message.recipients
+ recipients += message.root.watcher_recipients
+ recipients += message.board.watcher_recipients
+ recipients.uniq.each do |recipient|
+ Mailer.deliver_message_posted(message, recipient)
+ end
+ end
end
end
View
6 app/models/news_observer.rb
@@ -14,6 +14,10 @@
class NewsObserver < ActiveRecord::Observer
def after_create(news)
- Mailer.deliver_news_added(news) if Setting.notified_events.include?('news_added')
+ if Setting.notified_events.include?('news_added')
+ news.recipients.each do |recipient|
+ Mailer.deliver_news_added(news, recipient)
+ end
+ end
end
end
View
80 app/models/principal.rb
@@ -31,6 +31,10 @@ class Principal < ActiveRecord::Base
before_create :set_default_empty_values
+ def to_liquid
+ PrincipalDrop.new(self)
+ end
+
def name(formatter = nil)
to_s
end
@@ -44,6 +48,82 @@ def <=>(principal)
end
end
+ def active?
+ true
+ end
+
+ def logged?
+ true # TODO: should all principals default to logged or not?
+ end
+
+ # Return true if the user is allowed to do the specified action on a specific context
+ # Action can be:
+ # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
+ # * a permission Symbol (eg. :edit_project)
+ # Context can be:
+ # * a project : returns true if user is allowed to do the specified action on this project
+ # * a group of projects : returns true if user is allowed on every project
+ # * nil with options[:global] set : check if user has at least one role allowed for this action,
+ # or falls back to Non Member / Anonymous permissions depending if the user is logged
+ def allowed_to?(action, context, options={})
+ if context && context.is_a?(Project)
+ # No action allowed on archived projects
+ return false unless context.active?
+ # No action allowed on disabled modules
+ return false unless context.allows_to?(action)
+ # Admin users are authorized for anything else
+ return true if admin?
+
+ roles = roles_for_project(context)
+ return false unless roles
+ roles.detect {|role| (context.is_public? || role.member?) && role.allowed_to?(action)}
+
+ elsif context && context.is_a?(Array)
+ # Authorize if user is authorized on every element of the array
+ context.map do |project|
+ allowed_to?(action,project,options)
+ end.inject do |memo,allowed|
+ memo && allowed
+ end
+ elsif options[:global]
+ # Admin users are always authorized
+ return true if admin?
+
+ # authorize if user has at least one role that has this permission
+ roles = memberships.collect {|m| m.roles}.flatten.uniq
+ roles.detect {|r| r.allowed_to?(action)} || (self.logged? ? Role.non_member.allowed_to?(action) : Role.anonymous.allowed_to?(action))
+ else
+ false
+ end
+ end
+
+ # Is the user allowed to do the specified action on any project?
+ # See allowed_to? for the actions and valid options.
+ def allowed_to_globally?(action, options)
+ allowed_to?(action, nil, options.reverse_merge(:global => true))
+ end
+
+ # Return user's roles for project
+ def roles_for_project(project)
+ roles = []
+ # No role on archived projects
+ return roles unless project && project.active?
+ if logged?
+ # Find project membership
+ membership = memberships.detect {|m| m.project_id == project.id}
+ if membership
+ roles = membership.roles
+ else
+ @role_non_member ||= Role.non_member
+ roles << @role_non_member
+ end
+ else
+ @role_anonymous ||= Role.anonymous
+ roles << @role_anonymous
+ end
+ roles
+ end
+
protected
# Make sure we don't try to insert NULL values (see #4632)
View
17 app/models/project.rb
@@ -82,6 +82,16 @@ class Project < ActiveRecord::Base
named_scope :active, { :conditions => "#{Project.table_name}.status = #{STATUS_ACTIVE}"}
named_scope :all_public, { :conditions => { :is_public => true } }
named_scope :visible, lambda { { :conditions => Project.visible_by(User.current) } }
+ named_scope :like, lambda {|q|
+ s = "%#{q.to_s.strip.downcase}%"
+ {
+ :conditions => ["LOWER(name) LIKE ?", s]
+ }
+ }
+
+ def to_liquid
+ ProjectDrop.new(self)
+ end
def initialize(attributes = nil)
super
@@ -131,6 +141,11 @@ def self.visible_by(user=nil)
end
end
+ # Is the project visible to the current user
+ def visible?
+ User.current.allowed_to?(:view_project, self)
+ end
+
def self.allowed_to_condition(user, permission, options={})
base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
if perm = Redmine::AccessControl.permission(permission)
@@ -616,7 +631,7 @@ def self.project_tree(projects, &block)
while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
ancestors.pop
end
- yield project, ancestors.size
+ yield project, ancestors.size if block_given?
ancestors << project
end
end
View
168 app/models/query.rb
@@ -12,64 +12,7 @@
# See doc/COPYRIGHT.rdoc for more details.
#++
-class QueryColumn
- attr_accessor :name, :sortable, :groupable, :default_order
- include Redmine::I18n
-
- def initialize(name, options={})
- self.name = name
- self.sortable = options[:sortable]
- self.groupable = options[:groupable] || false
- if groupable == true
- self.groupable = name.to_s
- end
- self.default_order = options[:default_order]
- @caption_key = options[:caption] || "field_#{name}"
- end
-
- def caption
- l(@caption_key)
- end
-
- # Returns true if the column is sortable, otherwise false
- def sortable?
- !sortable.nil?
- end
-
- def value(issue)
- issue.send name
- end
-end
-
-class QueryCustomFieldColumn < QueryColumn
-
- def initialize(custom_field)
- self.name = "cf_#{custom_field.id}".to_sym
- self.sortable = custom_field.order_statement || false
- if %w(list date bool int).include?(custom_field.field_format)
- self.groupable = custom_field.order_statement
- end
- self.groupable ||= false
- @cf = custom_field
- end
-
- def caption
- @cf.name
- end
-
- def custom_field
- @cf
- end
-
- def value(issue)
- cv = issue.custom_values.detect {|v| v.custom_field_id == @cf.id}
- cv && @cf.cast_value(cv.value)
- end
-end
-
class Query < ActiveRecord::Base
- class StatementInvalid < ::ActiveRecord::StatementInvalid
- end
belongs_to :project
belongs_to :user
@@ -90,6 +33,7 @@ class StatementInvalid < ::ActiveRecord::StatementInvalid
"*" => :label_all,
">=" => :label_greater_or_equal,
"<=" => :label_less_or_equal,
+ "><" => :label_between,