Commit d1609c731d1a14066b2864a25196f5bbeb69b4a9

Authored by Daniela Feitosa
2 parents 527a4b0d 9f616e0b

Merge remote branch 'unb-gama/AI2852-article_access_level' into AI2852

Conflicts:
	app/models/person.rb
	db/schema.rb
app/controllers/my_profile/cms_controller.rb
... ... @@ -2,6 +2,8 @@ class CmsController < MyProfileController
2 2  
3 3 protect 'edit_profile', :profile, :only => [:set_home_page]
4 4  
  5 + include ArticleHelper
  6 +
5 7 def self.protect_if(*args)
6 8 before_filter(*args) do |c|
7 9 user, profile = c.send(:user), c.send(:profile)
... ... @@ -70,6 +72,14 @@ class CmsController < MyProfileController
70 72 translations if @article.translatable?
71 73 continue = params[:continue]
72 74  
  75 + @article.article_privacy_exceptions = params[:q].split(/,/).map{|n| environment.people.find n.to_i} unless params[:q].nil?
  76 +
  77 + @tokenized_children = prepare_to_token_input(
  78 + profile.members.map{|m|
  79 + m if @article.article_privacy_exceptions.include?(m)
  80 + }.compact
  81 + )
  82 +
73 83 refuse_blocks
74 84 record_coming
75 85 if request.post?
... ... @@ -130,6 +140,8 @@ class CmsController < MyProfileController
130 140  
131 141 continue = params[:continue]
132 142 if request.post?
  143 + @article.article_privacy_exceptions = params[:q].split(/,/).map{|n| environment.people.find n.to_i} unless params[:q].nil?
  144 +
133 145 if @article.save
134 146 if continue
135 147 redirect_to :action => 'edit', :id => @article
... ... @@ -290,6 +302,12 @@ class CmsController < MyProfileController
290 302 render :text => article_list_to_json(results), :content_type => 'application/json'
291 303 end
292 304  
  305 + def search_article_privacy_exceptions
  306 + arg = params[:q].downcase
  307 + result = profile.members.find(:all, :conditions => ['LOWER(name) LIKE ?', "%#{arg}%"])
  308 + render :text => prepare_to_token_input(result).to_json
  309 + end
  310 +
293 311 def media_upload
294 312 files_uploaded = []
295 313 parent = check_parent(params[:parent_id])
... ...
app/helpers/application_helper.rb
... ... @@ -40,6 +40,8 @@ module ApplicationHelper
40 40  
41 41 include Noosfero::Gravatar
42 42  
  43 + include TokenHelper
  44 +
