Commit 48c34b8c997297ae26c177aa1d744686deabbcb3

Authored by Victor Costa
2 parents 7ccef053 538bb500

Merge branch 'master' into stable

Conflicts:
	public/javascripts/application.js
Showing 49 changed files with 1784 additions and 239 deletions   Show diff stats
DEVELOPMENT.md
... ... @@ -19,6 +19,26 @@
19 19 * In the case the original author is a committer, he/she should feel free to
20 20 commit directly if after 1 week nobody has provided any kind of feedback.
21 21  
  22 + * Developers who are not committers should feel free to ping committers if
  23 + they do not get feedback on their contributions after 1 week.
  24 +
  25 + * On GitLab, one can just add a comment to the merge request; one can also
  26 + @-mention specific committers or other developers who have expertise on
  27 + the area of the contribution.
  28 +
  29 + * Committers should follow the activity of the project, and try to help
  30 + reviewing contributions from others as much as possible.
  31 +
  32 + * On GitLab one can get emails for all activity on a project by setting the
  33 + [notification settings](https://gitlab.com/profile/notifications) to
  34 + "watch".
  35 +
  36 + * Anyone can help by reviewing contributions. Committers are the only ones
  37 + who can give the final approval to a contribution, but everyone is welcome
  38 + to help with code review, testing, etc.
  39 +
  40 + * See note above about setting up notification on GitLab.
  41 +
22 42 * Committers should feel free to push trivial (or urgent) changes directly.
23 43 There are no strict rule on what makes a change trivial or urgent; committers
24 44 are expected to exercise good judgement on a case by case basis.
... ...
app/helpers/application_helper.rb
... ... @@ -433,19 +433,19 @@ module ApplicationHelper
433 433 end
434 434  
435 435 def theme_site_title
436   - theme_include('site_title')
  436 + @theme_site_title ||= theme_include 'site_title'
437 437 end
438 438  
439 439 def theme_header
440   - theme_include('header')
  440 + @theme_header ||= theme_include 'header'
441 441 end
442 442  
443 443 def theme_footer
444   - theme_include('footer')
  444 + @theme_footer ||= theme_include 'footer'
445 445 end
446 446  
447 447 def theme_extra_navigation
448   - theme_include('navigation')
  448 + @theme_extra_navigation ||= theme_include 'navigation'
449 449 end
450 450  
451 451 def is_testing_theme
... ... @@ -674,13 +674,14 @@ module ApplicationHelper
674 674 html.join "\n"
675 675 end
676 676  
  677 + def theme_javascript_src
  678 + script = File.join theme_path, 'theme.js'
  679 + script if File.exists? File.join(Rails.root, 'public', script)
  680 + end
  681 +
677 682 def theme_javascript_ng
678   - script = File.join(theme_path, 'theme.js')
679   - if File.exists?(File.join(Rails.root, 'public', script))
680   - javascript_include_tag script
681   - else
682   - nil
683   - end
  683 + script = theme_javascript_src
  684 + javascript_include_tag script if script
684 685 end
685 686  
686 687 def file_field_or_thumbnail(label, image, i)
... ...
app/helpers/layout_helper.rb
... ... @@ -18,6 +18,8 @@ module LayoutHelper
18 18 unless plugins_javascripts.empty?
19 19 output += javascript_include_tag plugins_javascripts, :cache => "cache/plugins-#{Digest::MD5.hexdigest plugins_javascripts.to_s}"
20 20 end
  21 + output += theme_javascript_ng.to_s
  22 +
21 23 output
22 24 end
23 25  
... ... @@ -87,6 +89,10 @@ module LayoutHelper
87 89 theme_path + '/style.css'
88 90 end
89 91  
  92 + def layout_template
  93 + if profile then profile.layout_template else environment.layout_template end
  94 + end
  95 +
90 96 def addthis_javascript
91 97 if NOOSFERO_CONF['addthis_enabled']
92 98 '<script src="https://s7.addthis.com/js/152/addthis_widget.js"></script>'
... ...
app/models/person.rb
... ... @@ -21,6 +21,12 @@ class Person &lt; Profile
21 21 { :select => 'DISTINCT profiles.*', :joins => :role_assignments, :conditions => [conditions] }
22 22 }
23 23  
  24 + scope :by_role, lambda { |roles|
  25 + roles = [roles] unless roles.kind_of?(Array)
  26 + { :select => 'DISTINCT profiles.*', :joins => :role_assignments, :conditions => ['role_assignments.role_id IN (?)',
  27 +roles] }
  28 + }
  29 +
24 30 def has_permission_with_plugins?(permission, profile)
25 31 permissions = [has_permission_without_plugins?(permission, profile)]
26 32 permissions += plugins.map do |plugin|
... ...
app/models/profile.rb
... ... @@ -146,8 +146,8 @@ class Profile &lt; ActiveRecord::Base
146 146 alias_method_chain :count, :distinct
147 147 end
148 148  
149   - def members_by_role(role)
150   - Person.members_of(self).all(:conditions => ['role_assignments.role_id = ?', role.id])
  149 + def members_by_role(roles)
  150 + Person.members_of(self).by_role(roles)
151 151 end
152 152  
153 153 acts_as_having_boxes
... ...
app/views/layouts/_content.html.erb 0 → 100644
... ... @@ -0,0 +1,4 @@
  1 +<div id="content-inner">
  2 + <%= insert_boxes(yield) %>
  3 + <br style='clear: both'/>
  4 +</div><!-- end id="content-inner" -->
... ...
app/views/layouts/application-ng.html.erb
... ... @@ -72,10 +72,7 @@
72 72 <div id="navigation-end"></div>
73 73 </div><!-- end id="navigation" -->
74 74 <div id="content">
75   - <div id="content-inner">
76   - <%= insert_boxes(yield) %>
77   - <br style='clear: both'/>
78   - </div><!-- end id="content-inner" -->
  75 + <%= render 'layouts/content' %>
79 76 </div><!-- end id="content" -->
80 77 </div><!-- end id="wrap-2" -->
81 78 </div><!-- end id="wrap-1" -->
... ... @@ -84,7 +81,6 @@
84 81 <%= theme_footer %>
85 82 </div><!-- end id="theme-footer" -->
86 83 <%= noosfero_layout_features %>
87   - <%= theme_javascript_ng %>
88 84 <%= addthis_javascript %>
89 85 <%=
90 86 @plugins.dispatch(:body_ending).map do |content|
... ...
config/application.rb
... ... @@ -111,9 +111,7 @@ module Noosfero
111 111 # Make sure the secret is at least 30 characters and all random,
112 112 # no regular words or you'll be exposed to dictionary attacks.
113 113 config.secret_token = noosfero_session_secret
114   - config.action_dispatch.session = {
115   - :key => '_noosfero_session',
116   - }
  114 + config.session_store :cookie_store, :key => '_noosfero_session'
117 115  
118 116 config.time_zone = File.read('/etc/timezone').split("\n").first
119 117 config.active_record.default_timezone = :local
... ...
db/migrate/20140724134600_remove_environment_statistics_block_sooner.rb 0 → 100644
... ... @@ -0,0 +1,9 @@
  1 +class RemoveEnvironmentStatisticsBlockSooner < ActiveRecord::Migration
  2 + def self.up
  3 + update("UPDATE blocks SET type = 'StatisticsBlock' WHERE type = 'EnvironmentStatisticsBlock'")
  4 + end
  5 +
  6 + def self.down
  7 + say("Nothing to undo (cannot recover the data)")
  8 + end
  9 +end
... ...
debian/control
... ... @@ -60,6 +60,11 @@ Depends:
60 60 dbconfig-common,
61 61 adduser,
62 62 exim4 | mail-transport-agent,
  63 +# to minimize upgrade issues:
  64 + ruby-feedparser (>= 0.7-3~),
  65 + ruby-eventmachine (>= 0.12.10-4~),
  66 + ruby-rack (>= 1.4.5-2~),
  67 + ruby-tzinfo (>= 1.1.0-2~),
63 68 ${misc:Depends}
64 69 Recommends:
65 70 postgresql,
... ...
etc/noosfero/varnish-noosfero.vcl
... ... @@ -10,6 +10,13 @@ sub vcl_recv {
10 10 }
11 11 }
12 12  
  13 +sub vcl_deliver {
  14 + # Force clients to aways hit the server again for HTML pages
  15 + if (resp.http.Content-Type ~ "^text/html") {
  16 + set resp.http.Cache-Control = "no-cache";
  17 + }
  18 +}
  19 +
