Skip to content
Browse files

Import from old repository.

  • Loading branch information...
0 parents commit 43af72ef644f8e12e7e7f30ac0000a0b11bcaa8b @seancribbs seancribbs committed Jun 20, 2008
56 README
@@ -0,0 +1,56 @@
+= LDAP
+
+Created by: Sean Cribbs
+ version: 0.1
+
+The LDAP extension allows access to LDAP directory information from
+within Radiant pages. It includes an admin interface where you can create,
+test, and save 'canned' queries that can be reused in your pages.
+
+== Setup
+
+1) The Ruby-LDAP library is required. Instructions for installing the library can be
+found at http://ruby-ldap.sourceforge.net/
+
+2) Make sure your instance of OpenLDAP is configured correctly. I had to disable TLS
+certificate validation on mine to get SSL to work. If you have a cert from a root CA,
+this will not be a problem.
+
+3) Checkout or copy the extension into vendor/extensions/ldap under your instance of Radiant.
+
+4) Run rake db:migrate:extensions to create the schema.
+
+5) Either through the included admin interface ('Edit Settings'), script/console
+(Radiant::Config model), or through a database administration and query tool, set these values
+in the 'config' table (sample values provided, explanation is just for reference).
+
+key value explanation
+------------------ ------------------------------ ---------------------
+ldap.server yourservername.com The server/IP where the LDAP directory resides.
+ldap.port 389 The port the LDAP server listens on, 389 generally (636 for SSL).
+ldap.base_dn o=company The root of all queries, unless otherwise specified.
+ldap.use_ssl false "true" or "false" - Use SSL to connect.
+ldap.bind_user cn=someuser,ou=admin,o=company A fully qualified DN to authenticate as.
+ldap.bind_password password The password of the authentication user.
+
+6) Copy 'directory.gif' to RADIANT_ROOT/public/images (this may be unnecessary in the future).
+
+7) Fire up Radiant and try it out! Tag usage is described using the DSL/tag reference UI
+and the tags are available on all pages.
+
+== Notes
+
+* Not all LDAP directories support root_dse, so the LDAP extension does not use root_dse
+to determine any information about your directory schema. All information is 'in the raw'.
+
+* Any information returned by the query is in the order determined by the LDAP directory.
+No external sorting algorithms have been applied yet.
+
+* Fields/attributes that have multiple values are currently rendered as joined with commas.
+This will be changed in a future release to use nested tags so output can be more flexible.
+
+== To-dos
+
+* Use 'password' field type on bind_password in settings UI.
+* 'Use SSL?' checkbox not maintaining state.
+* Implement flexible sorting for records.
25 Rakefile
@@ -0,0 +1,25 @@
+require 'rake'
+require 'rake/testtask'
+require 'rake/rdoctask'
+
+desc 'Default: run unit tests.'
+task :default => :test
+
+desc 'Test the personnel_directory extension.'
+Rake::TestTask.new(:test) do |t|
+ t.libs << 'lib'
+ t.pattern = 'test/**/*_test.rb'
+ t.verbose = true
+end
+
+desc 'Generate documentation for the personnel_directory extension.'
+Rake::RDocTask.new(:rdoc) do |rdoc|
+ rdoc.rdoc_dir = 'rdoc'
+ rdoc.title = 'PersonnelDirectoryExtension'
+ rdoc.options << '--line-numbers' << '--inline-source'
+ rdoc.rdoc_files.include('README')
+ rdoc.rdoc_files.include('lib/**/*.rb')
+end
+
+# Load any custom rakefiles for extension
+Dir[File.dirname(__FILE__) + '/tasks/*.rake'].sort.each { |f| require f }
62 app/controllers/ldap_controller.rb
@@ -0,0 +1,62 @@
+class LdapController < ApplicationController
+
+ def index
+ @ldap_queries = LdapQuery.find(:all, :order => "name asc")
+ @ldap_query = LdapQuery.new
+ end
+
+ def new
+ @ldap_query = LdapQuery.new
+ end
+
+ def create
+ @ldap_query = LdapQuery.new(params[:ldap_query])
+ if @ldap_query.save
+ flash[:notice] = "Query successfully saved."
+ redirect_to :action => "index"
+ else
+ flash[:error] = "There was an error saving your query."
+ render :action => "new"
+ end
+ end
+
+ def edit
+ @ldap_query = LdapQuery.find(params[:id])
+ end
+
+ def update
+ @ldap_query = LdapQuery.find(params[:id])
+ if @ldap_query.update_attributes(params[:ldap_query])
+ flash[:notice] = "Query successfully updated."
+ redirect_to :action => "index"
+ else
+ flash[:error] = "There was an error saving your query."
+ render :action => "edit"
+ end
+ end
+
+ def destroy
+ LdapQuery.find(params[:id]).destroy
+ flash[:notice] = "Query deleted."
+ redirect_to :action => "index"
+ end
+
+ # For editing LDAP configuration
+ def settings
+ @config = LdapSystem.instance
+ if request.post?
+ @config = LdapSystem.instance
+ @config.attributes = params[:config]
+ flash[:notice] = "LDAP configuration saved."
+ end
+ end
+
+ def test_query
+ @ldap_query = LdapQuery.new(params[:ldap_query])
+ @results = @ldap_query.execute
+ render :update do |page|
+ page.replace_html :results, :partial => "results"
+ page.visual_effect :appear, 'results'
+ end
+ end
+end
30 app/models/ldap_query.rb
@@ -0,0 +1,30 @@
+require 'ldap'
+class LdapQuery < ActiveRecord::Base
+
+ def self.scopes
+ (LDAP.constants - Object.constants).
+ grep(/LDAP_SCOPE(.*)/).
+ inject({}) { |hash, i| hash.merge i => LDAP.const_get(i) }
+ end
+
+ validates_presence_of :name
+ validates_numericality_of :scope, :allow_nil => true
+ validates_inclusion_of :scope, :in => LdapQuery.scopes.values, :allow_nil => true
+ validates_format_of :base_dn, :with => /^(\w+=\w+)(,\w+=\w+)*$/,
+ :message => "must be a comma-separated LDAP scope, e.g. ou=Example,o=World.",
+ :allow_nil => true
+ validates_format_of :attrs, :with => /^\s*\w+\s*(,\s*\w+)*/,
+ :message => 'must be a comma-separated list of LDAP attributes, e.g. "dn, givenName, mail".',
+ :allow_nil => true
+
+ def scope=(val)
+ write_attribute :scope, val.to_i
+ end
+
+ def execute
+ attrs = attributes.reject { |k,v|
+ v.nil? or (v.respond_to? :empty? and v.empty?) or k.to_s == 'name'
+ }.symbolize_keys
+ LdapSystem.search(attrs)
+ end
+end
80 app/models/ldap_system.rb
@@ -0,0 +1,80 @@
+require 'ldap'
+class LdapSystem
+ include Simpleton
+ attr_accessor :connection
+
+ def connection
+ connect if @connection.err.is_a?(LDAP::Error) or @connection.nil?
+ @connection
+ end
+
+ def search(options = {})
+ options.symbolize_keys!.reverse_merge! :base_dn => base_dn,
+ :filter => "(objectClass=*)",
+ :scope => LDAP::LDAP_SCOPE_SUBTREE,
+ :attrs => ["dn", "givenName", "sn", "mail"],
+ :sort => "sn"
+
+ if options[:attrs].is_a? String
+ options[:attrs] = options[:attrs].split(",").collect(&:strip)
+ end
+ connection.simple_bind(bind_user, bind_password) unless connection.bound?
+ results = connection.search2(options[:base_dn], options[:scope], options[:filter], options[:attrs]) rescue []
+ results.collect { |r| dearrayify(r) }
+ #.sort {|x,y|
+ # (x.has_key? options[:sort] and y.has_key? options[:sort]) ?
+ # x[options[:sort]] <=> y[options[:sort]] :
+ # 0 }
+ end
+
+ def attributes=(hsh)
+ hsh.each do |key, value|
+ if respond_to? "#{key}="
+ send "#{key}=", value
+ end
+ end
+ end
+
+ def use_ssl
+ @use_ssl ||= Radiant::Config["ldap.use_ssl"] == "true"
+ end
+
+ def use_ssl=(value)
+ @use_ssl = Radiant::Config["ldap.use_ssl"] = value.to_s
+ end
+
+ def port
+ @port ||= Radiant::Config["ldap.port"].to_i
+ end
+
+ def port=(value)
+ @port = Radiant::Config["ldap.port"] = value.to_s
+ end
+
+ [:server, :base_dn, :bind_user, :bind_password].each do |key|
+ class_eval %{
+ def #{key}
+ @#{key} ||= Radiant::Config["ldap.#{key}"]
+ end
+
+ def #{key}=(value)
+ @#{key} = Radiant::Config["ldap.#{key}"] = value
+ end
+ }
+ end
+
+ private
+ def initialize
+ connect
+ end
+
+ def connect
+ @connection = (use_ssl ? LDAP::SSLConn.new(server, port) : LDAP::Conn.new(server, port)) rescue nil
+ end
+
+ def dearrayify(hsh)
+ hsh.inject({}) do |h, (k,v)|
+ h.merge k => ((v.is_a?(Array) and v.size == 1) ? v.first : v)
+ end
+ end
+end
120 app/models/ldap_tags.rb
@@ -0,0 +1,120 @@
+module LdapTags
+ include Radiant::Taggable
+
+ TLDS = %w{com org net edu info mil gov biz ws}
+
+ desc %{ <r:crypt_email>someaddress@us.com</r:crypt_email>
+ <r:crypt_email><r:label>Somebody</r:label><r:address>someaddress@us.com</r:address></r:crypt_email>
+
+ Encrypts the email address so it cannot be picked up easily by spambots. Requires email.js to be
+ included in the page.}
+ tag "crypt_email" do |tag|
+ hash = tag.locals.params = {}
+ contents = tag.expand
+ address = hash['address'].blank? ? contents : hash['address']
+ label = hash['label']
+ if address =~ /([\w.%-]+)@([\w.-]+)\.([A-z]{2,4})/
+ user, domain, tld = $1, $2, $3
+ tld_num = TLDS.index(tld)
+ unless label.blank?
+ %{<script type="text/javascript">
+ // <![CDATA[
+ mail2('#{user}', '#{domain}', #{tld_num}, '', "#{label}");
+ // ]]>
+ </script>
+ }
+ else
+ %{<script type="text/javascript">
+ // <![CDATA[
+ mail('#{user}', '#{domain}', #{tld_num}, '');
+ // ]]>
+ </script>
+ }
+ end
+ else
+ label.blank? ? "": label
+ end
+ end
+
+ tag "crypt_email:label" do |tag|
+ tag.locals.params['label'] = tag.expand.strip
+ end
+
+ tag "crypt_email:address" do |tag|
+ tag.locals.params['address'] = tag.expand.strip
+ end
+
+ desc %{ The namespace for all LDAP directory queries.}
+ tag "directory" do |tag|
+ tag.expand
+ end
+
+ desc %{ <r:directory:query name="Some Saved Query">...</r:directory:query>
+ <r:directory:query filter="(objectClass=*)" [base="o=radiant"] [attrs="givenName, sn"]>...</r:directory:query>
+
+ Queries the LDAP directory on the given canned query or on parameters specified in the tag and
+ renders the contained block if results are returned.}
+ tag "directory:query" do |tag|
+ name = tag.attr['name']
+ base, filter, attrs = tag.attr['base'], tag.attr['filter'], tag.attr['attrs']
+ if name and query = LdapQuery.find_by_name(name)
+ tag.locals.results = query.execute
+ tag.expand unless tag.locals.results.empty?
+ elsif filter
+ query = LdapQuery.new :base_dn => base, :filter => filter, :attrs => attrs
+ tag.locals.results = query.execute
+ tag.expand unless tag.locals.results.empty?
+ else
+ raise TagError, "Must specify at least a filter on directory queries."
+ end
+ end
+
+ desc %{ Renders the contained block on each result returned from the LDAP query.}
+ tag "directory:query:each" do |tag|
+ output = ""
+ tag.locals.results.each do |result|
+ tag.locals.result = result
+ output << tag.expand
+ end
+ output
+ end
+
+ desc %{ <r:fetch attr="sn" />
+
+ Fetches an attribute from a query result. If not used inside the 'each' tag,
+ it will fetch the attribute from the first result.}
+ tag "directory:query:fetch" do |tag|
+ result = tag.locals.result || tag.locals.results.first
+ key = tag.attr["attr"]
+ raise TagError, "Must specify an LDAP attribute to get." unless key
+ case result[key]
+ when Array
+ result[key].join(", ")
+ when String
+ result[key]
+ else
+ ""
+ end
+ end
+
+ desc %{ <r:if_attr attr="sn">...</r:if_attr>
+
+ Renders the contained block if the specified attribute exists for the current query result.}
+ tag "directory:query:if_attr" do |tag|
+ result = tag.locals.result || tag.locals.results.first
+ key = tag.attr["attr"]
+ raise TagError, "Must specify an LDAP attribute to get." unless key
+ tag.expand if result[key]
+ end
+
+ desc %{ <r:unless_attr attr="sn">...</r:unless_attr>
+
+ Renders the contained block unless the specified attribute exists for the current query result.}
+ tag "directory:query:unless_attr" do |tag|
+ result = tag.locals.result || tag.locals.results.first
+ key = tag.attr["attr"]
+ raise TagError, "Must specify an LDAP attribute to get." unless key
+ tag.expand unless result[key]
+ end
+
+end
28 app/views/ldap/_form.rhtml
@@ -0,0 +1,28 @@
+<div class="form-area">
+ <p class="title">
+ <label for="ldap_query_name">Name</label><br />
+ <%= f.text_field :name, :class => "textbox", :maxlength => 255 %>
+ </p>
+ <p class="content">
+ <label for="ldap_query_base_dn">Base DN</label><br/>
+ <%= f.text_field :base_dn %>
+ </p>
+ <p class="content">
+ <label for="ldap_query_filter">Filter</label><br/>
+ <%= f.text_field :filter %>
+ </p>
+ <p class="content">
+ <label for="ldap_query_attrs">Attributes</label><br/>
+ <%= f.text_field :attrs %>
+ </p>
+ <p class="content">
+ <label for="ldap_query_scope">Scope</label><br/>
+ <%= f.select :scope, LdapQuery.scopes.to_a %>
+ </p>
+</div>
+<p class="buttons">
+ <%= save_model_button(@ldap_query) %>
+ <%= submit_to_remote :test, "Test Query",
+ :url => { :action => "test_query" },
+ :html => {:class => "button"}, :failure => "alert('Query failed');" %>
+</p>
10 app/views/ldap/_ldap_query.rhtml
@@ -0,0 +1,10 @@
+<tr class="node level-0 no-children" id="ldap_query_<%= ldap_query.id %>">
+ <td class="snippet"><%= image_tag "directory.gif", :align => "center", :class => "icon", :alt => "directory-icon" %>
+ <%= link_to ldap_query.name, :action => "edit", :id => ldap_query %></td>
+ <td><%= h ldap_query.base_dn %></td>
+ <td><%= h ldap_query.filter %></td>
+ <td><%= h ldap_query.attrs %></td>
+ <td><%= link_to image_tag("/images/admin/remove.png", :alt => "Remove"),
+ {:action => "destroy", :id => ldap_query},
+ :confirm => "Are you sure you want to delete this query?" %></td>
+</tr>
16 app/views/ldap/_results.rhtml
@@ -0,0 +1,16 @@
+<%= link_to_function "[x] close", update_page {|page| page.visual_effect :fade, 'results'}, :style=> "display: block; float:right;" %>
+<h1>Query Results</h1>
+<table>
+<tr>
+ <% @results.first.keys.each do |key| %>
+ <th><%= key %></th>
+ <% end %>
+</tr>
+<% @results.each do |result| %>
+<tr>
+ <% result.each_value do |value| %>
+ <td><%= (value.is_a? Array) ? value.to_yaml : value %></td>
+ <% end %>
+</tr>
+<% end %>
+</table>
29 app/views/ldap/_results_box.rhtml
@@ -0,0 +1,29 @@
+<% content_for 'page_css' do %>
+ #results {
+ position: fixed;
+ bottom: 0;
+ left: 0;
+ width: 100%;
+ z-index: 1000;
+ border-top: 2px solid gray;
+ height: 25%;
+ overflow: scroll;
+ background: white;
+ padding: 5px;
+ }
+ #results table {
+ border: 1px outset;
+ border-collapse: collapse;
+ border-spacing: 1px;
+ empty-cells: hide;
+ }
+ #results table td {
+ border: 1px inset;
+ padding: 2px;
+ }
+ #results table th {
+ background: #eee;
+ }
+<% end %>
+<div id="results" style="display: none;">
+</div>
5 app/views/ldap/edit.rhtml
@@ -0,0 +1,5 @@
+<%= render :partial => "results_box" %>
+<h1>Edit LDAP Query</h1>
+<% form_for :ldap_query, @ldap_query, :url => { :action => 'update', :id => @ldap_query } do |f| %>
+ <%= render :partial => "form", :locals => {:f => f} %>
+<% end %>
29 app/views/ldap/index.rhtml
@@ -0,0 +1,29 @@
+<h1>LDAP Queries</h1>
+<p style="float: right;">
+<%= link_to "New Query", :action => "new" %><br/>
+<%= link_to "Edit Settings", :action => "settings" %>
+</p>
+<table id="ldap_queries" class="index" cellspacing="0" cellpadding="0" border="0" style="clear:both">
+<thead>
+ <tr>
+ <th>Name</th>
+ <th>Base</th>
+ <th>Filter</th>
+ <th>Attributes</th>
+ <th class="modify">Modify</th>
+ </tr>
+ <tbody>
+ <% if @ldap_queries.size > 0 %>
+ <%= render :partial => "ldap_query", :collection => @ldap_queries %>
+ <% else %>
+ <tr>
+ <td class="note" colspan="5">No saved queries</td>
+ </tr>
+ <% end %>
+ </tbody>
+</table>
+<script type="text/javascript">
+// <![CDATA[
+ new RuledTable("ldap_queries");
+// ]]>
+</script>
5 app/views/ldap/new.rhtml
@@ -0,0 +1,5 @@
+<%= render :partial => "results_box" %>
+<h1>New LDAP Query</h1>
+<% form_for :ldap_query, @ldap_query, :url => { :action => 'create' } do |f| %>
+ <%= render :partial => "form", :locals => {:f => f} %>
+<% end %>
32 app/views/ldap/settings.rhtml
@@ -0,0 +1,32 @@
+<h1>Edit LDAP Settings</h1>
+<% form_for :config, @config, :url => {:action => "settings" } do |f| %>
+<div class="form-area">
+ <p class="content">
+ <label for="config_server">Server</label><br/>
+ <%= f.text_field :server %>
+ </p>
+ <p class="content">
+ <label for="config_base_dn">Base DN</label><br/>
+ <%= f.text_field :base_dn %>
+ </p>
+ <p class="content">
+ <label for="config_port">Port</label><br/>
+ <%= f.text_field :port %>
+ </p>
+ <p class="content">
+ <label for="config_bind_user">Bind Username</label><br/>
+ <%= f.text_field :bind_user %>
+ </p>
+ <p class="content">
+ <label for="config_bind_password">Bind Password</label><br/>
+ <%= f.text_field :bind_password %>
+ </p>
+ <p class="content">
+ <label for="config_use_ssl">Use SSL?</label>
+ <%= f.check_box :use_ssl, {}, "true", "false" %>
+ </p>
+</div>
+<p class="buttons">
+<%= submit_tag "Save Settings" %> <%= link_to "Cancel", :action => "index" %>
+</p>
+<% end %>
15 db/migrate/001_create_ldap_extension_schema.rb
@@ -0,0 +1,15 @@
+class CreateLdapExtensionSchema < ActiveRecord::Migration
+ def self.up
+ create_table :ldap_queries do |t|
+ t.column :name, :string
+ t.column :filter, :string
+ t.column :base_dn, :string
+ t.column :scope, :integer
+ t.column :attrs, :string
+ end
+ end
+
+ def self.down
+ drop_table :ldap_queries
+ end
+end
19 ldap_extension.rb
@@ -0,0 +1,19 @@
+class LdapExtension < Radiant::Extension
+ version "0.1"
+ description "Accesses LDAP directory and allows saved queries."
+ url "http://dev.radiantcms.org/svn/radiant/branches/mental/extensions/ldap"
+
+ define_routes do |map|
+ map.connect 'admin/ldap/:action/:id', :controller => 'ldap'
+ end
+
+ def activate
+ admin.tabs.add "LDAP", "/admin/ldap", :after => "Layouts", :visibility => [:all]
+ Page.send :include, LdapTags
+ end
+
+ def deactivate
+ admin.tabs.remove "LDAP"
+ end
+
+end
23 lib/tasks/ldap_extension_tasks.rake
@@ -0,0 +1,23 @@
+namespace :radiant do
+ namespace :extensions do
+ namespace :ldap do
+
+ desc "Runs the migration of the LDAP extension"
+ task :migrate => :environment do
+ require 'radiant/extension_migrator'
+ if ENV["VERSION"]
+ LdapExtension.migrator.migrate(ENV["VERSION"].to_i)
+ else
+ LdapExtension.migrator.migrate
+ end
+ end
+
+ desc "Copies LDAP extension assets to the public directory"
+ task :update => :environment do
+ FileUtils.cp LdapExtension.root + "/public/images/directory.gif", RAILS_ROOT + "/public/images"
+ FileUtils.cp LdapExtension.root + "/public/javascripts/email.js", RAILS_ROOT + "/public/javascripts"
+ end
+
+ end
+ end
+end
BIN public/images/directory.gif
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
72 public/javascripts/email.js
@@ -0,0 +1,72 @@
+// Email.js version 5
+var tld_ = new Array()
+tld_[0] = "com";
+tld_[1] = "org";
+tld_[2] = "net";
+tld_[3] = "edu";
+tld_[4] = "info";
+tld_[5] = "mil";
+tld_[6] = "gov";
+tld_[7] = "biz";
+tld_[8] = "ws";
+tld_[10] = "co.uk";
+tld_[11] = "org.uk";
+tld_[12] = "gov.uk";
+tld_[13] = "ac.uk";
+var topDom_ = 13;
+var m_ = "mailto:";
+var a_ = "@";
+var d_ = ".";
+
+function mail(name, dom, tl, params)
+{
+ var s = e(name,dom,tl);
+ document.write('<a href="'+m_+s+params+'">'+s+'</a>');
+}
+function mail2(name, dom, tl, params, display)
+{
+ document.write('<a href="'+m_+e(name,dom,tl)+params+'">'+display+'</a>');
+}
+function mail3(names, doms, tls, params, display)
+{
+ var a = "";
+ for(var i = 0; i < names.length; i++)
+ {
+ if(i > 0)
+ a += ",";
+ a += e(names[i], doms[i], tls[i]);
+ }
+ document.write('<a href="'+m_+a+params+'">'+display+'</a>');
+}
+function mail4(name, dom, tl, display)
+{
+ document.write('<option value="'+e(name,dom,tl)+'">'+display+'</option>');
+}
+function mail5(ary)
+{
+ for(var i = 0; i < ary.length; i++)
+ mail4(ary[i][0], ary[i][1], ary[i][2], ary[i][3]);
+}
+function e(name, dom, tl)
+{
+ var s = name+a_;
+ if (tl!=-2)
+ {
+ s+= dom;
+ if (tl>=0)
+ s+= d_+tld_[tl];
+ }
+ else
+ s+= swapper(dom);
+ return s;
+}
+function swapper(d)
+{
+ var s = "";
+ for (var i=0; i<d.length; i+=2)
+ if (i+1==d.length)
+ s+= d.charAt(i)
+ else
+ s+= d.charAt(i+1)+d.charAt(i);
+ return s.replace(/\?/g,'.');
+}
11 test/functional/ldap_extension_test.rb
@@ -0,0 +1,11 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class LdapExtensionTest < Test::Unit::TestCase
+
+ def test_initialization
+ assert_equal RADIANT_ROOT + '/vendor/extensions/ldap', LdapExtension.root
+ assert_equal 'Ldap', LdapExtension.extension_name
+ assert Page.included_modules.include?(LdapTags)
+ end
+
+end
18 test/test_helper.rb
@@ -0,0 +1,18 @@
+# Load the environment
+unless defined? RADIANT_ROOT
+ ENV["RAILS_ENV"] = "test"
+ require "#{File.expand_path(File.dirname(__FILE__) + "/../../../../")}/config/environment"
+end
+require "#{RADIANT_ROOT}/test/test_helper"
+
+class Test::Unit::TestCase
+
+ # Include a helper to make testing Radius tags easier
+ test_helper :extension_tags
+
+ # Add the fixture directory to the fixture path
+ self.fixture_path << File.dirname(__FILE__) + "/fixtures"
+
+ # Add more helper methods to be used by all extension tests here...
+
+end

0 comments on commit 43af72e

Please sign in to comment.
Something went wrong with that request. Please try again.