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 "i18n", "~> 0.4.2"
|
||||||
gem "rubytree", "~> 0.5.2", :require => 'tree'
|
gem "rubytree", "~> 0.5.2", :require => 'tree'
|
||||||
gem "rdoc", ">= 2.4.2"
|
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
|
# Needed only on RUBY_VERSION = 1.8, ruby 1.9+ compatible interpreters should bring their csv
|
||||||
gem "fastercsv", "~> 1.5.0", :platforms => [:ruby_18, :jruby, :mingw_18]
|
gem "fastercsv", "~> 1.5.0", :platforms => [:ruby_18, :jruby, :mingw_18]
|
||||||
|
|
||||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1,3 @@
|
||||||
|
class IssueStatusDrop < BaseDrop
|
||||||
|
allowed_methods :name
|
||||||
|
end
|
|
@ -0,0 +1,3 @@
|
||||||
|
class PrincipalDrop < BaseDrop
|
||||||
|
allowed_methods :name
|
||||||
|
end
|
|
@ -0,0 +1,3 @@
|
||||||
|
class ProjectDrop < BaseDrop
|
||||||
|
allowed_methods :name, :identifier
|
||||||
|
end
|
|
@ -0,0 +1,3 @@
|
||||||
|
class TrackerDrop < BaseDrop
|
||||||
|
allowed_methods :name
|
||||||
|
end
|
|
@ -0,0 +1,3 @@
|
||||||
|
class WikiPageDrop < BaseDrop
|
||||||
|
allowed_methods :title
|
||||||
|
end
|
|
@ -16,7 +16,6 @@ require 'forwardable'
|
||||||
require 'cgi'
|
require 'cgi'
|
||||||
|
|
||||||
module ApplicationHelper
|
module ApplicationHelper
|
||||||
include Redmine::WikiFormatting::Macros::Definitions
|
|
||||||
include Redmine::I18n
|
include Redmine::I18n
|
||||||
include GravatarHelper::PublicMethods
|
include GravatarHelper::PublicMethods
|
||||||
|
|
||||||
|
@ -461,7 +460,29 @@ module ApplicationHelper
|
||||||
project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
|
project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
|
||||||
only_path = options.delete(:only_path) == false ? false : true
|
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 = []
|
@parsed_headings = []
|
||||||
text = parse_non_pre_blocks(text) do |text|
|
text = parse_non_pre_blocks(text) do |text|
|
||||||
|
@ -1006,4 +1027,23 @@ module ApplicationHelper
|
||||||
def link_to_content_update(text, url_params = {}, html_options = {})
|
def link_to_content_update(text, url_params = {}, html_options = {})
|
||||||
link_to(text, url_params, html_options)
|
link_to(text, url_params, html_options)
|
||||||
end
|
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
|
end
|
||||||
|
|
|
@ -103,6 +103,10 @@ class Issue < ActiveRecord::Base
|
||||||
(usr || User.current).allowed_to?(:view_issues, self.project)
|
(usr || User.current).allowed_to?(:view_issues, self.project)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def to_liquid
|
||||||
|
IssueDrop.new(self)
|
||||||
|
end
|
||||||
|
|
||||||
def after_initialize
|
def after_initialize
|
||||||
if new_record?
|
if new_record?
|
||||||
# set default values for new records only
|
# 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?
|
IssueStatus.update_all("is_default=#{connection.quoted_false}", ['id <> ?', id]) if self.is_default?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def to_liquid
|
||||||
|
IssueStatusDrop.new(self)
|
||||||
|
end
|
||||||
|
|
||||||
# Returns the default status for new issues
|
# Returns the default status for new issues
|
||||||
def self.default
|
def self.default
|
||||||
find(:first, :conditions =>["is_default=?", true])
|
find(:first, :conditions =>["is_default=?", true])
|
||||||
|
|
|
@ -31,6 +31,10 @@ class Principal < ActiveRecord::Base
|
||||||
|
|
||||||
before_create :set_default_empty_values
|
before_create :set_default_empty_values
|
||||||
|
|
||||||
|
def to_liquid
|
||||||
|
PrincipalDrop.new(self)
|
||||||
|
end
|
||||||
|
|
||||||
def name(formatter = nil)
|
def name(formatter = nil)
|
||||||
to_s
|
to_s
|
||||||
end
|
end
|
||||||
|
|
|
@ -83,6 +83,10 @@ class Project < ActiveRecord::Base
|
||||||
named_scope :all_public, { :conditions => { :is_public => true } }
|
named_scope :all_public, { :conditions => { :is_public => true } }
|
||||||
named_scope :visible, lambda { { :conditions => Project.visible_by(User.current) } }
|
named_scope :visible, lambda { { :conditions => Project.visible_by(User.current) } }
|
||||||
|
|
||||||
|
def to_liquid
|
||||||
|
ProjectDrop.new(self)
|
||||||
|
end
|
||||||
|
|
||||||
def initialize(attributes = nil)
|
def initialize(attributes = nil)
|
||||||
super
|
super
|
||||||
|
|
||||||
|
@ -131,6 +135,11 @@ class Project < ActiveRecord::Base
|
||||||
end
|
end
|
||||||
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={})
|
def self.allowed_to_condition(user, permission, options={})
|
||||||
base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
|
base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
|
||||||
if perm = Redmine::AccessControl.permission(permission)
|
if perm = Redmine::AccessControl.permission(permission)
|
||||||
|
|
|
@ -35,6 +35,10 @@ class Tracker < ActiveRecord::Base
|
||||||
name <=> tracker.name
|
name <=> tracker.name
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def to_liquid
|
||||||
|
TrackerDrop.new(self)
|
||||||
|
end
|
||||||
|
|
||||||
def self.all
|
def self.all
|
||||||
find(:all, :order => 'position')
|
find(:all, :order => 'position')
|
||||||
end
|
end
|
||||||
|
|
|
@ -53,6 +53,10 @@ class WikiPage < ActiveRecord::Base
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def to_liquid
|
||||||
|
WikiPageDrop.new(self)
|
||||||
|
end
|
||||||
|
|
||||||
def visible?(user=User.current)
|
def visible?(user=User.current)
|
||||||
!user.nil? && user.allowed_to?(:view_wiki_pages, project)
|
!user.nil? && user.allowed_to?(:view_wiki_pages, project)
|
||||||
end
|
end
|
||||||
|
|
|
@ -49,6 +49,9 @@ Rails::Initializer.run do |config|
|
||||||
# (by default production uses :info, the others :debug)
|
# (by default production uses :info, the others :debug)
|
||||||
# config.log_level = :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
|
# Enable page/fragment caching by setting a file-based store
|
||||||
# (remember to create the caching directory and make it readable to the application)
|
# (remember to create the caching directory and make it readable to the application)
|
||||||
# config.action_controller.cache_store = :file_store, "#{RAILS_ROOT}/tmp/cache"
|
# config.action_controller.cache_store = :file_store, "#{RAILS_ROOT}/tmp/cache"
|
||||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1,8 @@
|
||||||
|
module ChiliProject
|
||||||
|
module Liquid
|
||||||
|
module LiquidExt
|
||||||
|
::Liquid::Block.send(:include, Block)
|
||||||
|
::Liquid::Context.send(:include, Context)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -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
|
|
@ -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
|
|
@ -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)
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1,4 @@
|
||||||
|
module ChiliProject::Liquid::Tags
|
||||||
|
class Tag < ::Liquid::Tag
|
||||||
|
end
|
||||||
|
end
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
end
|
||||||
|
|
||||||
def to_html(format, text, options = {}, &block)
|
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
|
# Text retrieved from the cache store may be frozen
|
||||||
# We need to dup it so we can do in-place substitutions with gsub!
|
# We need to dup it so we can do in-place substitutions with gsub!
|
||||||
cache_store.fetch cache_key do
|
cache_store.fetch cache_key do
|
||||||
|
@ -50,10 +50,6 @@ module Redmine
|
||||||
else
|
else
|
||||||
formatter_for(format).new(text).to_html
|
formatter_for(format).new(text).to_html
|
||||||
end
|
end
|
||||||
if block_given?
|
|
||||||
execute_macros(text, block)
|
|
||||||
end
|
|
||||||
text
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Returns a cache key for the given text +format+, +object+ and +attribute+ or nil if no caching should be done
|
# 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
|
def cache_store
|
||||||
ActionController::Base.cache_store
|
ActionController::Base.cache_store
|
||||||
end
|
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
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -12,106 +12,55 @@
|
||||||
# See doc/COPYRIGHT.rdoc for more details.
|
# 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 Redmine
|
||||||
module WikiFormatting
|
module WikiFormatting
|
||||||
module Macros
|
module Macros
|
||||||
module Definitions
|
@available_macros = {}
|
||||||
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 = {}
|
|
||||||
|
|
||||||
class << self
|
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)
|
def register(&block)
|
||||||
|
ActiveSupport::Deprecation.warn("Macros are deprecated. Use Liquid filters and tags instead", caller.drop(3))
|
||||||
class_eval(&block) if block_given?
|
class_eval(&block) if block_given?
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
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.
|
# Defines a new macro with the given name and block.
|
||||||
def macro(name, &block)
|
def macro(name, &block)
|
||||||
name = name.to_sym if name.is_a?(String)
|
name = name.to_sym if name.is_a?(String)
|
||||||
@@available_macros[name] = @@desc || ''
|
@available_macros[name] = @desc || ''
|
||||||
@@desc = nil
|
@desc = nil
|
||||||
raise "Can not create a macro without a block!" unless block_given?
|
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
|
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
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,7 +3,7 @@ wiki_contents_001:
|
||||||
text: |-
|
text: |-
|
||||||
h1. CookBook documentation
|
h1. CookBook documentation
|
||||||
|
|
||||||
{{child_pages}}
|
{% child_pages %}
|
||||||
|
|
||||||
Some updated [[documentation]] here with gzipped history
|
Some updated [[documentation]] here with gzipped history
|
||||||
updated_on: 2007-03-07 00:10:51 +01:00
|
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
|
This is a link to a ticket: #2
|
||||||
And this is an included page:
|
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
|
updated_on: 2007-03-08 00:18:07 +01:00
|
||||||
page_id: 2
|
page_id: 2
|
||||||
id: 2
|
id: 2
|
||||||
|
|
|
@ -18,7 +18,7 @@ require 'wiki_controller'
|
||||||
class WikiController; def rescue_action(e) raise e end; end
|
class WikiController; def rescue_action(e) raise e end; end
|
||||||
|
|
||||||
class WikiControllerTest < ActionController::TestCase
|
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
|
def setup
|
||||||
@controller = WikiController.new
|
@controller = WikiController.new
|
||||||
|
|
|
@ -593,7 +593,7 @@ RAW
|
||||||
|
|
||||||
h1. Included
|
h1. Included
|
||||||
|
|
||||||
{{include(Child_1)}}
|
{% include 'Child_1' %}
|
||||||
RAW
|
RAW
|
||||||
|
|
||||||
expected = '<ul class="toc">' +
|
expected = '<ul class="toc">' +
|
||||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
def test_macro_hello_world
|
||||||
text = "{{hello_world}}"
|
text = "{{hello_world}}"
|
||||||
assert textilizable(text).match(/Hello world!/)
|
assert textilizable(text).match(/Hello world!/)
|
||||||
# escaping
|
|
||||||
text = "!{{hello_world}}"
|
|
||||||
assert_equal '<p>{{hello_world}}</p>', textilizable(text)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_macro_include
|
def test_macro_include
|
||||||
|
@ -59,7 +56,7 @@ class Redmine::WikiFormatting::MacrosTest < HelperTestCase
|
||||||
assert textilizable(text).match(/CookBook documentation/)
|
assert textilizable(text).match(/CookBook documentation/)
|
||||||
|
|
||||||
text = "{{include(unknowidentifier:somepage)}}"
|
text = "{{include(unknowidentifier:somepage)}}"
|
||||||
assert textilizable(text).match(/Page not found/)
|
assert textilizable(text).match(/No such page/)
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_macro_child_pages
|
def test_macro_child_pages
|
||||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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…
Reference in New Issue