13 20 sub vcl_error {
14 21 set obj.http.Content-Type = "text/html; charset=utf-8";
15 22  
... ...
lib/noosfero.rb
... ... @@ -70,16 +70,6 @@ module Noosfero
70 70 end
71 71 end
72 72  
73   - def self.term(t)
74   - self.terminology.get(t)
75   - end
76   - def self.terminology
77   - @terminology ||= Noosfero::Terminology::Default.instance
78   - end
79   - def self.terminology=(term)
80   - @terminology = term
81   - end
82   -
83 73 def self.url_options
84 74 case Rails.env
85 75 when 'development'
... ...
lib/noosfero/plugin.rb
... ... @@ -88,18 +88,29 @@ class Noosfero::Plugin
88 88 # This is a generic method that initialize any possible filter defined by a
89 89 # plugin to a specific controller
90 90 def load_plugin_filters(plugin)
91   - plugin_methods = plugin.instance_methods.select {|m| m.to_s.end_with?('_filters')}
92   - plugin_methods.each do |plugin_method|
93   - controller_class = plugin_method.to_s.gsub('_filters', '').camelize.constantize
94   - filters = plugin.new.send(plugin_method)
95   - filters = [filters] if !filters.kind_of?(Array)
96   -
97   - filters.each do |plugin_filter|
98   - filter_method = (plugin.name.underscore.gsub('/','_') + '_' + plugin_filter[:method_name]).to_sym
99   - controller_class.send(plugin_filter[:type], filter_method, (plugin_filter[:options] || {}))
100   - controller_class.send(:define_method, filter_method) do
101   - instance_eval(&plugin_filter[:block]) if environment.plugin_enabled?(plugin)
102   - end
  91 + Rails.configuration.to_prepare do
  92 + filters = plugin.new.send 'application_controller_filters' rescue []
  93 + Noosfero::Plugin.add_controller_filters ApplicationController, plugin, filters
  94 +
  95 + plugin_methods = plugin.instance_methods.select {|m| m.to_s.end_with?('_filters')}
  96 + plugin_methods.each do |plugin_method|
  97 + controller_class = plugin_method.to_s.gsub('_filters', '').camelize.constantize
  98 +
  99 + filters = plugin.new.send(plugin_method)
  100 + Noosfero::Plugin.add_controller_filters controller_class, plugin, filters
  101 + end
  102 + end
  103 + end
  104 +
  105 + def add_controller_filters(controller_class, plugin, filters)
  106 + unless filters.is_a?(Array)
  107 + filters = [filters]
  108 + end
  109 + filters.each do |plugin_filter|
  110 + filter_method = (plugin.name.underscore.gsub('/','_') + '_' + plugin_filter[:method_name]).to_sym
  111 + controller_class.send(plugin_filter[:type], filter_method, (plugin_filter[:options] || {}))
  112 + controller_class.send(:define_method, filter_method) do
  113 + instance_exec(&plugin_filter[:block]) if environment.plugin_enabled?(plugin)
103 114 end
104 115 end
105 116 end
... ...
lib/tasks/release.rake
... ... @@ -137,7 +137,17 @@ EOF
137 137 new_version += '~rc1'
138 138 end
139 139 else
140   - new_version.sub!(/~rc[0-9]+/, '')
  140 + if new_version =~ /~rc\d+/
  141 + new_version.sub!(/~rc[0-9]+/, '')
  142 + else
  143 + components = new_version.split('.').map(&:to_i)
  144 + if components.size < 3
  145 + components << 1
  146 + else
  147 + components[-1] += 1
  148 + end
  149 + new_version = components.join('.')
  150 + end
141 151 end
142 152  
143 153 puts "Current version: #{$version}"
... ...
lib/unifreire_terminology.rb
... ... @@ -1,44 +0,0 @@
1   -require 'noosfero/terminology'
2   -
3   -class UnifreireTerminology < Noosfero::Terminology::Custom
4   -
5   - def initialize
6   - # NOTE: the hash values must be marked for translation!!
7   - super({
8   - 'Enterprises' => N_('Institutions'),
9   - 'enterprises' => N_('institutions'),
10   - 'The enterprises where this user works.' => N_('The institution where this user belongs.'),
11   - 'A block that displays your enterprises' => N_('A block that displays your institutions.'),
12   - 'All enterprises' => N_('All institutions'),
13   - 'Disable search for enterprises' => N_('Disable search for institutions'),
14   - 'One enterprise' => N_('One institution'),
15   - '%{num} enterprises' => N_('%{num} institutions'),
16   - 'Favorite Enterprises' => N_('Favorite Institutions'),
17   - 'This user\'s favorite enterprises.' => N_('This user\'s favorite institutions'),
18   - 'A block that displays your favorite enterprises' => N_('A block that displays your favorite institutions'),
19   - 'All favorite enterprises' => N_('All favorite institutions'),
20   - 'A search for enterprises by products selled and local' => N_('A search for institutions by products selled and local'),
21   - 'Edit message for disabled enterprises' => N_('Edit message for disabled institutions'),
22   - 'Add favorite enterprise' => N_('Add favorite institution'),
23   - 'Validation info is the information the enterprises will see about how your organization processes the enterprises validations it receives: validation methodology, restrictions to the types of enterprises the organization validates etc.' => N_('Validation info is the information the institutions will see about how your organization processes the institutions validations it receives: validation methodology, restrictions to the types of institutions the organization validates etc.'),
24   - 'Here are all <b>%s</b>\'s enterprises.' => N_('Here are all <b>%s</b>\'s institutions.'),
25   - 'Here are all <b>%s</b>\'s favorite enterprises.' => N_('Here are all <b>%s</b>\'s favorite institutions.'),
26   - 'Favorite Enterprises' => N_('Favorite Institutions'),
27   - 'Enterprises in "%s"' => N_('Institutions in "%s"'),
28   - 'Register a new Enterprise' => N_('Register a new Institution'),
29   - 'Events' => N_('Schedule'),
30   - 'Manage enterprise fields' => N_('Manage institutions fields'),
31   - "%s's enterprises" => N_("%s's institutions"),
32   - 'Activate your enterprise' => N_('Activate your institution'),
33   - 'Enterprise activation code' => N_('Institution activation code'),
34   - 'Disable activation of enterprises' => N_('Disable activation of institutions'),
35   - "%s's favorite enterprises" => N_("%s's favorite institutions"),
36   - 'Disable Enterprise' => N_('Disable Institution'),
37   - 'Enable Enterprise' => N_('Enable Institution'),
38   - 'Enterprise Validation' => N_('Institution Validation'),
39   - 'Enterprise Info and settings' => N_('Institution Info and settings'),
40   - 'Enterprises are disabled when created' => N_('Institutions are disabled when created'),
41   - })
42   - end
43   -
44   -end
lib/zen3_terminology.rb
... ... @@ -1,88 +0,0 @@
1   -require 'noosfero/terminology'
2   -
3   -class Zen3Terminology < Noosfero::Terminology::Custom
4   -
5   - def initialize
6   - # NOTE: the hash values must be marked for translation!!
7   - super({
8   - 'My Home Page' => N_('My ePortfolio'),
9   - 'Homepage' => N_('ePortfolio'),
10   - 'Communities' => N_('Groups'),
11   - 'communities' => N_('groups'),
12   - 'A block that displays your communities' => N_('A block that displays your groups'),
13   - 'A block that displays your friends' => N_('A block that displays your contacts'),
14   - 'The communities in which the user is a member' => N_('The groups in which the user is a member'),
15   - 'All communities' => N_('All groups'),
16   - 'Community' => N_('Group'),
17   - 'One community' => N_('One group'),
18   - '%{num} communities' => N_('%{num} groups'),
19   - 'Disable search for communities' => N_('Disable search for groups'),
20   - 'Enterprises' => N_('Organizations'),
21   - 'enterprises' => N_('organizations'),
22   - 'The enterprises where this user works.' => N_('The organizations where this user works.'),
23   - 'A block that displays your enterprises' => N_('A block that displays your organizations.'),
24   - 'All enterprises' => N_('All organizations'),
25   - 'Disable search for enterprises' => N_('Disable search for organizations'),
26   - 'One enterprise' => N_('One organization'),
27   - '%{num} enterprises' => N_('%{num} organizations'),
28   - 'Favorite Enterprises' => N_('Favorite Organizations'),
29   - 'This user\'s favorite enterprises.' => N_('This user\'s favorite organizations'),
30   - 'A block that displays your favorite enterprises' => N_('A block that displays your favorite organizations'),
31   - 'All favorite enterprises' => N_('All favorite organizations'),
32   - 'A search for enterprises by products selled and local' => N_('A search for organizations by products selled and local'),
33   - 'Edit message for disabled enterprises' => N_('Edit message for disabled organizations'),
34   - 'Add enterprise as favorite' => N_('Add organization as favorite'),
35   - 'Validation info is the information the enterprises will see about how your organization processes the enterprises validations it receives: validation methodology, restrictions to the types of enterprises the organization validates etc.' => N_('Validation info is the information the organizations will see about how your organization processes the organizations validations it receives: validation methodology, restrictions to the types of organizations the organization validates etc.'),
36   - 'Here are all <b>%s</b>\'s enterprises.' => N_('Here all all <b>%s</b>\'s organizations.'),
37   - 'Here are all <b>%s</b>\'s favorite enterprises.' => N_('Here are all <b>%s</b>\'s favorite organizations.'),
38   - 'Favorite Enterprises' => N_('Favorite Organizations'),
39   - 'Enterprises in "%s"' => N_('Organizations in "%s"'),
40   - 'Register a new Enterprise' => N_('Register a new organization'),
41   - 'One friend' => N_('One contact'),
42   - '%s friends' => N_('%s contacts'),
43   - '%s communities' => N_('%s groups'),
44   - 'Are you sure you want to remove %s from your friends list?' => N_('Are you sure you want to remove %s from your contacts list?'),
45   - 'Note that %s will still have you as a friend, unless he/she also wants to remove you from his/her friend list.' => N_('Note that %s will still have you as a contact, unless he/she also wants to remove you from his/her contact list.'),
46   - 'Yes, I want to remove %s from my friend list' => N_('Yes, I want to remove %s from my contact list'),
47   - 'Adding %s as a friend' => N_('Adding %s as a contact'),
48   - 'Are you sure you want to add %s as your friend?' => N_('Are you sure you want to add %s as your contact?'),
49   - 'Note that %s will need to accept being added as your friend.' => N_('Note that %s will need to accept being added as your contact.'),
50   - 'Classify your new friend %s: ' => N_('Classify your new contact %s: '),
51   - 'Yes, I want to add %s as my friend' => N_('Yes, I want to add %s as my contact'),
52   - 'Manage friends' => N_('Manage contacts'),
53   - 'Add friend' => N_('Add contact'),
54   - 'Removing friend: %s' => N_('Removing friend: %s'),
55   - 'Clicking on this button will remove your friend relation with %s.' => N_('Clicking on this button will remove your contact relation with %s.'),
56   - 'You have no friends yet.' => N_('You have no contacts yet.'),
57   - '%s\'s friends' => N_('%s\'s contacts'),
58   - 'Here are all <b>%s</b>\'s friends.' => N_('Here are all <b>%s</b>\'s contacts.'),
59   - 'Friends' => N_('Contacts'),
60   - 'Creating new community' => N_('Creating new group'),
61   - 'Do you want to join this community?' => N_('Do you want to join this group?'),
62   - 'Activate your enterprise' => N_('Activate your organization'),
63   - 'Enterprise activation code' => N_('Organization activation code'),
64   - 'Disable activation of enterprises' => N_('Disable activation of organizations'),
65   - 'Manage community fields' => N_('Manage group fields'),
66   - 'Create a new community' => N_('Create a new group'),
67   - 'Preferred domain name:' => N_('Choose your host community:'),
68   - 'My communities' => N_('My groups'),
69   - 'Community Info and settings' => N_('Group Info and Settings'),
70   - '{#} community' => N_('{#} group'),
71   - '{#} communities' => N_('{#} groups'),
72   - '{#} enterprise' => N_('{#} organization'),
73   - '{#} enterprises' => N_('{#} organizations'),
74   - '{#} friend' => N_('{#} contact'),
75   - '{#} friends' => N_('{#} contacts'),
76   - "%s's favorite enterprises" => N_("%s's favorite organizations"),
77   - 'Disable Enterprise' => N_('Disable Organization'),
78   - 'Enable Enterprise' => N_('Enable Organization'),
79   - 'Enterprise Validation' => N_('Organization Validation'),
80   - 'Enterprise Info and settings' => N_('Organization Info and settings'),
81   - 'Choose the communities you want to join and/or create your own.' => N_('Choose the groups you want to join and/or create your own.'),
82   - 'New community' => N_('New group'),
83   - "Tags are important to new users, they'll be able to find your new community more easily." => N_("Tags are important to new users, they'll be able to find your new group more easily."),
84   - 'Enterprises are disabled when created' => N_('Organizations are disabled when created'),
85   - })
86   - end
87   -
88   -end
plugins/pjax/lib/pjax_plugin.rb 0 → 100644
... ... @@ -0,0 +1,52 @@
  1 +class PjaxPlugin < Noosfero::Plugin
  2 +
  3 + def self.plugin_name
  4 + I18n.t('pjax_plugin.lib.plugin.name')
  5 + end
  6 +
  7 + def self.plugin_description
  8 + I18n.t('pjax_plugin.lib.plugin.description')
  9 + end
  10 +
  11 + def stylesheet?
  12 + true
  13 + end
  14 +
  15 + def js_files
  16 + ['jquery.pjax.js', 'patchwork.js', 'loading-overlay', 'pjax', ].map{ |j| "javascripts/#{j}" }
  17 + end
  18 +
  19 + def head_ending
  20 + #TODO: add pjax meta
  21 + end
  22 +
  23 + def body_beginning
  24 + lambda{ render 'pjax_layouts/load_state_script' }
  25 + end
  26 +
  27 + PjaxCheck = lambda do
  28 + return unless request.headers['X-PJAX']
  29 + # raise makes pjax fallback to a regular request
  30 + raise "Pjax can't be used here" if params[:controller] == 'account'
  31 +
  32 + @pjax = true
  33 + @pjax_loaded_themes = request.headers['X-PJAX-Themes'].to_s.split(',') || []
  34 +
  35 + unless self.respond_to? :get_layout_with_pjax
  36 + self.class.send :define_method, :get_layout_with_pjax do
  37 + if @pjax then 'pjax' else get_layout_without_pjax end
  38 + end
  39 + self.class.alias_method_chain :get_layout, :pjax
  40 + end
  41 + end
  42 +
  43 + def application_controller_filters
  44 + [{
  45 + :type => 'before_filter', :method_name => 'pjax_check',
  46 + :options => {}, :block => PjaxCheck,
  47 + }]
  48 + end
  49 +
  50 + protected
  51 +
  52 +end
... ...
plugins/pjax/locales/en.yml 0 → 100644
... ... @@ -0,0 +1,12 @@
  1 +
  2 +"en": &en-US
  3 + pjax_plugin:
  4 + lib:
  5 + plugin:
  6 + name: "pjax plugin"
  7 + description: "Use pjax for page's links"
  8 +
  9 +'en_US':
  10 + <<: *en-US
  11 +'en-US':
  12 + <<: *en-US
... ...
plugins/pjax/locales/pt.yml 0 → 100644
... ... @@ -0,0 +1,13 @@
  1 +
  2 +"pt": &pt-BR
  3 + pjax_plugin:
  4 + lib:
  5 + plugin:
  6 + name: "pjax plugin"
  7 + description: "Usa o pjax para os links da página"
  8 +
  9 +'pt_BR':
  10 + <<: *pt-BR
  11 +'pt-BR':
  12 + <<: *pt-BR
  13 +
... ...
plugins/pjax/public/images/loading-gears.gif 0 → 100644

617 KB

plugins/pjax/public/images/loading-overlay.gif 0 → 100644

3.4 KB

plugins/pjax/public/javascripts/jquery.pjax.js 0 → 100644
... ... @@ -0,0 +1,838 @@
  1 +// jquery.pjax.js
  2 +// copyright chris wanstrath
  3 +// https://github.com/defunkt/jquery-pjax
  4 +
  5 +(function($){
  6 +
  7 +// When called on a container with a selector, fetches the href with
  8 +// ajax into the container or with the data-pjax attribute on the link
  9 +// itself.
  10 +//
  11 +// Tries to make sure the back button and ctrl+click work the way
  12 +// you'd expect.
  13 +//
  14 +// Exported as $.fn.pjax
  15 +//
  16 +// Accepts a jQuery ajax options object that may include these
  17 +// pjax specific options:
  18 +//
  19 +//
  20 +// container - Where to stick the response body. Usually a String selector.
  21 +// $(container).html(xhr.responseBody)
  22 +// (default: current jquery context)
  23 +// push - Whether to pushState the URL. Defaults to true (of course).
  24 +// replace - Want to use replaceState instead? That's cool.
  25 +//
  26 +// For convenience the second parameter can be either the container or
  27 +// the options object.
  28 +//
  29 +// Returns the jQuery object
  30 +function fnPjax(selector, container, options) {
  31 + var context = this
  32 + return this.on('click.pjax', selector, function(event) {
  33 + var opts = $.extend({}, optionsFor(container, options))
  34 + if (!opts.container)
  35 + opts.container = $(this).attr('data-pjax') || context
  36 + handleClick(event, opts)
  37 + })
  38 +}
  39 +
  40 +// Public: pjax on click handler
  41 +//
  42 +// Exported as $.pjax.click.
  43 +//
  44 +// event - "click" jQuery.Event
  45 +// options - pjax options
  46 +//
  47 +// Examples
  48 +//
  49 +// $(document).on('click', 'a', $.pjax.click)
  50 +// // is the same as
  51 +// $(document).pjax('a')
  52 +//
  53 +// $(document).on('click', 'a', function(event) {
  54 +// var container = $(this).closest('[data-pjax-container]')
  55 +// $.pjax.click(event, container)
  56 +// })
  57 +//
  58 +// Returns nothing.
  59 +function handleClick(event, container, options) {
  60 + options = optionsFor(container, options)
  61 +
  62 + var link = event.currentTarget
  63 +
  64 + if (link.tagName.toUpperCase() !== 'A')
  65 + throw "$.fn.pjax or $.pjax.click requires an anchor element"
  66 +
  67 + // Middle click, cmd click, and ctrl click should open
  68 + // links in a new tab as normal.
  69 + if ( event.which > 1 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey )
  70 + return
  71 +
  72 + // Ignore cross origin links
  73 + if ( location.protocol !== link.protocol || location.hostname !== link.hostname )
  74 + return
  75 +
  76 + // Ignore anchors on the same page
  77 + if (link.hash && link.href.replace(link.hash, '') ===
  78 + location.href.replace(location.hash, ''))
  79 + return
  80 +
  81 + // Ignore empty anchor "foo.html#"
  82 + if (link.href === location.href + '#')
  83 + return
  84 +
  85 + var defaults = {
  86 + url: link.href,
  87 + container: $(link).attr('data-pjax'),
  88 + target: link
  89 + }
  90 +
  91 + var opts = $.extend({}, defaults, options)
  92 + var clickEvent = $.Event('pjax:click')
  93 + $(link).trigger(clickEvent, [opts])
  94 +
  95 + if (!clickEvent.isDefaultPrevented()) {
  96 + pjax(opts)
  97 + event.preventDefault()
  98 + }
  99 +}
  100 +
  101 +// Public: pjax on form submit handler
  102 +//
  103 +// Exported as $.pjax.submit
  104 +//
  105 +// event - "click" jQuery.Event
  106 +// options - pjax options
  107 +//
  108 +// Examples
  109 +//
  110 +// $(document).on('submit', 'form', function(event) {
  111 +// var container = $(this).closest('[data-pjax-container]')
  112 +// $.pjax.submit(event, container)
  113 +// })
  114 +//
  115 +// Returns nothing.
  116 +function handleSubmit(event, container, options) {
  117 + options = optionsFor(container, options)
  118 +
  119 + var form = event.currentTarget
  120 +
  121 + if (form.tagName.toUpperCase() !== 'FORM')
  122 + throw "$.pjax.submit requires a form element"
  123 +
  124 + var defaults = {
  125 + type: form.method.toUpperCase(),
  126 + url: form.action,
  127 + data: $(form).serializeArray(),
  128 + container: $(form).attr('data-pjax'),
  129 + target: form
  130 + }
  131 +
  132 + pjax($.extend({}, defaults, options))
  133 +
  134 + event.preventDefault()
  135 +}
  136 +
  137 +// Loads a URL with ajax, puts the response body inside a container,
  138 +// then pushState()'s the loaded URL.
  139 +//
  140 +// Works just like $.ajax in that it accepts a jQuery ajax
  141 +// settings object (with keys like url, type, data, etc).
  142 +//
  143 +// Accepts these extra keys:
  144 +//
  145 +// container - Where to stick the response body.
  146 +// $(container).html(xhr.responseBody)
  147 +// push - Whether to pushState the URL. Defaults to true (of course).
  148 +// replace - Want to use replaceState instead? That's cool.
  149 +//
  150 +// Use it just like $.ajax:
  151 +//
  152 +// var xhr = $.pjax({ url: this.href, container: '#main' })
  153 +// console.log( xhr.readyState )
  154 +//
  155 +// Returns whatever $.ajax returns.
  156 +function pjax(options) {
  157 + options = $.extend(true, {}, $.ajaxSettings, pjax.defaults, options)
  158 +
  159 + if ($.isFunction(options.url)) {
  160 + options.url = options.url()
  161 + }
  162 +
  163 + var target = options.target
  164 +
  165 + var hash = parseURL(options.url).hash
  166 +
  167 + var context = options.context = findContainerFor(options.container)
  168 +
  169 + // We want the browser to maintain two separate internal caches: one
  170 + // for pjax'd partial page loads and one for normal page loads.
  171 + // Without adding this secret parameter, some browsers will often
  172 + // confuse the two.
  173 + if (!options.data) options.data = {}
  174 + options.data._pjax = context.selector
  175 +
  176 + function fire(type, args) {
  177 + var event = $.Event(type, { relatedTarget: target })
  178 + context.trigger(event, args)
  179 + return !event.isDefaultPrevented()
  180 + }
  181 +
  182 + var timeoutTimer
  183 +
  184 + options.beforeSend = function(xhr, settings) {
  185 + // No timeout for non-GET requests
  186 + // Its not safe to request the resource again with a fallback method.
  187 + if (settings.type !== 'GET') {
  188 + settings.timeout = 0
  189 + }
  190 +
  191 + xhr.setRequestHeader('X-PJAX', 'true')
  192 + xhr.setRequestHeader('X-PJAX-Container', context.selector)
  193 +
  194 + if (!fire('pjax:beforeSend', [xhr, settings]))
  195 + return false
  196 +
  197 + if (settings.timeout > 0) {
  198 + timeoutTimer = setTimeout(function() {
  199 + if (fire('pjax:timeout', [xhr, options]))
  200 + xhr.abort('timeout')
  201 + }, settings.timeout)
  202 +
  203 + // Clear timeout setting so jquerys internal timeout isn't invoked
  204 + settings.timeout = 0
  205 + }
  206 +
  207 + options.requestUrl = parseURL(settings.url).href
  208 + }
  209 +
  210 + options.complete = function(xhr, textStatus) {
  211 + if (timeoutTimer)
  212 + clearTimeout(timeoutTimer)
  213 +
  214 + fire('pjax:complete', [xhr, textStatus, options])
  215 +
  216 + fire('pjax:end', [xhr, options])
  217 + }
  218 +
  219 + options.error = function(xhr, textStatus, errorThrown) {
  220 + var container = extractContainer("", xhr, options)
  221 +
  222 + var allowed = fire('pjax:error', [xhr, textStatus, errorThrown, options])
  223 + if (options.type == 'GET' && textStatus !== 'abort' && allowed) {
  224 + locationReplace(container.url)
  225 + }
  226 + }
  227 +
  228 + options.success = function(data, status, xhr) {
  229 + // If $.pjax.defaults.version is a function, invoke it first.
  230 + // Otherwise it can be a static string.
  231 + var currentVersion = (typeof $.pjax.defaults.version === 'function') ?
  232 + $.pjax.defaults.version() :
  233 + $.pjax.defaults.version
  234 +
  235 + var latestVersion = xhr.getResponseHeader('X-PJAX-Version')
  236 +
  237 + var container = extractContainer(data, xhr, options)
  238 +
  239 + // If there is a layout version mismatch, hard load the new url
  240 + if (currentVersion && latestVersion && currentVersion !== latestVersion) {
  241 + locationReplace(container.url)
  242 + return
  243 + }
  244 +
  245 + // If the new response is missing a body, hard load the page
  246 + if (!container.contents) {
  247 + locationReplace(container.url)
  248 + return
  249 + }
  250 +
  251 + pjax.state = {
  252 + id: options.id || uniqueId(),
  253 + url: container.url,
  254 + title: container.title,
  255 + container: context.selector,
  256 + fragment: options.fragment,
  257 + timeout: options.timeout
  258 + }
  259 +
  260 + if (options.push || options.replace) {
  261 + window.history.replaceState(pjax.state, container.title, container.url)
  262 + }
  263 +
  264 + // Clear out any focused controls before inserting new page contents.
  265 + document.activeElement.blur()
  266 +
  267 + if (container.title) document.title = container.title
  268 + context.html(container.contents)
  269 +
  270 + // FF bug: Won't autofocus fields that are inserted via JS.
  271 + // This behavior is incorrect. So if theres no current focus, autofocus
  272 + // the last field.
  273 + //
  274 + // http://www.w3.org/html/wg/drafts/html/master/forms.html
  275 + var autofocusEl = context.find('input[autofocus], textarea[autofocus]').last()[0]
  276 + if (autofocusEl && document.activeElement !== autofocusEl) {
  277 + autofocusEl.focus();
  278 + }
  279 +
  280 + executeScriptTags(container.scripts)
  281 +
  282 + // Scroll to top by default
  283 + if (typeof options.scrollTo === 'number')
  284 + $(window).scrollTop(options.scrollTo)
  285 +
  286 + // If the URL has a hash in it, make sure the browser
  287 + // knows to navigate to the hash.
  288 + if ( hash !== '' ) {
  289 + // Avoid using simple hash set here. Will add another history
  290 + // entry. Replace the url with replaceState and scroll to target
  291 + // by hand.
  292 + //
  293 + // window.location.hash = hash
  294 + var url = parseURL(container.url)
  295 + url.hash = hash
  296 +
  297 + pjax.state.url = url.href
  298 + window.history.replaceState(pjax.state, container.title, url.href)
  299 +
  300 + var target = $(url.hash)
  301 + if (target.length) $(window).scrollTop(target.offset().top)
  302 + }
  303 +
  304 + fire('pjax:success', [data, status, xhr, options])
  305 + }
  306 +
  307 +
  308 + // Initialize pjax.state for the initial page load. Assume we're
  309 + // using the container and options of the link we're loading for the
  310 + // back button to the initial page. This ensures good back button
  311 + // behavior.
  312 + if (!pjax.state) {
  313 + pjax.state = {
  314 + id: uniqueId(),
  315 + url: window.location.href,
  316 + title: document.title,
  317 + container: context.selector,
  318 + fragment: options.fragment,
  319 + timeout: options.timeout
  320 + }
  321 + window.history.replaceState(pjax.state, document.title)
  322 + }
  323 +
  324 + // Cancel the current request if we're already pjaxing
  325 + var xhr = pjax.xhr
  326 + if ( xhr && xhr.readyState < 4) {
  327 + xhr.onreadystatechange = $.noop
  328 + xhr.abort()
  329 + }
  330 +
  331 + pjax.options = options
  332 + var xhr = pjax.xhr = $.ajax(options)
  333 +
  334 + if (xhr.readyState > 0) {
  335 + if (options.push && !options.replace) {
  336 + // Cache current container element before replacing it
  337 + cachePush(pjax.state.id, context.clone().contents())
  338 +
  339 + window.history.pushState(null, "", stripPjaxParam(options.requestUrl))
  340 + }
  341 +
  342 + fire('pjax:start', [xhr, options])
  343 + fire('pjax:send', [xhr, options])
  344 + }
  345 +
  346 + return pjax.xhr
  347 +}
  348 +
  349 +// Public: Reload current page with pjax.
  350 +//
  351 +// Returns whatever $.pjax returns.
  352 +function pjaxReload(container, options) {
  353 + var defaults = {
  354 + url: window.location.href,
  355 + push: false,
  356 + replace: true,
  357 + scrollTo: false
  358 + }
  359 +
  360 + return pjax($.extend(defaults, optionsFor(container, options)))
  361 +}
  362 +
  363 +// Internal: Hard replace current state with url.
  364 +//
  365 +// Work for around WebKit
  366 +// https://bugs.webkit.org/show_bug.cgi?id=93506
  367 +//
  368 +// Returns nothing.
  369 +function locationReplace(url) {
  370 + window.history.replaceState(null, "", "#")
  371 + window.location.replace(url)
  372 +}
  373 +
  374 +
  375 +var initialPop = true
  376 +var initialURL = window.location.href
  377 +var initialState = window.history.state
  378 +
  379 +// Initialize $.pjax.state if possible
  380 +// Happens when reloading a page and coming forward from a different
  381 +// session history.
  382 +if (initialState && initialState.container) {
  383 + pjax.state = initialState
  384 +}
  385 +
  386 +// Non-webkit browsers don't fire an initial popstate event
  387 +if ('state' in window.history) {
  388 + initialPop = false
  389 +}
  390 +
  391 +// popstate handler takes care of the back and forward buttons
  392 +//
  393 +// You probably shouldn't use pjax on pages with other pushState
  394 +// stuff yet.
  395 +function onPjaxPopstate(event) {
  396 + var state = event.state
  397 +
  398 + if (state && state.container) {
  399 + // When coming forward from a separate history session, will get an
  400 + // initial pop with a state we are already at. Skip reloading the current
  401 + // page.
  402 + if (initialPop && initialURL == state.url) return
  403 +
  404 + // If popping back to the same state, just skip.
  405 + // Could be clicking back from hashchange rather than a pushState.
  406 + if (pjax.state.id === state.id) return
  407 +
  408 + var container = $(state.container)
  409 + if (container.length) {
  410 + var direction, contents = cacheMapping[state.id]
  411 +
  412 + if (pjax.state) {
  413 + // Since state ids always increase, we can deduce the history
  414 + // direction from the previous state.
  415 + direction = pjax.state.id < state.id ? 'forward' : 'back'
  416 +
  417 + // Cache current container before replacement and inform the
  418 + // cache which direction the history shifted.
  419 + cachePop(direction, pjax.state.id, container.clone().contents())
  420 + }
  421 +
  422 + var popstateEvent = $.Event('pjax:popstate', {
  423 + state: state,
  424 + direction: direction
  425 + })
  426 + container.trigger(popstateEvent)
  427 +
  428 + var options = {
  429 + id: state.id,
  430 + url: state.url,
  431 + container: container,
  432 + push: false,
  433 + fragment: state.fragment,
  434 + timeout: state.timeout,
  435 + scrollTo: false
  436 + }
  437 +
  438 + if (contents) {
  439 + container.trigger('pjax:start', [null, options])
  440 +
  441 + if (state.title) document.title = state.title
  442 + container.html(contents)
  443 + pjax.state = state
  444 +
  445 + container.trigger('pjax:end', [null, options])
  446 + } else {
  447 + pjax(options)
  448 + }
  449 +
  450 + // Force reflow/relayout before the browser tries to restore the
  451 + // scroll position.
  452 + container[0].offsetHeight
  453 + } else {
  454 + locationReplace(location.href)
  455 + }
  456 + }
  457 + initialPop = false
  458 +}
  459 +
  460 +// Fallback version of main pjax function for browsers that don't
  461 +// support pushState.
  462 +//
  463 +// Returns nothing since it retriggers a hard form submission.
  464 +function fallbackPjax(options) {
  465 + var url = $.isFunction(options.url) ? options.url() : options.url,
  466 + method = options.type ? options.type.toUpperCase() : 'GET'
  467 +
  468 + var form = $('<form>', {
  469 + method: method === 'GET' ? 'GET' : 'POST',
  470 + action: url,
  471 + style: 'display:none'
  472 + })
  473 +
  474 + if (method !== 'GET' && method !== 'POST') {
  475 + form.append($('<input>', {
  476 + type: 'hidden',
  477 + name: '_method',
  478 + value: method.toLowerCase()
  479 + }))
  480 + }
  481 +
  482 + var data = options.data
  483 + if (typeof data === 'string') {
  484 + $.each(data.split('&'), function(index, value) {
  485 + var pair = value.split('=')
  486 + form.append($('<input>', {type: 'hidden', name: pair[0], value: pair[1]}))
  487 + })
  488 + } else if (typeof data === 'object') {
  489 + for (key in data)
  490 + form.append($('<input>', {type: 'hidden', name: key, value: data[key]}))
  491 + }
  492 +
  493 + $(document.body).append(form)
  494 + form.submit()
  495 +}
  496 +
  497 +// Internal: Generate unique id for state object.
  498 +//
  499 +// Use a timestamp instead of a counter since ids should still be
  500 +// unique across page loads.
  501 +//
  502 +// Returns Number.
  503 +function uniqueId() {
  504 + return (new Date).getTime()
  505 +}
  506 +
  507 +// Internal: Strips _pjax param from url
  508 +//
  509 +// url - String
  510 +//
  511 +// Returns String.
  512 +function stripPjaxParam(url) {
  513 + return url
  514 + .replace(/\?_pjax=[^&]+&?/, '?')
  515 + .replace(/_pjax=[^&]+&?/, '')
  516 + .replace(/[\?&]$/, '')
  517 +}
  518 +
  519 +// Internal: Parse URL components and returns a Locationish object.
  520 +//
  521 +// url - String URL
  522 +//
  523 +// Returns HTMLAnchorElement that acts like Location.
  524 +function parseURL(url) {
  525 + var a = document.createElement('a')
  526 + a.href = url
  527 + return a
  528 +}
  529 +
  530 +// Internal: Build options Object for arguments.
  531 +//
  532 +// For convenience the first parameter can be either the container or
  533 +// the options object.
  534 +//
  535 +// Examples
  536 +//
  537 +// optionsFor('#container')
  538 +// // => {container: '#container'}
  539 +//
  540 +// optionsFor('#container', {push: true})
  541 +// // => {container: '#container', push: true}
  542 +//
  543 +// optionsFor({container: '#container', push: true})
  544 +// // => {container: '#container', push: true}
  545 +//
  546 +// Returns options Object.
  547 +function optionsFor(container, options) {
  548 + // Both container and options
  549 + if ( container && options )
  550 + options.container = container
  551 +
  552 + // First argument is options Object
  553 + else if ( $.isPlainObject(container) )
  554 + options = container
  555 +
  556 + // Only container
  557 + else
  558 + options = {container: container}
  559 +
  560 + // Find and validate container
  561 + if (options.container)
  562 + options.container = findContainerFor(options.container)
  563 +
  564 + return options
  565 +}
  566 +
  567 +// Internal: Find container element for a variety of inputs.
  568 +//
  569 +// Because we can't persist elements using the history API, we must be
  570 +// able to find a String selector that will consistently find the Element.
  571 +//
  572 +// container - A selector String, jQuery object, or DOM Element.
  573 +//
  574 +// Returns a jQuery object whose context is `document` and has a selector.
  575 +function findContainerFor(container) {
  576 + container = $(container)
  577 +
  578 + if ( !container.length ) {
  579 + throw "no pjax container for " + container.selector
  580 + } else if ( container.selector !== '' && container.context === document ) {
  581 + return container
  582 + } else if ( container.attr('id') ) {
  583 + return $('#' + container.attr('id'))
  584 + } else {
  585 + throw "cant get selector for pjax container!"
  586 + }
  587 +}
  588 +
  589 +// Internal: Filter and find all elements matching the selector.
  590 +//
  591 +// Where $.fn.find only matches descendants, findAll will test all the
  592 +// top level elements in the jQuery object as well.
  593 +//
  594 +// elems - jQuery object of Elements
  595 +// selector - String selector to match
  596 +//
  597 +// Returns a jQuery object.
  598 +function findAll(elems, selector) {
  599 + return elems.filter(selector).add(elems.find(selector));
  600 +}
  601 +
  602 +function parseHTML(html) {
  603 + return $.parseHTML(html, document, true)
  604 +}
  605 +
  606 +// Internal: Extracts container and metadata from response.
  607 +//
  608 +// 1. Extracts X-PJAX-URL header if set
  609 +// 2. Extracts inline <title> tags
  610 +// 3. Builds response Element and extracts fragment if set
  611 +//
  612 +// data - String response data
  613 +// xhr - XHR response
  614 +// options - pjax options Object
  615 +//
  616 +// Returns an Object with url, title, and contents keys.
  617 +function extractContainer(data, xhr, options) {
  618 + var obj = {}
  619 +
  620 + // Prefer X-PJAX-URL header if it was set, otherwise fallback to
  621 + // using the original requested url.
  622 + obj.url = stripPjaxParam(xhr.getResponseHeader('X-PJAX-URL') || options.requestUrl)
  623 +
  624 + // Attempt to parse response html into elements
  625 + if (/<html/i.test(data)) {
  626 + var $head = $(parseHTML(data.match(/<head[^>]*>([\s\S.]*)<\/head>/i)[0]))
  627 + var $body = $(parseHTML(data.match(/<body[^>]*>([\s\S.]*)<\/body>/i)[0]))
  628 + } else {
  629 + var $head = $body = $(parseHTML(data))
  630 + }
  631 +
  632 + // If response data is empty, return fast
  633 + if ($body.length === 0)
  634 + return obj
  635 +
  636 + // If there's a <title> tag in the header, use it as
  637 + // the page's title.
  638 + obj.title = findAll($head, 'title').last().text()
  639 +
  640 + if (options.fragment) {
  641 + // If they specified a fragment, look for it in the response
  642 + // and pull it out.
  643 + if (options.fragment === 'body') {
  644 + var $fragment = $body
  645 + } else {
  646 + var $fragment = findAll($body, options.fragment).first()
  647 + }
  648 +
  649 + if ($fragment.length) {
  650 + obj.contents = $fragment.contents()
  651 +
  652 + // If there's no title, look for data-title and title attributes
  653 + // on the fragment
  654 + if (!obj.title)
  655 + obj.title = $fragment.attr('title') || $fragment.data('title')
  656 + }
  657 +
  658 + } else if (!/<html/i.test(data)) {
  659 + obj.contents = $body
  660 + }
  661 +
  662 + // Clean up any <title> tags
  663 + if (obj.contents) {
  664 + // Remove any parent title elements
  665 + obj.contents = obj.contents.not(function() { return $(this).is('title') })
  666 +
  667 + // Then scrub any titles from their descendants
  668 + obj.contents.find('title').remove()
  669 +
  670 + // Gather all script[src] elements
  671 + //obj.scripts = findAll(obj.contents, 'script[src]').remove()
  672 + obj.contents = obj.contents.not(obj.scripts)
  673 + }
  674 +
  675 + // Trim any whitespace off the title
  676 + if (obj.title) obj.title = $.trim(obj.title)
  677 +
  678 + return obj
  679 +}
  680 +
  681 +// Load an execute scripts using standard script request.
  682 +//
  683 +// Avoids jQuery's traditional $.getScript which does a XHR request and
  684 +// globalEval.
  685 +//
  686 +// scripts - jQuery object of script Elements
  687 +//
  688 +// Returns nothing.
  689 +function executeScriptTags(scripts) {
  690 + if (!scripts) return
  691 +
  692 + var existingScripts = $('script[src]')
  693 +
  694 + scripts.each(function() {
  695 + var src = this.src
  696 + var matchedScripts = existingScripts.filter(function() {
  697 + return this.src === src
  698 + })
  699 + if (matchedScripts.length) return
  700 +
  701 + var script = document.createElement('script')
  702 + script.type = $(this).attr('type')
  703 + script.src = $(this).attr('src')
  704 + document.head.appendChild(script)
  705 + })
  706 +}
  707 +
  708 +// Internal: History DOM caching class.
  709 +var cacheMapping = {}
  710 +var cacheForwardStack = []
  711 +var cacheBackStack = []
  712 +
  713 +// Push previous state id and container contents into the history
  714 +// cache. Should be called in conjunction with `pushState` to save the
  715 +// previous container contents.
  716 +//
  717 +// id - State ID Number
  718 +// value - DOM Element to cache
  719 +//
  720 +// Returns nothing.
  721 +function cachePush(id, value) {
  722 + cacheMapping[id] = value
  723 + cacheBackStack.push(id)
  724 +
  725 + // Remove all entires in forward history stack after pushing
  726 + // a new page.
  727 + while (cacheForwardStack.length)
  728 + delete cacheMapping[cacheForwardStack.shift()]
  729 +
  730 + // Trim back history stack to max cache length.
  731 + while (cacheBackStack.length > pjax.defaults.maxCacheLength)
  732 + delete cacheMapping[cacheBackStack.shift()]
  733 +}
  734 +
  735 +// Shifts cache from directional history cache. Should be
  736 +// called on `popstate` with the previous state id and container
  737 +// contents.
  738 +//
  739 +// direction - "forward" or "back" String
  740 +// id - State ID Number
  741 +// value - DOM Element to cache
  742 +//
  743 +// Returns nothing.
  744 +function cachePop(direction, id, value) {
  745 + var pushStack, popStack
  746 + cacheMapping[id] = value
  747 +
  748 + if (direction === 'forward') {
  749 + pushStack = cacheBackStack
  750 + popStack = cacheForwardStack
  751 + } else {
  752 + pushStack = cacheForwardStack
  753 + popStack = cacheBackStack
  754 + }
  755 +
  756 + pushStack.push(id)
  757 + if (id = popStack.pop())
  758 + delete cacheMapping[id]
  759 +}
  760 +
  761 +// Public: Find version identifier for the initial page load.
  762 +//
  763 +// Returns String version or undefined.
  764 +function findVersion() {
  765 + return $('meta').filter(function() {
  766 + var name = $(this).attr('http-equiv')
  767 + return name && name.toUpperCase() === 'X-PJAX-VERSION'
  768 + }).attr('content')
  769 +}
  770 +
  771 +// Install pjax functions on $.pjax to enable pushState behavior.
  772 +//
  773 +// Does nothing if already enabled.
  774 +//
  775 +// Examples
  776 +//
  777 +// $.pjax.enable()
  778 +//
  779 +// Returns nothing.
  780 +function enable() {
  781 + $.fn.pjax = fnPjax
  782 + $.pjax = pjax
  783 + $.pjax.enable = $.noop
  784 + $.pjax.disable = disable
  785 + $.pjax.click = handleClick
  786 + $.pjax.submit = handleSubmit
  787 + $.pjax.reload = pjaxReload
  788 + $.pjax.defaults = {
  789 + timeout: 650,
  790 + push: true,
  791 + replace: false,
  792 + type: 'GET',
  793 + dataType: 'html',
  794 + scrollTo: 0,
  795 + maxCacheLength: 20,
  796 + version: findVersion
  797 + }
  798 + $(window).on('popstate.pjax', onPjaxPopstate)
  799 +}
  800 +
  801 +// Disable pushState behavior.
  802 +//
  803 +// This is the case when a browser doesn't support pushState. It is
  804 +// sometimes useful to disable pushState for debugging on a modern
  805 +// browser.
  806 +//
  807 +// Examples
  808 +//
  809 +// $.pjax.disable()
  810 +//
  811 +// Returns nothing.
  812 +function disable() {
  813 + $.fn.pjax = function() { return this }
  814 + $.pjax = fallbackPjax
  815 + $.pjax.enable = enable
  816 + $.pjax.disable = $.noop
  817 + $.pjax.click = $.noop
  818 + $.pjax.submit = $.noop
  819 + $.pjax.reload = function() { window.location.reload() }
  820 +
  821 + $(window).off('popstate.pjax', onPjaxPopstate)
  822 +}
  823 +
  824 +
  825 +// Add the state property to jQuery's event object so we can use it in
  826 +// $(window).bind('popstate')
  827 +if ( $.inArray('state', $.event.props) < 0 )
  828 + $.event.props.push('state')
  829 +
  830 +// Is pjax supported by this browser?
  831 +$.support.pjax =
  832 + window.history && window.history.pushState && window.history.replaceState &&
  833 + // pushState isn't reliable on iOS until 5.
  834 + !navigator.userAgent.match(/((iPod|iPhone|iPad).+\bOS\s+[1-4]|WebApps\/.+CFNetwork)/)
  835 +
  836 +$.support.pjax ? enable() : disable()
  837 +
  838 +})(jQuery);
... ...
plugins/pjax/public/javascripts/loading-overlay.js 0 → 100644
... ... @@ -0,0 +1,34 @@
  1 +if (typeof loading_overlay === 'undefined') {
  2 +
  3 +// block user actions while making a post. Also indicate the network transaction
  4 +loading_overlay = {
  5 +
  6 + show: function (selector) {
  7 + var element = jQuery(selector);
  8 + var overlay = jQuery('<div>', {
  9 + class: 'loading-overlay',
  10 + css: {
  11 + width: element.outerWidth(),
  12 + height: element.outerHeight(),
  13 + left: element.position().left,
  14 + top: element.position().top,
  15 + marginLeft: parseFloat(element.css('margin-left')),
  16 + marginTop: parseFloat(element.css('margin-top')),
  17 + marginRight: parseFloat(element.css('margin-right')),
  18 + marginBottom: parseFloat(element.css('margin-bottom')),
  19 + },
  20 + }).appendTo(element).get(0);
  21 +
  22 + overlay.dest = element;
  23 + element.loading_overlay = overlay;
  24 + },
  25 +
  26 + hide: function (selector) {
  27 + var element = jQuery(selector);
  28 + var overlay = element.find('.loading-overlay');
  29 + overlay.remove();
  30 + },
  31 +
  32 +};
  33 +
  34 +}
... ...
plugins/pjax/public/javascripts/patchwork.js 0 → 100644
... ... @@ -0,0 +1,41 @@
  1 +var patch = (function () {
  2 + /*jshint evil: true */
  3 +
  4 + "use strict";
  5 +
  6 + var global = new Function("return this;")(), // Get a reference to the global object
  7 + fnProps = Object.getOwnPropertyNames(Function); // Get the own ("static") properties of the Function constructor
  8 +
  9 + return function (original, originalRef, patches) {
  10 +
  11 + var ref = global[originalRef] = original, // Maintain a reference to the original constructor as a new property on the global object
  12 + args = [],
  13 + newRef, // This will be the new patched constructor
  14 + i;
  15 +
  16 + patches.called = patches.called || originalRef; // If we are not patching static calls just pass them through to the original function
  17 +
  18 + for (i = 0; i < original.length; i++) { // Match the arity of the original constructor
  19 + args[i] = "a" + i; // Give the arguments a name (native constructors don't care, but user-defined ones will break otherwise)
  20 + }
  21 +
  22 + if (patches.constructed) { // This string is evaluated to create the patched constructor body in the case that we are patching newed calls
  23 + args.push("'use strict'; return (!!this ? " + patches.constructed + " : " + patches.called + ").apply(null, arguments);");
  24 + } else { // This string is evaluated to create the patched constructor body in the case that we are only patching static calls
  25 + args.push("'use strict'; return (!!this ? new (Function.prototype.bind.apply(" + originalRef + ", [{}].concat([].slice.call(arguments))))() : " + patches.called + ".apply(null, arguments));");
  26 + }
  27 +
  28 + newRef = new (Function.prototype.bind.apply(Function, [{}].concat(args)))(); // Create a new function to wrap the patched constructor
  29 + newRef.prototype = original.prototype; // Keep a reference to the original prototype to ensure instances of the patch appear as instances of the original
  30 + newRef.prototype.constructor = newRef; // Ensure the constructor of patched instances is the patched constructor
  31 +
  32 + Object.getOwnPropertyNames(ref).forEach(function (property) { // Add any "static" properties of the original constructor to the patched one
  33 + if (fnProps.indexOf(property) < 0) { // Don't include static properties of Function since the patched constructor will already have them
  34 + newRef[property] = ref[property];
  35 + }
  36 + });
  37 +
  38 + return newRef; // Return the patched constructor
  39 + };
  40 +
  41 +}());
0 42 \ No newline at end of file
... ...
plugins/pjax/public/javascripts/pjax.js 0 → 100644
... ... @@ -0,0 +1,179 @@
  1 +
  2 +pjax = {
  3 +
  4 + states: {},
  5 + current_state: null,
  6 + initial_state: null,
  7 +
  8 + themes: {},
  9 +
  10 + load: function() {
  11 + var target = jQuery('#content');
  12 + var content = jQuery('#content-inner');
  13 + var loadingTarget = jQuery('#content');
  14 +
  15 + var container = '.pjax-container';
  16 + target.addClass('pjax-container');
  17 +
  18 + jQuery(document).pjax('a', container);
  19 +
  20 + jQuery(document).on('pjax:beforeSend', function(event, xhr, settings) {
  21 + var themes = jQuery.map(pjax.themes, function(theme) { return theme.id }).join(',');
  22 + xhr.setRequestHeader('X-PJAX-Themes', themes);
  23 + });
  24 +
  25 + jQuery(document).on('pjax:send', function(event) {
  26 + /* initial state is only initialized after the first navigation,
  27 + * so we do associate it here */
  28 + if (!pjax.states[jQuery.pjax.state.id])
  29 + pjax.states[jQuery.pjax.state.id] = pjax.initial_state;
  30 +
  31 + loading_overlay.show(loadingTarget);
  32 + });
  33 + jQuery(document).on('pjax:complete', function(event) {
  34 + loading_overlay.hide(loadingTarget);
  35 + });
  36 +
  37 + jQuery(document).on('pjax:popstate', function(event) {
  38 + pjax.popstate(event.state, event.direction);
  39 + });
  40 +
  41 + jQuery(document).on('pjax:timeout', function(event) {
  42 + // Prevent default timeout redirection behavior
  43 + event.preventDefault();
  44 + });
  45 +
  46 + pjax.patch.document_write();
  47 + //pjax.patch.xhr();
  48 + },
  49 +
  50 + update: function(state, from_state) {
  51 + if (!from_state)
  52 + from_state = this.current_state || this.initial_state;
  53 +
  54 + if (state.layout_template != from_state.layout_template) {
  55 + var lt_css = jQuery('head link[href^="/designs/templates"]');
  56 + lt_css.attr('href', lt_css.attr('href').replace(/templates\/.+\/stylesheets/, 'templates/'+state.layout_template+'/stylesheets'));
  57 + }
  58 +
  59 + if (state.theme.id != from_state.theme.id)
  60 + this.update_theme(state, from_state);
  61 +
  62 + document.body.className = state.body_classes;
  63 +
  64 + render_all_jquery_ui_widgets();
  65 +
  66 + userDataCallback(noosfero.user_data);
  67 +
  68 + // theme's update dependent on content. must be last thing to run
  69 + if (state.theme_update_js)
  70 + jQuery.globalEval(state.theme_update_js);
  71 +
  72 + pjax.current_state = state;
  73 + },
  74 +
  75 + update_theme: function(state, from_state) {
  76 + // wait for the new theme css load
  77 + this.loading.show(function() {
  78 + return !pjax.css_loaded('/designs/themes/'+state.theme.id+'/style.css');
  79 + });
  80 +
  81 + var css = jQuery('head link[href^="/designs/themes/'+from_state.theme.id+'/style"]');
  82 + css.attr('href', css.attr('href').replace(/themes\/.+\/style/, 'themes/'+state.theme.id+'/style'));
  83 +
  84 + jQuery('head link[rel="shortcut icon"]').attr('href', state.theme.favicon);
  85 +
  86 + jQuery('#theme-header').html(state.theme.header);
  87 + jQuery('#site-title').html(state.theme.site_title);
  88 + jQuery('#navigation ul').html(state.theme.extra_navigation);
  89 + jQuery('#theme-footer').html(state.theme.footer);
  90 +
  91 + jQuery('head script[src^="/designs/themes/'+from_state.theme.id+'/theme.js"]').remove();
  92 + if (state.theme.js_src) {
  93 + var script = document.createElement('script');
  94 + script.type = 'text/javascript', script.src = state.theme.js_src;
  95 + document.head.appendChild(script);
  96 + }
  97 + },
  98 +
  99 + popstate: function(state, direction) {
  100 + state = pjax.states[state.id];
  101 + var from_state = pjax.states[jQuery.pjax.state.id];
  102 + pjax.update(state, from_state);
  103 + },
  104 +
  105 + loading: {
  106 + repeatCallback: null,
  107 +
  108 + show: function(repeatCallback) {
  109 + this.repeatCallback = repeatCallback;
  110 + this.gears().show();
  111 + this.pool();
  112 + },
  113 +
  114 + pool: function() {
  115 + setTimeout(this.timeout, 50);
  116 + },
  117 +
  118 + timeout: function() {
  119 + var repeat = pjax.loading.repeatCallback();
  120 + if (repeat)
  121 + pjax.loading.pool();
  122 + else
  123 + pjax.loading.gears().hide();
  124 + },
  125 +
  126 + gears: function() {
  127 + var gears = jQuery('#pjax-loading-gears');
  128 + if (!gears.length) {
  129 + gears = jQuery('<div>', {
  130 + id: 'pjax-loading-gears',
  131 + });
  132 + gears.appendTo(document.body);
  133 + }
  134 +
  135 + return gears;
  136 + },
  137 + },
  138 +
  139 + css_loaded: function(path) {
  140 + var found = false;
  141 + for (index in document.styleSheets) {
  142 + var stylesheet = document.styleSheets[index];
  143 + if (!stylesheet.href)
  144 + continue;
  145 +
  146 + found = stylesheet.href.indexOf(path) != -1;
  147 + if (found)
  148 + break;
  149 + }
  150 + return found;
  151 + },
  152 +
  153 + patch: {
  154 +
  155 + document_write: function () {
  156 + // document.write doesn't work after ready state
  157 + document._write = document.write;
  158 + document.write = function (data) {
  159 + if (document.readyState != 'loading')
  160 + content.append(data);
  161 + else
  162 + document._write(data);
  163 + };
  164 + },
  165 +
  166 + xhr: function () {
  167 + XMLHttpRequest = patch(XMLHttpRequest, '_XMLHttpRequest', {
  168 + constructed: function() {
  169 + console.log('here')
  170 +
  171 + var args = [].slice.call(arguments);
  172 + return new (Function.prototype.bind.apply(_XMLHttpRequest, [{}].concat(args)));
  173 + },
  174 + });
  175 + },
  176 +
  177 + },
  178 +};
  179 +
... ...
plugins/pjax/public/style.css 0 → 120000
... ... @@ -0,0 +1 @@
  1 +stylesheets/pjax.css
0 2 \ No newline at end of file
... ...
plugins/pjax/public/stylesheets/_loading-overlay.scss 0 → 100644
... ... @@ -0,0 +1,7 @@
  1 +
  2 +.loading-overlay {
  3 + position: absolute;
  4 + background-image: url(/plugins/pjax/images/loading-overlay.gif);
  5 + opacity: 0.1;
  6 + z-index: 10;
  7 +}
... ...
plugins/pjax/public/stylesheets/pjax.css 0 → 100644
... ... @@ -0,0 +1,16 @@
  1 +.loading-overlay {
  2 + position: absolute;
  3 + background-image: url(/plugins/pjax/images/loading-overlay.gif);
  4 + opacity: 0.1;
  5 + z-index: 10; }
  6 +
  7 +#pjax-loading-gears {
  8 + display: none;
  9 + position: fixed;
  10 + top: 0;
  11 + left: 0;
  12 + right: 0;
  13 + bottom: 0;
  14 + z-index: 9999;
  15 + background: white url(/plugins/pjax/images/loading-gears.gif) no-repeat center center;
  16 + background-size: 5%; }
