Merge remote-tracking branch 'meineerde/issues/unstable/604-liquid-rebased' into unstable

This commit is contained in:
Eric Davis 2011-11-25 01:44:19 -08:00
commit e6fe1fc776
45 changed files with 1162 additions and 130 deletions

View File

@ -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]

23
app/drops/base_drop.rb Normal file
View File

@ -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

65
app/drops/issue_drop.rb Normal file
View File

@ -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

View File

@ -0,0 +1,3 @@
class IssueStatusDrop < BaseDrop
allowed_methods :name
end

View File

@ -0,0 +1,3 @@
class PrincipalDrop < BaseDrop
allowed_methods :name
end

View File

@ -0,0 +1,3 @@
class ProjectDrop < BaseDrop
allowed_methods :name, :identifier
end

View File

@ -0,0 +1,3 @@
class TrackerDrop < BaseDrop
allowed_methods :name
end

View File

@ -0,0 +1,3 @@
class WikiPageDrop < BaseDrop
allowed_methods :title
end

View File

@ -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

View File

@ -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

View File

@ -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])

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,8 @@
module ChiliProject
module Liquid
module LiquidExt
::Liquid::Block.send(:include, Block)
::Liquid::Context.send(:include, Context)
end
end
end

View File

@ -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

View File

@ -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 = '<div class="flash error">' + escaped_error + '</div>'
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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,4 @@
module ChiliProject::Liquid::Tags
class Tag < ::Liquid::Tag
end
end

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,96 @@
module ChiliProject
module Liquid
class Template < ::Liquid::Template
# creates a new <tt>Template</tt> 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 <tt>Template.register_filter</tt>
#
# Following options can be passed:
#
# * <tt>filters</tt> : array with local filters
# * <tt>registers</tt> : 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

View File

@ -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

View File

@ -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
"<div class=\"flash error\">Error executing the <strong>#{macro}</strong> macro (#{e})</div>"
end || all
else
all
end
end
end
end
end
end

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -593,7 +593,7 @@ RAW
h1. Included
{{include(Child_1)}}
{% include 'Child_1' %}
RAW
expected = '<ul class="toc">' +

View File

@ -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

View File

@ -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

View File

@ -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?('<ul>'), "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 /<h1.*?>\s*Parent.*?<\/h1>/, formatted
assert_match /<h1.*?>\s*Child1.*?<\/h1>/, formatted
assert_match /<h1.*?>\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 '<script>alert(\"foo\"):</script>' %}"
formatted = textilizable(text)
assert formatted.include?("No such page '&lt;script&gt;alert(&quot;foo&quot;):&lt;/script&gt;'")
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

View File

@ -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 '<p>{{hello_world}}</p>', 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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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