diff --git a/Gemfile b/Gemfile
index 49d96fb1..2bb6fb48 100644
--- a/Gemfile
+++ b/Gemfile
@@ -6,6 +6,7 @@ gem "coderay", "~> 0.9.7"
gem "i18n", "~> 0.4.2"
gem "rubytree", "~> 0.5.2", :require => 'tree'
gem "rdoc", ">= 2.4.2"
+gem "liquid", "~> 2.3.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]
diff --git a/app/drops/base_drop.rb b/app/drops/base_drop.rb
new file mode 100644
index 00000000..18355cc1
--- /dev/null
+++ b/app/drops/base_drop.rb
@@ -0,0 +1,23 @@
+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
diff --git a/app/drops/issue_drop.rb b/app/drops/issue_drop.rb
new file mode 100644
index 00000000..40d3f572
--- /dev/null
+++ b/app/drops/issue_drop.rb
@@ -0,0 +1,65 @@
+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
diff --git a/app/drops/issue_status_drop.rb b/app/drops/issue_status_drop.rb
new file mode 100644
index 00000000..88d1a9dc
--- /dev/null
+++ b/app/drops/issue_status_drop.rb
@@ -0,0 +1,3 @@
+class IssueStatusDrop < BaseDrop
+ allowed_methods :name
+end
diff --git a/app/drops/principal_drop.rb b/app/drops/principal_drop.rb
new file mode 100644
index 00000000..24a94ea9
--- /dev/null
+++ b/app/drops/principal_drop.rb
@@ -0,0 +1,3 @@
+class PrincipalDrop < BaseDrop
+ allowed_methods :name
+end
diff --git a/app/drops/project_drop.rb b/app/drops/project_drop.rb
new file mode 100644
index 00000000..f14a7799
--- /dev/null
+++ b/app/drops/project_drop.rb
@@ -0,0 +1,3 @@
+class ProjectDrop < BaseDrop
+ allowed_methods :name, :identifier
+end
diff --git a/app/drops/tracker_drop.rb b/app/drops/tracker_drop.rb
new file mode 100644
index 00000000..3bcf4a0e
--- /dev/null
+++ b/app/drops/tracker_drop.rb
@@ -0,0 +1,3 @@
+class TrackerDrop < BaseDrop
+ allowed_methods :name
+end
diff --git a/app/drops/wiki_page_drop.rb b/app/drops/wiki_page_drop.rb
new file mode 100644
index 00000000..27c9be4f
--- /dev/null
+++ b/app/drops/wiki_page_drop.rb
@@ -0,0 +1,3 @@
+class WikiPageDrop < BaseDrop
+ allowed_methods :title
+end
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 5e24410f..f621138c 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -16,7 +16,6 @@ require 'forwardable'
require 'cgi'
module ApplicationHelper
- include Redmine::WikiFormatting::Macros::Definitions
include Redmine::I18n
include GravatarHelper::PublicMethods
@@ -461,7 +460,29 @@ module ApplicationHelper
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
+ ChiliProject::Liquid::Legacy.run_macros(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.macro_backwards_compatibility)
+
+ # 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
+ # Skip Liquid if there is a syntax error
+ end
@parsed_headings = []
text = parse_non_pre_blocks(text) do |text|
@@ -940,7 +961,7 @@ module ApplicationHelper
# Expands the current menu item using JavaScript based on the params
def expand_current_menu
current_menu_class =
- case
+ case
when params[:controller] == "timelog"
"reports"
when params[:controller] == 'projects' && params[:action] == 'changelog'
@@ -959,7 +980,7 @@ module ApplicationHelper
params[:controller]
end
-
+
javascript_tag("jQuery.menu_expand({ menuItem: '.#{current_menu_class}' });")
end
@@ -1006,4 +1027,23 @@ module ApplicationHelper
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
+ self.instance_variables.reject do |ivar|
+ ivar.match(/@_/) || # Rails "internal" variables: @_foo
+ ivar.match(/@template/) ||
+ ivar == '@output_buffer' ||
+ ivar == '@cookies' ||
+ ivar == '@helpers' ||
+ ivar == '@real_format' ||
+ ivar == '@assigns_added' ||
+ ivar == '@assigns' ||
+ ivar == '@view_paths' ||
+ ivar == '@controller'
+ end.inject({}) do |acc,ivar|
+ acc[ivar.sub('@','')] = instance_variable_get(ivar)
+ acc
+ end
+ end
+
end
diff --git a/app/models/issue.rb b/app/models/issue.rb
index cc754014..233e2bb1 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -103,6 +103,10 @@ class Issue < ActiveRecord::Base
(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
diff --git a/app/models/issue_status.rb b/app/models/issue_status.rb
index 7078237d..637aaafb 100644
--- a/app/models/issue_status.rb
+++ b/app/models/issue_status.rb
@@ -28,6 +28,10 @@ class IssueStatus < ActiveRecord::Base
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])
diff --git a/app/models/principal.rb b/app/models/principal.rb
index 00207f5e..3c390bb2 100644
--- a/app/models/principal.rb
+++ b/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
diff --git a/app/models/project.rb b/app/models/project.rb
index 36c2606d..7ea21188 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -83,6 +83,10 @@ class Project < ActiveRecord::Base
named_scope :all_public, { :conditions => { :is_public => true } }
named_scope :visible, lambda { { :conditions => Project.visible_by(User.current) } }
+ def to_liquid
+ ProjectDrop.new(self)
+ end
+
def initialize(attributes = nil)
super
@@ -131,6 +135,11 @@ class Project < ActiveRecord::Base
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)
diff --git a/app/models/tracker.rb b/app/models/tracker.rb
index 353645f7..94b75845 100644
--- a/app/models/tracker.rb
+++ b/app/models/tracker.rb
@@ -35,6 +35,10 @@ class Tracker < ActiveRecord::Base
name <=> tracker.name
end
+ def to_liquid
+ TrackerDrop.new(self)
+ end
+
def self.all
find(:all, :order => 'position')
end
diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb
index 16566f05..d2208dfa 100644
--- a/app/models/wiki_page.rb
+++ b/app/models/wiki_page.rb
@@ -53,6 +53,10 @@ class WikiPage < ActiveRecord::Base
end
end
+ def to_liquid
+ WikiPageDrop.new(self)
+ end
+
def visible?(user=User.current)
!user.nil? && user.allowed_to?(:view_wiki_pages, project)
end
diff --git a/config/environment.rb b/config/environment.rb
index 3c174193..0ae61ffe 100644
--- a/config/environment.rb
+++ b/config/environment.rb
@@ -49,6 +49,9 @@ Rails::Initializer.run do |config|
# (by default production uses :info, the others :debug)
# config.log_level = :debug
+ # Liquid drops
+ config.autoload_paths += %W( #{RAILS_ROOT}/app/drops )
+
# Enable page/fragment caching by setting a file-based store
# (remember to create the caching directory and make it readable to the application)
# config.action_controller.cache_store = :file_store, "#{RAILS_ROOT}/tmp/cache"
diff --git a/lib/chili_project/liquid.rb b/lib/chili_project/liquid.rb
new file mode 100644
index 00000000..f2783d6a
--- /dev/null
+++ b/lib/chili_project/liquid.rb
@@ -0,0 +1,9 @@
+require 'chili_project/liquid/liquid_ext'
+require 'chili_project/liquid/filters'
+require 'chili_project/liquid/tags'
+
+module ChiliProject
+ module Liquid
+ Liquid::Template.file_system = FileSystem.new
+ end
+end
\ No newline at end of file
diff --git a/lib/chili_project/liquid/file_system.rb b/lib/chili_project/liquid/file_system.rb
new file mode 100644
index 00000000..d6fe60f9
--- /dev/null
+++ b/lib/chili_project/liquid/file_system.rb
@@ -0,0 +1,18 @@
+module ChiliProject
+ module Liquid
+ class FileSystem
+ def read_template_file(template_name, context)
+ raise ::Liquid::FileSystemError.new("Page not found") if template_name.blank?
+ project = Project.find(context['project'].identifier) if context['project'].present?
+
+ cross_project_page = template_name.include?(':')
+ page = Wiki.find_page(template_name.to_s.strip, :project => (cross_project_page ? nil : project))
+ if page.nil? || !page.visible?
+ raise ::Liquid::FileSystemError.new("No such page '#{template_name}'")
+ end
+
+ page.content
+ end
+ end
+ end
+end
\ No newline at end of file
diff --git a/lib/chili_project/liquid/filters.rb b/lib/chili_project/liquid/filters.rb
new file mode 100644
index 00000000..baa2ed5d
--- /dev/null
+++ b/lib/chili_project/liquid/filters.rb
@@ -0,0 +1,15 @@
+module ChiliProject
+ module Liquid
+ module Filters
+ def default(input, default)
+ input.to_s.strip.present? ? input : default
+ end
+
+ def strip(input)
+ input.to_s.strip
+ end
+ end
+
+ Template.register_filter(Filters)
+ end
+end
diff --git a/lib/chili_project/liquid/legacy.rb b/lib/chili_project/liquid/legacy.rb
new file mode 100644
index 00000000..c2a23768
--- /dev/null
+++ b/lib/chili_project/liquid/legacy.rb
@@ -0,0 +1,57 @@
+module ChiliProject
+ module Liquid
+ # Legacy is used to support older Redmine style macros by converting
+ # them to Liquid objects (tags, filters) on the fly by doing basic
+ # string substitution. This is done before the Liquid processing
+ # so the converted macros work like normal
+ #
+ module Legacy
+ # Holds the list of legacy macros
+ #
+ # @param [Regexp] :match The regex to match on the legacy macro
+ # @param [String] :replace The string to replace with. E.g. "%" converts
+ # "{{ }}" to "{% %}"
+ # @param [String] :new_name The new name of the Liquid object
+ def self.macros
+ @macros ||= {}
+ end
+
+ # "Runs" legacy macros by doing a gsub of their values to the new Liquid ones
+ #
+ # @param [String] content The pre-Liquid content
+ def self.run_macros(content)
+ macros.each do |macro_name, macro|
+ next unless macro[:match].present? && macro[:replace].present?
+ content.gsub!(macro[:match]) do |match|
+ # Use block form so $1 and $2 are set properly
+ "{#{macro[:replace]} #{macro[:new_name]} '#{$2}' #{macro[:replace]}}"
+ end
+ end
+ end
+
+ # Add support for a legacy macro syntax that was converted to liquid
+ #
+ # @param [String] name The legacy macro name
+ # @param [Symbol] liquid_type The type of Liquid object to use. Supported: :tag
+ # @param [optional, String] new_name The new name of the liquid object, used
+ # to rename a macro
+ def self.add(name, liquid_type, new_name=nil)
+ new_name = name unless new_name.present?
+ case liquid_type
+ when :tag
+
+ macros[name.to_s] = {
+ # Example values the regex matches
+ # {{name}}
+ # {{ name }}
+ # {{ name 'arg' }}
+ # {{ name('arg') }}
+ :match => Regexp.new(/\{\{(#{name})(?:\(([^\}]*)\))?\}\}/),
+ :replace => "%",
+ :new_name => new_name
+ }
+ end
+ end
+ end
+ end
+end
diff --git a/lib/chili_project/liquid/liquid_ext.rb b/lib/chili_project/liquid/liquid_ext.rb
new file mode 100644
index 00000000..d001afe4
--- /dev/null
+++ b/lib/chili_project/liquid/liquid_ext.rb
@@ -0,0 +1,8 @@
+module ChiliProject
+ module Liquid
+ module LiquidExt
+ ::Liquid::Block.send(:include, Block)
+ ::Liquid::Context.send(:include, Context)
+ end
+ end
+end
diff --git a/lib/chili_project/liquid/liquid_ext/block.rb b/lib/chili_project/liquid/liquid_ext/block.rb
new file mode 100644
index 00000000..f37dffc4
--- /dev/null
+++ b/lib/chili_project/liquid/liquid_ext/block.rb
@@ -0,0 +1,22 @@
+module ChiliProject
+ module Liquid
+ module LiquidExt
+ module Block
+ def self.included(base)
+ base.send(:include, InstanceMethods)
+ base.class_eval do
+ alias_method_chain :render_all, :cleaned_whitespace
+ end
+ end
+
+ module InstanceMethods
+ def render_all_with_cleaned_whitespace(list, context)
+ # Remove the leading newline in a block's content
+ list[0].sub!(/\A\r?\n/, "") if list[0].is_a?(String)
+ render_all_without_cleaned_whitespace(list, context)
+ end
+ end
+ end
+ end
+ end
+end
\ No newline at end of file
diff --git a/lib/chili_project/liquid/liquid_ext/context.rb b/lib/chili_project/liquid/liquid_ext/context.rb
new file mode 100644
index 00000000..1dc38739
--- /dev/null
+++ b/lib/chili_project/liquid/liquid_ext/context.rb
@@ -0,0 +1,40 @@
+module ChiliProject
+ module Liquid
+ module LiquidExt
+ module Context
+ def self.included(base)
+ base.send(:include, InstanceMethods)
+ base.class_eval do
+ alias_method_chain :handle_error, :formatting
+ end
+ end
+
+ module InstanceMethods
+ def handle_error_with_formatting(e)
+ error = handle_error_without_formatting(e)
+ escaped_error = registers[:view].send(:h, error) rescue CGI::escapeHTML(error)
+
+ html = '
' + escaped_error + '
'
+ html_result(html)
+ end
+
+ def html_result(html)
+ key = nil
+ while key.nil? || html_results.has_key?(key)
+ random = ActiveSupport::SecureRandom.hex(10)
+ # This string must be passed untouched through Liquid and textile
+ # It mustn't be changed in any way by any rendering stage.
+ key = "!!html_results.#{random}!!"
+ end
+ html_results[key] = html
+ key
+ end
+
+ def html_results
+ registers[:html_results] ||= {}
+ end
+ end
+ end
+ end
+ end
+end
\ No newline at end of file
diff --git a/lib/chili_project/liquid/tags.rb b/lib/chili_project/liquid/tags.rb
new file mode 100644
index 00000000..41a28f7b
--- /dev/null
+++ b/lib/chili_project/liquid/tags.rb
@@ -0,0 +1,38 @@
+module ChiliProject::Liquid
+ module Tags
+ class TagError < StandardError; end
+
+ def self.register_tag(name, klass, options={})
+ if options[:html]
+ html_class = Class.new do
+ def render(context)
+ result = @tag.render(context)
+ context.html_result(result)
+ end
+
+ def method_missing(*args, &block)
+ @tag.send(*args, &block)
+ end
+ end
+ html_class.send :define_method, :initialize do |*args|
+ @tag = klass.new(*args)
+ end
+ ::Liquid::Template.register_tag(name, html_class)
+ else
+ ::Liquid::Template.register_tag(name, klass)
+ end
+ end
+
+ register_tag('child_pages', ChildPages, :html => true)
+ register_tag('hello_world', HelloWorld)
+ register_tag('include', Include, :html => true)
+ register_tag('tag_list', TagList, :html => true)
+ register_tag('variable_list', VariableList, :html => true)
+ end
+end
+
+# FIXME: remove the deprecated syntax for 4.0, provide a way to migrate
+# existing pages to the new syntax.
+ChiliProject::Liquid::Legacy.add('child_pages', :tag)
+ChiliProject::Liquid::Legacy.add('hello_world', :tag)
+ChiliProject::Liquid::Legacy.add('include', :tag)
diff --git a/lib/chili_project/liquid/tags/child_pages.rb b/lib/chili_project/liquid/tags/child_pages.rb
new file mode 100644
index 00000000..ca373a3e
--- /dev/null
+++ b/lib/chili_project/liquid/tags/child_pages.rb
@@ -0,0 +1,60 @@
+module ChiliProject::Liquid::Tags
+ class ChildPages < Tag
+ def initialize(tag_name, markup, tokens)
+ markup = markup.strip.gsub(/["']/, '')
+ if markup.present?
+ tag_args = markup.split(',')
+ @args, @options = extract_macro_options(tag_args, :parent)
+ else
+ @args = []
+ @options = {}
+ end
+ super
+ end
+
+ def render(context)
+ # inside of a project
+ @project = Project.find(context['project'].identifier) if context['project'].present?
+
+ if @args.present?
+ page_name = @args.first.to_s
+ cross_project_page = page_name.include?(':')
+
+ page = Wiki.find_page(page_name, :project => (cross_project_page ? nil : @project))
+ # FIXME: :object and :attribute should be variables, not registers
+ elsif context.registers[:object].is_a?(WikiContent)
+ page = context.registers[:object].page
+ page_name = page.title
+ elsif @project
+ return render_all_pages(context)
+ else
+ raise TagError.new('With no argument, this tag can be called from projects only.')
+ end
+
+ raise TagError.new("No such page '#{page_name}'") if page.nil? || !User.current.allowed_to?(:view_wiki_pages, page.wiki.project)
+ pages = ([page] + page.descendants).group_by(&:parent_id)
+ context.registers[:view].render_page_hierarchy(pages, @options[:parent] ? page.parent_id : page.id)
+ end
+
+ private
+ def render_all_pages(context)
+ return '' unless @project.wiki.present? && @project.wiki.pages.present?
+ raise TagError.new('Page not found') if !User.current.allowed_to?(:view_wiki_pages, @project)
+
+ context.registers[:view].render_page_hierarchy(@project.wiki.pages.group_by(&:parent_id))
+ end
+
+ # @param args [Array, String] An array of strings in "key=value" format
+ # @param keys [Hash, Symbol] List of keyword args to extract
+ def extract_macro_options(args, *keys)
+ options = {}
+ args.each do |arg|
+ if arg.to_s.gsub(/["']/,'').strip =~ %r{^(.+)\=(.+)$} && keys.include?($1.downcase.to_sym)
+ options[$1.downcase.to_sym] = $2
+ args.pop
+ end
+ end
+ return [args, options]
+ end
+ end
+end
\ No newline at end of file
diff --git a/lib/chili_project/liquid/tags/hello_world.rb b/lib/chili_project/liquid/tags/hello_world.rb
new file mode 100644
index 00000000..7eff7654
--- /dev/null
+++ b/lib/chili_project/liquid/tags/hello_world.rb
@@ -0,0 +1,11 @@
+module ChiliProject::Liquid::Tags
+ class HelloWorld < Tag
+ def initialize(tag_name, markup, tokens)
+ super
+ end
+
+ def render(context)
+ "Hello world!"
+ end
+ end
+end
\ No newline at end of file
diff --git a/lib/chili_project/liquid/tags/include.rb b/lib/chili_project/liquid/tags/include.rb
new file mode 100644
index 00000000..6de3df92
--- /dev/null
+++ b/lib/chili_project/liquid/tags/include.rb
@@ -0,0 +1,82 @@
+#-- copyright
+# ChiliProject is a project management system.
+#
+# Copyright (C) 2010-2011 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.
+#++
+
+module ChiliProject::Liquid::Tags
+ class Include < ::Liquid::Include
+
+ # This method follows the basic flow of the default include in liquid
+ # We just add some additional flexibility. This method can be removed once
+ # https://github.com/Shopify/liquid/pull/78 got accepted
+ def render(context)
+ context.stack do
+ template = _read_template_from_file_system(context)
+ partial = Liquid::Template.parse _template_source(template)
+ variable = context[@variable_name || @template_name[1..-2]]
+
+ @attributes.each do |key, value|
+ context[key] = context[value]
+ end
+
+ if variable.is_a?(Array)
+ variable.collect do |variable|
+ context[@template_name[1..-2]] = variable
+ _render_partial(partial, template, context)
+ end
+ else
+ context[@template_name[1..-2]] = variable
+ _render_partial(partial, template, context)
+ end
+ end
+ end
+
+ private
+ def break_circle(context)
+ context.registers[:included_pages] ||= []
+
+ project = context['project'].identifier if context['project'].present?
+ template_name = context[@template_name]
+ cross_project_page = template_name.include?(':')
+ page_title = cross_project_page ? template_name : "#{project}:#{template_name}"
+
+ raise ::Liquid::FileSystemError.new("Circular inclusion detected") if context.registers[:included_pages].include?(page_title)
+ context.registers[:included_pages] << page_title
+
+ yield
+ ensure
+ context.registers[:included_pages].pop
+ end
+
+ def _template_source(wiki_content)
+ wiki_content.text
+ end
+
+ def _render_partial(partial, template, context)
+ break_circle(context) do
+ textile = partial.render(context)
+
+ # Call textilizable on the view so all of the helpers are loaded
+ # based on the view and not this tag
+ context.registers[:view].textilizable(textile, :attachments => template.page.attachments, :headings => false, :object => template)
+ end
+ end
+
+ def _read_template_from_file_system(context)
+ wiki_content = super
+
+ # Set the new project to that additional includes use the correct
+ # base project
+ context['project'] = wiki_content.page.wiki.project
+ wiki_content
+ end
+ end
+end
diff --git a/lib/chili_project/liquid/tags/tag.rb b/lib/chili_project/liquid/tags/tag.rb
new file mode 100644
index 00000000..4b7783dd
--- /dev/null
+++ b/lib/chili_project/liquid/tags/tag.rb
@@ -0,0 +1,4 @@
+module ChiliProject::Liquid::Tags
+ class Tag < ::Liquid::Tag
+ end
+end
diff --git a/lib/chili_project/liquid/tags/tag_list.rb b/lib/chili_project/liquid/tags/tag_list.rb
new file mode 100644
index 00000000..e409e8ca
--- /dev/null
+++ b/lib/chili_project/liquid/tags/tag_list.rb
@@ -0,0 +1,14 @@
+module ChiliProject::Liquid::Tags
+ class TagList < Tag
+ include ActionView::Helpers::TagHelper
+
+ def render(context)
+ content_tag('p', "Tags:") +
+ content_tag('ul',
+ ::Liquid::Template.tags.keys.sort.collect {|tag_name|
+ content_tag('li', content_tag('code', h(tag_name)))
+ }.join('')
+ )
+ end
+ end
+end
\ No newline at end of file
diff --git a/lib/chili_project/liquid/tags/variable_list.rb b/lib/chili_project/liquid/tags/variable_list.rb
new file mode 100644
index 00000000..51b85a48
--- /dev/null
+++ b/lib/chili_project/liquid/tags/variable_list.rb
@@ -0,0 +1,15 @@
+module ChiliProject::Liquid::Tags
+ class VariableList < Tag
+ include ActionView::Helpers::TagHelper
+
+ def render(context)
+ out = ''
+ context.environments.first.keys.sort.each do |liquid_variable|
+ next if liquid_variable == 'text' # internal variable
+ out << content_tag('li', content_tag('code', h(liquid_variable)))
+ end if context.environments.present?
+
+ content_tag('p', "Variables:") + content_tag('ul', out)
+ end
+ end
+end
diff --git a/lib/chili_project/liquid/template.rb b/lib/chili_project/liquid/template.rb
new file mode 100644
index 00000000..955cbf32
--- /dev/null
+++ b/lib/chili_project/liquid/template.rb
@@ -0,0 +1,96 @@
+module ChiliProject
+ module Liquid
+ class Template < ::Liquid::Template
+ # creates a new Template object from liquid source code
+ def self.parse(source)
+ template = self.new
+ template.parse(source)
+ template
+ end
+
+
+ def context_from_render_options(*args)
+ # This method is pulled out straight from the original
+ # Liquid::Template#render
+ context = case args.first
+ when ::Liquid::Context
+ args.shift
+ when Hash
+ ::Liquid::Context.new([args.shift, assigns], instance_assigns, registers, @rethrow_errors)
+ when nil
+ ::Liquid::Context.new(assigns, instance_assigns, registers, @rethrow_errors)
+ else
+ raise ArgumentError, "Expect Hash or Liquid::Context as parameter"
+ end
+
+ case args.last
+ when Hash
+ options = args.pop
+
+ if options[:registers].is_a?(Hash)
+ self.registers.merge!(options[:registers])
+ end
+
+ if options[:filters]
+ context.add_filters(options[:filters])
+ end
+
+ when Module
+ context.add_filters(args.pop)
+ when Array
+ context.add_filters(args.pop)
+ end
+ context
+ end
+
+ # Render takes a hash with local variables.
+ #
+ # if you use the same filters over and over again consider registering them globally
+ # with Template.register_filter
+ #
+ # Following options can be passed:
+ #
+ # * filters : array with local filters
+ # * registers : hash with register variables. Those can be accessed from
+ # filters and tags and might be useful to integrate liquid more with its host application
+ #
+ def render(*args)
+ return '' if @root.nil?
+
+ context = context_from_render_options(*args)
+ context.registers[:html_results] ||= {}
+
+ # ENTER THE RENDERING STAGE
+
+ # 1. Render the input as Liquid
+ # Some tags might register final HTML output in this stage.
+ begin
+ # for performance reasons we get a array back here. join will make a string out of it
+ result = @root.render(context)
+ result.respond_to?(:join) ? result.join : result
+ ensure
+ @errors = context.errors
+ end
+
+ # 2. Perform the Wiki markup transformation (e.g. Textile)
+ obj = context.registers[:object]
+ attr = context.registers[:attribute]
+ result = Redmine::WikiFormatting.to_html(Setting.text_formatting, result, :object => obj, :attribute => attr)
+
+ # 3. Now finally, replace the captured raw HTML bits in the final content
+ length = nil
+ # replace HTML results until we can find no additional variables
+ while length != context.registers[:html_results].length do
+ length = context.registers[:html_results].length
+ context.registers[:html_results].delete_if do |key, value|
+ # We use the block variant to avoid the evaluation of escaped
+ # characters in +value+ during substitution.
+ result.sub!(key) { |match| value }
+ end
+ end
+
+ result
+ end
+ end
+ end
+end
diff --git a/lib/chili_project/liquid/variables.rb b/lib/chili_project/liquid/variables.rb
new file mode 100644
index 00000000..ad23da38
--- /dev/null
+++ b/lib/chili_project/liquid/variables.rb
@@ -0,0 +1,14 @@
+module ChiliProject
+ module Liquid
+ module Variables
+ # Liquid "variables" that are used for backwards compatability with macros
+ #
+ # Variables are used in liquid like {{var}}
+ def self.macro_backwards_compatibility
+ {
+ 'macro_list' => "Use the '{% variable_list %}' tag to see all Liquid variables and '{% tag_list %}' to see all of the Liquid tags."
+ }
+ end
+ end
+ end
+end
diff --git a/lib/redmine/wiki_formatting.rb b/lib/redmine/wiki_formatting.rb
index cf5f4516..ac9ae734 100644
--- a/lib/redmine/wiki_formatting.rb
+++ b/lib/redmine/wiki_formatting.rb
@@ -41,7 +41,7 @@ module Redmine
end
def to_html(format, text, options = {}, &block)
- text = if Setting.cache_formatted_text? && text.size > 2.kilobyte && cache_store && cache_key = cache_key_for(format, options[:object], options[:attribute])
+ if Setting.cache_formatted_text? && text.size > 2.kilobyte && cache_store && cache_key = cache_key_for(format, options[:object], options[:attribute])
# Text retrieved from the cache store may be frozen
# We need to dup it so we can do in-place substitutions with gsub!
cache_store.fetch cache_key do
@@ -50,10 +50,6 @@ module Redmine
else
formatter_for(format).new(text).to_html
end
- if block_given?
- execute_macros(text, block)
- end
- text
end
# Returns a cache key for the given text +format+, +object+ and +attribute+ or nil if no caching should be done
@@ -67,33 +63,6 @@ module Redmine
def cache_store
ActionController::Base.cache_store
end
-
- MACROS_RE = /
- (!)? # escaping
- (
- \{\{ # opening tag
- ([\w]+) # macro name
- (\(([^\}]*)\))? # optional arguments
- \}\} # closing tag
- )
- /x unless const_defined?(:MACROS_RE)
-
- # Macros substitution
- def execute_macros(text, macros_runner)
- text.gsub!(MACROS_RE) do
- esc, all, macro = $1, $2, $3.downcase
- args = ($5 || '').split(',').each(&:strip)
- if esc.nil?
- begin
- macros_runner.call(macro, args)
- rescue => e
- "Error executing the #{macro} macro (#{e})
"
- end || all
- else
- all
- end
- end
- end
end
end
end
diff --git a/lib/redmine/wiki_formatting/macros.rb b/lib/redmine/wiki_formatting/macros.rb
index 8c01eac0..28ea57d7 100644
--- a/lib/redmine/wiki_formatting/macros.rb
+++ b/lib/redmine/wiki_formatting/macros.rb
@@ -12,106 +12,55 @@
# See doc/COPYRIGHT.rdoc for more details.
#++
+# DECREACATED SINCE 3.0 - TO BE REMOVED IN 4.0
+# The whole macro concept is deprecated. It is to be completely replaced by
+# Liquid tags and variables.
+
+require 'dispatcher'
+
module Redmine
module WikiFormatting
module Macros
- module Definitions
- def exec_macro(name, obj, args)
- method_name = "macro_#{name}"
- send(method_name, obj, args) if respond_to?(method_name)
- end
-
- def extract_macro_options(args, *keys)
- options = {}
- while args.last.to_s.strip =~ %r{^(.+)\=(.+)$} && keys.include?($1.downcase.to_sym)
- options[$1.downcase.to_sym] = $2
- args.pop
- end
- return [args, options]
- end
- end
-
- @@available_macros = {}
+ @available_macros = {}
class << self
- # Called with a block to define additional macros.
- # Macro blocks accept 2 arguments:
- # * obj: the object that is rendered
- # * args: macro arguments
- #
- # Plugins can use this method to define new macros:
- #
- # Redmine::WikiFormatting::Macros.register do
- # desc "This is my macro"
- # macro :my_macro do |obj, args|
- # "My macro output"
- # end
- # end
def register(&block)
+ ActiveSupport::Deprecation.warn("Macros are deprecated. Use Liquid filters and tags instead", caller.drop(3))
class_eval(&block) if block_given?
end
private
+ # Sets description for the next macro to be defined
+ def desc(txt)
+ @desc = txt
+ end
+
# Defines a new macro with the given name and block.
def macro(name, &block)
name = name.to_sym if name.is_a?(String)
- @@available_macros[name] = @@desc || ''
- @@desc = nil
+ @available_macros[name] = @desc || ''
+ @desc = nil
raise "Can not create a macro without a block!" unless block_given?
- Definitions.send :define_method, "macro_#{name}".downcase, &block
+
+ tag = Class.new(::Liquid::Tag) do
+ def initialize(tag_name, markup, tokens)
+ if markup =~ self.class::Syntax
+ @args = $1[1..-2].split(',').collect(&:strip)
+ else
+ raise ::Liquid::SyntaxError.new("Syntax error in tag '#{name}'")
+ end
+ end
+ end
+ tag.send :define_method, :render do |context|
+ context.registers[:view].instance_exec context.registers[:object], @args, &block
+ end
+ tag.const_set 'Syntax', /(#{::Liquid::QuotedFragment})/
+
+ Dispatcher.to_prepare do
+ ChiliProject::Liquid::Tags.register_tag(name, tag, :html => true)
+ ChiliProject::Liquid::Legacy.add(name, :tag)
+ end
end
-
- # Sets description for the next macro to be defined
- def desc(txt)
- @@desc = txt
- end
- end
-
- # Builtin macros
- desc "Sample macro."
- macro :hello_world do |obj, args|
- "Hello world! Object: #{obj.class.name}, " + (args.empty? ? "Called with no argument." : "Arguments: #{args.join(', ')}")
- end
-
- desc "Displays a list of all available macros, including description if available."
- macro :macro_list do
- out = ''
- @@available_macros.keys.collect(&:to_s).sort.each do |macro|
- out << content_tag('dt', content_tag('code', macro))
- out << content_tag('dd', textilizable(@@available_macros[macro.to_sym]))
- end
- content_tag('dl', out)
- end
-
- desc "Displays a list of child pages. With no argument, it displays the child pages of the current wiki page. Examples:\n\n" +
- " !{{child_pages}} -- can be used from a wiki page only\n" +
- " !{{child_pages(Foo)}} -- lists all children of page Foo\n" +
- " !{{child_pages(Foo, parent=1)}} -- same as above with a link to page Foo"
- macro :child_pages do |obj, args|
- args, options = extract_macro_options(args, :parent)
- page = nil
- if args.size > 0
- page = Wiki.find_page(args.first.to_s, :project => @project)
- elsif obj.is_a?(WikiContent)
- page = obj.page
- else
- raise 'With no argument, this macro can be called from wiki pages only.'
- end
- raise 'Page not found' if page.nil? || !User.current.allowed_to?(:view_wiki_pages, page.wiki.project)
- pages = ([page] + page.descendants).group_by(&:parent_id)
- render_page_hierarchy(pages, options[:parent] ? page.parent_id : page.id)
- end
-
- desc "Include a wiki page. Example:\n\n !{{include(Foo)}}\n\nor to include a page of a specific project wiki:\n\n !{{include(projectname:Foo)}}"
- macro :include do |obj, args|
- page = Wiki.find_page(args.first.to_s, :project => @project)
- raise 'Page not found' if page.nil? || !User.current.allowed_to?(:view_wiki_pages, page.wiki.project)
- @included_wiki_pages ||= []
- raise 'Circular inclusion detected' if @included_wiki_pages.include?(page.title)
- @included_wiki_pages << page.title
- out = textilizable(page.content, :text, :attachments => page.attachments, :headings => false)
- @included_wiki_pages.pop
- out
end
end
end
diff --git a/test/fixtures/wiki_contents.yml b/test/fixtures/wiki_contents.yml
index bf2797b8..060ec167 100644
--- a/test/fixtures/wiki_contents.yml
+++ b/test/fixtures/wiki_contents.yml
@@ -3,7 +3,7 @@ wiki_contents_001:
text: |-
h1. CookBook documentation
- {{child_pages}}
+ {% child_pages %}
Some updated [[documentation]] here with gzipped history
updated_on: 2007-03-07 00:10:51 +01:00
@@ -17,7 +17,7 @@ wiki_contents_002:
This is a link to a ticket: #2
And this is an included page:
- {{include(Page with an inline image)}}
+ {% include 'Page with an inline image' %}
updated_on: 2007-03-08 00:18:07 +01:00
page_id: 2
id: 2
diff --git a/test/functional/wiki_controller_test.rb b/test/functional/wiki_controller_test.rb
index d8b442df..b583b728 100644
--- a/test/functional/wiki_controller_test.rb
+++ b/test/functional/wiki_controller_test.rb
@@ -18,8 +18,8 @@ require 'wiki_controller'
class WikiController; def rescue_action(e) raise e end; end
class WikiControllerTest < ActionController::TestCase
- fixtures :projects, :users, :roles, :members, :member_roles, :enabled_modules, :wikis, :wiki_pages, :wiki_contents, :journals, :attachments
-
+ fixtures :projects, :users, :roles, :members, :member_roles, :enabled_modules, :wikis, :wiki_pages, :wiki_contents, :journals, :attachments, :enumerations
+
def setup
@controller = WikiController.new
@request = ActionController::TestRequest.new
diff --git a/test/unit/helpers/application_helper_test.rb b/test/unit/helpers/application_helper_test.rb
index d1a6e255..9f85b101 100644
--- a/test/unit/helpers/application_helper_test.rb
+++ b/test/unit/helpers/application_helper_test.rb
@@ -593,7 +593,7 @@ RAW
h1. Included
-{{include(Child_1)}}
+{% include 'Child_1' %}
RAW
expected = '' +
diff --git a/test/unit/issue_drop_test.rb b/test/unit/issue_drop_test.rb
new file mode 100644
index 00000000..b3c034a2
--- /dev/null
+++ b/test/unit/issue_drop_test.rb
@@ -0,0 +1,89 @@
+require File.expand_path('../../test_helper', __FILE__)
+
+class IssueDropTest < ActiveSupport::TestCase
+ include ApplicationHelper
+
+ def setup
+ @project = Project.generate!
+ @issue = Issue.generate_for_project!(@project)
+ User.current = @user = User.generate!
+ @role = Role.generate!(:permissions => [:view_issues])
+ Member.generate!(:principal => @user, :project => @project, :roles => [@role])
+ @drop = @issue.to_liquid
+ end
+
+ context "drop" do
+ should "be a IssueDrop" do
+ assert @drop.is_a?(IssueDrop), "drop is not a IssueDrop"
+ end
+ end
+
+
+ [
+ :tracker,
+ :project,
+ :subject,
+ :description,
+ :due_date,
+ :category,
+ :status,
+ :assigned_to,
+ :priority,
+ :fixed_version,
+ :author,
+ :created_on,
+ :updated_on,
+ :start_date,
+ :done_ratio,
+ :estimated_hours,
+ :parent
+ ].each do |attribute|
+
+ should "IssueDrop##{attribute} should return the actual #{attribute} attribute" do
+ assert @issue.respond_to?(attribute), "Issue does not have an #{attribute} method"
+ assert @drop.respond_to?(attribute), "IssueDrop does not have an #{attribute} method"
+
+ assert_equal @issue.send(attribute), @drop.send(attribute)
+ end
+ end
+
+ context "custom fields" do
+ setup do
+ @field = IssueCustomField.generate!(:name => 'The Name', :field_format => 'string', :is_for_all => true, :trackers => @project.trackers)
+ @field_name_conflict = IssueCustomField.generate!(:name => 'Subject', :field_format => 'string', :is_for_all => true, :trackers => @project.trackers)
+ @issue.custom_fields = [{'id' => @field.id, 'value' => 'Custom field value'},
+ {'id' => @field_name_conflict.id, 'value' => 'Second subject'}]
+ assert @issue.save
+ assert_equal "Custom field value", @issue.reload.custom_value_for(@field).value
+ assert_equal "Second subject", @issue.reload.custom_value_for(@field_name_conflict).value
+ @drop = @issue.to_liquid
+ end
+
+ should "be accessible under #custom_field(name)" do
+ assert_equal @issue.reload.custom_value_for(@field).value, @drop.custom_field('The Name')
+ end
+
+ should "be accessible under the custom field name (lowercase, underscored)" do
+ assert_equal @issue.reload.custom_value_for(@field).value, @drop.the_name
+
+ assert textilizable("{{issue.the_name}}").include?("Custom field value")
+ end
+
+ should "not be accessible under the custom field name if it conflict with an existing drop method" do
+ assert_equal @issue.subject, @drop.subject # no confict
+ end
+ end
+
+ should "only load an object if it's visible to the current user" do
+ assert User.current.logged?
+ assert @issue.visible?
+
+ @private_project = Project.generate!(:is_public => false)
+ @private_issue = Issue.generate_for_project!(@private_project)
+
+ assert !@private_issue.visible?, "Issue is visible"
+ @private_drop = IssueDrop.new(@private_issue)
+ assert_equal nil, @private_drop.instance_variable_get("@object")
+ assert_equal nil, @private_drop.subject
+ end
+end
\ No newline at end of file
diff --git a/test/unit/issue_status_drop_test.rb b/test/unit/issue_status_drop_test.rb
new file mode 100644
index 00000000..83cbe24e
--- /dev/null
+++ b/test/unit/issue_status_drop_test.rb
@@ -0,0 +1,21 @@
+require File.expand_path('../../test_helper', __FILE__)
+
+class IssueStatusDropTest < ActiveSupport::TestCase
+ def setup
+ @issue_status = IssueStatus.generate!
+ @drop = @issue_status.to_liquid
+ end
+
+ context "drop" do
+ should "be a IssueStatusDrop" do
+ assert @drop.is_a?(IssueStatusDrop), "drop is not a IssueStatusDrop"
+ end
+ end
+
+
+ context "#name" do
+ should "return the name" do
+ assert_equal @issue_status.name, @drop.name
+ end
+ end
+end
diff --git a/test/unit/lib/chili_project/liquid_test.rb b/test/unit/lib/chili_project/liquid_test.rb
new file mode 100644
index 00000000..2fb25020
--- /dev/null
+++ b/test/unit/lib/chili_project/liquid_test.rb
@@ -0,0 +1,211 @@
+#-- encoding: UTF-8
+#-- copyright
+# ChiliProject is a project management system.
+#
+# Copyright (C) 2010-2011 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.
+#++
+require File.expand_path('../../../../test_helper', __FILE__)
+
+class ChiliProject::LiquidTest < ActionView::TestCase
+ include ApplicationHelper
+
+ context "hello_world tag" do
+ should "render 'Hello world!'" do
+ text = "{% hello_world %}"
+ assert_match /Hello world!/, textilizable(text)
+ end
+ end
+
+ context "variable_list tag" do
+ should "render a list of the current variables" do
+ text = "{% variable_list %}"
+ formatted = textilizable(text)
+
+ assert formatted.include?(''), "Not in a list format"
+ assert formatted.include?('current_user')
+ end
+ end
+
+ context "child_pages tag" do
+ context "with no arg" do
+ context "and @project set" do
+ should "should list all wiki pages for the current project" do
+ @project = Project.generate!.reload
+ wiki = @project.wiki
+ top = WikiPage.generate!(:wiki => wiki, :title => 'Top', :content => WikiContent.new(:text => 'top page'))
+ child1 = WikiPage.generate!(:wiki => wiki, :title => 'Child1', :content => WikiContent.new(:text => 'child'), :parent => top)
+
+ text = "{% child_pages %}"
+ formatted = textilizable(text)
+
+ assert formatted.include?('pages-hierarchy')
+ assert formatted.include?('Child1')
+ assert formatted.include?('Top')
+ end
+ end
+
+ context "and no @project set" do
+ should "render a warning" do
+ text = "{% child_pages %}"
+ formatted = textilizable(text)
+
+ assert_match /flash error/, formatted
+ assert formatted.include?('With no argument, this tag can be called from projects only')
+ end
+ end
+
+ end
+
+ context "with a valid WikiPage arg" do
+ should "list all child pages for the wiki page" do
+ @project = Project.generate!.reload
+ wiki = @project.wiki
+ top = WikiPage.generate!(:wiki => wiki, :title => 'Top', :content => WikiContent.new(:text => 'top page'))
+ child1 = WikiPage.generate!(:wiki => wiki, :title => 'Child1', :content => WikiContent.new(:text => 'child'), :parent => top)
+
+ text = "{% child_pages 'Top' %}"
+ formatted = textilizable(text)
+
+ assert formatted.include?('pages-hierarchy')
+ assert formatted.include?('Child1')
+ assert !formatted.include?('Top')
+ end
+
+ should "allow cross project listings even when outside of a project" do
+ project = Project.generate!.reload # project not an ivar
+ wiki = project.wiki
+ top = WikiPage.generate!(:wiki => wiki, :title => 'Top', :content => WikiContent.new(:text => 'top page'))
+ child1 = WikiPage.generate!(:wiki => wiki, :title => 'Child1', :content => WikiContent.new(:text => 'child'), :parent => top)
+
+ text = "{% child_pages #{project.identifier}:'Top' %}"
+ formatted = textilizable(text)
+
+ assert formatted.include?('pages-hierarchy')
+ assert formatted.include?('Child1')
+ assert !formatted.include?('Top')
+ end
+
+ should "show the WikiPage when parent=1 is set" do
+ @project = Project.generate!.reload
+ wiki = @project.wiki
+ top = WikiPage.generate!(:wiki => wiki, :title => 'Top', :content => WikiContent.new(:text => 'top page'))
+ child1 = WikiPage.generate!(:wiki => wiki, :title => 'Child1', :content => WikiContent.new(:text => 'child'), :parent => top)
+
+ text = "{% child_pages 'Top', 'parent=1' %}"
+ formatted = textilizable(text)
+
+ assert formatted.include?('pages-hierarchy')
+ assert formatted.include?('Child1')
+ assert formatted.include?('Top')
+
+ end
+ end
+
+ context "with an invalid arg" do
+ should "render a warning" do
+ @project = Project.generate!.reload
+ wiki = @project.wiki
+ top = WikiPage.generate!(:wiki => wiki, :title => 'Top', :content => WikiContent.new(:text => 'top page'))
+ child1 = WikiPage.generate!(:wiki => wiki, :title => 'Child1', :content => WikiContent.new(:text => 'child'), :parent => top)
+
+ text = "{% child_pages 1 %}"
+ formatted = textilizable(text)
+
+ assert_match /flash error/, formatted
+ assert formatted.include?('No such page')
+
+ end
+ end
+ end
+
+ context "include tag" do
+ setup do
+ @project = Project.generate!.reload
+ @wiki = @project.wiki
+ @included_page = WikiPage.generate!(:wiki => @wiki, :title => 'Included_Page', :content => WikiContent.new(:text => 'included page [[Second_Page]]'))
+
+ @project2 = Project.generate!.reload
+ @cross_project_page = WikiPage.generate!(:wiki => @project2.wiki, :title => 'Second_Page', :content => WikiContent.new(:text => 'second page'))
+
+ end
+
+ context "with a direct page" do
+ should "show the included page's content" do
+ text = "{% include 'Included Page' %}"
+ formatted = textilizable(text)
+
+ assert formatted.include?('included page')
+ end
+ end
+
+ context "with a cross-project page" do
+ should "show the included page's content" do
+ text = "{% include '#{@project2.identifier}:Second Page' %}"
+ formatted = textilizable(text)
+
+ assert formatted.include?('second page')
+ end
+ end
+
+ context "with recursive includes" do
+ should "render all child pages" do
+ parent = WikiPage.generate!(:wiki => @wiki, :title => 'Recursive_Parent', :content => WikiContent.new(:text => "h1. Parent\r\n{% include 'Recursive_Child1' %}"))
+ child1 = WikiPage.generate!(:wiki => @wiki, :title => 'Recursive_Child1', :content => WikiContent.new(:text => "h1. Child1\r\n{% include 'Recursive_Child2' %}"))
+ child2 = WikiPage.generate!(:wiki => @wiki, :title => 'Recursive_Child2', :content => WikiContent.new(:text => 'h1. Child2'))
+
+ formatted = textilizable(parent.reload.text)
+
+ assert_match /\s*Parent.*?<\/h1>/, formatted
+ assert_match /\s*Child1.*?<\/h1>/, formatted
+ assert_match /\s*Child2.*?<\/h1>/, formatted
+
+ # make sure there are no dangling html result variables
+ assert_no_match /!!html_results.*?!!/, formatted
+ end
+ end
+
+ context "with a circular inclusion" do
+ should "render a warning" do
+ circle_page = WikiPage.generate!(:wiki => @wiki, :title => 'Circle', :content => WikiContent.new(:text => '{% include "Circle2" %}'))
+ circle_page2 = WikiPage.generate!(:wiki => @wiki, :title => 'Circle2', :content => WikiContent.new(:text => '{% include "Circle" %}'))
+ formatted = textilizable(circle_page.reload.text)
+
+ assert_match /flash error/, formatted
+ assert_match 'Circular inclusion detected', formatted
+ end
+ end
+
+ context "with an invalid arg" do
+ should "render a warning" do
+ text = "{% include '404' %}"
+ formatted = textilizable(text)
+
+ assert_match /flash error/, formatted
+ assert formatted.include?('No such page')
+ end
+
+ should "HTML escape the error" do
+ text = "{% include '' %}"
+ formatted = textilizable(text)
+
+ assert formatted.include?("No such page '<script>alert("foo"):</script>'")
+ end
+ end
+
+ context "legacy" do
+ should "map to native include" do
+ text = "{{include(#{@project2.identifier}:Second_Page)}}"
+ formatted = textilizable(text)
+
+ assert formatted.include?('second page')
+ end
+ end
+ end
+end
diff --git a/test/unit/lib/redmine/wiki_formatting/macros_test.rb b/test/unit/lib/redmine/wiki_formatting/macros_test.rb
index 40a6968d..731e97a6 100644
--- a/test/unit/lib/redmine/wiki_formatting/macros_test.rb
+++ b/test/unit/lib/redmine/wiki_formatting/macros_test.rb
@@ -39,9 +39,6 @@ class Redmine::WikiFormatting::MacrosTest < HelperTestCase
def test_macro_hello_world
text = "{{hello_world}}"
assert textilizable(text).match(/Hello world!/)
- # escaping
- text = "!{{hello_world}}"
- assert_equal '{{hello_world}}
', textilizable(text)
end
def test_macro_include
@@ -59,7 +56,7 @@ class Redmine::WikiFormatting::MacrosTest < HelperTestCase
assert textilizable(text).match(/CookBook documentation/)
text = "{{include(unknowidentifier:somepage)}}"
- assert textilizable(text).match(/Page not found/)
+ assert textilizable(text).match(/No such page/)
end
def test_macro_child_pages
diff --git a/test/unit/principal_drop_test.rb b/test/unit/principal_drop_test.rb
new file mode 100644
index 00000000..b19adfd0
--- /dev/null
+++ b/test/unit/principal_drop_test.rb
@@ -0,0 +1,15 @@
+require File.expand_path('../../test_helper', __FILE__)
+
+class PrincipalDropTest < ActiveSupport::TestCase
+ def setup
+ @principal = Principal.generate!
+ @drop = @principal.to_liquid
+ end
+
+
+ context "#name" do
+ should "return the name" do
+ assert_equal @principal.name, @drop.name
+ end
+ end
+end
diff --git a/test/unit/project_drop_test.rb b/test/unit/project_drop_test.rb
new file mode 100644
index 00000000..677bda04
--- /dev/null
+++ b/test/unit/project_drop_test.rb
@@ -0,0 +1,42 @@
+require File.expand_path('../../test_helper', __FILE__)
+
+class ProjectDropTest < ActiveSupport::TestCase
+ def setup
+ @project = Project.generate!
+ User.current = @user = User.generate!
+ @role = Role.generate!
+ Member.generate!(:principal => @user, :project => @project, :roles => [@role])
+ @drop = @project.to_liquid
+ end
+
+ context "drop" do
+ should "be a ProjectDrop" do
+ assert @drop.is_a?(ProjectDrop), "drop is not a ProjectDrop"
+ end
+ end
+
+
+ context "#name" do
+ should "return the project name" do
+ assert_equal @project.name, @drop.name
+ end
+ end
+
+ context "#identifier" do
+ should "return the project identifier" do
+ assert_equal @project.identifier, @drop.identifier
+ end
+ end
+
+ should "only load an object if it's visible to the current user" do
+ assert User.current.logged?
+ assert @project.visible?
+
+ @private_project = Project.generate!(:is_public => false)
+
+ assert !@private_project.visible?, "Project is visible"
+ @private_drop = ProjectDrop.new(@private_project)
+ assert_equal nil, @private_drop.instance_variable_get("@object")
+ assert_equal nil, @private_drop.name
+ end
+end
diff --git a/test/unit/tracker_drop_test.rb b/test/unit/tracker_drop_test.rb
new file mode 100644
index 00000000..ff619d72
--- /dev/null
+++ b/test/unit/tracker_drop_test.rb
@@ -0,0 +1,20 @@
+require File.expand_path('../../test_helper', __FILE__)
+
+class TrackerDropTest < ActiveSupport::TestCase
+ def setup
+ @tracker = Tracker.generate!
+ @drop = @tracker.to_liquid
+ end
+
+ context "drop" do
+ should "be a TrackerDrop" do
+ assert @drop.is_a?(TrackerDrop), "drop is not a TrackerDrop"
+ end
+ end
+
+ context "#name" do
+ should "return the name" do
+ assert_equal @tracker.name, @drop.name
+ end
+ end
+end
diff --git a/test/unit/wiki_page_drop_test.rb b/test/unit/wiki_page_drop_test.rb
new file mode 100644
index 00000000..5093c22a
--- /dev/null
+++ b/test/unit/wiki_page_drop_test.rb
@@ -0,0 +1,40 @@
+require File.expand_path('../../test_helper', __FILE__)
+
+class WikiPageDropTest < ActiveSupport::TestCase
+ def setup
+ @project = Project.generate!
+ @wiki = Wiki.generate(:project => @project)
+ @wiki_page = WikiPage.generate!(:wiki => @wiki)
+ User.current = @user = User.generate!
+ @role = Role.generate!(:permissions => [:view_wiki_pages])
+ Member.generate!(:principal => @user, :project => @project, :roles => [@role])
+ @drop = @wiki_page.to_liquid
+ end
+
+ context "drop" do
+ should "be a WikiPageDrop" do
+ assert @drop.is_a?(WikiPageDrop), "drop is not a WikiPageDrop"
+ end
+ end
+
+
+ context "#title" do
+ should "return the title of the wiki page" do
+ assert_equal @wiki_page.title, @drop.title
+ end
+ end
+
+ should "only load an object if it's visible to the current user" do
+ assert User.current.logged?
+ assert @wiki_page.visible?
+
+ @private_project = Project.generate!(:is_public => false)
+ @private_wiki = Wiki.generate!(:project => @private_project)
+ @private_wiki_page = WikiPage.generate!(:wiki => @private_wiki)
+
+ assert !@private_wiki_page.visible?, "WikiPage is visible"
+ @private_drop = WikiPageDrop.new(@private_wiki_page)
+ assert_equal nil, @private_drop.instance_variable_get("@object")
+ assert_equal nil, @private_drop.title
+ end
+end