... ...
plugins/pjax/public/stylesheets/pjax.scss 0 → 100644
... ... @@ -0,0 +1,14 @@
  1 +@import 'loading-overlay';
  2 +
  3 +#pjax-loading-gears {
  4 + display: none; //default
  5 + position: fixed;
  6 + top: 0;
  7 + left: 0;
  8 + right: 0;
  9 + bottom: 0;
  10 + z-index: 9999;
  11 + background: white url(/plugins/pjax/images/loading-gears.gif) no-repeat center center;
  12 + background-size: 5%;
  13 +}
  14 +
... ...
plugins/pjax/vendor/plugins/xhr_status_except_pjax/init.rb 0 → 100644
... ... @@ -0,0 +1,11 @@
  1 +class ActionDispatch::Request
  2 +
  3 + def xml_http_request_with_pjax?
  4 + xml_http_request_without_pjax? and @env['HTTP_X_PJAX'].blank?
  5 + end
  6 +
  7 +end
  8 +
  9 + ActionDispatch::Request.send :alias_method_chain, :xml_http_request?, :pjax
  10 + ActionDispatch::Request.send :alias_method, :xhr?, :xml_http_request?
  11 +
... ...
plugins/pjax/views/layouts/pjax.html.erb 0 → 100644
... ... @@ -0,0 +1,19 @@
  1 +<%
  2 + update_js = render(:file => "#{Rails.root}/public/designs/themes/#{current_theme}/pjax_update.js.erb").to_json rescue nil
  3 +%>
  4 +
  5 +<title><%= h page_title %></title>
  6 +
  7 +<%= render :file => "#{Rails.root}/public/designs/themes/#{current_theme}/layouts/_content.html.erb" rescue
  8 + render "layouts/content" %>
  9 +
  10 +<%= javascript_tag do %>
  11 + <%= render 'pjax_shared/load_state.js' %>
  12 +
  13 + <% if update_js %>
  14 + state.theme_update_js = <%= update_js %>;
  15 + <% end %>
  16 +
  17 + pjax.update(state);
  18 +<% end %>
  19 +
