Commit 26edc24e79b6bf5d9cae99c6868fa519d56de983
Exists in
theme-brasil-digital-from-staging
and in
9 other branches
Merge branch 'rails3-custom_fields' into stable
Conflicts: app/views/profile/_person_profile.html.erb
Showing
21 changed files
with
332 additions
and
129 deletions
Show diff stats
app/controllers/public/contact_controller.rb
1 | 1 | class ContactController < PublicController |
2 | 2 | |
3 | - before_filter :login_required | |
4 | - | |
5 | 3 | needs_profile |
6 | 4 | |
7 | 5 | def new |
8 | - @contact | |
6 | + @contact = build_contact | |
9 | 7 | if request.post? && params[:confirm] == 'true' |
10 | - @contact = user.build_contact(profile, params[:contact]) | |
11 | 8 | @contact.city = (!params[:city].blank? && City.exists?(params[:city])) ? City.find(params[:city]).name : nil |
12 | 9 | @contact.state = (!params[:state].blank? && State.exists?(params[:state])) ? State.find(params[:state]).name : nil |
13 | 10 | if @contact.deliver |
... | ... | @@ -16,8 +13,17 @@ class ContactController < PublicController |
16 | 13 | else |
17 | 14 | session[:notice] = _('Contact not sent') |
18 | 15 | end |
16 | + end | |
17 | + end | |
18 | + | |
19 | + protected | |
20 | + | |
21 | + def build_contact | |
22 | + params[:contact] ||= {} | |
23 | + if logged_in? | |
24 | + user.build_contact profile, params[:contact] | |
19 | 25 | else |
20 | - @contact = user.build_contact(profile) | |
26 | + Contact.new params[:contact].merge(dest: profile) | |
21 | 27 | end |
22 | 28 | end |
23 | 29 | ... | ... |
app/helpers/application_helper.rb
... | ... | @@ -1313,7 +1313,7 @@ module ApplicationHelper |
1313 | 1313 | end |
1314 | 1314 | |
1315 | 1315 | def template_options(kind, field_name) |
1316 | - templates = environment.send(kind).templates.order('name') | |
1316 | + templates = environment.send(kind).templates | |
1317 | 1317 | return '' if templates.count == 0 |
1318 | 1318 | if templates.count == 1 |
1319 | 1319 | if templates.first.custom_fields == {} |
... | ... | @@ -1327,10 +1327,16 @@ module ApplicationHelper |
1327 | 1327 | content_tag('div', custom_fields) |
1328 | 1328 | end |
1329 | 1329 | else |
1330 | - options = options_for_select(templates.collect{ |template| [template.name, template.id]}) | |
1331 | - content_tag('div', | |
1332 | - content_tag('div', content_tag('label', _('Profile organization'), :class => 'formlabel') + (select_tag 'profile_data[template_id]', options, :onchange => 'show_fields_for_template(this);')), | |
1333 | - :id => 'template-options') | |
1330 | + radios = templates.map do |template| | |
1331 | + content_tag('li', labelled_radio_button(link_to(template.name, template.url, :target => '_blank'), "#{field_name}[template_id]", template.id, environment.is_default_template?(template))) | |
1332 | + end.join("\n") | |
1333 | + | |
1334 | + content_tag('div', content_tag('label', _('Profile organization'), :for => 'template-options', :class => 'formlabel') + | |
1335 | + content_tag('p', _('Your profile will be created according to the selected template. Click on the options to view them.'), :style => 'margin: 5px 15px;padding: 0px 10px;') + | |
1336 | + content_tag('ul', radios, :style => 'list-style: none; padding-left: 20px; margin-top: 0.5em;'), | |
1337 | + :id => 'template-options', | |
1338 | + :style => 'margin-top: 1em' | |
1339 | + ) | |
1334 | 1340 | end |
1335 | 1341 | end |
1336 | 1342 | ... | ... |
app/views/contact/new.html.erb
... | ... | @@ -13,11 +13,16 @@ |
13 | 13 | |
14 | 14 | <%= required_fields_message %> |
15 | 15 | |
16 | - <% location_fields = select_city(true) %> | |
16 | + <% unless logged_in? %> | |
17 | + <%= required f.text_field(:name) %> | |
18 | + <%= required f.text_field(:email) %> | |
19 | + <% end %> | |
17 | 20 | |
21 | + <% location_fields = select_city(true) %> | |
18 | 22 | <% unless environment.enabled?('disable_select_city_for_contact') || location_fields.blank? %> |
19 | 23 | <%= labelled_form_field _('City and state'), location_fields %> |
20 | 24 | <% end %> |
25 | + | |
21 | 26 | <%= required f.text_field(:subject) %> |
22 | 27 | |
23 | 28 | <%= render :file => 'shared/tiny_mce' %> |
... | ... | @@ -25,5 +30,9 @@ |
25 | 30 | |
26 | 31 | <%= labelled_form_field check_box(:contact, :receive_a_copy) + _('I want to receive a copy of the message in my e-mail.'), '' %> |
27 | 32 | |
28 | - <%= submit_button(:send, _('Send'), :onclick => "$('confirm').value = 'true'") %> | |
33 | + <% unless logged_in? %> | |
34 | + <%= recaptcha_tags :ajax => true, :display => {:theme => 'clean'} %> | |
35 | + <% end %> | |
36 | + | |
37 | + <%= submit_button(:send, _('Send'), :onclick => "jQuery('#confirm').val('true')") %> | |
29 | 38 | <% end %> | ... | ... |
app/views/layouts/application-ng.html.erb
... | ... | @@ -12,24 +12,9 @@ |
12 | 12 | <meta name="twitter:title" content="<%= h page_title %>"> |
13 | 13 | <meta name="twitter:description" content="<%= meta_description_tag(@page) %>"> |
14 | 14 | |
15 | - <!-- Open Graph --> | |
16 | - <meta property="og:type" content="<%= @page ? 'article' : 'website' %>"> | |
17 | - <meta property="og:url" content="<%= @page ? url_for(@page.url) : top_url %>"> | |
18 | - <meta property="og:title" content="<%= h page_title %>"> | |
19 | - <meta property="og:site_name" content="<%= profile ? profile.name : @environment.name %>"> | |
20 | - <meta property="og:description" content="<%= meta_description_tag(@page) %>"> | |
21 | - | |
22 | 15 | <!-- site root --> |
23 | 16 | <meta property="noosfero:root" content="<%= Noosfero.root %>"/> |
24 | 17 | |
25 | - <% if @page %> | |
26 | - <meta property="article:published_time" content="<%= show_date(@page.published_at) %>"> | |
27 | - <% @page.body_images_paths.each do |img| %> | |
28 | - <meta name="twitter:image" content="<%= img.to_s %>"> | |
29 | - <meta property="og:image" content="<%= img.to_s %>"> | |
30 | - <% end %> | |
31 | - <% end %> | |
32 | - | |
33 | 18 | <link rel="shortcut icon" href="<%= image_path(theme_favicon) %>" type="image/x-icon" /> |
34 | 19 | <%= noosfero_javascript %> |
35 | 20 | <%= noosfero_stylesheets %> | ... | ... |
app/views/profile/_person_profile.html.erb
... | ... | @@ -0,0 +1,12 @@ |
1 | +open_graph: | |
2 | + domain: cirandas.net | |
3 | + environment_logo: 'http://cirandas.net/designs/themes/cirandas-responsive/images/cirandas-logo-110.png' | |
4 | + types: | |
5 | + article: article | |
6 | + product: app_cirandas:sse_product | |
7 | + uploaded_file: app_cirandas:document | |
8 | + image: app_cirandas:picture | |
9 | + profile: app_cirandas:profile | |
10 | + person: app_cirandas:user | |
11 | + community: app_cirandas:community | |
12 | + enterprise: app_cirandas:sse_initiative | ... | ... |
... | ... | @@ -0,0 +1,32 @@ |
1 | +require_dependency 'article' | |
2 | + | |
3 | +class Article | |
4 | + | |
5 | + Metadata = { | |
6 | + 'og:type' => MetadataPlugin.og_types[:article], | |
7 | + 'og:url' => proc{ |a, c| c.og_url_for a.url }, | |
8 | + 'og:title' => proc{ |a, c| a.title }, | |
9 | + 'og:image' => proc do |a, c| | |
10 | + result = a.body_images_paths | |
11 | + result = "#{a.profile.environment.top_url}#{a.profile.image.public_filename}" if a.profile.image if result.blank? | |
12 | + result = MetadataPlugin.config[:open_graph][:environment_logo] if result.blank? | |
13 | + result | |
14 | + end, | |
15 | + 'og:see_also' => [], | |
16 | + 'og:site_name' => proc{ |a, c| a.profile.name }, | |
17 | + 'og:updated_time' => proc{ |a, c| a.updated_at.iso8601 }, | |
18 | + 'og:locale:locale' => proc{ |a, c| a.environment.default_language }, | |
19 | + 'og:locale:alternate' => proc{ |a, c| a.environment.languages - [a.environment.default_language] }, | |
20 | + 'twitter:image' => proc{ |a, c| a.body_images_paths }, | |
21 | + 'article:expiration_time' => "", # In the future we might want to populate this | |
22 | + 'article:modified_time' => proc{ |a, c| a.updated_at.iso8601 }, | |
23 | + 'article:published_time' => proc{ |a, c| a.published_at.iso8601 }, | |
24 | + 'article:section' => "", # In the future we might want to populate this | |
25 | + 'article:tag' => proc{ |a, c| a.tags.map &:name }, | |
26 | + 'og:description' => proc{ |a, c| ActionView::Base.full_sanitizer.sanitize a.body }, | |
27 | + 'og:rich_attachment' => "", | |
28 | + } | |
29 | + | |
30 | + | |
31 | + | |
32 | +end | ... | ... |
... | ... | @@ -0,0 +1,18 @@ |
1 | +require_dependency 'enterprise' | |
2 | +require_dependency "#{File.dirname __FILE__}/profile" | |
3 | + | |
4 | +class Enterprise | |
5 | + | |
6 | + Metadata = Metadata.merge({ | |
7 | + 'og:type' => MetadataPlugin.og_types[:enterprise], | |
8 | + 'business:contact_data:email' => proc{ |e, c| e.contact_email }, | |
9 | + 'business:contact_data:phone_number' => proc{ |e, c| e.contact_phone }, | |
10 | + 'business:contact_data:street_address' => proc{ |e, c| e.address }, | |
11 | + 'business:contact_data:locality' => proc{ |e, c| e.city }, | |
12 | + 'business:contact_data:region' => proc{ |e, c| e.state }, | |
13 | + 'business:contact_data:postal_code' => proc{ |e, c| e.zip_code }, | |
14 | + 'business:contact_data:country_name' => proc{ |e| e.country }, | |
15 | + 'place:location:latitude' => proc{ |e, c| p.lat }, | |
16 | + 'place:location:longitude' => proc{ |e, c| p.lng }, | |
17 | + }) | |
18 | +end | ... | ... |
... | ... | @@ -0,0 +1,13 @@ |
1 | +require_dependency 'environment' | |
2 | + | |
3 | +class Environment | |
4 | + | |
5 | + Metadata = { | |
6 | + 'og:site_name' => proc{ |e, c| e.name }, | |
7 | + 'og:description' => proc{ |e, c| e.name }, | |
8 | + 'og:url' => proc{ |e, c| e.top_url }, | |
9 | + 'og:locale:locale' => proc{ |e, c| e.default_language }, | |
10 | + 'og:locale:alternate' => proc{ |e, c| e.languages - [e.default_language] } | |
11 | + } | |
12 | + | |
13 | +end | ... | ... |
... | ... | @@ -0,0 +1,25 @@ |
1 | +require_dependency 'product' | |
2 | + | |
3 | +class Product | |
4 | + | |
5 | + Metadata = { | |
6 | + 'og:type' => MetadataPlugin.og_types[:product], | |
7 | + 'og:url' => proc{ |p, c| c.og_url_for p.url }, | |
8 | + 'og:gr_hascurrencyvalue' => proc{ |p, c| p.price.to_f }, | |
9 | + 'og:gr_hascurrency' => proc{ |p, c| p.environment.currency_unit }, | |
10 | + 'og:title' => proc{ |p, c| p.name }, | |
11 | + 'og:description' => proc{ |p, c| ActionView::Base.full_sanitizer.sanitize p.description }, | |
12 | + 'og:image' => proc{ |p, c| "#{p.environment.top_url}#{p.image.public_filename}" if p.image }, | |
13 | + 'og:image:type' => proc{ |p, c| p.image.content_type if p.image }, | |
14 | + 'og:image:height' => proc{ |p, c| p.image.height if p.image }, | |
15 | + 'og:image:width' => proc{ |p, c| p.image.width if p.image }, | |
16 | + 'og:see_also' => [], | |
17 | + 'og:site_name' => proc{ |p, c| c.og_url_for p.profile.url }, | |
18 | + 'og:updated_time' => proc{ |p, c| p.updated_at.iso8601 }, | |
19 | + 'og:locale:locale' => proc{ |p, c| p.environment.default_language }, | |
20 | + 'og:locale:alternate' => proc{ |p, c| p.environment.languages - [p.environment.default_language] }, | |
21 | + } | |
22 | + | |
23 | + protected | |
24 | + | |
25 | +end | ... | ... |
... | ... | @@ -0,0 +1,24 @@ |
1 | +require_dependency 'profile' | |
2 | + | |
3 | +class Profile | |
4 | + | |
5 | + Metadata = { | |
6 | + 'og:type' => MetadataPlugin.og_types[:profile], | |
7 | + 'og:image' => proc{ |p, c| "#{p.environment.top_url}#{p.image.public_filename}" if p.image }, | |
8 | + 'og:title' => proc{ |p, c| p.short_name nil }, | |
9 | + 'og:url' => proc do |p, c| | |
10 | + #force profile identifier for custom domains and fixed host. see og_url_for | |
11 | + c.og_url_for p.url.merge(profile: p.identifier) | |
12 | + end, | |
13 | + 'og:description' => proc{ |p, c| p.description }, | |
14 | + 'og:updated_time' => proc{ |p, c| p.updated_at.iso8601 }, | |
15 | + 'place:location:latitude' => proc{ |p, c| p.lat }, | |
16 | + 'place:location:longitude' => proc{ |p, c| p.lng }, | |
17 | + 'og:locale:locale' => proc{ |p, c| p.environment.default_language }, | |
18 | + 'og:locale:alternate' => proc{ |p, c| p.environment.languages - [p.environment.default_language] }, | |
19 | + 'og:site_name' => "", | |
20 | + 'og:see_also' => "", | |
21 | + 'og:rich_attachment' => "", | |
22 | + } | |
23 | + | |
24 | +end | ... | ... |
... | ... | @@ -0,0 +1,17 @@ |
1 | +require_dependency 'uploaded_file' | |
2 | +require_dependency "#{File.dirname __FILE__}/article" | |
3 | + | |
4 | +class UploadedFile | |
5 | + | |
6 | + Metadata = { | |
7 | + 'og:type' => proc do |u, c| | |
8 | + type = if u.image? then :image else :uploaded_file end | |
9 | + MetadataPlugin.og_types[type] | |
10 | + end, | |
11 | + 'og:url' => proc{ |u, c| c.og_url_for u.url.merge(view: true) }, | |
12 | + 'og:title' => proc{ |u, c| u.title }, | |
13 | + 'og:image' => proc{ |u, c| "#{u.environment.top_url}#{u.public_filename}" if u.image? }, | |
14 | + 'og:description' => proc{ |u, c| u.abstract || u.title }, | |
15 | + } | |
16 | + | |
17 | +end | ... | ... |
... | ... | @@ -0,0 +1,59 @@ |
1 | + | |
2 | +class MetadataPlugin < Noosfero::Plugin | |
3 | + | |
4 | + def self.plugin_name | |
5 | + I18n.t 'metadata_plugin.lib.plugin.name' | |
6 | + end | |
7 | + | |
8 | + def self.plugin_description | |
9 | + I18n.t 'metadata_plugin.lib.plugin.description' | |
10 | + end | |
11 | + | |
12 | + def self.config | |
13 | + @config ||= HashWithIndifferentAccess.new(YAML.load File.read("#{File.dirname __FILE__}/../config.yml")) rescue {} | |
14 | + end | |
15 | + | |
16 | + def self.og_types | |
17 | + @og_types ||= self.config[:open_graph][:types] rescue {} | |
18 | + end | |
19 | + | |
20 | + def head_ending | |
21 | + plugin = self | |
22 | + lambda do | |
23 | + options = MetadataPlugin::Spec::Controllers[controller.controller_path.to_sym] | |
24 | + options ||= MetadataPlugin::Spec::Controllers[:profile] if controller.is_a? ProfileController | |
25 | + options ||= MetadataPlugin::Spec::Controllers[:environment] | |
26 | + return unless options | |
27 | + | |
28 | + return unless object = case variable = options[:variable] | |
29 | + when Proc then instance_exec(&variable) rescue nil | |
30 | + else instance_variable_get variable | |
31 | + end | |
32 | + return unless metadata = (object.class.const_get(:Metadata) rescue nil) | |
33 | + | |
34 | + metadata.map do |property, contents| | |
35 | + contents = contents.call(object, plugin) rescue nil if contents.is_a? Proc | |
36 | + next if contents.blank? | |
37 | + | |
38 | + Array(contents).map do |content| | |
39 | + content = content.call(object, plugin) rescue nil if content.is_a? Proc | |
40 | + next if content.blank? | |
41 | + tag 'meta', property: property, content: content | |
42 | + end.join | |
43 | + end.join | |
44 | + end | |
45 | + end | |
46 | + | |
47 | + # context HELPERS | |
48 | + def og_url_for options | |
49 | + options.delete :port | |
50 | + options[:host] = self.class.config[:open_graph][:domain] rescue context.send(:environment).default_hostname | |
51 | + Noosfero::Application.routes.url_helpers.url_for options | |
52 | + end | |
53 | + | |
54 | + protected | |
55 | + | |
56 | +end | |
57 | + | |
58 | +ActiveSupport.run_load_hooks :metadata_plugin, MetadataPlugin | |
59 | + | ... | ... |
... | ... | @@ -0,0 +1,29 @@ |
1 | + | |
2 | +class MetadataPlugin::Spec | |
3 | + | |
4 | + Controllers = { | |
5 | + manage_products: { | |
6 | + variable: :@product, | |
7 | + }, | |
8 | + content_viewer: { | |
9 | + variable: proc do | |
10 | + if profile and profile.home_page_id == @page.id | |
11 | + @profile | |
12 | + elsif @page.respond_to? :encapsulated_file | |
13 | + @page.encapsulated_file | |
14 | + else | |
15 | + @page | |
16 | + end | |
17 | + end, | |
18 | + }, | |
19 | + # fallback | |
20 | + profile: { | |
21 | + variable: :@profile, | |
22 | + }, | |
23 | + # last fallback | |
24 | + environment: { | |
25 | + variable: :@environment, | |
26 | + }, | |
27 | + } | |
28 | + | |
29 | +end | ... | ... |
public/javascripts/application.js
... | ... | @@ -808,9 +808,12 @@ Array.min = function(array) { |
808 | 808 | }; |
809 | 809 | |
810 | 810 | function hideAndGetUrl(link) { |
811 | + document.body.style.cursor = 'wait'; | |
811 | 812 | link.hide(); |
812 | 813 | url = jQuery(link).attr('href'); |
813 | - jQuery.get(url); | |
814 | + jQuery.get(url, function( data ) { | |
815 | + document.body.style.cursor = 'default'; | |
816 | + }); | |
814 | 817 | } |
815 | 818 | |
816 | 819 | jQuery(function($){ | ... | ... |
script/git-upgrade
... | ... | @@ -2,105 +2,23 @@ |
2 | 2 | |
3 | 3 | set -e |
4 | 4 | |
5 | -export RAILS_ENV=production | |
6 | - | |
7 | 5 | say(){ |
8 | - echo -e "\033[33;01m$0: $1\033[m" | |
9 | -} | |
10 | - | |
11 | -get_value(){ | |
12 | - ruby -ryaml -e "puts YAML.load_file('config/database.yml')['$RAILS_ENV']['$1']" | |
13 | -} | |
14 | - | |
15 | -usage(){ | |
16 | - echo "usage: $0 [OPTIONS]" | |
17 | - echo | |
18 | - echo "Options:" | |
19 | - echo | |
20 | - echo " -s, --shell Opens a shell just after upgrading code and" | |
21 | - echo " database to make manual steps if needed" | |
22 | - echo | |
23 | - echo " -h, --help Displays the help (this screen)" | |
24 | - echo | |
25 | - echo " -v, --version Displays Noosfero current version" | |
26 | - echo | |
27 | - exit $1 | |
28 | -} | |
29 | - | |
30 | -version(){ | |
31 | - version=$(ruby -Ilib -rnoosfero -e 'puts Noosfero::VERSION') | |
32 | - echo "Noosfero version $version" | |
33 | - exit 0 | |
34 | -} | |
35 | - | |
36 | -stop_service(){ | |
37 | - say "Stopping service" | |
38 | - ./script/production stop || say "Stop failed, trying to continue anyway" | |
39 | - sudo /etc/init.d/memcached restart | |
40 | -} | |
41 | - | |
42 | -start_service(){ | |
43 | - say "Starting service" | |
44 | - ./script/production start | |
45 | -} | |
46 | - | |
47 | -upgrade_code(){ | |
48 | - say "Upgrading code" | |
49 | - | |
50 | - # db:migrate always changes this | |
51 | - git checkout db/schema.rb | |
52 | - | |
53 | - git pull --quiet | |
54 | - | |
55 | - say "Compiling translations" | |
56 | - rake noosfero:translations:compile 2>/dev/null || (echo "Translations compilation failed; run manually to check"; false) | |
57 | - | |
58 | - # remove cached files | |
59 | - rm -f public/javascripts/cache*.js | |
60 | - rm -f public/stylesheets/cache*.css | |
6 | + if [ -t 1 ]; then | |
7 | + printf "\033[33;01m$0: $1\033[m\n" | |
8 | + fi | |
61 | 9 | } |
62 | 10 | |
63 | -upgrade_database(){ | |
64 | - say "Upgrading database" | |
65 | - | |
66 | - rake db:migrate | |
11 | +say "Upgrading code" | |
67 | 12 | |
68 | - if test "$shell" = "yes"; then | |
69 | - echo "################################################" | |
70 | - echo "# Noosfero upgrade shell #" | |
71 | - echo "################################################" | |
72 | - echo "# #" | |
73 | - echo "# If you need to do any manual steps during #" | |
74 | - echo "# this upgrade, now is the time. #" | |
75 | - echo "# #" | |
76 | - echo "# After you finish, just exit this shell and #" | |
77 | - echo "# the upgrade will proceed #" | |
78 | - echo "################################################" | |
79 | - export PS1="[Noosfero upgrade] $PS1" | |
80 | - bash --rcfile config/bashrc | |
81 | - fi | |
82 | -} | |
13 | +last_passed=$(curl --silent --fail http://ci.noosfero.org/noosfero/LAST_SUCCESS_HEAD || true) | |
14 | +if [ -n "$last_passed" ]; then | |
15 | + git fetch | |
16 | + git reset --hard "$last_passed" | |
17 | +else | |
18 | + exit | |
19 | +fi | |
83 | 20 | |
84 | -shell=no | |
85 | -while test $# -gt 0; do | |
86 | - case "$1" in | |
87 | - -s|--shell) | |
88 | - shell=yes | |
89 | - ;; | |
90 | - -h|--help) | |
91 | - usage 0 | |
92 | - ;; | |
93 | - -v|--version) | |
94 | - version | |
95 | - ;; | |
96 | - *) | |
97 | - usage 1 | |
98 | - ;; | |
99 | - esac | |
100 | - shift | |
101 | -done | |
21 | +say "Compiling translations" | |
22 | +rake noosfero:translations:compile 2>/dev/null || (echo "Translations compilation failed; run manually to check"; false) | |
102 | 23 | |
103 | -stop_service | |
104 | -upgrade_code | |
105 | -upgrade_database | |
106 | -start_service | |
24 | +./script/production restart | ... | ... |
test/functional/contact_controller_test.rb
... | ... | @@ -90,11 +90,12 @@ class ContactControllerTest < ActionController::TestCase |
90 | 90 | assert_no_tag :tag => 'select', :attributes => {:name => 'state'} |
91 | 91 | end |
92 | 92 | |
93 | - should 'not allow if not logged' do | |
93 | + should 'show name, email and captcha if not logged' do | |
94 | 94 | logout |
95 | 95 | get :new, :profile => profile.identifier |
96 | - assert_response :redirect | |
97 | - assert_redirected_to :controller => 'account', :action => 'login' | |
96 | + assert_tag :tag => 'input', :attributes => {:name => 'contact[name]'} | |
97 | + assert_tag :tag => 'input', :attributes => {:name => 'contact[email]'} | |
98 | + assert_tag :attributes => {id: 'dynamic_recaptcha'} | |
98 | 99 | end |
99 | 100 | |
100 | 101 | should 'identify sender' do | ... | ... |