Merge remote-tracking branch 'meineerde/issues/unstable/604-liquid-rebased' into unstable
This commit is contained in:
commit
e6fe1fc776
1
Gemfile
1
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]
|
||||
|
||||
|
23
app/drops/base_drop.rb
Normal file
23
app/drops/base_drop.rb
Normal 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
65
app/drops/issue_drop.rb
Normal 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
|
3
app/drops/issue_status_drop.rb
Normal file
3
app/drops/issue_status_drop.rb
Normal file
@ -0,0 +1,3 @@
|
||||
class IssueStatusDrop < BaseDrop
|
||||
allowed_methods :name
|
||||
end
|
3
app/drops/principal_drop.rb
Normal file
3
app/drops/principal_drop.rb
Normal file
@ -0,0 +1,3 @@
|
||||
class PrincipalDrop < BaseDrop
|
||||
allowed_methods :name
|
||||
end
|
3
app/drops/project_drop.rb
Normal file
3
app/drops/project_drop.rb
Normal file
@ -0,0 +1,3 @@
|
||||
class ProjectDrop < BaseDrop
|
||||
allowed_methods :name, :identifier
|
||||
end
|
3
app/drops/tracker_drop.rb
Normal file
3
app/drops/tracker_drop.rb
Normal file
@ -0,0 +1,3 @@
|
||||
class TrackerDrop < BaseDrop
|
||||
allowed_methods :name
|
||||
end
|
3
app/drops/wiki_page_drop.rb
Normal file
3
app/drops/wiki_page_drop.rb
Normal file
@ -0,0 +1,3 @@
|
||||
class WikiPageDrop < BaseDrop
|
||||
allowed_methods :title
|
||||
end
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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])
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
|
9
lib/chili_project/liquid.rb
Normal file
9
lib/chili_project/liquid.rb
Normal 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
|
18
lib/chili_project/liquid/file_system.rb
Normal file
18
lib/chili_project/liquid/file_system.rb
Normal 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
|
15
lib/chili_project/liquid/filters.rb
Normal file
15
lib/chili_project/liquid/filters.rb
Normal 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
|
57
lib/chili_project/liquid/legacy.rb
Normal file
57
lib/chili_project/liquid/legacy.rb
Normal 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
|
8
lib/chili_project/liquid/liquid_ext.rb
Normal file
8
lib/chili_project/liquid/liquid_ext.rb
Normal file
@ -0,0 +1,8 @@
|
||||
module ChiliProject
|
||||
module Liquid
|
||||
module LiquidExt
|
||||
::Liquid::Block.send(:include, Block)
|
||||
::Liquid::Context.send(:include, Context)
|
||||
end
|
||||
end
|
||||
end
|
22
lib/chili_project/liquid/liquid_ext/block.rb
Normal file
22
lib/chili_project/liquid/liquid_ext/block.rb
Normal 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
|
40
lib/chili_project/liquid/liquid_ext/context.rb
Normal file
40
lib/chili_project/liquid/liquid_ext/context.rb
Normal 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
|
38
lib/chili_project/liquid/tags.rb
Normal file
38
lib/chili_project/liquid/tags.rb
Normal 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)
|
60
lib/chili_project/liquid/tags/child_pages.rb
Normal file
60
lib/chili_project/liquid/tags/child_pages.rb
Normal 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
|
11
lib/chili_project/liquid/tags/hello_world.rb
Normal file
11
lib/chili_project/liquid/tags/hello_world.rb
Normal 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
|
82
lib/chili_project/liquid/tags/include.rb
Normal file
82
lib/chili_project/liquid/tags/include.rb
Normal 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
|
4
lib/chili_project/liquid/tags/tag.rb
Normal file
4
lib/chili_project/liquid/tags/tag.rb
Normal file
@ -0,0 +1,4 @@
|
||||
module ChiliProject::Liquid::Tags
|
||||
class Tag < ::Liquid::Tag
|
||||
end
|
||||
end
|
14
lib/chili_project/liquid/tags/tag_list.rb
Normal file
14
lib/chili_project/liquid/tags/tag_list.rb
Normal 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
|
15
lib/chili_project/liquid/tags/variable_list.rb
Normal file
15
lib/chili_project/liquid/tags/variable_list.rb
Normal 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
|
96
lib/chili_project/liquid/template.rb
Normal file
96
lib/chili_project/liquid/template.rb
Normal 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
|
14
lib/chili_project/liquid/variables.rb
Normal file
14
lib/chili_project/liquid/variables.rb
Normal 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
|
@ -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
|
||||
|
@ -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
|
||||
|
4
test/fixtures/wiki_contents.yml
vendored
4
test/fixtures/wiki_contents.yml
vendored
@ -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
|
||||
|
@ -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
|
||||
|
@ -593,7 +593,7 @@ RAW
|
||||
|
||||
h1. Included
|
||||
|
||||
{{include(Child_1)}}
|
||||
{% include 'Child_1' %}
|
||||
RAW
|
||||
|
||||
expected = '<ul class="toc">' +
|
||||
|
89
test/unit/issue_drop_test.rb
Normal file
89
test/unit/issue_drop_test.rb
Normal 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
|
21
test/unit/issue_status_drop_test.rb
Normal file
21
test/unit/issue_status_drop_test.rb
Normal 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
|
211
test/unit/lib/chili_project/liquid_test.rb
Normal file
211
test/unit/lib/chili_project/liquid_test.rb
Normal 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 '<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
|
@ -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
|
||||
|
15
test/unit/principal_drop_test.rb
Normal file
15
test/unit/principal_drop_test.rb
Normal 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
|
42
test/unit/project_drop_test.rb
Normal file
42
test/unit/project_drop_test.rb
Normal 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
|
20
test/unit/tracker_drop_test.rb
Normal file
20
test/unit/tracker_drop_test.rb
Normal 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
|
40
test/unit/wiki_page_drop_test.rb
Normal file
40
test/unit/wiki_page_drop_test.rb
Normal 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
|
Loading…
x
Reference in New Issue
Block a user