... ...
plugins/pjax/views/pjax_layouts/_load_state_script.html.erb 0 → 100644
... ... @@ -0,0 +1,7 @@
  1 +<script type="text/javascript">
  2 + jQuery(function($) {
  3 + pjax.load();
  4 +
  5 + <%= render 'pjax_shared/load_state.js' %>
  6 + });
  7 +</script>
... ...
plugins/pjax/views/pjax_shared/_load_state.js.erb 0 → 100644
... ... @@ -0,0 +1,26 @@
  1 +var theme_id = <%= current_theme.to_json %>;
  2 +
  3 +<% if @pjax_loaded_themes.blank? or not @pjax_loaded_themes.include? current_theme %>
  4 + pjax.themes[theme_id] = {
  5 + id: theme_id,
  6 + favicon: '<%= image_path theme_favicon %>',
  7 + header: '<%= escape_javascript theme_header %>',
  8 + site_title: '<%= escape_javascript theme_site_title %>',
  9 + extra_navigation: '<%= escape_javascript theme_extra_navigation %>',
  10 + footer: '<%= escape_javascript theme_footer %>',
  11 + js_src: <%= theme_javascript_src.to_json %>,
  12 + };
  13 +<% end %>
  14 +
  15 +var state = {
  16 + body_classes: <%= body_classes.to_json %>,
  17 + layout_template: <%= layout_template.to_json %>,
  18 + theme: pjax.themes[theme_id],
  19 +};
  20 +
  21 +if (jQuery.pjax.state)
  22 + pjax.states[jQuery.pjax.state.id] = state;
  23 +
  24 +if (!pjax.initial_state)
  25 + pjax.initial_state = state;
  26 +