43 45 def locale
44 46 (@page && !@page.language.blank?) ? @page.language : FastGettext.locale
45 47 end
... ... @@ -1300,10 +1302,6 @@ module ApplicationHelper
1300 1302 content_tag(:div, content_tag(:ul, titles) + raw(contents), :class => 'ui-tabs')
1301 1303 end
1302 1304  
1303   - def jquery_token_input_messages_json(hintText = _('Type in an keyword'), noResultsText = _('No results'), searchingText = _('Searching...'))
1304   - "hintText: '#{hintText}', noResultsText: '#{noResultsText}', searchingText: '#{searchingText}'"
1305   - end
1306   -
1307 1305 def delete_article_message(article)
1308 1306 if article.folder?
1309 1307 _("Are you sure that you want to remove the folder \"#{article.name}\"? Note that all the items inside it will also be removed!")
... ... @@ -1344,50 +1342,6 @@ module ApplicationHelper
1344 1342 )
1345 1343 end
1346 1344  
1347   - def token_input_field_tag(name, element_id, search_action, options = {}, text_field_options = {}, html_options = {})
1348   - options[:min_chars] ||= 3
1349   - options[:hint_text] ||= _("Type in a search term")
1350   - options[:no_results_text] ||= _("No results")
1351   - options[:searching_text] ||= _("Searching...")
1352   - options[:search_delay] ||= 1000
1353   - options[:prevent_duplicates] ||= true
1354   - options[:backspace_delete_item] ||= false
1355   - options[:focus] ||= false
1356   - options[:avoid_enter] ||= true
1357   - options[:on_result] ||= 'null'
1358   - options[:on_add] ||= 'null'
1359   - options[:on_delete] ||= 'null'
1360   - options[:on_ready] ||= 'null'
1361   -
1362   - result = text_field_tag(name, nil, text_field_options.merge(html_options.merge({:id => element_id})))
1363   - result += javascript_tag("jQuery('##{element_id}')
1364   - .tokenInput('#{url_for(search_action)}', {
1365   - minChars: #{options[:min_chars].to_json},
1366   - prePopulate: #{options[:pre_populate].to_json},
1367   - hintText: #{options[:hint_text].to_json},
1368   - noResultsText: #{options[:no_results_text].to_json},
1369   - searchingText: #{options[:searching_text].to_json},
1370   - searchDelay: #{options[:serach_delay].to_json},
1371   - preventDuplicates: #{options[:prevent_duplicates].to_json},
1372   - backspaceDeleteItem: #{options[:backspace_delete_item].to_json},
1373   - queryParam: #{name.to_json},
1374   - tokenLimit: #{options[:token_limit].to_json},
1375   - onResult: #{options[:on_result]},
1376   - onAdd: #{options[:on_add]},
1377   - onDelete: #{options[:on_delete]},
1378   - onReady: #{options[:on_ready]},
1379   - });
1380   - ")
1381   - result += javascript_tag("jQuery('##{element_id}').focus();") if options[:focus]
1382   - if options[:avoid_enter]
1383   - result += javascript_tag("jQuery('#token-input-#{element_id}')
1384   - .live('keydown', function(event){
1385   - if(event.keyCode == '13') return false;
1386   - });")
1387   - end
1388   - result
1389   - end
1390   -
1391 1345 def expirable_content_reference(content, action, text, url, options = {})
1392 1346 reason = @plugins.dispatch("content_expire_#{action.to_s}", content).first
1393 1347 options[:title] = reason
... ...
app/helpers/article_helper.rb
1 1 module ArticleHelper
2 2  
3   - def custom_options_for_article(article)
  3 + include TokenHelper
  4 +
  5 + def custom_options_for_article(article, tokenized_children)
4 6 @article = article
5   - content_tag('h4', _('Visibility')) +
6   - content_tag('div',
7   - content_tag('div',
8   - radio_button(:article, :published, true) +
9   - content_tag('label', _('Public (visible to other people)'), :for => 'article_published_true')
10   - ) +
11   - content_tag('div',
12   - radio_button(:article, :published, false) +
13   - content_tag('label', _('Private'), :for => 'article_published_false')
14   - )
15   - ) +
  7 +
  8 + visibility_options(@article, tokenized_children) +
16 9 content_tag('h4', _('Options')) +
17 10 content_tag('div',
18 11 (article.profile.has_members? ?
... ... @@ -53,6 +46,28 @@ module ArticleHelper
53 46 )
54 47 end
55 48  
  49 + def visibility_options(article, tokenized_children)
  50 + content_tag('h4', _('Visibility')) +
  51 + content_tag('div',
  52 + content_tag('div',
  53 + radio_button(:article, :published, true) +
  54 + content_tag('label', _('Public (visible to other people)'), :for => 'article_published_true')
  55 + ) +
  56 + content_tag('div',
  57 + radio_button(:article, :published, false) +
  58 + content_tag('label', _('Private'), :for => 'article_published_false', :id => "label_private")
  59 + ) +
  60 + (article.profile.class == Community ? content_tag('div',
  61 + content_tag('label', _('Fill in the search field to add the exception users to see this content'), :id => "text-input-search-exception-users") +
  62 + token_input_field_tag(:q, 'search-article-privacy-exceptions', {:action => 'search_article_privacy_exceptions'},
  63 + {:focus => false, :hint_text => _('Type in a search term for a user'), :pre_populate => tokenized_children})) :
  64 + ''))
  65 + end
  66 +
  67 + def prepare_to_token_input(array)
  68 + array.map { |object| {:id => object.id, :name => object.name} }
  69 + end
  70 +
56 71 def cms_label_for_new_children
57 72 _('New article')
58 73 end
... ...
app/helpers/blog_helper.rb
1 1 module BlogHelper
2 2  
3   - def custom_options_for_article(article)
  3 + include ArticleHelper
  4 +
  5 + def custom_options_for_article(article,tokenized_children)
4 6 @article = article
5 7 hidden_field_tag('article[published]', 1) +
6   - hidden_field_tag('article[accept_comments]', 0)
  8 + hidden_field_tag('article[accept_comments]', 0) +
  9 + visibility_options(article,tokenized_children)
7 10 end
8 11  
9 12 def cms_label_for_new_children
... ...
app/helpers/cms_helper.rb
... ... @@ -22,9 +22,9 @@ module CmsHelper
22 22  
23 23 attr_reader :environment
24 24  
25   - def options_for_article(article)
  25 + def options_for_article(article, tokenized_children=nil)
26 26 article_helper = helper_for_article(article)
27   - article_helper.custom_options_for_article(article)
  27 + article_helper.custom_options_for_article(article, tokenized_children)
28 28 end
29 29  
30 30 def link_to_article(article)
... ...
app/helpers/folder_helper.rb
1 1 module FolderHelper
2 2  
3 3 include ShortFilename
  4 + include ArticleHelper
4 5  
5 6 def list_articles(articles, recursive = false)
6 7 if !articles.blank?
... ... @@ -60,19 +61,10 @@ module FolderHelper
60 61 "icon-new icon-new%s" % klass.icon_name
61 62 end
62 63  
63   - def custom_options_for_article(article)
  64 + def custom_options_for_article(article,tokenized_children)
64 65 @article = article
65   - content_tag('h4', _('Visibility')) +
66   - content_tag('div',
67   - content_tag('div',
68   - radio_button(:article, :published, true) +
69   - content_tag('label', _('Public (visible to other people)'), :for => 'article_published_true')
70   - ) +
71   - content_tag('div',
72   - radio_button(:article, :published, false) +
73   - content_tag('label', _('Private'), :for => 'article_published_false')
74   - )
75   - ) +
  66 +
  67 + visibility_options(article,tokenized_children) +
76 68 content_tag('div',
77 69 hidden_field_tag('article[accept_comments]', 0)
78 70 )
... ...
app/helpers/token_helper.rb 0 → 100644
... ... @@ -0,0 +1,51 @@
  1 +module TokenHelper
  2 +
  3 + def jquery_token_input_messages_json(hintText = _('Type in an keyword'), noResultsText = _('No results'), searchingText = _('Searching...'))
  4 + "hintText: '#{hintText}', noResultsText: '#{noResultsText}', searchingText: '#{searchingText}'"
  5 + end
  6 +
  7 + def token_input_field_tag(name, element_id, search_action, options = {}, text_field_options = {}, html_options = {})
  8 + options[:min_chars] ||= 3
  9 + options[:hint_text] ||= _("Type in a search term")
  10 + options[:no_results_text] ||= _("No results")
  11 + options[:searching_text] ||= _("Searching...")
  12 + options[:search_delay] ||= 1000
  13 + options[:prevent_duplicates] ||= true
  14 + options[:backspace_delete_item] ||= false
  15 + options[:focus] ||= false
  16 + options[:avoid_enter] ||= true
  17 + options[:on_result] ||= 'null'
  18 + options[:on_add] ||= 'null'
  19 + options[:on_delete] ||= 'null'
  20 + options[:on_ready] ||= 'null'
  21 +
  22 + result = text_field_tag(name, nil, text_field_options.merge(html_options.merge({:id => element_id})))
  23 + result += javascript_tag("jQuery('##{element_id}')
  24 + .tokenInput('#{url_for(search_action)}', {
  25 + minChars: #{options[:min_chars].to_json},
  26 + prePopulate: #{options[:pre_populate].to_json},
  27 + hintText: #{options[:hint_text].to_json},
  28 + noResultsText: #{options[:no_results_text].to_json},
  29 + searchingText: #{options[:searching_text].to_json},
  30 + searchDelay: #{options[:serach_delay].to_json},
  31 + preventDuplicates: #{options[:prevent_duplicates].to_json},
  32 + backspaceDeleteItem: #{options[:backspace_delete_item].to_json},
  33 + queryParam: #{name.to_json},
  34 + tokenLimit: #{options[:token_limit].to_json},
  35 + onResult: #{options[:on_result]},
  36 + onAdd: #{options[:on_add]},
  37 + onDelete: #{options[:on_delete]},
  38 + onReady: #{options[:on_ready]},
  39 + });
  40 + ")
  41 + result += javascript_tag("jQuery('##{element_id}').focus();") if options[:focus]
  42 + if options[:avoid_enter]
  43 + result += javascript_tag("jQuery('#token-input-#{element_id}')
  44 + .live('keydown', function(event){
  45 + if(event.keyCode == '13') return false;
  46 + });")
  47 + end
  48 + result
  49 + end
  50 +
  51 +end
0 52 \ No newline at end of file
... ...
app/models/article.rb
... ... @@ -69,6 +69,7 @@ class Article < ActiveRecord::Base
69 69 settings_items :allow_members_to_edit, :type => :boolean, :default => false
70 70 settings_items :moderate_comments, :type => :boolean, :default => false
71 71 settings_items :followers, :type => Array, :default => []
  72 + has_and_belongs_to_many :article_privacy_exceptions, :class_name => 'Person', :join_table => 'article_privacy_exceptions'
72 73  
73 74 belongs_to :reference_article, :class_name => "Article", :foreign_key => 'reference_article_id'
74 75  
... ... @@ -470,7 +471,8 @@ class Article < ActiveRecord::Base
470 471  
471 472 def display_unpublished_article_to?(user)
472 473 user == author || allow_view_private_content?(user) || user == profile ||
473   - user.is_admin?(profile.environment) || user.is_admin?(profile)
  474 + user.is_admin?(profile.environment) || user.is_admin?(profile) ||
  475 + article_privacy_exceptions.include?(user)
474 476 end
475 477  
476 478 def display_to?(user = nil)
... ...
app/models/person.rb
... ... @@ -64,6 +64,7 @@ class Person < Profile
64 64 has_many :scraps_sent, :class_name => 'Scrap', :foreign_key => :sender_id, :dependent => :destroy
65 65  
66 66 has_and_belongs_to_many :acepted_forums, :class_name => 'Forum', :join_table => 'terms_forum_people'
  67 + has_and_belongs_to_many :articles_with_access, :class_name => 'Article', :join_table => 'article_privacy_exceptions'
67 68  
68 69 named_scope :more_popular,
69 70 :select => "#{Profile.qualified_column_names}, count(friend_id) as total",
... ...
app/views/cms/edit.rhtml
1 1 <%= error_messages_for 'article' %>
  2 +<%= javascript_include_tag "article.js" %>
2 3  
3 4 <div class='<%= (environment.enabled?('media_panel') ? 'with_media_panel' : 'no_media_panel') %>'>
4 5 <% labelled_form_for 'article', @article, :html => { :multipart => true, :class => @type } do |f| %>
... ... @@ -33,7 +34,7 @@
33 34 <%= content_tag( 'small', _('Separate tags with commas') ) %>
34 35  
35 36 <div id='edit-article-options'>
36   - <%= options_for_article(@article) %>
  37 + <%= options_for_article(@article, @tokenized_children) %>
37 38 </div>
38 39  
39 40 <% button_bar do %>
... ...
db/migrate/20140108132730_article_privacy_exceptions.rb 0 → 100644
... ... @@ -0,0 +1,12 @@
  1 +class ArticlePrivacyExceptions < ActiveRecord::Migration
  2 + def self.up
  3 + create_table :article_privacy_exceptions, :id => false do |t|
  4 + t.integer :article_id
  5 + t.integer :person_id
  6 + end
  7 + end
  8 +
  9 + def self.down
  10 + drop_table :article_privacy_exceptions
  11 + end
  12 +end
... ...
db/schema.rb
... ... @@ -9,7 +9,7 @@
9 9 #
10 10 # It's strongly recommended to check this file into your version control system.
11 11  
12   -ActiveRecord::Schema.define(:version => 20131128161159) do
  12 +ActiveRecord::Schema.define(:version => 20140108132730) do
13 13  
14 14 create_table "abuse_reports", :force => true do |t|
15 15 t.integer "reporter_id"
... ... @@ -46,6 +46,11 @@ ActiveRecord::Schema.define(:version =&gt; 20131128161159) do
46 46 add_index "action_tracker_notifications", ["profile_id", "action_tracker_id"], :name => "index_action_tracker_notif_on_prof_id_act_tracker_id", :unique => true
47 47 add_index "action_tracker_notifications", ["profile_id"], :name => "index_action_tracker_notifications_on_profile_id"
48 48  
  49 + create_table "article_privacy_exceptions", :id => false, :force => true do |t|
  50 + t.integer "article_id"
  51 + t.integer "person_id"
  52 + end
  53 +
49 54 create_table "article_versions", :force => true do |t|
50 55 t.integer "article_id"
51 56 t.integer "version"
... ... @@ -460,6 +465,7 @@ ActiveRecord::Schema.define(:version =&gt; 20131128161159) do
460 465 t.boolean "is_template", :default => false
461 466 t.integer "template_id"
462 467 t.string "redirection_after_login"
  468 + t.text "settings"
463 469 end
464 470  
465 471 add_index "profiles", ["environment_id"], :name => "index_profiles_on_environment_id"
... ...
features/edit_article.feature
... ... @@ -22,6 +22,70 @@ Feature: edit article
22 22 And I go to joaosilva's control panel
23 23 Then I should see "My Folder"
24 24  
  25 + @selenium
  26 + Scenario: denied access folder for a not logged user
  27 + Given the following communities
  28 + | name | identifier | owner |
  29 + | Free Software | freesoftware | joaosilva |
  30 + And the following users
  31 + | login | name |
  32 + | mario | Mario Souto |
  33 + | maria | Maria Silva |
  34 + And "Mario Souto" is a member of "Free Software"
  35 + And "Maria Silva" is a member of "Free Software"
  36 + And I am on freesoftware's control panel
  37 + And I follow "Manage Content"
  38 + And I follow "New content"
  39 + When I follow "Folder"
  40 + And I fill in "Title" with "My Folder"
  41 + And I choose "article_published_false"
  42 + And I press "Save"
  43 + And I log off
  44 + And I go to /freesoftware/my-folder
  45 + Then I should see "Access denied"
  46 +
  47 + @selenium
  48 + Scenario: show exception users field when you choose the private option
  49 + Given the following communities
  50 + | name | identifier | owner |
  51 + | Free Software | freesoftware | joaosilva |
  52 + And the following users
  53 + | login | name |
  54 + | mario | Mario Souto |
  55 + | maria | Maria Silva |
  56 + And "Mario Souto" is a member of "Free Software"
  57 + And "Maria Silva" is a member of "Free Software"
  58 + And I am on freesoftware's control panel
  59 + And I follow "Manage Content"
  60 + And I follow "New content"
  61 + When I follow "Folder"
  62 + And I fill in "Title" with "My Folder"
  63 + And I choose "article_published_false"
  64 + Then I should see "Fill in the search field to add the exception users to see this content"
  65 +
  66 + @selenium
  67 + Scenario: allowed user should see the content of a folder
  68 + Given the following communities
  69 + | name | identifier | owner |
  70 + | Free Software | freesoftware | joaosilva |
  71 + And the following users
  72 + | login | name |
  73 + | mario | Mario Souto |
  74 + | maria | Maria Silva |
  75 + And the following articles
  76 + | owner | name | body |
  77 + | freesoftware | My Folder | ... |
  78 + And "Mario Souto" is a member of "Free Software"
  79 + And "Maria Silva" is a member of "Free Software"
  80 + And I go to /freesoftware/my-folder
  81 + When I follow "Edit"
  82 + And I choose "article_published_false"
  83 + And I press "Save"
  84 + And I add to "My Folder" the following exception "Maria Silva"
  85 + And I am logged in as "maria"
  86 + And I go to /freesoftware/my-folder
  87 + Then I should see "My Folder"
  88 +
25 89 Scenario: redirect to the created folder
26 90 Given I am on joaosilva's control panel
27 91 And I follow "Manage Content"
... ... @@ -40,6 +104,7 @@ Feature: edit article
40 104 When I follow "Cancel" within ".main-block"
41 105 Then I should be on joaosilva's cms
42 106  
  107 + @selenium
43 108 Scenario: display tag list field when creating event
44 109 Given I am on joaosilva's control panel
45 110 And I follow "Manage Content"
... ...
features/step_definitions/content_steps.rb
... ... @@ -8,3 +8,15 @@ When /^I create a content of type &quot;([^\&quot;]*)&quot; with the following data$/ do |conte
8 8  
9 9 click_button "Save"
10 10 end
  11 +
  12 +And /^I add to "([^\"]*)" the following exception "([^\"]*)"$/ do |article_name, user_exception|
  13 + article = Article.find_by_name(article_name)
  14 + community = article.profile
  15 + raise "The article profile is not a community." unless community.class == Community
  16 +
  17 + my_user = community.members.find_by_name(user_exception)
  18 + raise "Could not find #{user_exception} in #{community.name} community." if my_user.nil?
  19 +
  20 + article.article_privacy_exceptions << my_user
  21 + article.save
  22 +end
11 23 \ No newline at end of file
... ...
public/javascripts/article.js
... ... @@ -172,4 +172,19 @@ jQuery(function($) {
172 172 return false;
173 173 });
174 174  
  175 + function show_hide_token_input() {
  176 + if($("#article_published_false").attr('checked'))
  177 + $("#text-input-search-exception-users").parent("div").css('display', 'block');
  178 + else
  179 + $("#text-input-search-exception-users").parent("div").css('display', 'none');
  180 + }
  181 +
  182 + if( $("#token-input-search-article-privacy-exceptions").length == 1 ) {
  183 + show_hide_token_input();
  184 +
  185 + //Hide / Show the text area
  186 + $("#article_published_false").click(show_hide_token_input);
  187 + $("#article_published_true").click(show_hide_token_input);
  188 + }
  189 +
175 190 });
... ...