... ...
plugins/profile_members_headlines/README.md 0 → 100644
... ... @@ -0,0 +1,38 @@
  1 +README - Profile Members Headlines (ProfileMembersHeadlines Plugin)
  2 +===================================================================
  3 +
  4 +ProfileMembersHeadlines is a plugin that allow users to add a block that
  5 +displays the most recent post from members with the defined roles.
  6 +
  7 +The user also can configure the limit of headlines.
  8 +
  9 +This block will be available for all layout columns of communities and enterprises.
  10 +
  11 +INSTALL
  12 +=======
  13 +
  14 +Enable Plugin
  15 +-------------
  16 +
  17 +Also, you need to enable ProfileMembersHeadlines Plugin at you Noosfero:
  18 +
  19 +cd <your_noosfero_dir>
  20 +./script/noosfero-plugins enable profile_members_headlines
  21 +
  22 +Active Plugin
  23 +-------------
  24 +
  25 +As a Noosfero administrator user, go to administrator panel:
  26 +
  27 +- Click on "Enable/disable plugins" option
  28 +- Click on "Profile Members Headlines Plugin" checkbox
  29 +
  30 +Running DisplayContent tests
  31 +--------------------
  32 +
  33 +$ rake test:noosfero_plugins:profile_members_headlines_plugin
  34 +
  35 +LICENSE
  36 +=======
  37 +
  38 +See Noosfero license.
... ...
plugins/profile_members_headlines/lib/ext/person.rb 0 → 100644
... ... @@ -0,0 +1,13 @@
  1 +require_dependency 'person'
  2 +class Person
  3 +
  4 + def has_headline?
  5 + !headline.nil?
  6 + end
  7 +
  8 + def headline
  9 + return nil unless blog
  10 + blog.posts.published.first
  11 + end
  12 +
  13 +end
... ...
plugins/profile_members_headlines/lib/profile_members_headlines_block.rb 0 → 100644
... ... @@ -0,0 +1,43 @@
  1 +class ProfileMembersHeadlinesBlock < Block
  2 +
  3 + settings_items :interval, :type => 'integer', :default => 10
  4 + settings_items :limit, :type => 'integer', :default => 10
  5 + settings_items :navigation, :type => 'boolean', :default => true
  6 + settings_items :filtered_roles, :type => Array, :default => []
  7 +
  8 + attr_accessible :interval, :limit, :navigation, :filtered_roles
  9 +
  10 + def self.description
  11 + _('Display headlines from members of a community')
  12 + end
  13 +
  14 + def help
  15 + _('This block displays one post from members of a community.')
  16 + end
  17 +
  18 + include Noosfero::Plugin::HotSpot
  19 +
  20 + def default_title
  21 + _('Profile members headlines')
  22 + end
  23 +
  24 + def filtered_roles=(array)
  25 + self.settings[:filtered_roles] = array.map(&:to_i).select { |r| !r.to_i.zero? }
  26 + end
  27 +
  28 + def authors_list
  29 + result = owner.members_by_role(filtered_roles).public.includes([:image,:domains,:preferred_domain,:environment]).order('updated_at DESC')
  30 +
  31 + result.all(:limit => limit * 5).select { |p| p.has_headline?
  32 +}.slice(0..limit-1)
  33 + end
  34 +
  35 + def content(args={})
  36 + block = self
  37 + members = authors_list
  38 + proc do
  39 + render :file => 'blocks/headlines', :locals => { :block => block, :members => members }
  40 + end
  41 + end
  42 +
  43 +end
... ...
plugins/profile_members_headlines/lib/profile_members_headlines_plugin.rb 0 → 100644
... ... @@ -0,0 +1,23 @@
  1 +require_dependency File.dirname(__FILE__) + '/profile_members_headlines_block'
  2 +require 'ext/person'
  3 +
  4 +class ProfileMembersHeadlinesPlugin < Noosfero::Plugin
  5 +
  6 + def self.plugin_name
  7 + "Profile Members Headlines Plugin"
  8 + end
  9 +
  10 + def self.plugin_description
  11 + _("A plugin that adds a block where you can display posts from members.")
  12 + end
  13 +
  14 + def self.extra_blocks
  15 + { ProfileMembersHeadlinesBlock => { :type => [Community], :position =>
  16 +['1'] }}
  17 + end
  18 +
  19 + def stylesheet?
  20 + true
  21 + end
  22 +
  23 +end
... ...
plugins/profile_members_headlines/public/images/calendario-ico.png 0 → 100644

379 Bytes

plugins/profile_members_headlines/public/style.css 0 → 100644
... ... @@ -0,0 +1,59 @@
  1 +.profile-members-headlines-block {
  2 + width: 100%;
  3 +}
  4 +
  5 +.profile-members-headlines-block .headlines-container {
  6 + height: 180px;
  7 +}
  8 +
  9 +.profile-members-headlines-block .headlines-container .author,
  10 +.profile-members-headlines-block .headlines-container .post {
  11 + vertical-align: top;
  12 + min-width: 150px;
  13 +}
  14 +
  15 +.profile-members-headlines-block .headlines-container .author {
  16 + position: absolute;
  17 +}
  18 +
  19 +.profile-members-headlines-block .headlines-container .author p {
  20 + text-align: center;
  21 +}
  22 +
  23 +.profile-members-headlines-block .headlines-container .post {
  24 + margin-left: 160px;
  25 +}
  26 +
  27 +.profile-members-headlines-block #content .headlines-container h4 {
  28 + margin-top: 15px;
  29 + font-weight: bold;
  30 +}
  31 +
  32 +.profile-members-headlines-block .headlines-container a {
  33 + text-decoration: none;
  34 +}
  35 +
  36 +.profile-members-headlines-block .headlines-container a:hover {
  37 + text-decoration: underline;
  38 +}
  39 +
  40 +.profile-members-headlines-block .headlines-container .post .date {
  41 + margin-top: 15px;
  42 +}
  43 +
  44 +.profile-members-headlines-block .headlines-block-pager {
  45 + text-align: center;
  46 +}
  47 +
  48 +.profile-members-headlines-block .headlines-block-pager a {
  49 + padding: 0px 5px;
  50 +}
  51 +
  52 +.profile-members-headlines-block .headlines-block-pager a.activeSlide {
  53 + text-decoration: none;
  54 +}
  55 +
  56 +.profile-members-headlines-block .headlines-block-pager a.activeSlide:visited,
  57 +.profile-members-headlines-block .headlines-block-pager a.activeSlide {
  58 + color: #000;
  59 +}
... ...
plugins/profile_members_headlines/test/test_helper.rb 0 → 100644
... ... @@ -0,0 +1 @@
  1 +require File.dirname(__FILE__) + '/../../../test/test_helper'
... ...
plugins/profile_members_headlines/test/unit/profile_members_headlines_block_test.rb 0 → 100644
... ... @@ -0,0 +1,81 @@
  1 +require 'test_helper'
  2 +
  3 +class ProfileMembersHeadlinesBlockTest < ActiveSupport::TestCase
  4 +
  5 + include Noosfero::Plugin::HotSpot
  6 +
  7 + def setup
  8 + @environment = fast_create(Environment)
  9 + @environment.enable_plugin(ProfileMembersHeadlinesPlugin)
  10 +
  11 + @member1 = fast_create(Person)
  12 + @member2 = fast_create(Person)
  13 + @community = fast_create(Community)
  14 + community.add_member member1
  15 + community.add_member member2
  16 + end
  17 + attr_accessor :environment, :community, :member1, :member2
  18 +
  19 + should 'inherit from Block' do
  20 + assert_kind_of Block, ProfileMembersHeadlinesBlock.new
  21 + end
  22 +
  23 + should 'describe itself' do
  24 + assert_not_equal Block.description, ProfileMembersHeadlinesBlock.description
  25 + end
  26 +
  27 + should 'provide a default title' do
  28 + assert_not_equal Block.new.default_title, ProfileMembersHeadlinesBlock.new.default_title
  29 + end
  30 +
  31 + should 'not have authors if they have no blog' do
  32 + block = ProfileMembersHeadlinesBlock.create
  33 + block.stubs(:owner).returns(community)
  34 +
  35 + self.expects(:render).with(:file => 'blocks/headlines', :locals => { :block => block, :members => []}).returns('file-without-authors-and-headlines')
  36 + assert_equal 'file-without-authors-and-headlines', instance_eval(&block.content)
  37 + end
  38 +
  39 + should 'display headlines file' do
  40 + block = ProfileMembersHeadlinesBlock.create
  41 + block.stubs(:owner).returns(community)
  42 + blog = fast_create(Blog, :profile_id => member1.id)
  43 + post = fast_create(TinyMceArticle, :name => 'headlines', :profile_id => member1.id, :parent_id => blog.id)
  44 + self.expects(:render).with(:file => 'blocks/headlines', :locals => { :block => block, :members => []}).returns('file-with-authors-and-headlines')
  45 + assert_equal 'file-with-authors-and-headlines', instance_eval(&block.content)
  46 + end
  47 +
  48 + should 'select only authors with articles and selected roles to display' do
  49 + role = Role.create!(:name => 'role1')
  50 + community.affiliate(member1, role)
  51 + block = ProfileMembersHeadlinesBlock.new(:limit => 1, :filtered_roles => [role.id])
  52 + block.expects(:owner).returns(community)
  53 + blog = fast_create(Blog, :profile_id => member1.id)
  54 + post = fast_create(TinyMceArticle, :name => 'headlines', :profile_id => member1.id, :parent_id => blog.id)
  55 + assert_equal [member1], block.authors_list
  56 + end
  57 +
  58 + should 'not select private authors to display' do
  59 + block = ProfileMembersHeadlinesBlock.new(:limit => 1)
  60 + block.expects(:owner).returns(community)
  61 + private_author = fast_create(Person, :public_profile => false)
  62 + blog = fast_create(Blog, :profile_id => private_author.id)
  63 + post = fast_create(TinyMceArticle, :name => 'headlines', :profile_id => private_author.id, :parent_id => blog.id)
  64 + assert_equal [], block.authors_list
  65 + end
  66 +
  67 + should 'filter authors by roles to display' do
  68 + role = Role.create!(:name => 'role1')
  69 + author = fast_create(Person)
  70 + community.affiliate(author, role)
  71 +
  72 + block = ProfileMembersHeadlinesBlock.new(:limit => 3, :filtered_roles =>
  73 +[role.id])
  74 + block.stubs(:owner).returns(community)
  75 + community.members.each do |member|
  76 + blog = fast_create(Blog, :profile_id => member.id)
  77 + post = fast_create(TinyMceArticle, :name => 'headlines', :profile_id => member.id, :parent_id => blog.id)
  78 + end
  79 + assert_equal [author], block.authors_list
  80 + end
  81 +end
... ...
plugins/profile_members_headlines/test/unit/profile_members_headlines_plugin_test.rb 0 → 100644
... ... @@ -0,0 +1,41 @@
  1 +require 'test_helper'
  2 +
  3 +class ProfileMembersHeadlinesPluginTest < ActiveSupport::TestCase
  4 +
  5 + include Noosfero::Plugin::HotSpot
  6 +
  7 + def setup
  8 + @environment = fast_create(Environment)
  9 + @environment.enable_plugin(ProfileMembersHeadlinesPlugin)
  10 + end
  11 + attr_accessor :environment
  12 +
  13 + should 'has a name' do
  14 + assert_not_equal Noosfero::Plugin.plugin_name, ProfileMembersHeadlinesPlugin.plugin_name
  15 + end
  16 +
  17 + should 'describe itself' do
  18 + assert_not_equal Noosfero::Plugin.plugin_description, ProfileMembersHeadlinesPlugin.plugin_description
  19 + end
  20 +
  21 + should 'return ProfileMembersHeadlinesBlock in extra_blocks class method' do
  22 + assert ProfileMembersHeadlinesPlugin.extra_blocks.keys.include?(ProfileMembersHeadlinesBlock)
  23 + end
  24 +
  25 + should 'ProfileMembersHeadlinesBlock not available for environment' do
  26 + assert_not_includes plugins.dispatch(:extra_blocks, :type => Environment), ProfileMembersHeadlinesBlock
  27 + end
  28 +
  29 + should 'ProfileMembersHeadlinesBlock not available for people' do
  30 + assert_not_includes plugins.dispatch(:extra_blocks, :type => Person), ProfileMembersHeadlinesBlock
  31 + end
  32 +
  33 + should "ProfileMembersHeadlinesBlock be available for community" do
  34 + assert_includes plugins.dispatch(:extra_blocks, :type => Community), ProfileMembersHeadlinesBlock
  35 + end
  36 +
  37 + should 'has stylesheet' do
  38 + assert ProfileMembersHeadlinesPlugin.new.stylesheet?
  39 + end
  40 +
  41 +end
... ...
plugins/profile_members_headlines/views/blocks/headlines.html.erb 0 → 100644
... ... @@ -0,0 +1,39 @@
  1 +<%= block_title(block.title) %>
  2 +
  3 +<% unless members.empty? %>
  4 + <div class='headlines-container'>
  5 + <% members.each do |member| %>
  6 + <div>
  7 + <% headline = member.headline %>
  8 + <%= link_to_profile(profile_image(member, :big) + content_tag(:p, member.short_name), member.identifier, {:class => 'author'}) %>
  9 + <div class='post'>
  10 + <h4><%= link_to(headline.title, headline.url, :class => 'title') %></h4>
  11 + <div class='lead'>
  12 + <%= headline.short_lead %>
  13 + </div>
  14 + <div class='date'>
  15 + <%= show_date(headline.published_at) %>
  16 + </div>
  17 + <div class='tags'>
  18 + <%= headline.tags.map { |t| link_to(t, :controller => 'profile', :profile => member.identifier, :action => 'tags', :id => t.name ) }.join("\n") %>
  19 + </div>
  20 + </div>
  21 + </div>
  22 + <% end %>
  23 + </div>
  24 + <% if block.navigation %>
  25 + <div class='headlines-block-pager'>
  26 + </div>
  27 + <% end %>
  28 +
  29 + <script>
  30 + (function($) {
  31 + var options = {fx: 'fade', pause: 1, fastOnEvent: 1, timeout: <%= block.interval * 1000 %>};
  32 + options.pager = '#block-<%= block.id %> .headlines-block-pager';
  33 + $('#block-<%= block.id %> .headlines-container').cycle(options);
  34 + })(jQuery);
  35 + </script>
  36 +<% else %>
  37 + <em><%= _('No headlines to be shown.') %></em>
  38 +<% end %>
  39 +
... ...
plugins/profile_members_headlines/views/box_organizer/_profile_members_headlines_block.html.erb 0 → 100644
... ... @@ -0,0 +1,13 @@
  1 +<div id="profile_members_headlines_plugin">
  2 +
  3 + <%= labelled_form_field _('Headlines transition:'), select('block', 'interval', [[_('No automatic transition'), 0]] + [1, 2, 3, 4, 5, 10, 20, 30, 60].map {|item| [n_('Every 1 second', 'Every %d seconds', item) % item, item]}) %>
  4 +
  5 + <%= labelled_form_field check_box(:block, :navigation) + _('Display navigation buttons'), '' %>
  6 + <%= labelled_form_field _('Limit of headlines'), text_field(:block, :limit, :size => 3) %>
  7 +
  8 + <%= label :block, :filtered_roles, _('Choose which roles should be displayed:'), :class => 'formlabel' %>
  9 +
  10 + <% Profile::Roles.organization_member_roles(environment).each do |role| %>
  11 + <%= labelled_check_box(role.name, 'block[filtered_roles][]', role.id, @block.filtered_roles.include?(role.id)) %>
  12 + <% end %>
  13 +</div>
... ...
plugins/profile_members_headlines/views/profile_design 0 → 120000
... ... @@ -0,0 +1 @@
  1 +box_organizer/
0 2 \ No newline at end of file
... ...
public/500.html
... ... @@ -105,11 +105,11 @@
105 105 <div id='pt' style='display: none' class='message'>
106 106 <h1>Problema temporário no sistema</h1>
107 107 <p>
108   - Nossa equipe técnica está trabalhando nele, por favor tente mais tarde. Perdoe o incoveniente.
  108 + Nossa equipe técnica está trabalhando nele, por favor tente mais tarde. Perdoe o inconveniente.
109 109 </p>
110 110 <ul>
111 111 <li><a href='javascript: history.back()'>Voltar</a></li>
112   - <li><a href='/'>Ir para a página inicial do site.</a></li>
  112 + <li><a href='/'>Ir para a página inicial do site</a></li>
113 113 </ul>
114 114 </div>
115 115  
... ...
public/javascripts/application.js
... ... @@ -518,6 +518,21 @@ function new_qualifier_row(selector, select_qualifiers, delete_button) {
518 518 jQuery(selector).append("<tr><td>" + select_qualifiers + "</td><td id='certifier-area-" + index + "'><select></select>" + delete_button + "</td></tr>");
519 519 }
520 520  
  521 +function userDataCallback(data) {
  522 + noosfero.user_data = data;
  523 + if (data.login) {
  524 + jQuery('head').append('<meta content="authenticity_token" name="csrf-param" />');
  525 + jQuery('head').append('<meta content="'+jQuery.cookie("_noosfero_.XSRF-TOKEN")+'" name="csrf-token" />');
  526 + }
  527 + if (data.notice) {
  528 + display_notice(data.notice);
  529 + // clear notice so that it is not display again in the case this function is called again.
  530 + data.notice = null;
  531 + }
  532 + // Bind this event to do more actions with the user data (for example, inside plugins)
  533 + jQuery(window).trigger("userDataLoaded", data);
  534 +};
  535 +
521 536 // controls the display of the login/logout stuff
522 537 jQuery(function($) {
523 538 $.ajaxSetup({
... ... @@ -528,18 +543,9 @@ jQuery(function($) {
528 543 });
529 544  
530 545 var user_data = noosfero_root() + '/account/user_data';
531   - $.getJSON(user_data, function userDataCallBack(data) {
532   - if (data.login) {
533   - $('head').append('<meta content="authenticity_token" name="csrf-param" />');
534   - $('head').append('<meta content="'+$.cookie("_noosfero_.XSRF-TOKEN")+'" name="csrf-token" />');
535   - }
536   - if (data.notice) {
537   - display_notice(data.notice);
538   - }
539   - // Bind this event to do more actions with the user data (for example, inside plugins)
540   - $(window).trigger("userDataLoaded", data);
541   - });
  546 + $.getJSON(user_data, userDataCallback)
542 547  
  548 + $.ajaxSetup({ cache: false });
543 549 });
544 550  
545 551 // controls the display of contact list
... ... @@ -582,12 +588,9 @@ function display_notice(message) {
582 588 }
583 589  
584 590 function open_chat_window(self_link, anchor) {
585   - if(anchor) {
586   - jQuery('#chat').show('fast');
587   - jQuery("#chat" ).trigger('opengroup', anchor);
588   - } else {
589   - jQuery('#chat').toggle('fast');
590   - }
  591 + anchor = anchor || '#';
  592 + var noosfero_chat_window = window.open(noosfero_root() + '/chat' + anchor,'noosfero_chat','width=900,height=500');
  593 + noosfero_chat_window.focus();
591 594 return false;
592 595 }
593 596  
... ... @@ -1050,7 +1053,7 @@ jQuery(document).ready(function(){
1050 1053 function apply_zoom_to_images(zoom_text) {
1051 1054 jQuery(function($) {
1052 1055 $(window).load( function() {
1053   - $('#article .article-body img:not(.disable-zoom)').each( function(index) {
  1056 + $('#article .article-body img').each( function(index) {
1054 1057 var original = original_image_dimensions($(this).attr('src'));
1055 1058 if ($(this).width() < original['width'] || $(this).height() < original['height']) {
1056 1059 $(this).wrap('<div class="zoomable-image" />');
... ... @@ -1069,41 +1072,3 @@ function apply_zoom_to_images(zoom_text) {
1069 1072 });
1070 1073 });
1071 1074 }
1072   -
1073   -function getQueryParams(qs) {
1074   - qs = qs.split("+").join(" ");
1075   - var params = {},
1076   - tokens,
1077   - re = /[?&]?([^=]+)=([^&]*)/g;
1078   - while (tokens = re.exec(qs)) {
1079   - params[decodeURIComponent(tokens[1])]
1080   - = decodeURIComponent(tokens[2]);
1081   - }
1082   - return params;
1083   -}
1084   -
1085   -var fullwidth=false;
1086   -function toggle_fullwidth(itemId){
1087   - if(fullwidth){
1088   - jQuery(itemId).removeClass("fullwidth");
1089   - jQuery("#fullscreen-btn").show()
1090   - jQuery("#exit-fullscreen-btn").hide()
1091   - fullwidth = false;
1092   - }
1093   - else{
1094   - jQuery(itemId).addClass("fullwidth");
1095   - jQuery("#exit-fullscreen-btn").show()
1096   - jQuery("#fullscreen-btn").hide()
1097   - fullwidth = true;
1098   - }
1099   - jQuery(window).trigger("toggleFullwidth", fullwidth);
1100   -}
1101   -
1102   -function fullscreenPageLoad(itemId){
1103   - jQuery(document).ready(function(){
1104   - var $_GET = getQueryParams(document.location.search);
1105   - if ($_GET['fullscreen']==1){
1106   - toggle_fullwidth(itemId);
1107   - }
1108   - });
1109   -}
... ...
test/unit/organization_test.rb
... ... @@ -383,6 +383,33 @@ class OrganizationTest &lt; ActiveSupport::TestCase
383 383 assert !organization.errors[:cnpj.to_s].present?
384 384 end
385 385  
  386 + should 'get members by role' do
  387 + community = fast_create(Community)
  388 + role1 = Role.create!(:name => 'role1')
  389 + person1 = fast_create(Person)
  390 + community.affiliate(person1, role1)
  391 + role2 = Role.create!(:name => 'role2')
  392 + person2 = fast_create(Person)
  393 + community.affiliate(person2, role2)
  394 +
  395 + assert_equal [person1], community.members_by_role([role1])
  396 + end
  397 +
  398 + should 'get members by more than one role' do
  399 + community = fast_create(Community)
  400 + role1 = Role.create!(:name => 'role1')
  401 + person1 = fast_create(Person)
  402 + community.affiliate(person1, role1)
  403 + role2 = Role.create!(:name => 'role2')
  404 + person2 = fast_create(Person)
  405 + community.affiliate(person2, role2)
  406 + role3 = Role.create!(:name => 'role3')
  407 + person3 = fast_create(Person)
  408 + community.affiliate(person3, role3)
  409 +
  410 + assert_equal [person2, person3], community.members_by_role([role2, role3])
  411 + end
  412 +
386 413 should 'return members by role in a json format' do
387 414 organization = fast_create(Organization)
388 415 p1 = create_user('person-1').person
... ...
vendor/plugins/noosfero_caching/init.rb
... ... @@ -49,11 +49,11 @@ module NoosferoHttpCaching
49 49  
50 50 # filter off all cookies except for plugin-provided ones that are
51 51 # path-specific (i.e path != "/").
52   - def remove_unwanted_cookies(cookie_list)
53   - return nil if cookie_list.nil?
54   - cookie_list.select do |c|
  52 + def remove_unwanted_cookies(set_cookie)
  53 + return nil if set_cookie.nil?
  54 + set_cookie.split(/\s*,\s*/).select do |c|
55 55 c =~ /^_noosfero_plugin_\w+=/ && c =~ /path=\/\w+/
56   - end
  56 + end.join(', ')
57 57 end
58 58  
59 59 end
... ... @@ -61,7 +61,7 @@ module NoosferoHttpCaching
61 61 end
62 62  
63 63 unless Rails.env.development?
64   - middleware = Rails.application.config.middleware
  64 + middleware = Noosfero::Application.config.middleware
65 65 ActionController::Base.send(:include, NoosferoHttpCaching)
66   - middleware.use NoosferoHttpCaching::Middleware
  66 + middleware.insert_before ::ActionDispatch::Cookies, NoosferoHttpCaching::Middleware
67 67 end
... ...