diff --git a/Gemfile.lock b/Gemfile.lock index 6755bfb09..2d900006f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -32,7 +32,6 @@ PATH remote: . specs: your_platform (1.0.1) - acts-as-dag (>= 2.5.7) acts_as_tree auto_html (>= 1.6.4) autosize-rails @@ -47,6 +46,7 @@ PATH edit_mode (>= 1.0.2) eventmachine (>= 1.0.7) execjs (>= 2.5.2) + faker fnordmetric font-awesome-rails (~> 4.3.0) foreman @@ -88,6 +88,7 @@ PATH slim_breadcrumb (>= 0.0.3) strong_parameters sugar-rails + table-formatter to_xls transaction_retry turboboost @@ -135,24 +136,22 @@ GEM minitest (~> 5.1) thread_safe (~> 0.3, >= 0.3.4) tzinfo (~> 1.1) - acts-as-dag (4.0.0) - activemodel - activerecord (~> 4.0, >= 4.0.0) - acts_as_tree (2.2.0) + acts_as_tree (2.4.0) activerecord (>= 3.0.0) addressable (2.3.8) ambry (0.3.1) arel (6.0.0) - auto_html (1.6.4) - redcarpet (~> 3.1) - rinku (~> 1.5.0) - autoprefixer-rails (6.0.3) + auto_html (2.0.0) + gemoji (~> 2.1) + redcarpet (~> 3.3) + rinku (~> 1.7) + tag_helper (~> 0.5) + autoprefixer-rails (6.3.6) execjs - json autosize-rails (1.18.17) rails (>= 3.1) - bcrypt (3.1.10) - best_in_place (3.0.3) + bcrypt (3.1.11) + best_in_place (3.1.0) actionpack (>= 3.2) railties (>= 3.2) binding_of_caller (0.7.2) @@ -178,7 +177,7 @@ GEM rack (>= 1.0.0) rack-test (>= 0.5.4) xpath (~> 2.0) - carrierwave (0.10.0) + carrierwave (0.11.0) activemodel (>= 3.2.0) activesupport (>= 3.2.0) json (>= 1.7) @@ -199,6 +198,7 @@ GEM execjs coffee-script-source (1.9.1.1) colored (1.2) + concurrent-ruby (1.0.1) connection_pool (2.2.0) coveralls (0.7.12) multi_json (~> 1.10) @@ -209,7 +209,7 @@ GEM daemons (1.2.3) database_cleaner (1.4.1) debug_inspector (0.0.2) - devise (3.5.2) + devise (3.5.6) bcrypt (~> 3.0) orm_adapter (~> 0.1) railties (>= 3.2.6, < 5) @@ -224,9 +224,9 @@ GEM jquery-rails jquery-turbolinks rails (>= 3.2) - em-hiredis (0.3.0) + em-hiredis (0.3.1) eventmachine (~> 1.0) - hiredis (~> 0.5.0) + hiredis (~> 0.6.0) em-websocket (0.3.8) addressable (>= 2.1.1) eventmachine (>= 0.12.9) @@ -234,13 +234,15 @@ GEM launchy (~> 2.1) mail (~> 2.2) erubis (2.7.0) - eventmachine (1.0.8) + eventmachine (1.0.9.1) execjs (2.6.0) factory_girl (4.5.0) activesupport (>= 3.0.0) factory_girl_rails (4.5.0) factory_girl (~> 4.5.0) railties (>= 3.0.0) + faker (1.6.3) + i18n (~> 0.5) fastercsv (1.5.5) ffi (1.9.8) fnordmetric (1.2.9) @@ -262,13 +264,13 @@ GEM foreman (0.78.0) thor (~> 0.19.1) formatador (0.2.5) - formtastic (3.1.3) + formtastic (3.1.4) actionpack (>= 3.2.13) fuubar (1.3.3) rspec (>= 2.14.0, < 3.1.0) ruby-progressbar (~> 1.4) gemoji (2.1.0) - geocoder (1.2.11) + geocoder (1.3.2) globalid (0.3.5) activesupport (>= 4.1.0) gravatar_image_tag (1.2.0) @@ -294,13 +296,13 @@ GEM tilt highline (1.6.21) hike (1.2.3) - hiredis (0.5.2) + hiredis (0.6.1) hitimes (1.2.2) http-cookie (1.0.2) domain_name (~> 0.5) i18n (0.7.0) - i18n-js (3.0.0.rc11) - i18n (~> 0.6) + i18n-js (3.0.0.rc12) + i18n (~> 0.6, >= 0.6.6) icalendar (2.3.0) jbuilder (2.2.12) activesupport (>= 3.0.0, < 5) @@ -325,7 +327,7 @@ GEM jquery-ui-rails (4.2.1) railties (>= 3.2.16) json (1.8.3) - judge (2.1.0) + judge (2.1.1) rails (>= 3.1) kgio (2.9.3) launchy (2.4.3) @@ -340,11 +342,11 @@ GEM loofah (2.0.3) nokogiri (>= 1.5.9) lumberjack (1.0.9) - merit (2.3.2) + merit (2.3.3) ambry (~> 0.3.0) method_source (0.8.2) mime-types (1.25.1) - mini_magick (4.3.6) + mini_magick (4.5.1) mini_portile (0.6.2) minitest (5.8.1) multi_json (1.11.2) @@ -359,7 +361,7 @@ GEM orm_adapter (0.5.0) passgen (1.0.2) pdf-core (0.5.1) - phony (2.15.4) + phony (2.15.20) poltergeist (1.6.0) capybara (~> 2.1) cliver (~> 0.3.1) @@ -372,14 +374,14 @@ GEM coderay (~> 1.1.0) method_source (~> 0.8.1) slop (~> 3.4) - public_activity (1.4.2) + public_activity (1.4.3) actionpack (>= 3.0.0) activerecord (>= 3.0) i18n (>= 0.5.0) railties (>= 3.0.0) rack (1.6.4) - rack-mini-profiler (0.9.7) - rack (>= 1.1.3) + rack-mini-profiler (0.9.9.2) + rack (>= 1.2.0) rack-protection (1.5.3) rack rack-ssl (1.4.1) @@ -405,8 +407,8 @@ GEM rails-deprecated_sanitizer (>= 1.0.1) rails-html-sanitizer (1.0.2) loofah (~> 2.0) - rails-i18n (4.0.5) - i18n (~> 0.6) + rails-i18n (4.0.8) + i18n (~> 0.7) railties (~> 4.0) rails-settings-cached (0.4.1) rails (>= 4.0.0) @@ -423,16 +425,14 @@ GEM rdoc (4.2.0) json (~> 1.4) redcarpet (3.3.2) - redis (3.2.1) + redis (3.2.2) redis-actionpack (4.0.1) actionpack (~> 4) redis-rack (~> 1.5.0) redis-store (~> 1.1.0) - redis-activesupport (4.1.4) + redis-activesupport (4.1.5) activesupport (>= 3, < 5) redis-store (~> 1.1.0) - redis-namespace (1.5.2) - redis (~> 3.0, >= 3.0.4) redis-rack (1.5.0) rack (~> 1.5) redis-store (~> 1.1.0) @@ -440,20 +440,20 @@ GEM redis-actionpack (~> 4) redis-activesupport (~> 4) redis-store (~> 1.1.0) - redis-store (1.1.6) + redis-store (1.1.7) redis (>= 2.2) ref (1.0.5) refile (0.5.5) mime-types rest-client (~> 1.8) sinatra (~> 1.4.5) - responders (2.1.0) - railties (>= 4.2.0, < 5) + responders (2.1.2) + railties (>= 4.2.0, < 5.1) rest-client (1.8.0) http-cookie (>= 1.0.2, < 2.0) mime-types (>= 1.16, < 3.0) netrc (~> 0.7) - rinku (1.5.1) + rinku (1.7.3) rspec (2.14.1) rspec-core (~> 2.14.0) rspec-expectations (~> 2.14.0) @@ -472,7 +472,7 @@ GEM rspec-mocks (~> 2.14.0) rspec-rerun (0.3.0) rspec - ruby-ole (1.2.11.8) + ruby-ole (1.2.12) ruby-progressbar (1.7.5) ruby2ruby (2.1.3) ruby_parser (~> 3.1) @@ -490,21 +490,19 @@ GEM rdoc (~> 4.0) sexp_processor (4.5.0) shellany (0.0.1) - sidekiq (3.4.2) - celluloid (~> 0.16.0) + sidekiq (4.1.1) + concurrent-ruby (~> 1.0) connection_pool (~> 2.2, >= 2.2.0) - json (~> 1.0) redis (~> 3.2, >= 3.2.1) - redis-namespace (~> 1.5, >= 1.5.2) - sidekiq-limit_fetch (2.4.2) - sidekiq (>= 2.6.5, < 4.0) + sidekiq-limit_fetch (3.1.0) + sidekiq (>= 4) simplecov (0.9.2) docile (~> 1.1.0) multi_json (~> 1.0) simplecov-html (~> 0.9.0) simplecov-html (0.9.0) - sinatra (1.4.6) - rack (~> 1.4) + sinatra (1.4.7) + rack (~> 1.5) rack-protection (~> 1.4) tilt (>= 1.3, < 3) slim_breadcrumb (0.0.3) @@ -514,7 +512,7 @@ GEM sass-rails slop (3.6.0) spork (0.9.2) - spreadsheet (1.0.8) + spreadsheet (1.1.2) ruby-ole (>= 1.0) spring (1.3.3) sprockets (2.12.4) @@ -532,6 +530,8 @@ GEM railties (>= 3.2.0) sugar-rails (1.4.1) railties (>= 3.0.0) + table-formatter (0.2.0) + tag_helper (0.5.0) term-ansicolor (1.3.0) tins (~> 1.0) terminal-table (1.4.5) @@ -579,7 +579,7 @@ GEM kgio (~> 2.6) rack raindrops (~> 0.7) - warden (1.2.3) + warden (1.2.6) rack (>= 1.0) web-console (2.1.3) activemodel (>= 4.0) @@ -594,7 +594,7 @@ GEM eventmachine (~> 1.0.0.beta.4) rack thin - will_paginate (3.0.7) + will_paginate (3.1.0) xpath (2.0.0) nokogiri (~> 1.3) yajl-ruby (1.2.1) @@ -650,3 +650,6 @@ DEPENDENCIES workflow_kit! yard your_platform! + +BUNDLED WITH + 1.11.2 diff --git a/app/controllers/group_members_controller.rb b/app/controllers/group_members_controller.rb index 15cfa6836..b2a172161 100644 --- a/app/controllers/group_members_controller.rb +++ b/app/controllers/group_members_controller.rb @@ -31,21 +31,11 @@ def load_and_authorize_group def load_and_authorize_memberships @memberships = @group.memberships_for_member_list @memberships = @memberships.started_after(params[:valid_from].to_datetime) if params[:valid_from].present? - - allowed_members = @group.members.accessible_by(current_ability) - allowed_memberships = @group.memberships.where(descendant_id: allowed_members.map(&:id)) - @memberships = @memberships & allowed_memberships + @memberships = @memberships.select { |membership| can? :read, membership.user } end def load_members_from_memberships - # Fill also the members into a separate variable. - # - @members = @group.members.includes(:links_as_child).where(dag_links: {id: @memberships.map(&:id)}) - - # For some special groups, the first method of retreiving the members does not work. - # Fallback to these slower methods: - @members = User.includes(:links_as_child).where(dag_links: {id: @memberships.map(&:id)}) if @members.empty? - @members = @memberships.collect { |membership| membership.user } if @members.empty? + @members = @memberships.collect { |membership| membership.user } end def load_own_memberships diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index 6f767800e..05d0ebd70 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -51,20 +51,11 @@ def show Rack::MiniProfiler.step('groups#show controller: cancan') do # Make sure only members that are allowed to be seen are in this array! # - allowed_member_ids = @group.members.accessible_by(current_ability).pluck(:id) - allowed_memberships = @group.memberships.where(descendant_id: allowed_member_ids) - @memberships = @memberships & allowed_memberships + @memberships = @memberships.to_a.select { |m| can? :read, m.user } end Rack::MiniProfiler.step('groups#show controller: fetch members') do - # Fill also the members into a separate variable. - # - @members = @group.members.includes(:links_as_child).where(dag_links: {id: @memberships.map(&:id)}) - - # For some special groups, the first method of retreiving the members does not work. - # Fallback to these slower methods: - @members = User.includes(:links_as_child).where(dag_links: {id: @memberships.map(&:id)}) if @members.empty? - @members = @memberships.collect { |membership| membership.user } if @members.empty? + @members = @memberships.collect { |membership| membership.user } end # for performance reasons deactivated for the moment. diff --git a/app/controllers/memberships_controller.rb b/app/controllers/memberships_controller.rb new file mode 100644 index 000000000..fc56cff7c --- /dev/null +++ b/app/controllers/memberships_controller.rb @@ -0,0 +1,85 @@ +class MembershipsController < ApplicationController + + before_action :find_membership, except: [:index, :create] + authorize_resource + + respond_to :json, :html + + def index + if params[:user_id] + @object = @user = User.find(params[:user_id]) + authorize! :manage, @user + + @memberships = Membership.where(user: @user).now_and_in_the_past.direct.to_a + elsif params[:group_id] + @object = @group = Group.find(params[:group_id]) + authorize! :manage, @group + + @memberships = Membership.where(group: @group).now_and_in_the_past.direct.to_a + end + + set_current_navable @object + set_current_title "#{t(:memberships)}: #{@object.title}" + set_current_activity :is_managing_member_lists, @object + end + + def update + if @membership.update_attributes!(membership_params) + respond_to do |format| + format.json do + respond_with @membership + end + end + end + end + + def destroy + @membership.try(:destroy) && head(:no_content) + end + + def create + if membership_params[:user_title].present? + @user_id = User.find_by_title(membership_params[:user_title]).id + @group = Group.find membership_params[:group_id] + begin + @membership = UserGroupMembership.create(membership_params.merge({user_id: @user_id})) + @membership.valid_from = Date.new(membership_params["valid_from(1i)"].to_i, + membership_params["valid_from(2i)"].to_i, + membership_params["valid_from(3i)"].to_i) + @membership.save! + redirect_to group_members_path(@membership.group), change: 'members' + rescue => error + redirect_to group_members_path(@group), change: 'members', alert: "#{t(:adding_member_did_not_work)} #{error.message}" + end + else + head :no_content + end + end + + + private + + def membership_params + if can? :manage, @membership + params.require(:membership).permit(:valid_to, :valid_from, :user_title, :user_id, :group_id, :id, + :valid_from_localized_date, :valid_to_localized_date, + :needs_review, :ancestor_id, :ancestor_type, :descendant_id, :descendant_type) + elsif can? :update, @membership + params.require(:membership).permit(:valid_to, :valid_from, :user_title, :user_id, :group_id, :id, + :valid_from_localized_date, :valid_to_localized_date) + end + end + + def find_membership + #if params[:id].present? + @membership = Membership.find params[:id] + #else + # user = User.find params[ :user_id ] if params[ :user_id ] + # group = Group.find params[ :group_id ] if params[ :group_id ] + # if user && group + # @user_group_membership = UserGroupMembership.with_invalid.find_by_user_and_group user, group + # end + #end + end + +end diff --git a/app/controllers/user_group_memberships_controller.rb b/app/controllers/user_group_memberships_controller.rb index 788e3e117..0fdceeb3c 100644 --- a/app/controllers/user_group_memberships_controller.rb +++ b/app/controllers/user_group_memberships_controller.rb @@ -5,24 +5,6 @@ class UserGroupMembershipsController < ApplicationController respond_to :json, :html - def index - if params[:user_id] - @object = @user = User.find(params[:user_id]) - authorize! :manage, @user - - @memberships = UserGroupMembership.now_and_in_the_past.find_all_by_user(@user) - elsif params[:group_id] - @object = @group = Group.find(params[:group_id]) - authorize! :manage, @group - - @memberships = UserGroupMembership.now_and_in_the_past.find_all_by_group(@group) - end - - set_current_navable @object - set_current_title "#{t(:memberships)}: #{@object.title}" - set_current_activity :is_managing_member_lists, @object - end - def create if membership_params[:user_title].present? @user_id = User.find_by_title(membership_params[:user_title]).id diff --git a/app/helpers/corporate_vita_helper.rb b/app/helpers/corporate_vita_helper.rb index de4998ace..3e4451dc7 100644 --- a/app/helpers/corporate_vita_helper.rb +++ b/app/helpers/corporate_vita_helper.rb @@ -1,33 +1,29 @@ module CorporateVitaHelper - def corporate_vita_for_user( user ) + def corporate_vita_for_user(user) render partial: 'users/corporate_vita', locals: { user: @user, } end - def status_group_membership_valid_from_best_in_place( membership ) - best_in_place( membership, - :valid_from_localized_date, # type: :date, - url: user_group_membership_path( id: membership.id, - controller: :user_group_memberships, - action: :update, - format: :json - ), - :class => "status_group_date_of_joining" - ) + def status_group_membership_valid_from_best_in_place(membership) + best_in_place(membership, + :valid_from_localized_date, # type: :date, + url: membership_path(id: membership.id, + controller: :memberships, + action: :update, + format: :json), + :class => "status_group_date_of_joining") end - def status_group_membership_promoted_on_event( membership ) + def status_group_membership_promoted_on_event(membership) event = membership.event - best_in_place( membership, - :event_by_name, - url: status_group_membership_path(membership), - class: 'status_event_by_name', + best_in_place(membership, + :event_by_name, + url: status_group_membership_path(membership), + class: 'status_event_by_name', # display_with: lambda do |v| # link_to membership.event.name, membership.event, :class => 'status_event_label' # end - ) + ) end - - end diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb index b677ca505..0dd366253 100644 --- a/app/helpers/events_helper.rb +++ b/app/helpers/events_helper.rb @@ -5,11 +5,11 @@ def group_to_create_the_event_for end def groups_the_current_user_can_create_events_for - current_user.groups.find_all_by_flag(:officers_parent).collect { |op| op.parent_groups.first } + current_user.officer_groups.collect { |officer_group| officer_group.scope_group } - [nil] end def first_group_the_current_user_can_create_events_for - current_user.groups.find_all_by_flag(:officers_parent).first.try(:parent_groups).try(:first) + current_user.officer_groups.detect { |officer_group| officer_group.scope_group }.try(:scope_group) end def everyone_group_if_the_user_can_create_events_there diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb index e296543fa..da6b4cb03 100644 --- a/app/helpers/groups_helper.rb +++ b/app/helpers/groups_helper.rb @@ -55,7 +55,7 @@ def membership_li( user, group ) def sub_group_membership_lis( options = {} ) c = "" c += membership_li( options[ :user ], options[ :group ] ) - sub_groups_where_user_is_member = options[ :group ].child_groups & options[ :user ].groups + sub_groups_where_user_is_member = options[ :group ].child_groups & options[ :user ].groups.to_a sub_groups_where_user_is_member.select! { |group| can? :create_post, group } if options[:require_post_ability] current_indent = options[ :indent ] + 1 max_indent = options[ :max_indent ] diff --git a/app/models/ability.rb b/app/models/ability.rb index 660984350..2ca2e6f25 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -272,6 +272,9 @@ def rights_for_signed_in_users # in order to update their corporate vita. # can :update, UserGroupMembership, :descendant_id => user.id + can :update, Membership do |membership| + membership.user.id == user.id + end # Everyone who can join an event, can add images to this event. # Then, he will automatically join the event. @@ -295,12 +298,12 @@ def rights_for_signed_in_users event.contact_people.include? user end can [:update, :create_attachment_for, :destroy], Page do |page| - page.ancestor_events.map(&:contact_people).flatten.include? user + page.ancestor_events.collect { |event| event.contact_people.to_a }.flatten.include? user end can [:update, :destroy], Attachment do |attachment| attachment.author == user and attachment.parent.kind_of?(Page) and - attachment.parent.ancestor_events.map(&:contact_people).flatten.include?(user) + attachment.parent.ancestor_events.collect { |event| event.contact_people.to_a }.flatten.include?(user) end # If a user can read an object, he can comment it. diff --git a/app/models/active_record_cache_extension.rb b/app/models/active_record_cache_extension.rb index 68ae6ba5a..d12d69ba6 100644 --- a/app/models/active_record_cache_extension.rb +++ b/app/models/active_record_cache_extension.rb @@ -103,6 +103,12 @@ def delete_cached(method_name) Rails.cache.delete_matched "#{self.cache_key}/#{method_name}/*" end + def refresh_cached(method_name) + self.delete_cached method_name + self.send method_name + return self + end + def bulk_delete_cached(method_name, objects) ids = objects.map &:id regex = /.*\/(#{ids.join('|')})(-.*|)\/#{method_name}.*/ diff --git a/app/models/array.rb b/app/models/array.rb index fd62583dc..d2d948e6b 100644 --- a/app/models/array.rb +++ b/app/models/array.rb @@ -12,4 +12,8 @@ def reload collect { |element| element.reload if element.respond_to?(:reload) } end + def pluck(attr_name) + map(&attr_name) + end + end \ No newline at end of file diff --git a/app/models/cache_store_extension.rb b/app/models/cache_store_extension.rb index 75d26398c..c13d46eb3 100644 --- a/app/models/cache_store_extension.rb +++ b/app/models/cache_store_extension.rb @@ -10,21 +10,31 @@ def uncached return result end - #def fetch(key, options = {}, &block) - # rescue_from_undefined_class_or_module do - # rescue_from_other_errors(block) do - # super(key, {force: @ignore_cache}.merge(options), &block) - # end - # end - #end + def fetch(key, options = {}, &block) + rescue_from_undefined_class_or_module do + rescue_from_too_big_to_marshal do + rescue_from_other_errors(block) do + super(key, {force: @ignore_cache}.merge(options), &block) + end + end + end + end def delete_regex(regex) if @data - keys = @data.keys.select { |key| key =~ regex } + keys = list_keys(regex) @data.del(*keys) if keys.count > 0 end end + def list_keys(regex) + regex = Regexp.new "#{regex.reload.cache_key}/*" if regex.kind_of? ActiveRecord::Base + @data.keys.select { |key| key =~ regex } + end + def ls(regex) + list_keys(regex) + end + # This autoloads classes or modules that are required to instanciate # the cached objects. # @@ -45,23 +55,23 @@ def rescue_from_undefined_class_or_module end private :rescue_from_undefined_class_or_module - # # This provides a solution to errors like - # # "year too big to marshal: 16 UTC". - # # - # # Note that this error confusingly does not neccessarily have - # # something to do with caching dates. - # # - # def rescue_from_too_big_to_marshal - # begin - # yield - # rescue ArgumentError, NameError => exc - # if exc.message.match(%r|year too big to marshal: (.+)|) - # yield.reload # Reloading the ActiveRecord objects can help. - # else - # raise exc - # end - # end - # end + # This provides a solution to errors like + # "year too big to marshal: 16 UTC". + # + # Note that this error confusingly does not neccessarily have + # something to do with caching dates. + # + def rescue_from_too_big_to_marshal + begin + yield + rescue ArgumentError, NameError => exc + if exc.message.match(%r|year too big to marshal: (.+)|) + yield.reload # Reloading the ActiveRecord objects can help. + else + raise exc + end + end + end def rescue_from_other_errors(block_without_fetch, &block_with_fetch) begin diff --git a/app/models/concerns/dag_link_repair.rb b/app/models/concerns/dag_link_repair.rb deleted file mode 100644 index 00e1bb927..000000000 --- a/app/models/concerns/dag_link_repair.rb +++ /dev/null @@ -1,88 +0,0 @@ -concern :DagLinkRepair do - - class_methods do - - # This starts all automatic dag link repair operations. - # - def repair - RedundantLinkRepairer.scan_and_repair - end - - class RedundantLinkRepairer - - def self.scan_and_repair - self.new.scan_and_repair - end - - def scan_and_repair - mute_sql_log - scan - delete_redundant_links - recalculate_links - print "\n\nFinished.\n".blue - unmute_sql_log - end - - def mute_sql_log - @old_log_level = ActiveRecord::Base.logger.level - ActiveRecord::Base.logger.level = 1 - end - def unmute_sql_log - ActiveRecord::Base.logger.level = @old_log_level - end - - # There are cases when an indirect membership is represented by multiple dag links - # by error. We don't know how those issues arise, yet. This method scans for such - # occurances. - # - # Example: - # - # Alle Amtsträger - # |-------- Alle Seniores -------. - # | | - # | | - # |------ Alle Admins ------- User - # - # In this example, the link between "Alle Amtsträger" and "User" should be one - # DagLink(direct: false, count: 2). But, by error, there are two DagLink objects. - # - def scan - @occurances = [] - print "Scanning for redundant links.\n".blue - DagLink.where(direct: false).each do |link| - redundant_links = DagLink.where( - ancestor_type: link.ancestor_type, ancestor_id: link.ancestor_id, - descendant_type: link.descendant_type, descendant_id: link.descendant_id - ) - if redundant_links.count > 1 - @occurances << redundant_links - print "DATA CORRUPTION: REDUNDANT INDIRECT LINKS: #{redundant_links.inspect}\n\n".red - else - print ".".green - end - end - return @occurances - end - - def delete_redundant_links - print "\n\nRepairing redundant links.\n".blue - @occurances.each do |redundant_links| - redundant_links[1..-1].each do |redundant_link| # all links but the first, which is the original one - redundant_link.delete - print ".".blue - end - end - end - - def recalculate_links - print "\n\nRecalculating affected indirect validity ranges.\n".blue - @occurances.each do |redundant_links| - original_link = redundant_links[0].becomes UserGroupMembership - original_link.recalculate_validity_range_from_direct_memberships - original_link.save - print ".".blue - end - end - end - end -end \ No newline at end of file diff --git a/app/models/concerns/dag_link_validity_range.rb b/app/models/concerns/dag_link_validity_range.rb new file mode 100644 index 000000000..9a7e5a975 --- /dev/null +++ b/app/models/concerns/dag_link_validity_range.rb @@ -0,0 +1,199 @@ +# +# In this project, user group memberships do not neccessarily last forever. +# They can begin at some time and end at some time. This is expressed by the +# ValidityRange of a membership. +# +# In the database, direct memberships are stored as DagLinks, since we've +# used the acts_as_dag gem earlier. +# +# +# ## Examples +# +# membership.valid_from # => time +# membership.valid_to # => time +# membership.invalidate +# +# +# ## Scopes +# +# The same functionality can be described from tho different perspectives: +# +# From the "validity perspective", a membership can be currently valid or +# invalid. One can filter memberships by their validity status. +# +# From the "time perspective", there are current memberships and past +# memberships. +# +# These perspectives are linked by the fact that "past memberships" are just +# memberships that are currently invalid but have been valid in the past. +# +# Validity Perspective: +# +# UserGroupMembership.valid +# UserGroupMembership.invalid +# UserGroupMembership.with_invalid +# +# Time Perspective: +# +# UserGroupMembership.now +# UserGroupMembership.past +# UserGroupMembership.now_and_past +# UserGroupMembership.now_and_in_the_past +# UserGroupMembership.at_time(time) +# +# Default Scope: +# +# By default, the `valid` scope is applied, i.e. only memberships are +# found that are valid at present time. To override this scope, use the +# either `with_invalid` scope. +# +# +# ## Caveats +# +# * There is only one `valid_from` and one `valid_to` time per object. +# Therefore, you can't keep track of first invalidating an object and +# later re-validating it. Re-validating an object loses the information +# of first invalidating it. +# +# * Currently, the future is not handled (`Article.future` and +# `article.invalidate at: 1.hour.from.now` do not work.) But this is +# planned to be implemented in the future. +# +# * Some functionality has been extracted out into the temporal_scopes gem +# in order to test the scopes easily. But, this code has been abandoned +# since the Rails-4 migration took more time. +# => https://github.com/fiedl/temporal_scopes/blob/master/lib/temporal_scopes/has_temporal_scopes.rb +# +# * Rails 5 supports an `.or(...)` syntax: +# https://github.com/rails/rails/pull/16052 +# TODO: Refactor the queries in the scopes when migrating to Rails 5. +# +concern :DagLinkValidityRange do + + included do + attr_accessible :valid_from, :valid_to, :valid_from_localized_date, :valid_to_localized_date + before_validation :set_valid_from_to_now + + # Validity Perspective + # TODO: Allow :valid to include memberships that BECOME valid in the future. + scope :valid, -> { where("valid_from IS NULL OR valid_from <= ?", Time.zone.now).where("valid_to IS NULL OR valid_to >= ?", Time.zone.now) } + scope :invalid, -> { with_invalid.where("valid_to < ?", Time.zone.now) } + # scope :with_invalid # This is defined as method below due to some issues. + scope :only_valid, -> { valid } + scope :only_invalid, -> { invalid } + + # Time Perspective + scope :now, -> { valid } + scope :past, -> { invalid } + scope :in_the_past, -> { invalid } + scope :with_past, -> { with_invalid } + scope :now_and_past, -> { with_invalid } + scope :now_and_in_the_past, -> { with_invalid } + scope :at_time, -> (time) { with_past.where("valid_from IS NULL OR valid_from <= ?", time).where("valid_to IS NULL OR valid_to >= ?", time) } + scope :this_year, -> { with_invalid.where("valid_from >= ?", "#{Time.zone.now.year}-01-01 00:00:00") } + scope :started_after, -> (time) { where('NOT valid_from IS NULL').where("valid_from >= ?", time) } + end + + class_methods do + # This scope widens the query such that also memberships that are not valid + # at the present time are returned. + # + # Have a look at `rewhere`: + # https://github.com/rails/rails/commit/f950b2699f97749ef706c6939a84dfc85f0b05f2#diff-bf6dd6226db3aab589916f09236881c7R562 + # + # But `rewhere` is not enough. We need more filtering: + # https://github.com/fiedl/temporal_scopes/blob/master/lib/temporal_scopes/has_temporal_scopes.rb + # + # TODO: Check if this still needs the extra filter when migrating to Rails 5. + # + def with_invalid + relation = unscope(where: [:valid_from, :valid_to]) + relation.where_values.delete_if { |query| query.to_s.include?("valid_from") || query.to_s.include?("valid_to") } + relation + end + end + + concerning :Invalidation do + # This method ends the membership, i.e. sets the end of the validity range + # to the given time. + # + # The following examples are equivalent (despite the return value): + # + # membership.make_invalid + # membership.make_invalid at: Time.zone.now + # membership.make_invalid Time.zone.now + # membership.invalidate # => membership + # membership.update_attribute :valid_to, Time.zone.now # => true + # + def make_invalid(time = Time.zone.now) + time = time[:at] if time.kind_of?(Hash) && time[:at] + self.update_attribute(:valid_to, time) + return self + end + + # This is just an alias for `make_invalid`. + # + def invalidate(time = Time.zone.now) + self.make_invalid(time) + end + + # This method determines whether the membership can be invalidated. + # Direct memberships can be invalidated, whereas indirect memberships cannot. + # The validity of indirect memberships is derived from the validity of the direct ones. + # + def can_be_invalidated? + self.direct? + end + end + + concerning :ValidityCheck do + # This method checks whether the membership is valid at the given time. + # + # This is not to be confused with ActiveRecord's `valid` method, which checks whether the + # record matches the requirements to store it in the database. + # + # The following examples are equivalent: + # + # membership.currently_valid? + # membership.valid_at? Time.zone.now + # + def valid_at?(time) + (self.valid_from == nil || self.valid_from <= time) && (self.valid_to == nil || self.valid_to >= time) + end + + # This method checks whether the present time lies within the validity range + # of the membership. + # + def currently_valid? + valid_at?(Time.zone.now) + end + end + + concerning :Localization do + def valid_from_localized_date + self.valid_from ? I18n.localize(self.valid_from.try(:to_date)) : "" + end + def valid_from_localized_date=(new_date) + self.valid_from = new_date.to_datetime + valid_from_will_change! + end + + def set_valid_from_to_now(force = false) + self.valid_from ||= Time.zone.now if self.new_record? or force + return self + end + + def valid_to_localized_date + self.valid_to ? I18n.localize(self.valid_to.try(:to_date)) : "" + end + def valid_to_localized_date=(new_date) + if new_date == "-" + self.valid_to = nil + else + self.valid_to = new_date.to_datetime + end + valid_to_will_change! + end + end + +end \ No newline at end of file diff --git a/app/models/concerns/group_mailing_lists.rb b/app/models/concerns/group_mailing_lists.rb index 21b11ebb6..d4d1fcb37 100644 --- a/app/models/concerns/group_mailing_lists.rb +++ b/app/models/concerns/group_mailing_lists.rb @@ -42,12 +42,12 @@ def user_matches_mailing_list_sender_filter?(user) # Everyone can contact officers. true elsif self.corporation.present? - # If the group has an associated corporation, all members + # If the group has an associated corporation, all members and officers # of the corporation can post. - user && user.member_of?(self.corporation) + user && (user.member_of?(self.corporation) || user.officer_or_subgroup_officer_of?(self.corporation)) else - # If this is a regular group, all group members can post. - user && user.member_of?(self) + # If this is a regular group, all group members and officers can post. + user && (user.member_of?(self) || user.officer_of?(self)) end else false diff --git a/app/models/concerns/group_member_assignment.rb b/app/models/concerns/group_member_assignment.rb new file mode 100644 index 000000000..cd4a4137a --- /dev/null +++ b/app/models/concerns/group_member_assignment.rb @@ -0,0 +1,67 @@ +concern :GroupMemberAssignment do + + # This assings the given user as a member to the group, i.e. this will + # create a UserGroupMembership. + # + def assign_user(user, options = {}) + if user and not user.in?(self.direct_members) + membership = Membership.create(user: user, group: self) + time_of_joining = options[:joined_at] || options[:at] || options[:time] || Time.zone.now + membership.update_attributes valid_from: time_of_joining + return membership + end + end + def assign(user, options = {}) + assign_user user, options + end + + # This method will remove a UserGroupMembership, i.e. terminate the membership + # of the given user in this group. + # + def unassign_user(user, options = {}) + if user and user.in?(self.members) + time_of_unassignment = options[:at] || options[:time] || Time.zone.now + Membership.where(user: user, group: self).first.invalidate(at: time_of_unassignment) + end + end + def unassign(user, options = {}) + unassign_user user, options + end + + # This returns a string of the titles of the direct members of this group. This is used + # for in-place editing, for example. + # + # The string would be something like this: + # + # "#{user1.title}, #{user2.title}, ..." + # + def direct_members_titles_string + direct_members.collect { |user| user.title }.join( ", " ) + end + + # This sets the memberships of a group according to the given string of user titles. + # + # For example, after calling + # + # direct_members_titles_string = "#{user1.title}, #{user2.title}", + # + # the users `user1` and `user2` are the only direct members of the group. + # The memberships are removed using the standard methods, which means that the memberships + # are only marked as deleted. See: acts_as_paranoid_dag gem. + # + def direct_members_titles_string=( titles_string ) + new_members_titles = titles_string.split( "," ) + new_members = new_members_titles.collect do |title| + u = User.find_by_title( title.strip ) + self.errors.add :direct_member_titles_string, 'user not found: #{title}' unless u + u + end + for member in self.direct_members + unassign_user member unless member.in? new_members if member + end + for new_member in new_members + assign_user new_member if new_member + end + end + +end \ No newline at end of file diff --git a/app/models/concerns/group_memberships.rb b/app/models/concerns/group_memberships.rb new file mode 100644 index 000000000..1dfecd4d6 --- /dev/null +++ b/app/models/concerns/group_memberships.rb @@ -0,0 +1,68 @@ +concern :GroupMemberships do + + def memberships(reload = false) + Membership.where(group: self).now + end + + def direct_memberships + memberships.direct + end + + def indirect_memberships + memberships.indirect + end + + def memberships_of(user) + memberships.where(user: user) + end + + def membership_of(user) + memberships_of(user).first + end + + def memberships_for_member_list + memberships.join_validity_ranges_of_indirect_memberships + end + def memberships_for_member_list_count + cached { memberships_for_member_list.count } + end + + # This method builds a new membership having this group (self) as group associated. + # + def build_membership + Membership.build(group: self) + end + + def latest_memberships + cached do + self.memberships.with_invalid + .select { |membership| membership.valid_from.present? } + .sort_by { |membership| membership.valid_from } + .last(10) + end + end + + def memberships_this_year + cached do + self.memberships.this_year + end + end + + def members(reload = false) + MemberCollection.new(memberships: memberships.join_validity_ranges_of_indirect_memberships, group: self) + end + + def member_ids(reload = false) + @member_ids = nil if reload + @member_ids ||= members.map(&:id) + end + + def direct_members(reload = false) + MemberCollection.new(memberships: memberships.direct, group: self) + end + + def indirect_members + members.indirect + end + +end \ No newline at end of file diff --git a/app/models/concerns/group_workflows.rb b/app/models/concerns/group_workflows.rb new file mode 100644 index 000000000..15b0578bc --- /dev/null +++ b/app/models/concerns/group_workflows.rb @@ -0,0 +1,40 @@ +# This handles the workflows associations of groups. +# +# These methods override the standard methods, which are usual ActiveRecord +# associations methods created by `HasDagLinks`. +# But since the `Workflow` in the main application inherits from +# `WorkflowKit::Workflow` and single table inheritance and polymorphic +# associations do not always work together as expected in rails, +# as can be seen here, http://stackoverflow.com/questions/9628610, +# we have to override these methods. +# +# ActiveRecord associations require 'WorkflowKit::Workflow' to be stored +# in the database's type column, but by asking for the `child_workflows` +# we want to get objects of the `Workflow` type, not `WorkflowKit::Workflow`, +# since Workflow objects may have additional methods, added by the main +# application. +# +concern :GroupWorkflows do + + def workflows + child_workflows + end + + def child_workflows + Workflow + .joins(:links_as_child) + .where(dag_links: {ancestor_type: 'Group', ancestor_id: self.id}) + .uniq + end + + def descendant_workflows + workflows_of_self_and_connected_groups + end + + def workflows_of_self_and_connected_groups + cached do + (workflows + connected_descendant_groups.collect(&:workflows)).flatten.uniq + end + end + +end \ No newline at end of file diff --git a/app/models/concerns/membership_collection_validity_range.rb b/app/models/concerns/membership_collection_validity_range.rb new file mode 100644 index 000000000..de18fa853 --- /dev/null +++ b/app/models/concerns/membership_collection_validity_range.rb @@ -0,0 +1,172 @@ +# +# In this project, user group memberships do not neccessarily last forever. +# They can begin at some time and end at some time. This is expressed by the +# ValidityRange of a membership. +# +# +# ## Examples +# +# membership.valid_from # => time +# membership.valid_to # => time +# membership.invalidate +# +# +# ## Scopes +# +# The same functionality can be described from tho different perspectives: +# +# From the "validity perspective", a membership can be currently valid or +# invalid. One can filter memberships by their validity status. +# +# From the "time perspective", there are current memberships and past +# memberships. +# +# These perspectives are linked by the fact that "past memberships" are just +# memberships that are currently invalid but have been valid in the past. +# +# Validity Perspective: +# +# Membership.valid +# Membership.invalid +# Membership.with_invalid +# +# Time Perspective: +# +# Membership.now +# Membership.past +# Membership.now_and_past +# Membership.now_and_in_the_past +# Membership.at_time(time) +# +# Default Scope: +# +# By default, the `valid` scope is applied, i.e. only memberships are +# found that are valid at present time. To override this scope, use the +# either `with_invalid` scope. +# +# +# ## Caveats +# +# * There is only one `valid_from` and one `valid_to` time per object. +# Therefore, you can't keep track of first invalidating an object and +# later re-validating it. Re-validating an object loses the information +# of first invalidating it. +# +# Therefore, when a user leaves and re-joins a group, this is represented +# by two separate Membership objects. +# +# * Currently, the future is not handled (`Article.future` and +# `article.invalidate at: 1.hour.from.now` do not work.) But this is +# planned to be implemented in the future. +# +# * Some functionality has been extracted out into the temporal_scopes gem +# in order to test the scopes easily. But, this code has been abandoned +# since the Rails-4 migration took more time. +# => https://github.com/fiedl/temporal_scopes/blob/master/lib/temporal_scopes/has_temporal_scopes.rb +# +# * Rails 5 supports an `.or(...)` syntax: +# https://github.com/rails/rails/pull/16052 +# TODO: Refactor the queries in the scopes when migrating to Rails 5. +# +concern :MembershipCollectionValidityRange do + + concerning :ValidityPerspective do + def valid + @valid = true + return self + end + + def invalid + @invalid = true + return self + end + + def with_invalid + @with_invalid = true + return self + end + end + + concerning :TimePerspective do + def now + @now = true + return self + end + + def past + @past = true + return self + end + + def in_the_past + @past = true + return self + end + + def with_past + @with_past = true + return self + end + + def now_and_past + @now_and_in_the_past = true + return self + end + + def now_and_in_the_past + @now_and_in_the_past = true + return self + end + + def at_time(time) + @at_time = time + return self + end + + def this_year + @this_year = true + return self + end + + def started_after(time) + @started_after = time + return self + end + end + + private + + def dag_links_for(attrs = {}) + links = DagLink.where(ancestor_type: 'Group', descendant_type: 'User', direct: true) + links = links.where(descendant_id: attrs[:user].id) if attrs[:user] + links = links.where(ancestor_id: attrs[:group].id) if attrs[:group] + links = links.where(descendant_id: attrs[:user_ids]) if attrs[:user_ids] + links = links.where(ancestor_id: attrs[:group_ids]) if attrs[:group_ids] + + unless attrs[:ignore_validity_range_filters] + # Validity Perspective + # + links = links.valid if @valid + links = links.invalid if @invalid + links = links.with_invalid if @with_invalid + + # Time Perspective + # + links = links.now if @now + links = links.past if @past + links = links.with_past if @with_past + links = links.now_and_in_the_past if @now_and_in_the_past + links = links.at_time(@at_time) if @at_time + links = links.this_year if @this_year + links = links.started_after(@started_after) if @started_after + end + unless attrs[:no_eager_loading] + # Include the associated objects to avoid the N+1 problem. + # + links = links.includes(:ancestor, :descendant) + end + + return links + end + +end \ No newline at end of file diff --git a/app/models/concerns/membership_persistence.rb b/app/models/concerns/membership_persistence.rb new file mode 100644 index 000000000..8ab5c56e6 --- /dev/null +++ b/app/models/concerns/membership_persistence.rb @@ -0,0 +1,98 @@ +concern :MembershipPersistence do + + # Direct memberships are stored as DagLinks in the database. + # This is, because we've used the acts_as_dag gem earlier: + # https://github.com/resgraph/acts-as-dag + # + # In contrast to the gem, we do not store indirect links + # in the database anymore, since this makes write operations + # too expensive for large graphs. + # + def dag_link + @dag_link ||= DagLink.where(ancestor_type: 'Group', descendant_type: 'User', direct: true, + ancestor_id: group.id, descendant_id: user.id).first + end + + def id + dag_link.try(:id) + end + + def persisted? + dag_link.try(:persisted?) || false + end + + def save + write_attributes_to_dag_link + dag_link.save + end + + def save! + raise 'Cannot save! Indirect memberships are non-persistent objects.' unless direct? + write_attributes_to_dag_link + dag_link.changed? ? dag_link.save! : true + end + + def update_attributes!(attrs = {}) + set_attributes(attrs) + save! + end + + def update_attributes(attrs = {}) + set_attributes(attrs) + save if direct? + end + + def reload + @dag_link = nil + @valid_from = dag_link.valid_from + @valid_to = dag_link.valid_to + return self + end + + delegate :destroyed?, :new_record?, to: :dag_link + + def destroyable? + direct? + end + + def destroy + (destroyable? && dag_link.try(:destroy)) || raise("could not destroy membership #{id}.") + end + + def _read_attribute(key) + send(key) if key.in? [:valid_from, :valid_to] + end + + private + + def write_attributes_to_dag_link + dag_link.valid_from = @valid_from + dag_link.valid_to = @valid_to + dag_link.ancestor_id = @group.id + dag_link.descendant_id = @user.id + end + + def set_attributes(attrs) + attrs.each do |key, value| + send("#{key}=", value) + end + end + + + class_methods do + def base_class + Membership + end + + def primary_key + :id + end + + def build(params) + group_id = params[:group_id] || params[:group].try(:id) + user_id = params[:user_id] || params[:user].try(:id) + Membership.new(dag_link: DagLink.new(ancestor_type: 'Group', ancestor_id: group_id, descendant_type: 'User', descendant_id: user_id)) + end + end + +end \ No newline at end of file diff --git a/app/models/concerns/membership_review.rb b/app/models/concerns/membership_review.rb new file mode 100644 index 000000000..a90af5f14 --- /dev/null +++ b/app/models/concerns/membership_review.rb @@ -0,0 +1,18 @@ +concern :MembershipReview do + + def needs_review? + direct? && dag_link.has_flag?(:needs_review) + end + + def needs_review=(new_needs_review) + new_needs_review = false if new_needs_review == "false" + direct? || raise('Only direct memberships can be reviewed.') + dag_link.add_flag :needs_review if new_needs_review + dag_link.remove_flag :needs_review if not new_needs_review + end + + def needs_review! + self.needs_review = true + end + +end \ No newline at end of file diff --git a/app/models/concerns/membership_validity_range.rb b/app/models/concerns/membership_validity_range.rb new file mode 100644 index 000000000..fa43de720 --- /dev/null +++ b/app/models/concerns/membership_validity_range.rb @@ -0,0 +1,62 @@ +# This handles the methods that can be used on memberships +# concerning validity ranges, for example, invalidating a membership. +# +# The finder methods can be found in MembershipCollectionValidityRange. +# +concern :MembershipValidityRange do + + concerning :Invalidation do + # This method ends the membership, i.e. sets the end of the validity range + # to the given time. + # + # The following examples are equivalent: + # + # membership.make_invalid + # membership.make_invalid at: Time.zone.now + # membership.make_invalid Time.zone.now + # membership.invalidate # => membership + # + def make_invalid(time = Time.zone.now) + dag_link.try(:make_invalid, time) + return self.reload + end + + # This is just an alias for `make_invalid`. + # + def invalidate(time = Time.zone.now) + self.make_invalid(time) + end + + # This method determines whether the membership can be invalidated. + # Direct memberships can be invalidated, whereas indirect memberships cannot. + # The validity of indirect memberships is derived from the validity of the direct ones. + # + def can_be_invalidated? + self.direct? + end + end + + concerning :ValidityCheck do + # This method checks whether the membership is valid at the given time. + # + # This is not to be confused with ActiveRecord's `valid` method, which checks whether the + # record matches the requirements to store it in the database. + # + # The following examples are equivalent: + # + # membership.currently_valid? + # membership.valid_at? Time.zone.now + # + def valid_at?(time) + (self.valid_from == nil || self.valid_from <= time) && (self.valid_to == nil || self.valid_to >= time) + end + + # This method checks whether the present time lies within the validity range + # of the membership. + # + def currently_valid? + valid_at?(Time.zone.now) + end + end + +end \ No newline at end of file diff --git a/app/models/concerns/membership_validity_range_localization.rb b/app/models/concerns/membership_validity_range_localization.rb new file mode 100644 index 000000000..ad875da3b --- /dev/null +++ b/app/models/concerns/membership_validity_range_localization.rb @@ -0,0 +1,26 @@ +concern :MembershipValidityRangeLocalization do + + def valid_from_localized_date + self.valid_from ? I18n.localize(self.valid_from.try(:to_date)) : "" + end + def valid_from_localized_date=(new_date) + self.valid_from = new_date.to_datetime + end + + def set_valid_from_to_now(force = false) + self.valid_from ||= Time.zone.now if self.new_record? or force + return self + end + + def valid_to_localized_date + self.valid_to ? I18n.localize(self.valid_to.try(:to_date)) : "" + end + def valid_to_localized_date=(new_date) + if new_date == "-" + self.valid_to = nil + else + self.valid_to = new_date.to_datetime + end + end + +end \ No newline at end of file diff --git a/app/models/concerns/structureable_connected_descendants.rb b/app/models/concerns/structureable_connected_descendants.rb new file mode 100644 index 000000000..32c9d92f1 --- /dev/null +++ b/app/models/concerns/structureable_connected_descendants.rb @@ -0,0 +1,9 @@ +concern :StructureableConnectedDescendants do + + def connected_descendants + connected_descendant_groups.collect do |g| + [g] + g.members.to_a + g.connected_descendant_pages + g.child_events + end.flatten.uniq + end + +end \ No newline at end of file diff --git a/app/models/concerns/structureable_connected_groups.rb b/app/models/concerns/structureable_connected_groups.rb new file mode 100644 index 000000000..442aa7d11 --- /dev/null +++ b/app/models/concerns/structureable_connected_groups.rb @@ -0,0 +1,66 @@ +# This extends the Structureabe objects by methods that deal with connected groups, +# which are groups that are related to the structureable object by other groups, +# but not via events or other non-group objects. +# +# Example: +# +# group1 +# |---- group2 --- group3 -------------- +# |---- event1 | +# | |------ attendees_group ---- user1 +# | +# officers_parent ---- officer_group --- user2 +# +# In the example, groups 1, 2, and 3 are connected groups. But the attendees_group +# is not connected to them, because a non-group object, event1, is in between. +# +# Despite `officers_parent` being a group, `user2` is not regarded as +# connected to `group1`, since officers aren't necessarily members of a group. +# +# The here implemented mechanism should be independent of the DagLink model, +# i.e. can only ask for directly connected objects. Therefore, it relies on caching +# rather than indirect graph connections to achieve the neccessary read performance. +# +concern :StructureableConnectedGroups do + + def delete_cache + super + @ancestor_groups = nil + @descendant_groups = nil + end + + def connected_ancestor_groups + Group.where(id: connected_ancestor_group_ids) + end + + def connected_ancestor_group_ids + cached { select_connected_groups(parent_groups).collect { |parent_group| [parent_group.id] + parent_group.connected_ancestor_group_ids }.flatten.uniq } + end + + def connected_descendant_groups + Group.where(id: connected_descendant_group_ids) + end + + def connected_descendant_group_ids + cached { select_connected_groups(child_groups).collect { |child_group| [child_group.id] + child_group.connected_descendant_group_ids }.flatten.uniq } + end + + def ancestor_groups(reload = false) + @ancestor_groups = nil if reload + @ancestor_groups ||= connected_ancestor_groups + end + + def descendant_groups(reload = false) + @descendant_groups = nil if reload + @descendant_groups ||= connected_descendant_groups + end + + private + + def select_connected_groups(groups) + groups.select do |group| + not group.has_flag? :officers_parent + end + end + +end \ No newline at end of file diff --git a/app/models/concerns/structureable_connected_leaf_groups.rb b/app/models/concerns/structureable_connected_leaf_groups.rb new file mode 100644 index 000000000..2db597718 --- /dev/null +++ b/app/models/concerns/structureable_connected_leaf_groups.rb @@ -0,0 +1,15 @@ +concern :StructureableConnectedLeafGroups do + + def leaf_groups + connected_leaf_groups + end + + def connected_leaf_groups + cached do + connected_descendant_groups.select do |group| + group.connected_descendant_groups.count == 0 + end + end + end + +end \ No newline at end of file diff --git a/app/models/concerns/structureable_connected_pages.rb b/app/models/concerns/structureable_connected_pages.rb new file mode 100644 index 000000000..30b66377e --- /dev/null +++ b/app/models/concerns/structureable_connected_pages.rb @@ -0,0 +1,25 @@ +# This determines which pages are directly connected, i.e. associated with the structureable. +# This means that the pages are not separated by a group or another object from the +# structureable. +# +# Example: +# +# @group +# | +# @page --- @subpage <--- connected to @group +# | +# @disconnected_group +# | +# @disconnected_group_page <--- not connected to @group +# +concern :StructureableConnectedPages do + + def connected_descendant_pages + Page.find connected_descendant_page_ids + end + + def connected_descendant_page_ids + cached { self.child_pages.collect { |child_page| [child_page.id] + child_page.connected_descendant_page_ids }.flatten } + end + +end \ No newline at end of file diff --git a/app/models/concerns/structureable_graph_cache.rb b/app/models/concerns/structureable_graph_cache.rb new file mode 100644 index 000000000..850e33e40 --- /dev/null +++ b/app/models/concerns/structureable_graph_cache.rb @@ -0,0 +1,63 @@ +# A lot of methods, which have a result that depends on the graph, +# are cached. +# +# class GraphNode +# is_structureable +# +# def some_method_depending_on_the_graph +# cached { calculate_result(...) } +# end +# end +# +# But this means that the graph-related cache has to be re-calculated +# whenever the graph changes. We can't re-calculate the whole graph +# whenever some small part changes, since this would be too expensive. +# +# Instead, the methods in this file determine which parts of the grpah +# are affacted by a change, and, which caches are to be re-calculated +# in response. +# +concern :StructureableGraphCache do + + concerning :OfficerHasChanged do + def refresh_cache_after_officer_has_changed + affected_nodes_after_officer_has_changed + .refresh_cached :find_admins + .refresh_cached :officers_of_self_and_parent_groups + + # TODO: `refresh_role_cache` + end + + def affected_nodes_after_officer_has_changed + ([self] + connected_descendant_groups).collect do |structureable| + [structureable] + structureable.connected_descendant_pages + structureable.child_events + structureable.child_users + end.flatten + end + end + + concerning :MembershipHasChanged do + def refresh_cache_after_membership_has_changed + affected_nodes_after_membership_has_changed + .refresh_cached :members + .refresh_cached :memberships + end + + def affected_nodes_after_membership_has_changed + connected_ancestor_groups + end + end + + concerning :SubgroupHasChanged do + def refresh_cache_after_subgroup_has_changed + affected_nodes_after_subgroup_has_changed + .refresh_cached :connected_descendant_groups + .refresh_cached :members + .refresh_cached :memberships + end + + def affected_nodes_after_subgroup_has_changed + connected_ancestor_groups + end + end + +end \ No newline at end of file diff --git a/app/models/concerns/user_memberships.rb b/app/models/concerns/user_memberships.rb new file mode 100644 index 000000000..442add9ac --- /dev/null +++ b/app/models/concerns/user_memberships.rb @@ -0,0 +1,35 @@ +concern :UserMemberships do + + def memberships + Membership.where(user: self).now + end + + def direct_memberships + memberships.direct + end + + def indirect_memberships + memberships.indirect + end + + def memberships_in(group) + memberships.where(group: group) + end + + def membership_in(group) + memberships_in(group).first + end + + def groups + GroupCollection.new(memberships: memberships.join_validity_ranges_of_indirect_memberships) + end + + def direct_groups + GroupCollection.new(memberships: direct_memberships) + end + + def indirect_groups + GroupCollection.new(memberships: indirect_memberships.join_validity_ranges_of_indirect_memberships) + end + +end \ No newline at end of file diff --git a/app/models/concerns/user_roles.rb b/app/models/concerns/user_roles.rb index 9adc8d774..8fa73cbbd 100644 --- a/app/models/concerns/user_roles.rb +++ b/app/models/concerns/user_roles.rb @@ -1,5 +1,5 @@ concern :UserRoles do - + # Roles and Rights # ========================================================================================== @@ -14,16 +14,16 @@ def role_for( structureable ) return :admin if self.admin_of? structureable return :member if self.member_of? structureable end - + # Member Status # ------------------------------------------------------------------------------------------ - - # This method is a dirty hack to preserve the obsolete role model mechanism, - # which is currently not in use, since the abilities are defined directly in the + + # This method is a dirty hack to preserve the obsolete role model mechanism, + # which is currently not in use, since the abilities are defined directly in the # Ability class. # # Options: - # + # # with_invalid, also_in_the_past : true/false # # TODO: refactor it together with the role model mechanism. @@ -31,15 +31,24 @@ def role_for( structureable ) def member_of?( object, options = {} ) if object.kind_of? Group if options[:with_invalid] or options[:also_in_the_past] - self.ancestor_group_ids.include? object.id - else # only current memberships: - self.group_ids.include? object.id # This uses the validity range mechanism + self.groups.with_past.include? object + else + self.groups.now.include? object end else self.ancestors.include? object end end + + # Officer Status + # ------------------------------------------------------------------------------------------ + + def officer_groups + cached { self.groups.select { |g| g.type == "OfficerGroup" } } + end + + # Admins # ------------------------------------------------------------------------------------------ @@ -69,7 +78,7 @@ def directly_administrated_objects( role = :admin ) if admin_groups.count > 0 objects = admin_groups.collect do |admin_group| admin_group.administrated_object - end + end - [nil] else [] end @@ -81,7 +90,7 @@ def administrated_objects( role = :admin ) objects = directly_administrated_objects( role ) if objects objects += objects.collect do |directly_administrated_object| - directly_administrated_object.descendants + directly_administrated_object.connected_descendants end.flatten objects else @@ -121,9 +130,9 @@ def former_member_of_corporation?( corporation ) # Developer Status # ========================================================================================== - # This method returns whether the user is a developer. This is needed, for example, - # to determine if some features are presented to the current_user. - # + # This method returns whether the user is a developer. This is needed, for example, + # to determine if some features are presented to the current_user. + # def developer? cached { self.developer } end @@ -137,10 +146,10 @@ def developer=( mark_as_developer ) Group.developers.unassign_user self end end - + # Beta Tester Status # ========================================================================================== - + def beta_tester? @beta_tester ||= self.beta_tester end @@ -154,7 +163,7 @@ def beta_tester=(mark_as_beta_tester) Group.find_or_create_by_flag(:beta_testers).child_users.destroy(self) end end - + # Global Admin Switch # ========================================================================================== @@ -172,15 +181,23 @@ def global_admin=(new_setting) UserGroupMembership.find_by_user_and_group(self, Group.everyone.admins_parent).try(:destroy) end end - + # Officers # ========================================================================================== - + def officer_of_anything? self.groups.detect { |g| g.type == 'OfficerGroup' } || false end + def officer_of?(obj) + obj.officer_groups.collect { |g| g.members.to_a }.flatten.include? self + end + + def officer_or_subgroup_officer_of?(obj) + obj.officers_groups_of_self_and_descendant_groups.collect { |g| g.members.to_a }.flatten.include? self + end + # Methods transferred from former Role class # ========================================================================================== @@ -190,7 +207,7 @@ def global_officer? end def is_global_officer? - cached { global_admin? || ancestor_groups.flagged(:global_officer).exists? } + cached { global_admin? || groups.flagged(:global_officer).any? } end def administrated_user_ids @@ -204,6 +221,6 @@ def administrates_user?(id) end return false end - - + + end \ No newline at end of file diff --git a/app/models/corporation.rb b/app/models/corporation.rb index 24329951f..7042bd493 100644 --- a/app/models/corporation.rb +++ b/app/models/corporation.rb @@ -35,7 +35,7 @@ def self.create_corporations_parent_group def is_first_corporation_this_user_has_joined?( user ) return false if not user.groups.include? self return true if user.corporations.count == 1 - this_membership_valid_from = UserGroupMembership.find_by_user_and_group( user, self ).valid_from + this_membership_valid_from = Membership.where(user: user, group: self).first.valid_from user.memberships.each do |membership| return false if membership.valid_from.to_i < this_membership_valid_from.to_i end @@ -76,5 +76,20 @@ def deceased_members def deceased_members_memberships child_groups.find_by_flag(:deceased_parent).try(:memberships) || [] end + + # This overrides the group memberships. + # + # For a corporation, the members of the 'former members' subgroup + # of the corporation are excluded, even though they still have + # memberships in the dag-link sense. + # + # They should not appear in member lists, list exports, mailing lists, + # et cetera. + # + alias_method :original_memberships, :memberships + def memberships + original_memberships.without(former_members, deceased_members) + end + end diff --git a/app/models/dag_link.rb b/app/models/dag_link.rb index ff4b28419..b7a19140b 100644 --- a/app/models/dag_link.rb +++ b/app/models/dag_link.rb @@ -1,24 +1,55 @@ -# -*- coding: utf-8 -*- +# In a graph, the DagLinks are the links between the nodes. +# "DAG" stands for directed acyclic graph. +# +# The functionality is mostly extracted from the acts_as_dag gem, +# which we have used earlier: +# https://github.com/resgraph/acts-as-dag/blob/master/lib/dag/dag.rb +# +# Now, in contrast to the gem, we only store direct links in the database. +# Indirect links exist only in memory and in cache. This way, we don't have +# redundancies and inconsistencies anymore. +# class DagLink < ActiveRecord::Base attr_accessible :ancestor_id, :ancestor_type, :count, :descendant_id, :descendant_type, :direct if defined? attr_accessible - acts_as_dag_links polymorphic: true + has_many_flags + + belongs_to :ancestor, :polymorphic => true + belongs_to :descendant, :polymorphic => true + + validates :ancestor_type, :presence => true + validates :descendant_type, :presence => true + + scope :with_ancestor, lambda { |ancestor| where(:ancestor_id => ancestor.id, :ancestor_type => ancestor.class.to_s) } + scope :with_descendant, lambda { |descendant| where(:descendant_id => descendant.id, :descendant_type => descendant.class.to_s) } + + scope :with_ancestor_point, lambda { |point| where(:ancestor_id => point.id, :ancestor_type => point.type) } + scope :with_descendant_point, lambda { |point| where(:descendant_id => point.id, :descendant_type => point.type) } + + scope :ancestor_nodes, lambda { joins(:ancestor) } + scope :descendant_nodes, lambda { joins(:descendant) } + + validates :ancestor, :presence => true + validates :descendant, :presence => true + + before_validation :fill_defaults, :on => :update + before_validation :fill_defaults, :on => :create # We have to workaround a bug in Rails 3 here. But, since Rails 3 is no longer fully supported, # this is not going to be fixed. - # + # # https://github.com/rails/rails/issues/7618 # # With our workaround, the `delete_cache` method is called on the `DagLink` when # `group.members.destroy(user)` is called. - # + # # See: app/models/active_record_associations_patches.rb # after_save { self.delay.delete_cache } before_destroy :delete_cache - - include DagLinkRepair - + + include DagLinkValidityRange + def fill_cache valid_from end @@ -27,6 +58,16 @@ def delete_cache super ancestor.try(:delete_cache) descendant.try(:delete_cache) + ancestor.connected_ancestor_groups.each { |g| g.delete_cached :connected_descendant_group_ids } if ancestor.respond_to? :parent_groups + descendant.connected_descendant_groups.each { |g| g.delete_cached :connected_ancestor_group_ids } if descendant.kind_of? Group end - + + # These are defaults that are needed while migrating from the + # acts_as_dag gem to the new mechanism. + # + def fill_defaults + self.direct = true if self.direct.nil? + self.count = 0 if self.count.nil? + end + end diff --git a/app/models/event.rb b/app/models/event.rb index 7e1bee65f..c1eb6f59f 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -6,7 +6,7 @@ class Event < ActiveRecord::Base has_many :attachments, as: :parent, dependent: :destroy - + # General Properties # ========================================================================================== @@ -14,7 +14,7 @@ class Event < ActiveRecord::Base def title name end - + def to_param "#{id} #{name} #{start_at.year}-#{start_at.month}-#{start_at.day}".parameterize end @@ -39,10 +39,10 @@ def group=( group ) def groups self.parent_groups end - + # Times # ========================================================================================== - + def localized_start_at I18n.localize start_at.to_time if start_at.present? end @@ -50,7 +50,7 @@ def localized_start_at=(string) attribute_will_change! :start_at self.start_at = string.present? ? LocalizedDateTimeParser.parse(string, Time).to_time : nil end - + def localized_end_at I18n.localize end_at.to_time if end_at.present? end @@ -58,12 +58,12 @@ def localized_end_at=(string) attribute_will_change! :end_at self.end_at = string.present? ? LocalizedDateTimeParser.parse(string, Time).to_time : nil end - + # Contact People and Attendees # ========================================================================================== - + def find_contact_people_group find_special_group :contact_people end @@ -76,7 +76,7 @@ def contact_people_group def contact_people contact_people_group.members end - + def find_attendees_group find_special_group :attendees end @@ -89,13 +89,13 @@ def attendees_group def attendees attendees_group.members end - + def destroy find_attendees_group.try(:destroy) find_contact_people_group.try(:destroy) super end - + # Scopes # ========================================================================================== @@ -118,39 +118,26 @@ def destroy # Date.today.to_datetime is 0h. # scope :upcoming, lambda { where("(start_at > ? AND end_at IS NULL) OR (end_at IS NOT NULL AND end_at > ?)", Date.today.to_datetime, Date.today.to_datetime) } - + def upcoming? Event.upcoming.pluck(:id).include? self.id end - - scope :direct, lambda { includes( :links_as_descendant ).where( :dag_links => { :direct => true } ) } - # Finder Methods # ========================================================================================== - def self.find_all_by_group( group ) - ancestor_id = group.id if group - self.includes( :links_as_descendant ) - .where( :dag_links => { - :ancestor_type => "Group", :ancestor_id => ancestor_id - } ) - .order('start_at') + def self.find_all_by_group(group) + self.where(id: ([group] + group.connected_descendant_groups).map(&:child_event_ids).flatten).order(:start_at) end - def self.find_all_by_groups( groups ) - group_ids = groups.collect { |g| g.id } - self.includes( :links_as_descendant ) - .where( :dag_links => { - :ancestor_type => "Group", :ancestor_id => group_ids - } ) - .order('start_at') + def self.find_all_by_groups(groups) + self.where(id: groups.collect { |g| [g] + g.connected_descendant_groups }.flatten.map(&:child_event_ids).flatten).order(:start_at) end - + def self.find_all_by_user(user) self.find_all_by_groups(user.groups).direct end - + # Calendar Export # ========================================================================================== @@ -172,7 +159,7 @@ def to_icalendar_event e.last_modified = Icalendar::Values::DateTime.new(self.updated_at) return e end - + def to_icalendar cal = Icalendar::Calendar.new cal.add_event self.to_icalendar_event @@ -183,18 +170,18 @@ def to_icalendar def to_ics self.to_icalendar.to_ical end - + def to_ical self.to_ics end - + # Example: # Group.find(12).events.to_ics # def self.to_ics self.to_icalendar.to_ical end - + def self.to_icalendar cal = Icalendar::Calendar.new self.all.each do |event| @@ -203,18 +190,18 @@ def self.to_icalendar cal.publish return cal end - + def self.to_ical self.to_ics end - + # Existance # ========================================================================================== - + # For some strange reason, the callbacks of the creation of an event appear to # prevent the event form being found in the database after its creation. - # + # # In the EventsController and in specs, we need to make sure that the event exists # before continuing. Otherwise `ActiveRecord::RecordNotFound` is raised in the controller # or when redirecting to the event. @@ -231,5 +218,5 @@ def wait_for_me_to_exist retry end end - + end diff --git a/app/models/group.rb b/app/models/group.rb index d90733c0c..13a6a0ac6 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -3,15 +3,15 @@ # This class represents a user group. Besides users, groups may have sub-groups as children. # One group may have several parent-groups. Therefore, the relations between groups, users, # etc. is stored using the DAG model, which is implemented by the `is_structureable` method. -# +# class Group < ActiveRecord::Base - + if defined? attr_accessible attr_accessible( :name, # just the name of the group; example: 'Corporation A' :body, # a description text displayed on the groups pages top - :token, # (optional) a short-name, abbreviation of the group's name, in + :token, # (optional) a short-name, abbreviation of the group's name, in # a global context; example: 'A' - :internal_token, # (optional) an internal abbreviation, i.e. used by the + :internal_token, # (optional) an internal abbreviation, i.e. used by the # members of the group; example: 'AC' :extensive_name, # (optional) a long version of the group's name; # example: 'The Corporation of A' @@ -19,20 +19,21 @@ class Group < ActiveRecord::Base # titles of the child users of the group. ) end - + include ActiveModel::ForbiddenAttributesProtection # TODO: Move into initializer - is_structureable(ancestor_class_names: %w(Group Page Event), + is_structureable(ancestor_class_names: %w(Group Page Event), descendant_class_names: %w(Group User Page Workflow Event Project)) is_navable has_profile_fields has_many :posts - + default_scope { includes(:flags) } - include GroupMixins::Memberships - include GroupMixins::Everyone + include GroupMemberships + include GroupMemberAssignment + include GroupMixins::Everyone include GroupMixins::Corporations include GroupMixins::Roles include GroupMixins::Guests @@ -42,6 +43,7 @@ class Group < ActiveRecord::Base include GroupMixins::Import include GroupMailingLists include GroupDummyUsers + include GroupWorkflows after_create :import_default_group_structure # from GroupMixins::Import after_save { self.delay.delete_cache } @@ -50,14 +52,14 @@ def delete_cache super ancestor_groups(true).each { |g| g.delete_cached(:leaf_groups); g.delete_cached(:status_groups) } end - + # General Properties # ========================================================================================== # The title of the group, i.e. a kind of caption, e.g. used in the tag of the # webpage. By default, this returns just the name of the group. But this may be changed # in the main application. - # + # def title self.name end @@ -69,7 +71,7 @@ def title def name I18n.t( super.to_sym, default: super ) if super.present? end - + def extensive_name if has_flag? :attendees name + (parent_events.first ? ": " + parent_events.first.name : '') @@ -83,7 +85,7 @@ def extensive_name name end end - + def name_with_corporation if self.corporation && self.corporation.id != self.id "#{self.name} (#{self.corporation.name})" @@ -91,9 +93,9 @@ def name_with_corporation self.name end end - + # This sets the format of the Group urls to be - # + # # example.com/groups/24-planeswalkers # # rather than just @@ -103,8 +105,8 @@ def name_with_corporation def to_param "#{id} #{title}".parameterize end - - + + # Mark this group of groups, i.e. the primary members of the group are groups, # not users. This does not effect the DAG structure, but may affect the way # the group is displayed. @@ -115,54 +117,25 @@ def group_of_groups? def group_of_groups=(add_the_flag) add_the_flag ? add_flag(:group_of_groups) : remove_flag(:group_of_groups) end - - - # Associated Objects - # ========================================================================================== - - # Workflows - # ------------------------------------------------------------------------------------------ - - # These methods override the standard methods, which are usual ActiveRecord associations - # methods created by the acts-as-dag gem - # (https://github.com/resgraph/acts-as-dag/blob/master/lib/dag/dag.rb). - # But since the Workflow in the main application - # inherits from WorkflowKit::Workflow and single table inheritance and polymorphic - # associations do not always work together as expected in rails, as can be seen here - # http://stackoverflow.com/questions/9628610/why-polymorphic-association-doesnt-work-for-sti-if-type-column-of-the-polymorph, - # we have to override these methods. - # - # ActiveRecord associations require 'WorkflowKit::Workflow' to be stored in the database's - # type column, but by asking for the `child_workflows` we want to get òbjects of the - # `Workflow` type, not `WorkflowKit::Workflow`, since Workflow objects may have - # additional methods, added by the main application. - # - def descendant_workflows - Workflow - .joins( :links_as_descendant ) - .where( :dag_links => { :ancestor_type => "Group", :ancestor_id => self.id } ) - .uniq - end - def child_workflows - self.descendant_workflows.where( :dag_links => { direct: true } ) - end + # Associated Objects + # ========================================================================================== # Events # ------------------------------------------------------------------------------------------ - + def events - self.descendant_events + Event.find_all_by_group(self) end def upcoming_events self.events.upcoming.order('start_at') end - - + + # Adress Labels (PDF) - # options: + # options: # - sender: Sender line including sender address. # - book_rate: Whether the "Büchersendung"/"Envois à taxe réduite" badge # is to be printed. @@ -189,8 +162,8 @@ def cached_members_postal_addresses_created_at # Groups # ------------------------------------------------------------------------------------------ - def descendant_groups_by_name( descendant_group_name ) - self.descendant_groups.where( :name => descendant_group_name ) + def descendant_groups_by_name(descendant_group_name) + self.descendant_groups.select { |g| g.name == descendant_group_name } end def corporation @@ -199,25 +172,13 @@ def corporation end end def corporation_id - (([self.id] + ancestor_group_ids) & Corporation.pluck(:id)).first + (([self.id] + connected_ancestor_group_ids) & Corporation.pluck(:id)).first end def corporation? kind_of? Corporation end - - # This returns all sub-groups of the corporation that have no - # sub-groups of their ownes except for officer groups. - # This is needed for the selection of status groups. - # - def leaf_groups - cached do - self.descendant_groups.order('id').includes(:flags).select do |group| - group.has_no_subgroups_other_than_the_officers_parent? and not group.is_officers_group? - end - end - end - + def find_deceased_members_parent_group self.descendant_groups.where(name: ["Verstorbene", "Deceased"]).limit(1).first end @@ -225,5 +186,11 @@ def deceased find_deceased_members_parent_group end + concerning :GroupDescendantUsers do + def descendant_users + raise('Changed interface! Please use `Group#members` or `Group#members.with_past`.') + end + end + end diff --git a/app/models/group_collection.rb b/app/models/group_collection.rb new file mode 100644 index 000000000..22413ae2a --- /dev/null +++ b/app/models/group_collection.rb @@ -0,0 +1,49 @@ +class GroupCollection + + def initialize(attrs = {}) + @memberships = attrs[:memberships] || raise('no memberships (MembershipCollection) given.') + @memberships.kind_of?(MembershipCollection) || raise('memberships needs to be a MembershipCollection.') + end + + def to_a + groups = @memberships.to_a.collect { |membership| membership.group } + groups = groups & Group.flagged(@flagged) if @flagged + return groups + end + + def flagged(flag) + @flagged = flag + return self + end + + def find_all_by_flag(flag) + flagged(flag) + end + + def now + @memberships = @memberships.now + return self + end + + def with_past + @memberships = @memberships.with_past + return self + end + + def past + @memberships = @memberships.past + return self + end + + def ids + self.map(&:id) + end + + def where + Group.where(id: ids) + end + + delegate :count, :first, :last, to: :to_a + delegate :map, :collect, :select, :detect, :include?, :any?, :+, :-, :&, to: :to_a + +end \ No newline at end of file diff --git a/app/models/group_mixins/guests.rb b/app/models/group_mixins/guests.rb index 3fbe8e6cf..fe8f7390b 100644 --- a/app/models/group_mixins/guests.rb +++ b/app/models/group_mixins/guests.rb @@ -1,7 +1,7 @@ # # All groups have associated special groups, for example the `guests_parent` group, which -# contains all guests of the group. -# +# contains all guests of the group. +# # This mixin provides the accessor methods for the guests_parent special group. # # The mechanism used in the mixin is defined in `StructureableMixins::HasSpecialGroups`. @@ -13,8 +13,8 @@ module GroupMixins::Guests included do # see, for example, http://stackoverflow.com/questions/5241527/splitting-a-class-into-multiple-files-in-ruby-on-rails end - - + + # Guests # ========================================================================================== @@ -39,7 +39,7 @@ def guests_parent! end def find_guest_users - guests_parent.descendant_users + guests_parent.members end def guests @@ -57,5 +57,5 @@ def guests def find_guests_groups find_guests_parent_group.descendant_groups end - -end + +end diff --git a/app/models/group_mixins/memberships.rb b/app/models/group_mixins/memberships.rb deleted file mode 100644 index cdf33e734..000000000 --- a/app/models/group_mixins/memberships.rb +++ /dev/null @@ -1,203 +0,0 @@ -# -# This module contains the methods of the Group model regarding the associated -# user group memberships and users, i.e. members. -# -module GroupMixins::Memberships - - extend ActiveSupport::Concern - - included do - - # User Group Memberships - # ========================================================================================== - - # This associates all UserGroupMembership objects of the group, including indirect - # memberships. - # - has_many( :memberships, - -> { where ancestor_type: 'Group', descendant_type: 'User' }, - class_name: 'UserGroupMembership', - foreign_key: :ancestor_id ) - - # This associates all memberships of the group that are direct, i.e. direct - # parent_group-child_user memberships. - # - has_many( :direct_memberships, - -> { where ancestor_type: 'Group', descendant_type: 'User', direct: true }, - class_name: 'UserGroupMembership', - foreign_key: :ancestor_id ) - - # This associates all memberships of the group that are indirect, i.e. - # ancestor_group-descendant_user memberships, where groups are between the - # ancestor_group and the descendant_user. - # - has_many( :indirect_memberships, - -> { where ancestor_type: 'Group', descendant_type: 'User', direct: false }, - class_name: 'UserGroupMembership', - foreign_key: :ancestor_id ) - - - # This method builds a new membership having this group (self) as group associated. - # - def build_membership - direct_memberships.build(descendant_type: 'User') - end - - # This returns the UserGroupMembership object that represents the membership of the - # given user in this group. - # - # options: - # - also_in_the_past - # - def membership_of(user, options = {}) - if options[:also_in_the_past] - base = UserGroupMembership.with_invalid - else - base = UserGroupMembership - end - base.find_by_user_and_group(user, self) - end - - # This returns a string of the titles of the direct members of this group. This is used - # for in-place editing, for example. - # - # The string would be something like this: - # - # "#{user1.title}, #{user2.title}, ..." - # - def direct_members_titles_string - direct_members.collect { |user| user.title }.join( ", " ) - end - - # This sets the memberships of a group according to the given string of user titles. - # - # For example, after calling - # - # direct_members_titles_string = "#{user1.title}, #{user2.title}", - # - # the users `user1` and `user2` are the only direct members of the group. - # The memberships are removed using the standard methods, which means that the memberships - # are only marked as deleted. See: acts_as_paranoid_dag gem. - # - def direct_members_titles_string=( titles_string ) - new_members_titles = titles_string.split( "," ) - new_members = new_members_titles.collect do |title| - u = User.find_by_title( title.strip ) - self.errors.add :direct_member_titles_string, 'user not found: #{title}' unless u - u - end - for member in self.direct_members - unassign_user member unless member.in? new_members if member - end - for new_member in new_members - assign_user new_member if new_member - end - end - - def memberships_including_members - memberships.includes(:descendant).order(valid_from: :desc) - end - - # This returns the memberships that appear in the member list - # of the group. - # - # For a regular group, these are just the usual memberships. - # For a corporation, the members of the 'former members' subgroup - # of the corporation are excluded, even though they still have - # memberships. - # - def memberships_for_member_list - cached do - if corporation? - ( - memberships_including_members - - becomes(Corporation).former_members_memberships - - becomes(Corporation).deceased_members_memberships - ) - else - memberships_including_members - end - end - end - def memberships_for_member_list_count - cached { memberships_for_member_list.count } - end - - def latest_memberships - cached do - self.memberships.with_invalid.reorder('valid_from DESC').limit(10).includes(:descendant) - end - end - - def memberships_this_year - cached do - self.memberships.this_year - end - end - - # User Assignment - # ========================================================================================== - - # This assings the given user as a member to the group, i.e. this will - # create a UserGroupMembership. - # - def assign_user( user, options = {} ) - if user and not user.in?(self.direct_members) - membership = UserGroupMembership.create(user: user, group: self) - time_of_joining = options[:joined_at] || options[:at] || options[:time] || Time.zone.now - membership.update_attribute(:valid_from, time_of_joining) - return membership - end - end - - # This method will remove a UserGroupMembership, i.e. terminate the membership - # of the given user in this group. - # - def unassign_user( user, options = {} ) - if user and user.in?(self.members) - time_of_unassignment = options[:at] || options[:time] || Time.zone.now - UserGroupMembership.find_by(user: user, group: self).invalidate(at: time_of_unassignment) - end - end - - - def calculate_validity_range_of_indirect_memberships - self.indirect_memberships.where(valid_from: nil).each do |membership| - membership.recalculate_validity_range_from_direct_memberships - membership.save - end - end - - - # Members - # ========================================================================================== - - # This associates the group members (users), direct ones as well as indirect ones. - # - # Attention! The conditions on the `memberships` association are ignored by Rails 3 - # when generating the SQL query. This is why the conditions have to be repeated here. - # - has_many(:members, - -> { where('dag_links.ancestor_type' => 'Group').uniq }, - through: :memberships, - source: :descendant, source_type: 'User' - ) - - # This associates only the direct group members (users). - # - has_many(:direct_members, - -> { where('dag_links.ancestor_type' => 'Group', 'dag_links.direct' => true).uniq }, - through: :direct_memberships, - source: :descendant, source_type: 'User' - ) - - # This associates only the indirect group members (users). - # - has_many(:indirect_members, - -> { where('dag_links.ancestor_type' => 'Group', 'dag_links.direct' => false).uniq }, - through: :indirect_memberships, - source: :descendant, source_type: 'User' - ) - - end -end diff --git a/app/models/group_mixins/roles.rb b/app/models/group_mixins/roles.rb index 4aa736718..64727e6c4 100644 --- a/app/models/group_mixins/roles.rb +++ b/app/models/group_mixins/roles.rb @@ -27,15 +27,15 @@ module GroupMixins::Roles # some_group.administrated_object == nil # def administrated_object - if self.ancestor_groups.find_all_by_flag( :officers_parent ).count == 0 and - not self.has_flag? :officers_parent - return nil - end object = self - until object.has_flag? :officers_parent + counter = 0 + until object.has_flag?(:officers_parent) object = object.parents.first + return nil if object.nil? + counter += 1 + counter < 5 || raise('This, aparently is no admins group.') end object = object.parents.first end - + end diff --git a/app/models/has_dag_links.rb b/app/models/has_dag_links.rb new file mode 100644 index 000000000..a26b085da --- /dev/null +++ b/app/models/has_dag_links.rb @@ -0,0 +1,87 @@ +# This defines the ActiveRecord::Base method `has_dag_links`. +# +# Usage: +# +# class Group < ActiveRecord::Base +# has_dag_links ancestor_class_names: ['Group'], +# descendant_class_names: ['Group', 'User'] +# end +# +# This code is extracted from the acts_as_dag gem. +# https://github.com/resgraph/acts-as-dag/blob/master/lib/dag/dag.rb +# +module HasDagLinks + def has_dag_links(options = {}) + conf = { + :class_name => nil, + :ancestor_class_names => [], + :descendant_class_names => [] + } + conf.update(options) + + has_many :links_as_ancestor, :as => :ancestor, :class_name => 'DagLink' + has_many :links_as_descendant, :as => :descendant, :class_name => 'DagLink' + has_many :links_as_parent, lambda { where(:direct => true) }, :as => :ancestor, :class_name => 'DagLink' + has_many :links_as_child, lambda { where(:direct => true) }, :as => :descendant, :class_name => 'DagLink' + + ancestor_table_names = [] + parent_table_names = [] + conf[:ancestor_class_names].each do |class_name| + table_name = class_name.tableize + self.class_eval <<-EOL2 + has_many :links_as_descendant_for_#{table_name}, lambda { where(:ancestor_type => '#{class_name}') }, :as => :descendant, :class_name => 'DagLink' + has_many :ancestor_#{table_name}, :through => :links_as_descendant_for_#{table_name}, :source => :ancestor, :source_type => '#{class_name}' + has_many :links_as_child_for_#{table_name}, lambda { where(:ancestor_type => '#{class_name}', :direct => true) }, :as => :descendant, :class_name => 'DagLink' + has_many :parent_#{table_name}, :through => :links_as_child_for_#{table_name}, :source => :ancestor, :source_type => '#{class_name}' + def root_for_#{table_name}? + self.links_as_descendant_for_#{table_name}.empty? + end + EOL2 + ancestor_table_names << ('ancestor_'+table_name) + parent_table_names << ('parent_'+table_name) + unless conf[:descendant_class_names].include?(class_name) + #this apparently is only one way is we can create some aliases making things easier + self.class_eval "has_many :#{table_name}, :through => :links_as_descendant_for_#{table_name}, :source => :ancestor, :source_type => '#{class_name}'" + end + end + + self.class_eval <<-EOL25 + def ancestors + #{ancestor_table_names.join(' + ')} + end + def parents + #{parent_table_names.join(' + ')} + end + EOL25 + + descendant_table_names = [] + child_table_names = [] + conf[:descendant_class_names].each do |class_name| + table_name = class_name.tableize + self.class_eval <<-EOL3 + has_many :links_as_ancestor_for_#{table_name}, lambda { where(:descendant_type => '#{class_name}') }, :as => :ancestor, :class_name => 'DagLink' + has_many :descendant_#{table_name}, :through => :links_as_ancestor_for_#{table_name}, :source => :descendant, :source_type => '#{class_name}' + has_many :links_as_parent_for_#{table_name}, lambda { where(:descendant_type => '#{class_name}', :direct => true) }, :as => :ancestor, :class_name => 'DagLink' + has_many :child_#{table_name}, :through => :links_as_parent_for_#{table_name}, :source => :descendant, :source_type => '#{class_name}' + def leaf_for_#{table_name}? + self.links_as_ancestor_for_#{table_name}.empty? + end + EOL3 + descendant_table_names << ('descendant_'+table_name) + child_table_names << ('child_'+table_name) + unless conf[:ancestor_class_names].include?(class_name) + self.class_eval "has_many :#{table_name}, :through => :links_as_ancestor_for_#{table_name}, :source => :descendant, :source_type => '#{class_name}'" + end + end + + self.class_eval <<-EOL35 + def descendants + #{descendant_table_names.join(' + ')} + end + def children + #{child_table_names.join(' + ')} + end + EOL35 + + end +end \ No newline at end of file diff --git a/app/models/list_export.rb b/app/models/list_export.rb index ba3ecb5f8..82ba63c62 100644 --- a/app/models/list_export.rb +++ b/app/models/list_export.rb @@ -1,3 +1,5 @@ +require 'csv' + # This class helps to export data to CSV, XLS and possibly others. # # Example: @@ -17,10 +19,10 @@ # * https://github.com/zdavatz/spreadsheet # * Formatting xls: http://scm.ywesee.com/?p=spreadsheet/.git;a=blob;f=lib/spreadsheet/format.rb # * to_xls gem example: http://stackoverflow.com/questions/15600987/ -# +# class ListExport attr_accessor :data, :preset, :csv_options - + def initialize(initial_data, initial_preset = nil) @data = initial_data; @preset = initial_preset @csv_options = { col_sep: ';', quote_char: '"' } @@ -28,13 +30,13 @@ def initialize(initial_data, initial_preset = nil) @data = processed_data @data = sorted_data end - + def columns case preset.to_s when 'address_list' [:last_name, :first_name, :name_affix, :postal_address_with_name_surrounding, - :postal_address, :cached_localized_postal_address_updated_at, - :postal_address_street, + :postal_address, :cached_localized_postal_address_updated_at, + :postal_address_street, :postal_address_postal_code, :postal_address_town, :postal_address_state, :postal_address_country, :postal_address_country_code, @@ -55,7 +57,7 @@ def columns [:last_name, :first_name, :name_affix, :personal_title, :academic_degree] end end - + def headers columns.collect do |column| if column.kind_of? Symbol @@ -65,15 +67,15 @@ def headers end end end - + def processed_data if preset.to_s.in?(['birthday_list', 'address_list', 'dpag_internetmarke', 'phone_list', 'email_list']) && @data.kind_of?(Group) - # To be able to generate lists from Groups as well as search results, these presets expect + # To be able to generate lists from Groups as well as search results, these presets expect # an Array of Users as data. If a Group is given instead, just take the group members as data. # @data = @data.members end - + # Make the extended methods available that are defined below. # if @data.respond_to?(:first) && @data.first.kind_of?(User) @@ -124,7 +126,7 @@ def processed_data # /FIXME - please uncomment: #@leaf_group_names = @leaf_groups.pluck(:name) #@leaf_group_ids = @leaf_groups.pluck(:id } - + @group.members.collect do |user| user = user.becomes(ListExportUser) row = { @@ -146,7 +148,7 @@ def processed_data # # From a list of groups, this creates one row per group. # The columns count the number of memberships valid from the year given by the column. - # + # # For the 'join_and_persist_statistics', only memberships are counted # that are still valid, i.e. still persist. # @@ -154,7 +156,7 @@ def processed_data # group1 24 22 25 28 ... # group2 31 28 27 32 ... # ... - # + # if @data.kind_of? Group @groups = @data.child_groups elsif @data.kind_of? Array @@ -164,7 +166,7 @@ def processed_data row = {} columns.each do |column| row[column] = if column.kind_of? Integer - year = column + year = column memberships = [] if preset.to_s == 'join_statistics' memberships = group.memberships.with_past @@ -204,16 +206,16 @@ def sorted_data data end end - + def raise_error_if_data_is_not_valid case preset.to_s when 'birthday_list', 'address_list', 'dpag_internetmarke', 'phone_list', 'email_list', 'name_list' data.kind_of?(Group) || data.first.kind_of?(User) || raise("Expecing Group or list of Users as data in ListExport with the preset '#{preset}'.") when 'member_development' data.kind_of?(Group) || raise('The member_development list can only be generated for a Group, not an Array of Users.') - end + end end - + def to_csv CSV.generate(csv_options) do |csv| csv << headers @@ -222,7 +224,7 @@ def to_csv if row.respond_to? :values row[column_name] elsif row.respond_to? column_name - row.try(:send, column_name) + row.try(:send, column_name) else raise "Don't know how to access the given attribute or value. Trying to access '#{column_name}' on '#{row}'." end @@ -230,13 +232,13 @@ def to_csv end end end - + def to_xls header_format = {weight: 'bold'} @data = @data.collect { |hash| HashWrapper.new(hash) } if @data.first.kind_of? Hash @data.to_xls(columns: columns, headers: headers, header_format: header_format) end - + def to_html (" <table class='datatable joining statistics'> @@ -253,17 +255,17 @@ def to_html </table> ").html_safe end - + def to_a @data end - + def to_s to_csv end - + private - + def helpers ActionController::Base.helpers end @@ -276,11 +278,11 @@ class HashWrapper def initialize(hash) @hash = hash end - + # This is a workaround for the to_xls gem, which requires to access the attributes # by method in order to write the columns in the correct order. # - def method_missing(method_name, *args, &block) + def method_missing(method_name, *args, &block) @hash[method_name] || @hash[method_name.to_sym] end end @@ -291,11 +293,11 @@ def method_missing(method_name, *args, &block) # require 'user' class ListExportUser < User - + def personal_title_and_name "#{personal_title} #{name}".strip end - + # Birthday, Date of Birth, Date of Death # def current_age @@ -307,7 +309,7 @@ def localized_birthday_this_year def localized_date_of_birth I18n.localize date_of_birth if date_of_birth end - + # Address # def postal_address_with_name_surrounding @@ -361,7 +363,7 @@ def address_label_text_after_name def dpag_postal_address_type "HOUSE" end - + def cache_key # Otherwise the cached information of the user won't be used. super.gsub('list_export_users/', 'users/') diff --git a/app/models/list_exports/base.rb b/app/models/list_exports/base.rb index d42383b21..9a608e707 100644 --- a/app/models/list_exports/base.rb +++ b/app/models/list_exports/base.rb @@ -1,3 +1,5 @@ +require 'csv' + # This class helps to export data to CSV, XLS and possibly others. # # Example: @@ -17,33 +19,33 @@ # * https://github.com/zdavatz/spreadsheet # * Formatting xls: http://scm.ywesee.com/?p=spreadsheet/.git;a=blob;f=lib/spreadsheet/format.rb # * to_xls gem example: http://stackoverflow.com/questions/15600987/ -# +# module ListExports class Base - + # The data that is to be exported is supposed to be some kind of Array. # The array can contain ActiveRecord objects or Hashes. # # The data array is filled, either in the initializer, or in a `from_xyz` method. # attr_accessor :data - + # This is a way to store export options when initializing. # attr_accessor :options - + def initialize(data, options = {}) @data = data @options = options end - + # Initialize from group, i.e. the group members are considered to be the # export data. # def self.from_group(group, options = {}) self.new(group.members.to_a, options.merge({group: group})) end - + # The columns that are to be exported are listed here as array of Symbols or Strings. # During the export, these names are used either as methods on the ActiveRecord objects, # or as keys for the Hashes in the `data`. @@ -51,7 +53,7 @@ def self.from_group(group, options = {}) def columns [] end - + # The headers of the tables are, by default, derived from the columns # that are to be exported. # @@ -64,7 +66,7 @@ def headers end end end - + # Wrapping the `data` Array as array of `DataRow` objects # unifies the access method: The columns can be accessed using the # `column(key)` method. @@ -74,7 +76,7 @@ def data_rows DataRow.new(object) end end - + # This exports the `data` into a csv formatted String. # def to_csv @@ -87,14 +89,14 @@ def to_csv end end end - + def csv_options {col_sep: ';', quote_char: '"'} end - + # This exports the `data` into xls format, which can be served via - # - # send_data(@list_export.to_xls, type: 'application/xls; charset=utf-8; header=present', + # + # send_data(@list_export.to_xls, type: 'application/xls; charset=utf-8; header=present', # filename: "#{@file_title}.xls") # # Internally, we use the to_xls gem: @@ -103,6 +105,6 @@ def csv_options def to_xls data_rows.to_xls(columns: columns, headers: headers, header_format: {weight: 'bold'}) end - + end end \ No newline at end of file diff --git a/app/models/member_collection.rb b/app/models/member_collection.rb new file mode 100644 index 000000000..b4d3b9d24 --- /dev/null +++ b/app/models/member_collection.rb @@ -0,0 +1,55 @@ +class MemberCollection + + def initialize(attrs = {}) + @group = attrs[:group] + @memberships = attrs[:memberships] || raise('no memberships (MembershipCollection) given.') + @memberships.kind_of?(MembershipCollection) || raise('memberships needs to be a MembershipCollection.') + end + + def to_a + @memberships.to_a.collect { |membership| membership.user } + end + + def find_all_by_flag(flag) + flagged(flag) + end + + def now + @memberships = @memberships.now + return self + end + + def with_past + @memberships = @memberships.with_past + return self + end + + def past + @memberships = @memberships.past + return self + end + def former + past + end + + def direct + @memberships = @memberships.direct + return self + end + + def indirect + @memberships = @memberships.indirect + return self + end + + delegate :count, :first, :last, to: :to_a + delegate :each, :map, :collect, :select, :include?, :+, :-, :&, to: :to_a + + # Add a user as another member. + # + def <<(user) + @group || raise('No :group given during MemberCollection initialization.') + @group << user + end + +end \ No newline at end of file diff --git a/app/models/membership.rb b/app/models/membership.rb new file mode 100644 index 000000000..57893c143 --- /dev/null +++ b/app/models/membership.rb @@ -0,0 +1,137 @@ +# This represents a user-group membership. +# +# Example: +# +# group1 --- page1 --- group2 --- group3 --- user1 +# | +# |------- user2 +# +# In the example, user1 has two memberships, one of them direct. +# user2 has one membership. +# +# Membership.where(user: user1).count == 2 +# Membership.where(user: user2).count == 1 +# +class Membership + + # http://guides.rubyonrails.org/active_model_basics.html#model + include ActiveModel::Model + + attr_accessor :user, :group, :valid_from, :valid_to + + include MembershipPersistence + include MembershipValidityRange + include MembershipValidityRangeLocalization + include MembershipReview + + def initialize(attrs = {}) + @dag_link = attrs[:dag_link] + @user = @dag_link.try(:descendant) || attrs[:user] + @group = @dag_link.try(:ancestor) || attrs[:group] + @valid_from = @dag_link.try(:valid_from) || attrs[:valid_from] + @valid_to = @dag_link.try(:valid_to) || attrs[:valid_to] + end + + def self.where(constraints = {}) + MembershipCollection.new.where(constraints) + end + + # This represents a single direct membership, which is identified by the id of the + # dag link that connects the user and the group of the membership. + # + def self.find(id) + if link = DagLink.find(id) + Membership.new(dag_link: link) + else + nil + end + end + + def self.direct + MembershipCollection.new.direct + end + + def ==(other_membership) + super || + other_membership.instance_of?(self.class) && + self.group.id == other_membership.group.id && + self.user.id == other_membership.user.id && + self.valid_from.try(:to_i) == other_membership.valid_from.try(:to_i) && + self.valid_to.try(:to_i) == other_membership.valid_to.try(:to_i) + end + + alias_method :eql?, :== + + def direct? + dag_link ? true : false + end + + def to_param + id.to_s + end + + def group_id + group.try(:id) + end + def group_id=(new_group_id) + group = Group.find new_group_id + end + + def user_id + user.try(:id) + end + + def user_title + user.try(:title) + end + def user_title=(new_user_title) + user = User.find_by_title new_user_title + end + + # Invalidate the current membership and move the user to the given group. + # + # membership.move_to other_group + # membership.move_to other_group, at: 1.hour.ago + # + def move_to(group_to_move_in, options = {}) + time = (options[:time] || options[:date] || options[:at] || Time.zone.now).to_datetime + self.invalidate at: time + new_membership = Membership.create(user: self.user, group: group_to_move_in) + new_membership.update_attributes valid_from: time + return new_membership + end + + + + # Create a membership of the user `u` in the group `g`. + # + # membership = Membership.create(user: u, group: g) + # membership = Membership.create(user: u, group: g, valid_from: 1.month_ago, valid_to: 1.day.ago) + # + def self.create(params) + user = params[:user] + user ||= User.find params[:user_id] if params[:user_id] + user ||= User.find_by_title params[:user_title] if params[:user_title] + raise "Could not create Membership without user." unless user + + group = params[ :group ] + group ||= Group.find params[:group_id] if params[:group_id] + raise "Could not create Membership without group." unless group + + new_dag_link = DagLink.create!(ancestor_id: group.id, ancestor_type: 'Group', + descendant_id: user.id, descendant_type: 'User', + valid_from: params[:valid_from] || Time.zone.now, + valid_to: params[:valid_to]) + + user.delete_cache + group.delete_cache + + Membership.new(user: user, group: group, + valid_from: new_dag_link.valid_from, valid_to: new_dag_link.valid_to) + end + + def inspect + "Membership(user: #{user_id}, group: #{group_id})" + end + +end \ No newline at end of file diff --git a/app/models/membership_collection.rb b/app/models/membership_collection.rb new file mode 100644 index 000000000..2e0c85dd4 --- /dev/null +++ b/app/models/membership_collection.rb @@ -0,0 +1,195 @@ +class MembershipCollection + + include MembershipCollectionValidityRange + + def where(constraints) + @user = constraints[:user] + @group = constraints[:group] + return self + end + + def direct + @direct = true + @indirect = false + return self + end + + def indirect + @indirect = true + @direct = false + return self + end + + def uniq + @uniq = true + return self + end + + def without(*without_members) + # `flatten` for `without(blondes, brunettes)`, which are two arrays. + @without_members = without_members.flatten + return self + end + + # If a user has two memberships in a group, differing in the validity range, + # this filter selects the first, i.e. earliest, membership for each group. + # + def first_per_group + @first_per_group = true + return self + end + + def uncached + @uncached = true + return self + end + + # Join the validity ranges of indirect memberships. + # + # group1 + # |------- subgroup1 -----| + # |------- subgroup2 --- user1 + # + # First, user1 joins subgroup1, then moves to subgroup2. + # + # |-----------| first indirect membership in group1 + # |--------- second indirect membership in group2 + # |--------------------- joined indirect membership + # + def join_validity_ranges_of_indirect_memberships + @join_validity_ranges_of_indirect_memberships = true + return self + end + + def to_a + Rails.cache.fetch [cache_key, 'to_a'], force: @uncached do + memberships = [] + memberships += find_all_direct_memberships unless @indirect + unless @direct + memberships += if @user and not @group + find_all_indirect_memberships_by_user + elsif @group and not @user + find_all_indirect_memberships_by_group + elsif @user and @group + find_all_indirect_memberships_by_user_and_group + end + end + memberships = memberships.uniq { |m| [m.group.id, m.user.id, m.valid_from, m.valid_to] } if @uniq + if @first_per_group + memberships = memberships.group_by { |m| [m.group, m.user] }.collect do |group_and_user, memberships| + min_valid_from_to_i = memberships.collect { |m| m.valid_from.to_i }.min + memberships.detect { |m| m.valid_from.to_i == min_valid_from_to_i } + end + end + memberships.select! { |m| not m.user.in? @without_members } if @without_members + memberships + end + end + + def groups + GroupCollection.new(memberships: self) + end + + delegate :count, :first, :last, :sort_by, to: :to_a + delegate :map, :collect, :select, :detect, :each, to: :to_a + + def include?(*other_memberships) + to_a.collect { |m| [m.group.id, m.user.id, m.valid_from, m.valid_to] } + .include?(*other_memberships.collect { |m| [m.group.id, m.user.id, m.valid_from, m.valid_to] }) + end + + def destroy_all + self.each do |membership| + membership.destroy if membership.destroyable? + end + end + + private + + def dag_links + dag_links_for user: @user, group: @group + end + + def find_all_direct_memberships(reload = false) + @direct_memberships = nil if reload + @direct_memberships ||= dag_links.collect do |direct_link| + Membership.new(user: direct_link.descendant, group: direct_link.ancestor, + valid_from: direct_link.valid_from, valid_to: direct_link.valid_to) + end + end + + def find_all_indirect_memberships_by_user + if @join_validity_ranges_of_indirect_memberships + indirect_groups = find_all_direct_memberships.collect { |m| m.group.connected_ancestor_groups }.flatten.uniq + indirect_groups.collect do |ancestor_group| + dag_links = dag_links_for( + group_ids: ancestor_group.connected_descendant_group_ids, user_ids: [@user.id], + ignore_validity_range_filters: true, no_eager_loading: true) + Membership.new(user: @user, group: ancestor_group, + valid_from: min_valid_from_of(dag_links), valid_to: max_valid_to_of(dag_links)) + end + else + find_all_direct_memberships.collect do |direct_membership| + direct_membership.group.connected_ancestor_groups.collect do |ancestor_group| + Membership.new(user: @user, group: ancestor_group, + valid_from: direct_membership.valid_from, valid_to: direct_membership.valid_to) + end + end + end.flatten + end + + def find_all_indirect_memberships_by_group + if @join_validity_ranges_of_indirect_memberships + user_ids = dag_links_for(group_ids: @group.connected_descendant_group_ids, no_eager_loading: true).pluck(:descendant_id).uniq + user_ids.collect do |user_id| + dag_links = dag_links_for( + group_ids: @group.connected_descendant_group_ids, user_ids: [user_id], + ignore_validity_range_filters: true, no_eager_loading: true) + Membership.new(user: User.find(user_id), group: @group, + valid_from: min_valid_from_of(dag_links), valid_to: max_valid_to_of(dag_links)) + end + else + dag_links_for(group_ids: @group.connected_descendant_group_ids).collect do |direct_link| + Membership.new(user: direct_link.descendant, group: @group, + valid_from: direct_link.valid_from, valid_to: direct_link.valid_to) + end + end + end + + def find_all_indirect_memberships_by_user_and_group + if @join_validity_ranges_of_indirect_memberships + dag_links = dag_links_for( + group_ids: @group.connected_descendant_group_ids, user_ids: [@user.id], + ignore_validity_range_filters: true, no_eager_loading: true) + [ Membership.new(user: @user, group: @group, + valid_from: min_valid_from_of(dag_links), valid_to: max_valid_to_of(dag_links)) ] + else + @group.connected_descendant_groups.collect do |descendant_group| + dag_links_for(group: descendant_group, user: @user).collect do |link| + Membership.new(user: @user, group: @group, valid_from: link.valid_from, valid_to: link.valid_to) + end + end.flatten - [nil] + end + end + + def min_valid_from_of(dag_links) + valid_from_nil = dag_links.where(valid_from: nil).present? + min_valid_from = valid_from_nil ? nil : dag_links.minimum(:valid_from) + end + + def max_valid_to_of(dag_links) + valid_to_nil = dag_links.where(valid_to: nil).present? + max_valid_to = valid_to_nil ? nil : dag_links.maximum(:valid_to) + end + + def cache_key + [primary_cache_scope, "membership_collection", + @user, @group, # we need both for Membership.where(user: ..., group: ...) despite the primary scope. + @direct, @indirect, @uniq, @first_per_group, @join_validity_ranges_of_indirect_memberships, @without_members, + @valid, @invalid, @with_invalid, @now, @past, @with_past, @now_and_in_the_past, @at_time, @this_year, @started_after] + end + def primary_cache_scope + @user || @group || raise('Neither user nor group given. Unable to find cache scope.') + end + +end \ No newline at end of file diff --git a/app/models/officer_group.rb b/app/models/officer_group.rb index 78425cf35..a5b53a478 100644 --- a/app/models/officer_group.rb +++ b/app/models/officer_group.rb @@ -7,7 +7,19 @@ def scope end def structureable - parent.parent_groups.first || parent.parent_pages.first + scope_group || scope_page + end + + # This is the group the officer is responsible for. + # + def scope_group + parent.parent_groups.first + end + + # This is the page the officer is responsible for. + # + def scope_page + parent.parent_pages.first end def parent diff --git a/app/models/role.rb b/app/models/role.rb index 5ccaab5a6..539868494 100644 --- a/app/models/role.rb +++ b/app/models/role.rb @@ -1,5 +1,5 @@ # Example: -# +# # Role.of(user).in(corporation).to_s # => "guest" # Role.of(user).in(corporation).guest? # => true # Role.find_all_by_user_and_group(user, corporation) @@ -8,14 +8,14 @@ # Role.of(user).for(user).to_s # => "global_admin" # class Role - + def initialize(given_user, given_object) @user = given_user @object = given_object end - + # Example: - # + # # Role.of(user).in(corporation).to_s # => "guest" # def self.of(given_user) @@ -23,7 +23,7 @@ def self.of(given_user) end # Example: - # + # # Role.of(user).in(corporation).to_s # => "guest" # def in(given_object) @@ -33,52 +33,56 @@ def in(given_object) def for(given_object) self.in(given_object) end - + def user @user || raise('User not given, when trying to determine Role.') end - + def object @object end def group - @object + @object if @object.kind_of?(Group) end - + # # Roles for groups # - + def current_member? member? && full_member? end - + + # To be a full member of a `group`, a `user` has + # (a) to be member of the `group` and the `group` has to be flagged `:full_members`. + # (b) to be member of one of the subgroups of `group` that is flagged `:full_members`. + # def full_member? - object.kind_of?(Group) && - ( user.groups.flagged(:full_members).where(id: group.descendant_group_ids).exists? || - user.groups.flagged(:full_members).exists?(group.id) ) + return false unless group + full_members_group_ids = ([group.id] + group.connected_descendant_group_ids) & Group.flagged(:full_members).pluck(:id) + (user.groups.map(&:id) & full_members_group_ids).count > 0 end - + def member? object && object.kind_of?(Group) && user.member_of?(object) end - + def guest? object && object.kind_of?(Group) && user.guest_of?(object) end - + def former_member? object && object.kind_of?(Group) && object.corporation? && user.former_member_of_corporation?(object) end - + def deceased_member? object && object.kind_of?(Group) && object.corporation? && user.id.in?(object.deceased_members.map(&:id)) && (not former_member?) end - + # # Roles for structureables # - + def global_admin? user.global_admin? end @@ -86,12 +90,12 @@ def global_admin? def admin? global_admin? || (object && object.admins_of_self_and_ancestors.include?(user)) end - + def officer? global_admin? || (object && object.officers_of_self_and_ancestors.include?(user)) end - - + + # Example # Role.of(user).for(page).to_s # => 'admin' def to_s @@ -106,17 +110,17 @@ def to_s return 'member' if member? return '' end - + # # Global Roles # def global_officer? global_admin? || (user.ancestor_groups.find_all_by_flag(:global_officer).count > 0) end - + # The system allows to simulate a certain role when viewing an object. # This determines which simulations are allowed. - # + # def allowed_preview_roles return ['global_admin', 'admin', 'officer', 'global_officer', 'user'] if global_admin? return ['admin', 'officer', 'user'] if admin? @@ -130,7 +134,7 @@ def allow_preview? # All above roles are also officer roles. officer? || global_officer? end - + # Finding administrated objects. # # Role.of(user).select_objects_where_user_is_admin(objects) @@ -152,14 +156,14 @@ def administrated_objects directly_administrated_objects + directly_administrated_objects.collect { |o| o.descendants }.flatten end def administrated_users - directly_administrated_groups.collect { |g| g.descendant_users }.flatten + directly_administrated_groups.collect { |g| g.members.to_a }.flatten.uniq end - - + + # This finder method returns all global admins. # def self.global_admins Group.find_everyone_group.try(:find_admins) || [] end - + end \ No newline at end of file diff --git a/app/models/status_group.rb b/app/models/status_group.rb index 79b16eb3e..3ed25e7f7 100644 --- a/app/models/status_group.rb +++ b/app/models/status_group.rb @@ -7,16 +7,14 @@ class StatusGroup < Group def self.find_all_by_corporation(corporation) - corporation.leaf_groups.select do |group| - group.ancestor_events.count == 0 - end + corporation.connected_leaf_groups end def self.find_all_by_user(user, options = {}) user_groups = options[:with_invalid] ? user.parent_groups : user.direct_groups user.corporations.collect do |corporation| StatusGroup.find_all_by_corporation(corporation) - end.flatten & user_groups + end.flatten & user_groups.to_a end def self.find_by_user_and_corporation(user, corporation) diff --git a/app/models/status_group_membership.rb b/app/models/status_group_membership.rb index c2ac216a8..9a150a15c 100644 --- a/app/models/status_group_membership.rb +++ b/app/models/status_group_membership.rb @@ -1,189 +1,190 @@ -# -*- coding: utf-8 -*- -# -# This class represents the membership of a user in a status group, i.e. a subgroup of a corporation -# representing a member status, e.g. the subgroup 'guests' or 'presidents'. -# -class StatusGroupMembership < UserGroupMembership - - # Status Group Memberships do have more properties than regular memberships; - # those new properties are, e.g., shown in the corporate_vita. - # Since rails apparently does not support Multi Table Inheritance, - # this associated model takes the additional properties. - # - has_one :status_group_membership_info, foreign_key: 'membership_id', inverse_of: :membership #, autosave: true - - delegate( :promoted_by_workflow, :promoted_by_workflow=, - :promoted_on_event, :promoted_on_event=, - :workflow, :workflow=, - :event, :event=, # alias methods - to: :find_or_create_status_group_membership_info ) - - attr_accessible :event_by_name if defined? attr_accessible - - # Alias Methods For Delegated Methods - # ========================================================================================== - - def create_event( params ) - find_or_create_status_group_membership_info.create_promoted_on_event( params ) - end - - # Access the event (promoted_on_event) by its name, since this is the way - # most likely done by a user interface. - # - # If a new event is created, assign the corporation associated with this status group - # as the group of the event. - # - def event_by_name - self.event.name if self.event - end - def event_by_name=( event_name ) - if event_name.present? - if Event.find_by_name( event_name ) - self.event = Event.find_by_name( event_name ) - else - self.create_event( name: event_name ) - self.event.group ||= self.corporation if self.corporation - self.event.start_at = self.created_at - self.event.save - end - else - self.event = nil - end - end - - - # Creator - # ========================================================================================== - - def self.create( params ) - super( params ).becomes StatusGroupMembership - end - - - # Finder Methods - # ========================================================================================== - - # Returns all memberships in status groups that belong to the given corporation. - # - # corporation A - # |------------- status group 1 - # | |-------- user 1 - # | |-------- user 2 - # |------------- status group 2 - # |-------- user 3 - # - # The method therefore will return all memberships of subgroups of the corporation. - # - def self.find_all_by_corporation( corporation ) - raise 'Expect parameter to be a Corporation' unless corporation.kind_of? Corporation - status_groups = corporation.status_groups - status_group_ids = status_groups.collect { |group| group.id } - links = self - .where(:descendant_type => "User") - .where(:ancestor_type => "Group") - .where(:ancestor_id => status_group_ids) - .order('valid_from') - return links - end - - # Returns all memberships of the given user in status groups. - # - def self.find_all_by_user( user ) - raise 'Expect parameter to be a User' unless user.kind_of? User - status_groups = user.status_groups(with_invalid: true) - status_group_ids = status_groups.collect { |group| group.id } - links = self - .where(:descendant_type => "User") - .where(:descendant_id => user.id) - .where(:ancestor_type => "Group") - .where(:ancestor_id => status_group_ids) - .order('valid_from') - return links - end - - # Returns all memberships of the given user in the given corporation. - # - def self.find_all_by_user_and_corporation( user, corporation ) - raise 'Expect parameter to be a User' unless user.kind_of? User - status_groups = user.status_groups(with_invalid: true) - status_groups &= corporation.status_groups - status_group_ids = status_groups.collect { |group| group.id } - links = self - .where(:descendant_type => "User") - .where(:descendant_id => user.id) - .where(:ancestor_type => "Group") - .where(:ancestor_id => status_group_ids) - .order('valid_from') - return links - end - - # This method overrides the default finder method in order to make - # sure the returned object is of the StatusGroupMembership type. - # - def self.find_by_user_and_group( user, group ) - self - .where(ancestor_id: group.id, ancestor_type: 'Group') - .where(descendant_id: user.id, descendant_type: 'User') - .limit(1) - .first - - # The #becomes method won't work here. - #membership = super( user, group ) - #membership ? StatusGroupMembership.with_invalid.find(membership.id) : nil - end - - - # Save Method - # ========================================================================================== - - # Since several important attributes of this model are delegated, it is likely to change - # a delegated attribute without changing a direct attribute. For example: - # - # membership.workflow = some_workflow # workflow is delegated - # membership.changed? # => false - # membership.status_group_membership_info.changed? # => true - # membership.save - # - # The regular `save` method would fail, because there are `no changes` to the membership - # itself. - # - # To circumvent this, this save method first saves the delegate model if necessary and - # then calls the regular `save` method. - # - def save(*args) - save_status_group_membership_info_if_changed - if changed? - return super(*args) - else - return true - end - end - - def update_attributes( attributes, options = {} ) - self.assign_attributes( attributes, options ) - save - end - - - # Callback Methods for the Delegation to status_group_membership_info - # ========================================================================================== - - private - - def find_or_create_status_group_membership_info - status_group_membership_info || create_status_group_membership_info - end - - # When .save is called on this instance, but only the associated object has changed through - # the delegated methods, this instance is not marked as changed. As a result, any call of - # .save will fail. - # - # This method compensates for the missing automatism. - # - def save_status_group_membership_info_if_changed - find_or_create_status_group_membership_info.promoted_by_workflow.try(:save) - find_or_create_status_group_membership_info.promoted_on_event.try(:save) - find_or_create_status_group_membership_info.save - end - -end +# # -*- coding: utf-8 -*- +# # +# # This class represents the membership of a user in a status group, i.e. a subgroup of a corporation +# # representing a member status, e.g. the subgroup 'guests' or 'presidents'. +# # +# class StatusGroupMembership < UserGroupMembership +# +# # Status Group Memberships do have more properties than regular memberships; +# # those new properties are, e.g., shown in the corporate_vita. +# # Since rails apparently does not support Multi Table Inheritance, +# # this associated model takes the additional properties. +# # +# has_one :status_group_membership_info, foreign_key: 'membership_id', inverse_of: :membership #, autosave: true +# +# delegate( :promoted_by_workflow, :promoted_by_workflow=, +# :promoted_on_event, :promoted_on_event=, +# :workflow, :workflow=, +# :event, :event=, # alias methods +# to: :find_or_create_status_group_membership_info ) +# +# attr_accessible :event_by_name if defined? attr_accessible +# +# # Alias Methods For Delegated Methods +# # ========================================================================================== +# +# def create_event( params ) +# find_or_create_status_group_membership_info.create_promoted_on_event( params ) +# end +# +# # Access the event (promoted_on_event) by its name, since this is the way +# # most likely done by a user interface. +# # +# # If a new event is created, assign the corporation associated with this status group +# # as the group of the event. +# # +# def event_by_name +# self.event.name if self.event +# end +# def event_by_name=( event_name ) +# if event_name.present? +# if Event.find_by_name( event_name ) +# self.event = Event.find_by_name( event_name ) +# else +# self.create_event( name: event_name ) +# self.event.group ||= self.corporation if self.corporation +# self.event.start_at = self.created_at +# self.event.save +# end +# else +# self.event = nil +# end +# end +# +# +# # Creator +# # ========================================================================================== +# +# def self.create( params ) +# super( params ).becomes StatusGroupMembership +# end +# +# +# # Finder Methods +# # ========================================================================================== +# +# # Returns all memberships in status groups that belong to the given corporation. +# # +# # corporation A +# # |------------- status group 1 +# # | |-------- user 1 +# # | |-------- user 2 +# # |------------- status group 2 +# # |-------- user 3 +# # +# # The method therefore will return all memberships of subgroups of the corporation. +# # +# def self.find_all_by_corporation( corporation ) +# raise 'Expect parameter to be a Corporation' unless corporation.kind_of? Corporation +# status_groups = corporation.status_groups +# status_group_ids = status_groups.collect { |group| group.id } +# links = self +# .where(:descendant_type => "User") +# .where(:ancestor_type => "Group") +# .where(:ancestor_id => status_group_ids) +# .order('valid_from') +# return links +# end +# +# # Returns all memberships of the given user in status groups. +# # +# def self.find_all_by_user( user ) +# raise 'Expect parameter to be a User' unless user.kind_of? User +# status_groups = user.status_groups(with_invalid: true) +# status_group_ids = status_groups.collect { |group| group.id } +# links = self +# .where(:descendant_type => "User") +# .where(:descendant_id => user.id) +# .where(:ancestor_type => "Group") +# .where(:ancestor_id => status_group_ids) +# .order('valid_from') +# return links +# end +# +# # Returns all memberships of the given user in the given corporation. +# # +# def self.find_all_by_user_and_corporation( user, corporation ) +# raise 'Expect parameter to be a User' unless user.kind_of? User +# status_groups = user.status_groups(with_invalid: true) +# status_groups &= corporation.status_groups +# status_group_ids = status_groups.collect { |group| group.id } +# links = self +# .where(:descendant_type => "User") +# .where(:descendant_id => user.id) +# .where(:ancestor_type => "Group") +# .where(:ancestor_id => status_group_ids) +# .order('valid_from') +# return links +# end +# +# # This method overrides the default finder method in order to make +# # sure the returned object is of the StatusGroupMembership type. +# # +# def self.find_by_user_and_group( user, group ) +# self +# .where(ancestor_id: group.id, ancestor_type: 'Group') +# .where(descendant_id: user.id, descendant_type: 'User') +# .limit(1) +# .first +# +# # The #becomes method won't work here. +# #membership = super( user, group ) +# #membership ? StatusGroupMembership.with_invalid.find(membership.id) : nil +# end +# +# +# # Save Method +# # ========================================================================================== +# +# # Since several important attributes of this model are delegated, it is likely to change +# # a delegated attribute without changing a direct attribute. For example: +# # +# # membership.workflow = some_workflow # workflow is delegated +# # membership.changed? # => false +# # membership.status_group_membership_info.changed? # => true +# # membership.save +# # +# # The regular `save` method would fail, because there are `no changes` to the membership +# # itself. +# # +# # To circumvent this, this save method first saves the delegate model if necessary and +# # then calls the regular `save` method. +# # +# def save(*args) +# save_status_group_membership_info_if_changed +# if changed? +# return super(*args) +# else +# return true +# end +# end +# +# def update_attributes( attributes, options = {} ) +# self.assign_attributes( attributes, options ) +# save +# end +# +# +# # Callback Methods for the Delegation to status_group_membership_info +# # ========================================================================================== +# +# private +# +# def find_or_create_status_group_membership_info +# status_group_membership_info || create_status_group_membership_info +# end +# +# # When .save is called on this instance, but only the associated object has changed through +# # the delegated methods, this instance is not marked as changed. As a result, any call of +# # .save will fail. +# # +# # This method compensates for the missing automatism. +# # +# def save_status_group_membership_info_if_changed +# find_or_create_status_group_membership_info.promoted_by_workflow.try(:save) +# find_or_create_status_group_membership_info.promoted_on_event.try(:save) +# find_or_create_status_group_membership_info.save +# end +# +# end +# \ No newline at end of file diff --git a/app/models/structureable.rb b/app/models/structureable.rb index 24288434f..d4bf30b9f 100644 --- a/app/models/structureable.rb +++ b/app/models/structureable.rb @@ -3,18 +3,18 @@ # This module provides the ActiveRecord::Base extension `is_structureable`, which characterizes # a model as part of the global dag_link structure in this project. All structureable objects # are nodes of this dag link. -# -# Examples: +# +# Examples: # @page1.parent_pages << @page2 # @page1.parents # => [ @page2, ... ] -# +# # @group.child_users << @user # @group.children # => [ @user, ... ] # @user.parents # => [ @group, ... ] -# -# For all methods that are provided, please consult the documentations of the +# +# For all methods that are provided, please consult the documentations of the # `acts-as-dag` gem and of the `acts_as_paranoid_dag` gem. -# +# # This module is included in ActiveRecord::Base via an initializer at # config/initializers/active_record_structureable_extension.rb # @@ -22,18 +22,18 @@ module Structureable # options: ancestor_class_names, descendant_class_names - # This method is used to declare a model as structureable, i.e. part of the global - # dag link structure. - # + # This method is used to declare a model as structureable, i.e. part of the global + # dag link structure. + # # Options: # ancestor_class_names # descendant_class_names # link_class_name (default: 'DagLink') - # + # # For detailed information on the options, please see the documentation of the # `acts-as-dag` gem, since these options are forwarded to the has_dag_links method. # http://rubydoc.info/github/resgraph/acts-as-dag/Dag#has_dag_links-instance_method - # + # # Example: # class Group < ActiveRecord::Base # is_structureable ancestor_class_names: %w(Group), descendant_class_names: %w(Group User) @@ -41,9 +41,9 @@ module Structureable # class User < ActiveRecord::Base # is_structureable ancestor_class_names: %w(Group) # end - # + # def is_structureable( options = {} ) - + # default options conf = { :link_class_name => 'DagLink' @@ -53,7 +53,7 @@ def is_structureable( options = {} ) # the model is part of the dag link structure. see gem `acts-as-dag` has_dag_links conf - + before_destroy :destroy_links # see Flagable model. @@ -63,52 +63,37 @@ def is_structureable( options = {} ) # This mixin loads the necessary methods to interact with them. # include StructureableMixins::HasSpecialGroups - + # To use `prepend` here allows to call `super` in the methods # defined in the module `StructureableInstanceMethods`. # prepend StructureableInstanceMethods + + # Use the connected-groups mechanism. + # + include StructureableConnectedGroups + include StructureableConnectedLeafGroups + include StructureableConnectedPages + include StructureableConnectedDescendants + + # Methods to manage the graph-related cache. + # + include StructureableGraphCache end module StructureableInstanceMethods - + # Include Rules, e.g. let this object have admins. - # + # include StructureableMixins::Roles # When a dag node is destroyed, also destroy the corresponding dag links. - # Otherwise, there would remain ghost dag links in the database that would - # corrupt the integrity of the database. - # - # If the database gets ever messed up like this, delete the concerning - # *direct* dag links by hand and then run this rake task to re-create - # the indirect dag links: - # - # rake reconstruct_indirect_dag_links:all - # + # Otherwise, there would remain ghost dag links in the database. + # def destroy_dag_links - - # destory only child and parent links, since the indirect links - # are destroyed automatically by the DagLink model then. - links = self.links_as_parent + self.links_as_child - - for link in links do - - if link.destroyable? - link.destroy - else - - # In facty, all these links should be destroyable. If this error should - # be raised, something really went wrong. Please send in a bug report then - # at http://github.com/fiedl/your_platform. - raise "Could not destroy dag links of the structureable object that should be deleted." + - " Please send in a bug report at http://github.com/fiedl/your_platform." - return false - end - - end + (links_as_parent + links_as_child).each(&:destroy) end - + # This somehow identifies which are the ancestors of this structureable. # For example, this is used in the breadcrumb helper. # @@ -122,7 +107,7 @@ def children_cache_key def destroy_links self.destroy_dag_links end - + # Move the node to another parent. # def move_to(parent_node) @@ -134,7 +119,7 @@ def move_to(parent_node) self.update_attribute :updated_at, old_updated_at end end - + # Adding child objects. # def <<(object) @@ -176,7 +161,7 @@ def <<(object) raise e if not File.basename($0) == 'rake' end end - - + + end end diff --git a/app/models/structureable_mixins/roles.rb b/app/models/structureable_mixins/roles.rb index f8b386001..b60f458ef 100644 --- a/app/models/structureable_mixins/roles.rb +++ b/app/models/structureable_mixins/roles.rb @@ -14,7 +14,7 @@ module StructureableMixins::Roles included do end - + def fill_cache super if respond_to?(:child_groups) # TODO: Refactor this. It should be possible to find the admins for a user. @@ -27,35 +27,38 @@ def fill_cache officers_of_self_and_ancestor_groups end end - + def delete_cache super delete_caches_concerning_roles end - + def delete_caches_concerning_roles - if self.class.base_class.name == 'Group' - # For an admins_parent, this is called recursively until the original group - # is reached. - # - # group - # |---- officers_parent - # |------------ admins_parent - # |------------ some officer group - # - if has_flag?(:officers_parent) || has_flag?(:admins_parent) - parent_groups.each do |group| - group.delete_cache - if group.descendants.count > 0 - bulk_delete_cached :admins_of_ancestors, group.descendants - bulk_delete_cached :admins_of_self_and_ancestors, group.descendants - bulk_delete_cached "*officers*", group.descendants - end - end - end - end + + # TODO: + + ### if self.class.base_class.name == 'Group' + ### # For an admins_parent, this is called recursively until the original group + ### # is reached. + ### # + ### # group + ### # |---- officers_parent + ### # |------------ admins_parent + ### # |------------ some officer group + ### # + ### if has_flag?(:officers_parent) || has_flag?(:admins_parent) + ### parent_groups.each do |group| + ### group.delete_cache + ### if group.descendants.count > 0 + ### bulk_delete_cached :admins_of_ancestors, group.descendants + ### bulk_delete_cached :admins_of_self_and_ancestors, group.descendants + ### bulk_delete_cached "*officers*", group.descendants + ### end + ### end + ### end + ### end end - + # Officers # ========================================================================================== @@ -74,7 +77,7 @@ def find_officers_parent_group end def create_officers_parent_group - if self.ancestor_groups(true).find_all_by_flag(:officers_parent).count == 0 and not self.has_flag?(:officers_parent) + if self.connected_ancestor_groups.detect { |g| g.has_flag? :officers_parent }.nil? and not self.has_flag?(:officers_parent) # Do not allow officer cascades. create_special_group(:officers_parent) end @@ -91,12 +94,12 @@ def officers_parent def officers_parent! find_officers_parent_group || raise('special group :officers_parent does not exist.') end - - + + def descendant_officer_groups self.descendant_groups.where(type: 'OfficerGroup') end - + def create_officer_group(attrs = {name: "New Office"}) g = officers_parent.child_groups.create(attrs) g.update_attribute :type, "OfficerGroup" @@ -120,17 +123,20 @@ def find_officers_groups def officers_groups self.officers_parent.descendant_officer_groups end - + def officer_groups + self.officers_groups + end + def direct_officers self.find_officers_parent_group.try(:descendant_users) || [] end - + def officers_of_self_and_parent_groups cached do direct_officers + (parent_groups.collect { |parent_group| parent_group.direct_officers }.flatten) end end - + def officers_groups_of_self_and_descendant_groups cached do self.find_officers_parent_groups_of_self_and_of_descendant_groups.collect do |officers_parent| @@ -138,27 +144,27 @@ def officers_groups_of_self_and_descendant_groups end.flatten.uniq end end - + def find_officers cached do if respond_to? :child_groups - find_officers_parent_group.try(:descendant_users) + find_officers_parent_group.try(:members) end || [] end end def officers_of_ancestors - cached { ancestors.collect { |ancestor| ancestor.find_officers }.flatten } + cached { ancestors.collect { |ancestor| ancestor.find_officers.to_a }.flatten } end - + def officers_of_ancestor_groups - cached { ancestor_groups.collect { |ancestor| ancestor.find_officers }.flatten } + cached { ancestor_groups.collect { |ancestor| ancestor.find_officers.to_a }.flatten } end - + def officers_of_self_and_ancestors cached { find_officers + officers_of_ancestors } end - + def officers_of_self_and_ancestor_groups cached { find_officers + officers_of_ancestor_groups } end @@ -167,7 +173,7 @@ def officers_of_self_and_ancestor_groups # def officers self.find_officers_parent_groups_of_self_and_of_descendant_groups.collect do |officers_parent| - officers_parent.descendant_users + officers_parent.members.to_a end.flatten.uniq end @@ -217,29 +223,29 @@ def admins_parent! end def admins - find_or_create_admins_parent_group.try( :descendant_users ) || [] + find_or_create_admins_parent_group.try(:members) || [] end def find_admins cached do if respond_to? :child_groups - find_admins_parent_group.try( :descendant_users ) + find_admins_parent_group.try(:members) end || [] end || [] end - + def admins_of_ancestors cached { ancestors.collect { |ancestor| ancestor.find_admins }.flatten } end - + def admins_of_ancestor_groups cached { ancestor_groups.collect { |ancestor| ancestor.find_admins }.flatten } end - + def admins_of_self_and_ancestors cached { find_admins + admins_of_ancestors } end - + def responsible_admins # responsible are: local admins + last global admin: cached { (admins_of_self_and_ancestors - Group.global_admins.members[0..-2]).uniq } @@ -292,7 +298,7 @@ def main_admins_parent! end def main_admins - main_admins_parent.descendant_users + main_admins_parent.members end end diff --git a/app/models/user.rb b/app/models/user.rb index f03bb0985..cae92d610 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -62,7 +62,7 @@ class User < ActiveRecord::Base # Mixins # ========================================================================================== - include UserMixins::Memberships + include UserMemberships include UserMixins::Identification include ProfileableMixins::Address include UserCorporations @@ -460,8 +460,8 @@ def last_group_in_first_corporation def corporate_vita_memberships_in(corporation) Rails.cache.fetch([self, 'corporate_vita_memberships_in', corporation], expires_in: 1.week) do - group_ids = corporation.status_groups.map(&:id) & self.parent_group_ids - self.memberships.with_past.where(ancestor_id: group_ids, ancestor_type: 'Group') + corporation_status_groups = corporation.status_groups + self.memberships.with_past.select { |m| m.group.in? corporation_status_groups } end end @@ -481,13 +481,13 @@ def status_groups(options = {}) def status_group_memberships self.status_groups.collect do |group| - StatusGroupMembership.find_by_user_and_group( self, group ) + self.memberships.where(group: group).first end end def current_status_membership_in( corporation ) if status_group = current_status_group_in(corporation) - StatusGroupMembership.find_by_user_and_group(self, status_group) + self.memberships.where(group: status_group).first end end @@ -521,16 +521,15 @@ def relationships # all workflows of all groups the user is a member of. # def workflows - my_workflows = [] - self.groups.each do |group| - my_workflows += group.child_workflows - end - return my_workflows + self.groups.collect do |group| + group.child_workflows + end.flatten - [nil] end def workflows_for(group) - (([group.becomes(Group)] + group.descendant_groups) & self.groups) - .collect { |g| g.child_workflows }.select { |w| not w.nil? }.flatten + (self.groups & ([group] + group.connected_descendant_groups)).collect do |group| + group.child_workflows + end.flatten - [nil] end def workflows_by_corporation @@ -568,12 +567,9 @@ def join(event_or_group) end def leave(event_or_group) if event_or_group.kind_of? Group - # TODO: Change to `unassign` when he can have multiple dag links between two nodes. - # event_or_group.members.destroy(self) - raise 'We need multiple dag links between two nodes!' + event_or_group.unassign self elsif event_or_group.kind_of? Event - # TODO: Change to `unassign` when he can have multiple dag links between two nodes. - event_or_group.attendees_group.members.destroy(self) + event_or_group.attendees_group.unassign self end end @@ -657,12 +653,12 @@ def self.find_all_non_hidden # This efficiently returns all flags of the groups the user is currently in. # - # For example, ony can find out with one sql query whether a user is hidden: + # For example, ony can find out with one query whether a user is hidden: # # user.group_flags.include? 'hidden_users' # def group_flags - groups.joins(:flags).pluck('flags.key') + cached { self.groups.collect { |g| g.flags_to_syms }.flatten } end diff --git a/app/models/user_group_membership.rb b/app/models/user_group_membership.rb index ab29afeae..839fe5bbb 100644 --- a/app/models/user_group_membership.rb +++ b/app/models/user_group_membership.rb @@ -68,6 +68,7 @@ def self.create( params ) # new_membership.set_valid_from_to_now(true) new_membership.save + new_membership.recalculate_indirect_validity_ranges return new_membership end diff --git a/app/models/user_group_membership_mixins/validity_range.rb b/app/models/user_group_membership_mixins/validity_range.rb index 4460ff28a..c868ca68b 100644 --- a/app/models/user_group_membership_mixins/validity_range.rb +++ b/app/models/user_group_membership_mixins/validity_range.rb @@ -66,136 +66,16 @@ # TODO: Refactor the queries in the scopes when migrating to Rails 5. # module UserGroupMembershipMixins::ValidityRange - extend ActiveSupport::Concern - - included do - attr_accessible :valid_from, :valid_to, :valid_from_localized_date, :valid_to_localized_date - before_validation :set_valid_from_to_now - + + included do default_scope { valid } - - # Validity Perspective - # TODO: Allow :valid to include memberships that BECOME valid in the future. - scope :valid, -> { where("valid_from IS NULL OR valid_from <= ?", Time.zone.now).where("valid_to IS NULL OR valid_to >= ?", Time.zone.now) } - scope :invalid, -> { with_invalid.where("valid_to < ?", Time.zone.now) } - # scope :with_invalid # This is defined as method below due to some issues. - scope :only_valid, -> { valid } - scope :only_invalid, -> { invalid } - - # Time Perspective - scope :now, -> { valid } - scope :past, -> { invalid } - scope :in_the_past, -> { invalid } - scope :with_past, -> { with_invalid } - scope :now_and_past, -> { with_invalid } - scope :now_and_in_the_past, -> { with_invalid } - scope :at_time, -> (time) { with_past.where("valid_from IS NULL OR valid_from <= ?", time).where("valid_to IS NULL OR valid_to >= ?", time) } - scope :this_year, -> { with_invalid.where("valid_from >= ?", "#{Time.zone.now.year}-01-01 00:00:00") } - scope :started_after, -> (time) { where('NOT valid_from IS NULL').where("valid_from >= ?", time) } end - module ClassMethods - # This scope widens the query such that also memberships that are not valid - # at the present time are returned. - # - # Have a look at `rewhere`: - # https://github.com/rails/rails/commit/f950b2699f97749ef706c6939a84dfc85f0b05f2#diff-bf6dd6226db3aab589916f09236881c7R562 - # - # But `rewhere` is not enough. We need more filtering: - # https://github.com/fiedl/temporal_scopes/blob/master/lib/temporal_scopes/has_temporal_scopes.rb - # - # TODO: Check if this still needs the extra filter when migrating to Rails 5. - # - def with_invalid - relation = unscope(where: [:valid_from, :valid_to]) - relation.where_values.delete_if { |query| query.to_s.include?("valid_from") || query.to_s.include?("valid_to") } - relation - end - end + # + # The rest has been moved to DagLinkValidityRange. + # - concerning :Invalidation do - # This method ends the membership, i.e. sets the end of the validity range - # to the given time. - # - # The following examples are equivalent (despite the return value): - # - # membership.make_invalid - # membership.make_invalid at: Time.zone.now - # membership.make_invalid Time.zone.now - # membership.invalidate # => membership - # membership.update_attribute :valid_to, Time.zone.now # => true - # - def make_invalid(time = Time.zone.now) - time = time[:at] if time.kind_of?(Hash) && time[:at] - self.update_attribute(:valid_to, time) - return self - end - - # This is just an alias for `make_invalid`. - # - def invalidate(time = Time.zone.now) - self.make_invalid(time) - end - - # This method determines whether the membership can be invalidated. - # Direct memberships can be invalidated, whereas indirect memberships cannot. - # The validity of indirect memberships is derived from the validity of the direct ones. - # - def can_be_invalidated? - self.direct? - end - end - - concerning :ValidityCheck do - # This method checks whether the membership is valid at the given time. - # - # This is not to be confused with ActiveRecord's `valid` method, which checks whether the - # record matches the requirements to store it in the database. - # - # The following examples are equivalent: - # - # membership.currently_valid? - # membership.valid_at? Time.zone.now - # - def valid_at?(time) - (self.valid_from == nil || self.valid_from <= time) && (self.valid_to == nil || self.valid_to >= time) - end - - # This method checks whether the present time lies within the validity range - # of the membership. - # - def currently_valid? - valid_at?(Time.zone.now) - end - end - - concerning :Localization do - def valid_from_localized_date - self.valid_from ? I18n.localize(self.valid_from.try(:to_date)) : "" - end - def valid_from_localized_date=(new_date) - self.valid_from = new_date.to_datetime - valid_from_will_change! - end - - def set_valid_from_to_now(force = false) - self.valid_from ||= Time.zone.now if self.new_record? or force - return self - end - - def valid_to_localized_date - self.valid_to ? I18n.localize(self.valid_to.try(:to_date)) : "" - end - def valid_to_localized_date=(new_date) - if new_date == "-" - self.valid_to = nil - else - self.valid_to = new_date.to_datetime - end - valid_to_will_change! - end - end end class Array diff --git a/app/models/user_group_membership_mixins/validity_range_for_indirect_memberships.rb b/app/models/user_group_membership_mixins/validity_range_for_indirect_memberships.rb index 03406264b..c81fa35d7 100644 --- a/app/models/user_group_membership_mixins/validity_range_for_indirect_memberships.rb +++ b/app/models/user_group_membership_mixins/validity_range_for_indirect_memberships.rb @@ -138,12 +138,16 @@ def recalculate_validity_range_from_direct_memberships! end end + def recalculate_indirect_validity_ranges + self.indirect_memberships.each do |indirect_membership| + indirect_membership.recalculate_validity_range_from_direct_memberships + indirect_membership.save + end + end + def recalculate_indirect_validity_ranges_if_needed if self.direct? and @need_to_recalculate_indirect_memberships == true - self.indirect_memberships.each do |indirect_membership| - indirect_membership.recalculate_validity_range_from_direct_memberships - indirect_membership.save - end + recalculate_indirect_validity_ranges end end private :recalculate_indirect_validity_ranges_if_needed diff --git a/app/models/user_mixins/memberships.rb b/app/models/user_mixins/memberships.rb deleted file mode 100644 index 3f712f7d8..000000000 --- a/app/models/user_mixins/memberships.rb +++ /dev/null @@ -1,75 +0,0 @@ -# -# This module contains the methods of the User model regarding the associated -# user group memberships and groups. -# -module UserMixins::Memberships - - extend ActiveSupport::Concern - - included do - - # User Group Memberships - # ========================================================================================== - - # This associates all UserGroupMembership objects of the group, including indirect - # memberships. - # - has_many( :memberships, - -> { where ancestor_type: 'Group', descendant_type: 'User' }, - class_name: 'UserGroupMembership', - foreign_key: :descendant_id ) - - # This associates all memberships of the group that are direct, i.e. direct - # parent_group-child_user memberships. - # - has_many( :direct_memberships, - -> { where ancestor_type: 'Group', descendant_type: 'User', direct: true }, - class_name: 'UserGroupMembership', - foreign_key: :descendant_id ) - - # This associates all memberships of the group that are indirect, i.e. - # ancestor_group-descendant_user memberships, where groups are between the - # ancestor_group and the descendant_user. - # - has_many( :indirect_memberships, - -> { where ancestor_type: 'Group', descendant_type: 'User', direct: false }, - class_name: 'UserGroupMembership', - foreign_key: :descendant_id ) - - - # This returns the membership of the user in the given group if existant. - # - def membership_in( group ) - memberships.where(ancestor_id: group.id).limit(1).first - end - - - # Groups the user is member of - # ========================================================================================== - - # This associates the groups the user is member of, direct as well as indirect. - # - has_many(:groups, - -> { where('dag_links.descendant_type' => 'User').uniq }, - through: :memberships, - source: :ancestor, source_type: 'Group' - ) - - # This associates only the direct groups. - # - has_many(:direct_groups, - -> { where('dag_links.descendant_type' => 'User', 'dag_links.direct' => true).uniq }, - through: :direct_memberships, - source: :ancestor, source_type: 'Group' - ) - - # This associates only the indirect groups. - # - has_many(:indirect_groups, - -> { where('dag_links.descendant_type' => 'User', 'dag_links.direct' => false).uniq }, - through: :indirect_memberships, - source: :ancestor, source_type: 'Group' - ) - - end -end diff --git a/app/models/workflow.rb b/app/models/workflow.rb index 22b20d317..12adf77c5 100644 --- a/app/models/workflow.rb +++ b/app/models/workflow.rb @@ -16,8 +16,12 @@ def name_as_verb .downcase end - def wah_group # => TODO: corporation - ( self.ancestor_groups & Corporation.all ).first + def corporation + (self.ancestor_groups & Corporation.all).first + end + + def ancestor_groups + connected_ancestor_groups end def self.find_or_create_mark_as_deceased_workflow diff --git a/app/views/user_group_memberships/_memberships_table.html.haml b/app/views/memberships/_memberships_table.html.haml similarity index 91% rename from app/views/user_group_memberships/_memberships_table.html.haml rename to app/views/memberships/_memberships_table.html.haml index 07e9fa3d2..b20cc64fb 100644 --- a/app/views/user_group_memberships/_memberships_table.html.haml +++ b/app/views/memberships/_memberships_table.html.haml @@ -20,12 +20,11 @@ %th Pfad %th Mitglied seit %th Mitglied bis - %th Direkte Mitgliedschaft %th Löschen %tbody - memberships.each do |membership| %tr{class: ((membership.group && membership.currently_valid?) ? "currently_valid" : "currently_invalid")} - %td.copy-to-clipboard{title: "UserGroupMembership.now_and_in_the_past.find(#{membership.id})"}= membership.id + %td.copy-to-clipboard{title: "Membership.find(#{membership.id})"}= membership.id %td - if @user - if membership.group @@ -62,7 +61,6 @@ = localize(membership.valid_to.to_date) if membership.valid_to - else = localize(membership.read_attribute(:valid_to).to_date) if membership.read_attribute(:valid_to) - %td= membership.direct? ? "<strong>direkt</strong>".html_safe : "count: #{membership.count}" %td - if membership.destroyable? and can?(:destroy, membership) = remove_button membership, show_only_in_edit_mode: false, confirm: "Dies löscht den Eintrag unwiderbringlich! Dies soll nicht dazu verwendet werden, um eine Mitgliedschaft zu beenden, sondern NUR, WENN es sich um einen FEHLERHAFTEN EINTRAG handelt. Soll der Eintrag wirklich gelöscht werden?" \ No newline at end of file diff --git a/app/views/user_group_memberships/index.html.haml b/app/views/memberships/index.html.haml similarity index 100% rename from app/views/user_group_memberships/index.html.haml rename to app/views/memberships/index.html.haml diff --git a/app/views/users/_corporate_vita.html.haml b/app/views/users/_corporate_vita.html.haml index eec5b6705..70afe0f3e 100644 --- a/app/views/users/_corporate_vita.html.haml +++ b/app/views/users/_corporate_vita.html.haml @@ -5,13 +5,13 @@ - if memberships.count > 0 %tr %th{ colspan: 3}= corporation.title - - for membership in memberships + - memberships.each do |membership| - needs_review = (can?(:manage, user) && membership.needs_review?) - needs_review_class = needs_review ? 'needs_review' : '' %tr{ class: "membership #{needs_review_class}" } %td - if needs_review - = link_to(user_group_membership_path(membership, 'user_group_membership[needs_review]' => false), method: :put, remote: true, :class => 'btn btn-small btn-success confirm-review-button', title: I18n.t(:confirm_review)) do + = link_to(membership_path(membership, 'membership[needs_review]' => false), method: :put, remote: true, :class => 'btn btn-small btn-success confirm-review-button', title: I18n.t(:confirm_review)) do = icon 'ok' %td.membership_valid_from - if can? :update, membership diff --git a/config/initializers/active_record_has_dag_links_extension.rb b/config/initializers/active_record_has_dag_links_extension.rb new file mode 100644 index 000000000..7b6119d0b --- /dev/null +++ b/config/initializers/active_record_has_dag_links_extension.rb @@ -0,0 +1,2 @@ +require "has_dag_links" +ActiveRecord::Base.extend HasDagLinks diff --git a/config/initializers/cache.rb b/config/initializers/cache.rb new file mode 100644 index 000000000..ac6318fcc --- /dev/null +++ b/config/initializers/cache.rb @@ -0,0 +1,40 @@ +# This configures the cache key length such that it can be persistent. +# +# +# ## Explanation +# +# A typical cache key of a User looks like this, where the last part +# comes from the User#updated_at timestamp. +# +# users/4190-20151103154958215857000 +# +# Unfortunately, after reloading the user from the database, the cache key +# is changed, because the timestamp is stored with less precision in the +# database as in memory. +# +# users/4190-20151103154958000000000 +# +# With our current database setup, the updated_at column is only stored with +# a precision of integer seconds. +# +# Therefore, we shorten the cache key to: +# +# users/4190-20151103154958 +# +# +# ## Cache Timestamp Formats +# +# Possible formats are defined at: `Time::DATE_FORMATS` +# http://api.rubyonrails.org/classes/Time.html +# +# :nsec (nanoseconds) +# :usec (microseconds) +# :number (seconds) <-- currently stored in database +# +Rails.application.config.active_record.cache_timestamp_format = :number + +class ActiveRecord::Base + def self.cache_timestamp_format + Rails.application.config.active_record.cache_timestamp_format + end +end \ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb index db3ed99a8..98ddda36e 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -46,7 +46,7 @@ get :profile, to: 'profile_fields#index' get :settings, to: 'user_settings#show' put :settings, to: 'user_settings#update' - get :memberships, to: 'user_group_memberships#index' + get :memberships, to: 'memberships#index' get :badges, to: 'user_badges#index' end get :settings, to: 'user_settings#index' @@ -64,7 +64,7 @@ get :officers, to: 'officers#index' get :settings, to: 'group_settings#index' get :mailing_lists, to: 'mailing_lists#index' - get :memberships, to: 'user_group_memberships#index' + get :memberships, to: 'memberships#index' end get :my_groups, to: 'groups#index_mine' @@ -90,6 +90,7 @@ end resources :profile_fields resources :workflows + resources :memberships resources :user_group_memberships resources :status_group_memberships resources :relationships diff --git a/lib/your_platform/engine.rb b/lib/your_platform/engine.rb index dc8fb8c30..80db0a8a6 100644 --- a/lib/your_platform/engine.rb +++ b/lib/your_platform/engine.rb @@ -17,7 +17,6 @@ require 'jquery-atwho-rails' # Data Structures -require 'acts-as-dag' require 'acts_as_tree' # Caching diff --git a/spec/features/corporate_vita_spec.rb b/spec/features/corporate_vita_spec.rb index 44c21b08f..44f76821b 100644 --- a/spec/features/corporate_vita_spec.rb +++ b/spec/features/corporate_vita_spec.rb @@ -83,14 +83,14 @@ describe 'change the date of promotion afterwards' do before do @first_promotion_workflow.execute( user_id: @user.id ) - @membership = UserGroupMembership.now_and_in_the_past.find_by_user_and_group( @user, @status_groups.first ) + @membership = Membership.where(user: @user, group: @status_groups.first).first visit user_path( @user ) end it 'should be possible to change the date' do within('#corporate_vita') do - @valid_from_formatted = I18n.localize @membership.valid_from.to_date + @valid_from_formatted = I18n.localize @membership.valid_from.in_time_zone(TEST_TIMEZONE).to_date page.should have_content @valid_from_formatted @@ -101,14 +101,14 @@ page.should have_field 'valid_from_localized_date', with: @valid_from_formatted end - @new_date = 10.days.ago.to_date + @new_date = 10.days.ago.in_time_zone(TEST_TIMEZONE).to_date fill_in "valid_from_localized_date", with: I18n.localize(@new_date) page.should have_no_selector("input") page.should have_content I18n.localize(@new_date) wait_for_ajax; wait_for_ajax # apparently, it needs two in order not to fail - UserGroupMembership.now_and_in_the_past.find(@membership.id).valid_from.to_date.should == @new_date + Membership.find(@membership.id).valid_from.to_date.should == @new_date end end @@ -154,17 +154,16 @@ describe 'change the date of promotion afterwards' do before do @first_promotion_workflow.execute( user_id: @user.id ) - @membership = UserGroupMembership.now_and_in_the_past.find_by_user_and_group( @user, @status_groups.first ) + @membership = Membership.where(user: @user, group: @status_groups.first).first visit user_path( @user ) end it 'should be possible to change the date' do within('#corporate_vita') do - @valid_from_formatted = I18n.localize @membership.valid_from.to_date + @valid_from_formatted = I18n.localize @membership.valid_from.in_time_zone(TEST_TIMEZONE).to_date page.should have_content @valid_from_formatted - # activate inplace editing of the date_field first('.best_in_place.status_group_date_of_joining').click @@ -172,14 +171,14 @@ page.should have_field 'valid_from_localized_date', with: @valid_from_formatted end - @new_date = 10.days.ago.to_date + @new_date = 10.days.ago.in_time_zone(TEST_TIMEZONE).to_date fill_in "valid_from_localized_date", with: I18n.localize(@new_date) page.should have_no_selector("input") page.should have_content I18n.localize(@new_date) wait_for_ajax; wait_for_ajax # apparently, it needs two in order not to fail - UserGroupMembership.now_and_in_the_past.find(@membership.id).valid_from.to_date.should == @new_date + Membership.find(@membership.id).valid_from.to_date.should == @new_date end end diff --git a/spec/features/events_spec.rb b/spec/features/events_spec.rb index effa635d3..59adf5864 100644 --- a/spec/features/events_spec.rb +++ b/spec/features/events_spec.rb @@ -229,8 +229,9 @@ end context "for officers", js: true do - background do - @group.officers_parent.child_groups.create(name: 'President').assign_user @user, at: 1.hour.ago + background do + @group.assign_user @user, at: 10.hours.ago + @group.create_officer_group(name: 'President').assign_user @user, at: 1.hour.ago login @user end @@ -342,7 +343,7 @@ background do @corporation = create :corporation_with_status_groups @corporation.status_groups.first.assign_user @user, at: 1.month.ago - @president = @corporation.officers_parent.child_groups.create name: 'President' + @president = @corporation.create_officer_group name: 'President' @president.assign_user @user, at: 5.days.ago @other_event = create :event @other_event.parent_groups << @corporation diff --git a/spec/features/group_post_spec.rb b/spec/features/group_post_spec.rb index 3dd88171d..b9bb3296b 100644 --- a/spec/features/group_post_spec.rb +++ b/spec/features/group_post_spec.rb @@ -5,14 +5,16 @@ background do # - # @super_group + # @parent_group # |-------- @group ------ @other_user - # | - # @officers ----- @user + # |--------- @user + # | + # @officers -- @user # @user = create :user_with_account @other_user = create :user_with_account @group = create :group + @group << @user @group << @other_user @parent_group = create :group @parent_group << @group diff --git a/spec/models/ability_spec.rb b/spec/models/ability_spec.rb index f7c758a65..31fc9a417 100644 --- a/spec/models/ability_spec.rb +++ b/spec/models/ability_spec.rb @@ -1,9 +1,9 @@ require 'spec_helper' require 'cancan/matchers' -# In order to call the user "he" rather than "it", +# In order to call the user "he" rather than "it", # we have to define an alias here. -# +# # http://stackoverflow.com/questions/12317558/alias-it-in-rspec # RSpec.configure do |c| @@ -11,13 +11,13 @@ end describe Ability do - + # I'm sorry. I do have problems with cancan's terminology, here. - # For me, the User can do something, i.e. I would ask + # For me, the User can do something, i.e. I would ask # # @user.can? :manage, @page # - # But for cancan, it's + # But for cancan, it's # # Ability.new(@user).can? :manage, @page # @@ -30,7 +30,7 @@ let(:ability) { Ability.new(user) } subject { ability } let(:the_user) { subject } - + context "(posts and comments)" do context "when the user is a member of a group" do before { @group = create(:group); @group.assign_user(user, at: 1.month.ago) } @@ -71,7 +71,7 @@ end end end - + context "(mentions)" do context "when mentioned in a comment" do before do @@ -102,47 +102,48 @@ he { should be_able_to :download, @post_attachment } end end - + context "when the user is global admin" do before { user.global_admin = true } - + he "should not be able to destroy events that are older than 10 minutes" do @event = create :event, name: "Recent Event" @event.update_attribute :created_at, 11.minutes.ago - + the_user.should_not be_able_to :destroy, @event end - + he "should be able to destroy recently created pages" do @page = create :page, title: "New Page" - + the_user.should be_able_to :destroy, @page end he "should not be able to destroy pages that are older than 10 minutes" do @page = create :page, title: "Old Page" @page.update_attribute :created_at, 11.minutes.ago - + the_user.should_not be_able_to :destroy, @page end - + he "should be able to change the 'hidden' attribute of any user" do @other_user = create :user the_user.should be_able_to :change_hidden, @other_user end end - + context "when the user is a group admin" do before do @group = create :group + @group << user @group.admins << user time_travel 2.seconds end - + he "should be able to update its profile fields" do @profile_field = @group.profile_fields.create(type: 'ProfileFieldTypes::Phone', value: '123') the_user.should be_able_to :update, @profile_field end - + he "should not be able to change the 'hidden' attribute of the group members" do @other_user = create :user; @group << @other_user the_user.should_not be_able_to :change_hidden, @other_user @@ -153,7 +154,7 @@ he { should be_able_to :update, user } he { should be_able_to :change_status, user } end - + context "when the user is officer of a group" do before do @group = create :group @@ -164,12 +165,12 @@ @parent_group = @group.parent_groups.create(name: "Parent Group") @unrelated_group = create :group end - + he "should not be able to update its profile fields" do @profile_field = @group.profile_fields.create(type: 'ProfileFieldTypes::Phone', value: '123') the_user.should_not be_able_to :update, @profile_field end - + describe "(events)" do he "should be able to create an event in his group" do the_user.should be_able_to :create_event, @group @@ -195,14 +196,14 @@ end he "should be able to destroy just created events in his domain" do @event = @group.child_events.create name: "Special Event" - + user.should be_in @group.officers_of_self_and_ancestors the_user.should be_able_to :destroy, @event end he "should not be able to destroy events that are older than 10 minutes" do @event = @group.child_events.create name: "Recent Event" @event.update_attribute :created_at, 11.minutes.ago - + the_user.should_not be_able_to :destroy, @event end he "should be able to invite users to an event" do @@ -216,13 +217,13 @@ end end end - + describe "when the user is a page admin" do before do @page = create :page @page.admins << user end - + he { should be_able_to :create_page_for, @page } he "should be able to destroy the sub-page" do @sub_page = @page.child_pages.create @@ -234,14 +235,14 @@ the_user.should_not be_able_to :destroy, @sub_page end end - + describe "when the user is a page officer" do before do @page = create :page @secretary = @page.officers_parent.child_groups.create name: 'Secretary' @secretary << user end - + he { should be_able_to :create_page_for, @page } he "should be able to destroy the sub-page" do @sub_page = @page.child_pages.create @@ -253,14 +254,14 @@ the_user.should_not be_able_to :destroy, @sub_page end end - + describe "when the user is contact person of an event" do before do @event = create :event @event.contact_people_group.assign_user user, at: 2.minutes.ago end he { should be_able_to :create_page_for, @event } - + describe "when he is author of a subpage of the event" do before do @page = @event.child_pages.create @@ -269,7 +270,7 @@ end he { should be_able_to :update, @page} he { should be_able_to :create_attachment_for, @page } - + describe "when he is author of an attachment" do before do @attachment = @page.attachments.create @@ -278,7 +279,7 @@ end he { should be_able_to :update, @attachment } he { should be_able_to :destroy, @attachment } - + describe "when the user is also a global officer (bug fix)" do before do @global_officers = create :group @@ -286,7 +287,7 @@ @global_officers.assign_user user, at: 1.year.ago end let(:ability) { Ability.new(user, preview_as: 'global_officer') } - + he { should be_able_to :update, @attachment } he { should be_able_to :destroy, @attachment } end @@ -310,14 +311,14 @@ @secretary = Group.everyone.create_officer_group name: 'Secretary' @secretary.add_flag :global_officer @secretary.assign_user user, at: 1.month.ago - + @any_group = create :group end he { should be_able_to :create_event_for, @any_group } - + he "should be able to post to any group" do @any_group = create :group - + the_user.should be_able_to :create_post, @any_group the_user.should be_able_to :create_post_for, @any_group the_user.should be_able_to :create_post_via_email, @any_group @@ -329,21 +330,21 @@ @event = @any_group.child_events.create @event.contact_people << user end - + he { should be_able_to :update, @event } he { should be_able_to :invite_to, @event } end end end - + describe "for users without account" do let(:user) { create(:user) } let(:ability) { Ability.new(user) } subject { ability } let(:the_user) { subject } - - + + describe "(public pages)" do before do @root = Page.find_or_create_root @@ -352,11 +353,11 @@ @some_public_page = @root.child_pages.create title: 'This page is public.' Page.public_website_page_ids(true) # reload cached ids end - + he "should be able to access the imprint page" do @page = create :page, title: "Imprint" @page.add_flag :imprint - + the_user.should be_able_to :read, @page end he { should be_able_to :read, Page.find_root } diff --git a/spec/models/ability_to_use_mailing_lists_spec.rb b/spec/models/ability_to_use_mailing_lists_spec.rb index 47533638f..7abace45c 100644 --- a/spec/models/ability_to_use_mailing_lists_spec.rb +++ b/spec/models/ability_to_use_mailing_lists_spec.rb @@ -1,9 +1,9 @@ require 'spec_helper' require 'cancan/matchers' -# In order to call the user "he" rather than "it", +# In order to call the user "he" rather than "it", # we have to define an alias here. -# +# # http://stackoverflow.com/questions/12317558/alias-it-in-rspec # RSpec.configure do |c| @@ -11,15 +11,15 @@ end describe Ability do - + before { @group = create :group } - + describe "for users without account" do let(:user) { nil } let(:ability) { Ability.new(nil) } subject { ability } let(:the_user) { subject } - + describe "for regular @groups" do describe "sender filter" do describe '(empty)' do @@ -60,7 +60,7 @@ end end end - + describe "for the @group being an OfficerGroup" do before { @group.type = "OfficerGroup"; @group.save; @group = Group.find(@group.id) } @@ -75,7 +75,7 @@ end end end - + describe "for the @group having a corporation" do before do @corporation = create :corporation_with_status_groups @@ -94,14 +94,14 @@ end end end - - + + # I'm sorry. I do have problems with cancan's terminology, here. - # For me, the User can do something, i.e. I would ask + # For me, the User can do something, i.e. I would ask # # @user.can? :manage, @page # - # But for cancan, it's + # But for cancan, it's # # Ability.new(@user).can? :manage, @page # @@ -114,7 +114,7 @@ let(:ability) { Ability.new(user) } subject { ability } let(:the_user) { subject } - + describe "without the user being any member" do describe "for regular @groups" do describe "sender filter" do @@ -156,10 +156,10 @@ end end end - + describe "for the @group being an OfficerGroup" do before { @group.type = "OfficerGroup"; @group.save; @group = Group.find(@group.id) } - + describe "sender filter" do describe '(empty)' do before { @group.mailing_list_sender_filter = ""; @group.save } @@ -171,13 +171,13 @@ end end end - + describe "for the @group having a corporation" do before do @corporation = create :corporation_with_status_groups @corporation << @group end - + describe "sender filter" do describe '(empty)' do before { @group.mailing_list_sender_filter = ""; @group.save } @@ -190,14 +190,14 @@ end end end - + describe "for corporation members" do before do @corporation = create :corporation_with_status_groups @corporation.status_groups.first.assign_user user @corporation << @group end - + describe "sender filter" do describe '(empty)' do before { @group.mailing_list_sender_filter = ""; @group.save } @@ -236,10 +236,10 @@ he { should_not be_able_to :create_post_for, @group } end end - + describe "for the @group being an OfficerGroup" do before { @group.type = "OfficerGroup"; @group.save; @group = Group.find(@group.id) } - + describe "sender filter" do describe '(empty)' do before { @group.mailing_list_sender_filter = ""; @group.save } @@ -255,7 +255,7 @@ describe "for @group members" do before { @group.assign_user user } - + describe "sender filter" do describe '(empty)' do before { @group.mailing_list_sender_filter = ""; @group.save } @@ -296,14 +296,15 @@ end end - describe "for officers of the @group" do + describe "for officers (and members) of the @group" do before do @corporation = create :corporation_with_status_groups @corporation << @group @officer_group = @group.create_officer_group name: 'President' @officer_group.assign_user user + @group.assign_user user end - + describe "sender filter" do describe '(empty)' do before { @group.mailing_list_sender_filter = ""; @group.save } @@ -350,7 +351,7 @@ @officer_group = @other_group.create_officer_group name: 'President' @officer_group.assign_user user end - + describe "sender filter" do describe '(empty)' do before { @group.mailing_list_sender_filter = ""; @group.save } @@ -393,7 +394,7 @@ describe "for global officers" do # Currently, we've got an override in place (in the Ability model) - # that allows global officers to post to any group, even if not + # that allows global officers to post to any group, even if not # specified by the Group#mailing_list_sender_filter. before do @@ -405,7 +406,7 @@ @officer_group.add_flag :global_officer @officer_group.assign_user user end - + describe "sender filter" do describe '(empty)' do before { @group.mailing_list_sender_filter = ""; @group.save } diff --git a/spec/models/cache_spec.rb b/spec/models/cache_spec.rb new file mode 100644 index 000000000..3efecb4b8 --- /dev/null +++ b/spec/models/cache_spec.rb @@ -0,0 +1,41 @@ +require 'spec_helper' + +describe "cache" do + + describe "cache_timestamp_format" do + subject { User.cache_timestamp_format} + it { should == :number } + end + + describe "cache_key" do + before { @user = create :user } + subject { @user.cache_key } + its(:length) { should == "users/#{@user.id}-20151103154958".length } + end + + describe "after storing the User#title in the cache" do + + class User + def title + cached { "#{self.personal_title} #{self.name}".strip } + end + end + before { @user = create :user; @user.title } + + describe "Rails.cache.ls (Rails.cache.list_keys)" do + subject { Rails.cache.ls @user } + its(:count) { should >= 1} + it { should include "#{@user.cache_key}/title" } + end + + describe "#delete_cache" do + subject { @user.delete_cache} + it "should remove all cache entries under the user's scope" do + Rails.cache.ls(@user).count.should >= 1 + subject + Rails.cache.ls(@user).count.should == 0 + end + end + end + +end \ No newline at end of file diff --git a/spec/models/concerns/group_member_assignment_spec.rb b/spec/models/concerns/group_member_assignment_spec.rb new file mode 100644 index 000000000..289c0b1f5 --- /dev/null +++ b/spec/models/concerns/group_member_assignment_spec.rb @@ -0,0 +1,79 @@ +require 'spec_helper' + +describe GroupMemberAssignment do + + # @indirect_group + # |------------ @group + # | |------ @user1 + # | |------ @user2 + # | + # |------------ @group2 + # + before do + @group = create(:group) + @user1 = create(:user); @group.assign_user(@user1) + @user2 = create(:user); @group.assign_user(@user2) + @user = @user1 + @membership1 = UserGroupMembership.find_by(user: @user1, group: @group) + @membership2 = UserGroupMembership.find_by(user: @user2, group: @group) + @indirect_group = @group.parent_groups.create + @indirect_membership1 = UserGroupMembership.find_by(user: @user1, group: @indirect_group) + @indirect_membership2 = UserGroupMembership.find_by(user: @user2, group: @indirect_group) + @group2 = @indirect_group.child_groups.create + end + + + # User Assignment + # ========================================================================================== + + describe "#assign_user" do + before { @membership1.destroy } + it "should assign the user to the group" do + @group.members.should_not include @user + @group.assign_user @user + @group.reload + @group.members.should include @user + end + describe "for users that are already members" do + before { @group.direct_members << @user } + it "should just keep them as members" do + @group.members.should include @user + @group.assign_user @user + @group.reload + @group.members.should include @user + end + end + end + + describe "#unassign_user" do + before { @membership1.destroy } + describe "if the user is a member" do + before { @group.direct_members << @user } + it "should remove the membership" do + @group.members.should include @user + @group.unassign_user @user + time_travel 2.seconds + @group.reload.members.should_not include @user + end + end + describe "if the user is not a member" do + it "should not raise an error" do + @group.members.should_not include @user + expect { @group.unassign_user @user }.to_not raise_error + end + end + end + + describe "#direct_member_titles_string" do + subject { @group.direct_members_titles_string } + it { should == "#{@user1.title}, #{@user2.title}" } + end + describe "#direct_member_titles_string=" do + before { @group.direct_members_titles_string = "#{@user1.title}"; time_travel 2.seconds } + it "should set the memberships according to the titles" do + @group.reload.memberships.should include( @membership1 ) + @group.reload.memberships.should_not include( @membership2 ) + end + end + +end diff --git a/spec/models/group_mixins/memberships_spec.rb b/spec/models/concerns/group_memberships_spec.rb similarity index 70% rename from spec/models/group_mixins/memberships_spec.rb rename to spec/models/concerns/group_memberships_spec.rb index 5678bb9dc..a44a19a2d 100644 --- a/spec/models/group_mixins/memberships_spec.rb +++ b/spec/models/concerns/group_memberships_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe GroupMixins::Memberships do +describe GroupMemberships do # @indirect_group # |------------ @group @@ -14,11 +14,11 @@ @user1 = create(:user); @group.assign_user(@user1) @user2 = create(:user); @group.assign_user(@user2) @user = @user1 - @membership1 = UserGroupMembership.find_by(user: @user1, group: @group) - @membership2 = UserGroupMembership.find_by(user: @user2, group: @group) + @membership1 = Membership.where(user: @user1, group: @group).first + @membership2 = Membership.where(user: @user2, group: @group).first @indirect_group = @group.parent_groups.create - @indirect_membership1 = UserGroupMembership.find_by(user: @user1, group: @indirect_group) - @indirect_membership2 = UserGroupMembership.find_by(user: @user2, group: @indirect_group) + @indirect_membership1 = Membership.where(user: @user1, group: @indirect_group).first + @indirect_membership2 = Membership.where(user: @user2, group: @indirect_group).first @group2 = @indirect_group.child_groups.create end @@ -41,13 +41,13 @@ before { @membership1.invalidate at: 10.minutes.ago } subject { @group.memberships } it { should include @membership2 } - it { should == [ @membership2 ] } + its(:count) { should == 1 } it "should not list the invalidated memberships, i.e. respect the default scope" do subject.should_not include @membership1 end end end - + describe "#direct_memberships" do describe "for a group having direct members" do subject { @group.direct_memberships } @@ -76,61 +76,17 @@ describe "#build_membership" do subject { @group.build_membership } - it { should be_kind_of UserGroupMembership } - its(:ancestor_type) { should == 'Group' } - its(:ancestor_id) { should == @group.id } - its(:descendant_type) { should == 'User' } - its(:descendant_id) { should == nil } + it { should be_kind_of Membership } + its(:group) { should == @group } + its(:user) { should == nil } its(:new_record?) { should == true } end describe "#membership_of( user )" do subject { @group.membership_of(@user1) } - it { should be_kind_of UserGroupMembership } + it { should be_kind_of Membership } it { should == @membership1 } end - - - # User Assignment - # ========================================================================================== - - describe "#assign_user" do - before { @membership1.destroy } - it "should assign the user to the group" do - @group.members.should_not include @user - @group.assign_user @user - @group.reload - @group.members.should include @user - end - describe "for users that are already members" do - before { @group.direct_members << @user } - it "should just keep them as members" do - @group.members.should include @user - @group.assign_user @user - @group.reload - @group.members.should include @user - end - end - end - - describe "#unassign_user" do - before { @membership1.destroy } - describe "if the user is a member" do - before { @group.direct_members << @user } - it "should remove the membership" do - @group.members.should include @user - @group.unassign_user @user - time_travel 2.seconds - @group.reload.members.should_not include @user - end - end - describe "if the user is not a member" do - it "should not raise an error" do - @group.members.should_not include @user - expect { @group.unassign_user @user }.to_not raise_error - end - end - end # Members @@ -183,26 +139,6 @@ @user.should be_in @group2.direct_members end end - describe "#members.destroy(user)" do - describe "for the membership being direct" do - subject { @group.members.destroy(@user1) } - it "should remove the user from the members list" do - @user1.should be_in @group.members - subject - @user1.should_not be_in @group.members - end - it "should remove the membership permanently" do - subject - UserGroupMembership.with_invalid.find_by_user_and_group(@user1, @group).should == nil - end - end - describe "for the membership being indirect" do - subject { @indirect_group.members.destroy(@user1) } - it "should raise an error" do - expect { subject }.to raise_error - end - end - end describe "#direct_members" do describe "for a group having direct members" do @@ -286,18 +222,5 @@ it { should include @user_unique2 } end end - - describe "#direct_member_titles_string" do - subject { @group.direct_members_titles_string } - it { should == "#{@user1.title}, #{@user2.title}" } - end - describe "#direct_member_titles_string=" do - before { @group.direct_members_titles_string = "#{@user1.title}"; time_travel 2.seconds } - it "should set the memberships according to the titles" do - @group.reload.memberships.should include( @membership1 ) - @group.reload.memberships.should_not include( @membership2 ) - end - end - - -end + +end \ No newline at end of file diff --git a/spec/models/concerns/membership_collection_validity_range_spec.rb b/spec/models/concerns/membership_collection_validity_range_spec.rb new file mode 100644 index 000000000..7a94d8ba1 --- /dev/null +++ b/spec/models/concerns/membership_collection_validity_range_spec.rb @@ -0,0 +1,58 @@ +require 'spec_helper' + +describe MembershipCollectionValidityRange do + + # @group1 --- @subgroup1 ------ + # | + # @group2 ------------------ @user1 + # | + # |---------------------- @user2 + # + before do + @group1 = create :group, name: 'group1' + @subgroup1 = @group1.child_groups.create name: 'group2' + @group2 = create :group, name: 'group2' + @user1 = create :user; @subgroup1 << @user1; @group2 << @user1 + @user2 = create :user; @group2 << @user2 + end + + describe "#valid" do + describe "if the dag link between @subgroup1 and @user1 has been invalidated" do + before { @subgroup1.links_as_parent.first.update_attribute :valid_to, 5.minutes.ago } + it "should limit the results to valid links" do + Membership.where(group: @group1).valid.count.should == 0 + Membership.where(group: @subgroup1).valid.count.should == 0 + Membership.where(group: @group2).valid.count.should == 2 + Membership.where(user: @user1).valid.count.should == 1 + Membership.where(user: @user2).valid.count.should == 1 + end + end + end + + describe "#invalid" do + describe "if the dag link between @subgroup1 and @user1 has been invalidated" do + before { @subgroup1.links_as_parent.first.update_attribute :valid_to, 5.minutes.ago } + it "should limit the results to invalid links" do + Membership.where(group: @group1).invalid.count.should == 1 + Membership.where(group: @subgroup1).invalid.count.should == 1 + Membership.where(group: @group2).invalid.count.should == 0 + Membership.where(user: @user1).invalid.count.should == 2 + Membership.where(user: @user2).invalid.count.should == 0 + end + end + end + + describe "#with_invalid" do + describe "if the dag link between @subgroup1 and @user1 has been invalidated" do + before { @subgroup1.links_as_parent.first.update_attribute :valid_to, 5.minutes.ago } + it "should include valid and invalid memberships in the results" do + Membership.where(group: @group1).with_invalid.count.should == 1 + Membership.where(group: @subgroup1).with_invalid.count.should == 1 + Membership.where(group: @group2).with_invalid.count.should == 2 + Membership.where(user: @user1).with_invalid.count.should == 3 + Membership.where(user: @user2).with_invalid.count.should == 1 + end + end + end + +end \ No newline at end of file diff --git a/spec/models/concerns/membership_review_spec.rb b/spec/models/concerns/membership_review_spec.rb new file mode 100644 index 000000000..f4e55a76e --- /dev/null +++ b/spec/models/concerns/membership_review_spec.rb @@ -0,0 +1,66 @@ +require 'spec_helper' + +describe MembershipReview do + before do + @user = create :user + @group = create :group + @membership = Membership.create user: @user, group: @group + end + + describe "needs_review?" do + subject { @membership.needs_review? } + describe "when unset" do + it { should == false } + end + describe "when set to true" do + before { @membership.needs_review = true; @membership.reload } + it { should == true } + end + end + + describe "needs_review=" do + describe "true" do + subject { @membership.needs_review = true; @membership.reload } + describe "when unset" do + specify { subject; @membership.needs_review?.should == true } + end + describe "when set to true" do + before { @membership.needs_review = true } + specify { subject; @membership.needs_review?.should == true } + end + describe "when set to false" do + before { @membership.needs_review = false } + specify { subject; @membership.needs_review?.should == true } + end + end + describe "false" do + subject { @membership.needs_review = false; @membership.reload } + describe "when unset" do + specify { subject; @membership.needs_review?.should == false } + end + describe "when set to true" do + before { @membership.needs_review = true } + specify { subject; @membership.needs_review?.should == false } + end + describe "when set to false" do + before { @membership.needs_review = false } + specify { subject; @membership.needs_review?.should == false } + end + end + end + + describe "needs_review!" do + subject { @membership.needs_review!; @membership.reload } + describe "when unset" do + specify { subject; @membership.needs_review?.should == true } + end + describe "when set to true" do + before { @membership.needs_review = true } + specify { subject; @membership.needs_review?.should == true } + end + describe "when set to false" do + before { @membership.needs_review = false } + specify { subject; @membership.needs_review?.should == true } + end + end +end \ No newline at end of file diff --git a/spec/models/concerns/membership_validity_range_localization_spec.rb b/spec/models/concerns/membership_validity_range_localization_spec.rb new file mode 100644 index 000000000..ebd450e5c --- /dev/null +++ b/spec/models/concerns/membership_validity_range_localization_spec.rb @@ -0,0 +1,112 @@ +require 'spec_helper' + +describe MembershipValidityRangeLocalization do + + # + # @group1 --- @user1 + # + before do + @group1 = create :group, name: 'group1' + @user1 = create :user + @membership = Membership.create user: @user1, group: @group1 + end + + describe "#valid_from_localized_date" do + subject { @membership.valid_from_localized_date } + describe "if no valid_from given" do + before { @membership.valid_from = nil } + it { should == "" } + end + describe "if a datetime given" do + before do + @time = "1.1.2013 12:30 UTC".to_datetime + @membership.valid_from = @time + end + it { should == "01.01.2013" } + end + end + + describe "#valid_from_localized_date=" do + describe "setting a date string" do + subject { @membership.valid_from_localized_date = "1.1.2013" } + it "should set the correct date" do + subject + @membership.valid_from.to_date.should == "1.1.2013".to_date + end + it "should set the date persistently when saved" do + subject + @membership.save + @membership.reload.valid_from.to_date.should == "1.1.2013".to_date + end + end + describe "setting an empty string" do + subject { @membership.valid_from_localized_date = "" } + it "should set valid_from to nil" do + subject + @membership.valid_from.should == nil + end + it "should set the date persistently when saved" do + subject + @membership.save + @membership.reload.valid_from.should == nil + end + end + describe "setting an invalid date" do + subject { @membership.valid_from_localized_date = "FOO BAR" } + it "should raise an error" do + expect { subject }.to raise_error + end + end + end + + + describe "#valid_to_localized_date" do + subject { @membership.valid_to_localized_date } + describe "if no valid_to given" do + before { @membership.valid_to = nil } + it { should == "" } + end + describe "if a datetime given" do + before do + @time = "1.1.2013 12:30 UTC".to_datetime + @membership.valid_to = @time + end + it { should == "01.01.2013" } + end + end + + describe "#valid_to_localized_date=" do + describe "setting a date string" do + subject { @membership.valid_to_localized_date = "1.1.2013" } + it "should set the correct date" do + subject + @membership.valid_to.to_date.should == "1.1.2013".to_date + end + it "should set the date persistently when saved" do + subject + @membership.save + @membership.reload.valid_to.to_date.should == "1.1.2013".to_date + end + end + describe "setting an empty string" do + subject { @membership.valid_to_localized_date = "" } + it "should set valid_to to nil" do + subject + @membership.valid_to.should == nil + end + it "should set the date persistently when saved" do + subject + @membership.save + @membership.reload.valid_to.should == nil + end + end + describe "setting an invalid date" do + subject { @membership.valid_to_localized_date = "FOO BAR" } + it "should raise an error" do + expect { subject }.to raise_error + end + end + end + + +end \ No newline at end of file diff --git a/spec/models/concerns/membership_validity_range_spec.rb b/spec/models/concerns/membership_validity_range_spec.rb new file mode 100644 index 000000000..01027a758 --- /dev/null +++ b/spec/models/concerns/membership_validity_range_spec.rb @@ -0,0 +1,164 @@ +require 'spec_helper' + +describe MembershipValidityRange do + + # @group1 --- @subgroup1 ------ + # | + # @group2 ------------------ @user1 + # | + # |---------------------- @user2 + # + before do + @group1 = create :group, name: 'group1' + @subgroup1 = @group1.child_groups.create name: 'subgroup1' + @group2 = create :group, name: 'group2' + @user1 = create :user, last_name: 'user1'; @subgroup1 << @user1; @group2 << @user1 + @user2 = create :user, last_name: 'user2'; @group2 << @user2 + end + + describe "#invalidate" do + describe "for direct memberships" do + before { @membership = Membership.where(user: @user1, group: @subgroup1).first } + + describe "without argument" do + subject { @membership.invalidate } + + it "sets the valid_to attribute on the DagLink to the current time" do + DagLink.where(ancestor_type: 'Group', descendant_type: 'User', direct: true, + ancestor_id: @subgroup1.id, descendant_id: @user1.id).first + .valid_to.should == nil + subject + DagLink.where(ancestor_type: 'Group', descendant_type: 'User', direct: true, + ancestor_id: @subgroup1.id, descendant_id: @user1.id).first + .valid_to.should > 1.second.ago + end + it "sets the valid_to attribute on the membership to the current time" do + @membership.valid_to.should == nil + subject + @membership.reload.valid_to.should > 1.second.ago + end + end + + describe "with time as argument" do + before { @time = 20.minutes.ago } + subject { @membership.invalidate at: @time } + + it "sets the valid_to attribute on the DagLink to the given time" do + DagLink.where(ancestor_type: 'Group', descendant_type: 'User', direct: true, + ancestor_id: @subgroup1.id, descendant_id: @user1.id).first + .valid_to.should == nil + subject + DagLink.where(ancestor_type: 'Group', descendant_type: 'User', direct: true, + ancestor_id: @subgroup1.id, descendant_id: @user1.id).first + .valid_to.to_i.should == @time.to_i + end + it "sets the valid_to attribute on the membership to the given time" do + @membership.valid_to.should == nil + subject + @membership.reload.valid_to.to_i.should == @time.to_i + end + end + + end + end + + describe "#currently_valid?" do + subject { @membership.currently_valid? } + describe "for direct memberships" do + describe "for a membership without validity range" do + before { @membership = Membership.where(user: @user1, group: @subgroup1).first } + it { should be_true } + end + describe "for a current membership" do + before do + @membership = Membership.where(user: @user1, group: @subgroup1).first + @membership.dag_link.update_attributes valid_from: 1.month.ago + @membership = Membership.where(user: @user1, group: @subgroup1).first + end + it { should be_true } + end + describe "for a past membership" do + before do + @membership = Membership.where(user: @user1, group: @subgroup1).first + @membership.dag_link.update_attributes valid_from: 1.month.ago, valid_to: 10.days.ago + @membership = Membership.where(user: @user1, group: @subgroup1).first + end + it { should be_false } + end + end + describe "for indirect memberships" do + describe "for a membership without validity range" do + before { @membership = Membership.where(user: @user1, group: @group1).first } + it { should be_true } + end + describe "for a current membership" do + before do + Membership.where(user: @user1, group: @subgroup1).first.dag_link.update_attribute :valid_from, 1.month.ago + @membership = Membership.where(user: @user1, group: @group1).first + end + it { should be_true } + end + describe "for a past membership" do + before do + Membership.where(user: @user1, group: @subgroup1).first.dag_link.update_attribute :valid_from, 1.month.ago + Membership.where(user: @user1, group: @subgroup1).first.dag_link.update_attribute :valid_to, 10.days.ago + @membership = Membership.where(user: @user1, group: @group1).first + end + it { should be_false } + end + end + end + + describe "#where" do + # + # @group1 --- @subgroup1 @user1 + # | + # |------- @subgroup2 + # + before do + @group1 = create :group, name: 'group1' + @subgroup1 = @group1.child_groups.create name: 'subgroup1' + @subgroup2 = @group1.child_groups.create name: 'subgroup2' + @user1 = create :user + end + subject { Membership.where(user: @user1, group: @group1) } + + describe "for user and group" do + before { @t1 = 2.years.ago; @t2 = 1.year.ago; @t3 = 1.month.ago; @t4 = nil } + describe "if the user has multiple direct memberships in that group" do + before do + @membership1 = Membership.create group: @group1, user: @user1, valid_from: @t1, valid_to: @t2 + @membership2 = Membership.create group: @group1, user: @user1, valid_from: @t3 + end + its(:count) { should == 2 } + specify "the memberships should have the correct validity range" do + subject.to_a.collect { |membership| membership.valid_from.try(:to_i) }.should include @t1.to_i, @t3.to_i + subject.to_a.collect { |membership| membership.valid_to.try(:to_i) }.should include @t2.to_i, @t4 + end + end + describe "if the user has multiple indirect memberships in that group" do + before do + @membership1 = Membership.create group: @subgroup1, user: @user1, valid_from: @t1, valid_to: @t2 + @membership2 = Membership.create group: @subgroup2, user: @user1, valid_from: @t3 + end + its(:count) { should == 2 } + specify "the memberships should have the correct validity range" do + subject.to_a.collect { |membership| membership.valid_from.try(:to_i) }.should include @t1.to_i, @t3.to_i + subject.to_a.collect { |membership| membership.valid_to.try(:to_i) }.should include @t2.to_i, @t4 + end + end + describe "if the user has direct and indirect memberships in that group" do + before do + @membership1 = Membership.create group: @group1, user: @user1, valid_from: @t1, valid_to: @t2 + @membership2 = Membership.create group: @subgroup1, user: @user1, valid_from: @t3 + end + its(:count) { should == 2 } + specify "the memberships should have the correct validity range" do + subject.to_a.collect { |membership| membership.valid_from.try(:to_i) }.should include @t1.to_i, @t3.to_i + subject.to_a.collect { |membership| membership.valid_to.try(:to_i) }.should include @t2.to_i, @t4 + end + end + end + end + +end diff --git a/spec/models/concerns/structureable_connected_groups_spec.rb b/spec/models/concerns/structureable_connected_groups_spec.rb new file mode 100644 index 000000000..2aba17e2d --- /dev/null +++ b/spec/models/concerns/structureable_connected_groups_spec.rb @@ -0,0 +1,81 @@ +require 'spec_helper' + +describe StructureableConnectedGroups do + + # Example: + # + # group1 + # |---- group2 --- group3 -------------- + # |---- event1 | + # | |------ attendees_group ---- user1 + # | + # officers_parent ---- officer_group --- user2 + # + # In the example, groups 1, 2, and 3 are connected groups. But the attendees_group + # is not connected to them, because a non-group object, event1, is in between. + # + # Despite `officers_parent` being a group, `user2` is not regarded as + # connected to `group1`, since officers aren't necessarily members of a group. + # + before do + @group1 = create :group, name: 'group1' + @group2 = @group1.child_groups.create name: 'group2' + @group3 = @group2.child_groups.create name: 'group3' + @event1 = @group1.child_events.create name: 'event1' + @attendees_group = @event1.attendees_group + @user1 = create :user; @group3 << @user1; @attendees_group << @user1 + @officers_parent = @group1.officers_parent + @officer_group = @officers_parent.child_groups.create name: 'officer_group' + @user2 = create :user; @officer_group << @user2 + end + + describe "#connected_ancestor_groups" do + describe "for @group3" do + subject { @group3.connected_ancestor_groups } + it { should include @group1, @group2 } + it { should_not include @group3 } + it { should_not include @attendees_group } + end + + describe "for @group2" do + subject { @group2.connected_ancestor_groups } + it { should include @group1 } + it { should_not include @group2 } + it { should_not include @group3, @attendees_group } + end + + describe "for @attendees_group" do + subject { @attendees_group.connected_ancestor_groups } + it { should == [] } + end + + describe "for @user1" do + subject { @user1.connected_ancestor_groups } + it { should include @group1, @group2, @group3 } + it { should include @attendees_group } + it { should_not include @event1 } + end + + describe "when officer groups are on the path" do + describe "for @user2" do + subject { @user2.connected_ancestor_groups } + it { should include @officer_group } + it { should_not include @group1 } + end + end + end + + describe "#connected_descendant_groups" do + describe "for @group1" do + subject { @group1.connected_descendant_groups } + it { should include @group2, @group3 } + it { should_not include @group1 } + it { should_not include @attendees_group } + it "should not include officer_parents or officer_groups" do + subject.should_not include @officers_parent + subject.should_not include @officer_group + end + end + end + +end \ No newline at end of file diff --git a/spec/models/concerns/structureable_connected_pages_spec.rb b/spec/models/concerns/structureable_connected_pages_spec.rb new file mode 100644 index 000000000..c30e6a4be --- /dev/null +++ b/spec/models/concerns/structureable_connected_pages_spec.rb @@ -0,0 +1,29 @@ +require 'spec_helper' + +describe StructureableConnectedPages do + + # + # @group + # | + # @page --- @subpage + # | + # @disconnected_group + # | + # @disconnected_group_page + # + before do + @group = create :group, name: 'group' + @page = @group.child_pages.create name: 'page' + @subpage = @page.child_pages.create name: 'subpage' + @disconnected_group = @subpage.child_groups.create name: 'disconnected_group' + @disconnected_group_page = @disconnected_group.child_pages.create name: 'disconnected_group_page' + end + + describe "#conncted_descendant_pages" do + subject { @group.connected_descendant_pages } + it { should include @page } + it { should include @subpage } + it { should_not include @disconnected_group_page } + end + +end \ No newline at end of file diff --git a/spec/models/concerns/structureable_graph_cache_spec.rb b/spec/models/concerns/structureable_graph_cache_spec.rb new file mode 100644 index 000000000..6ecafb25c --- /dev/null +++ b/spec/models/concerns/structureable_graph_cache_spec.rb @@ -0,0 +1,94 @@ +require 'spec_helper' + +describe StructureableGraphCache do + + # + # @root_page + # | + # @intranet_root_page + # | + # @everyone_group + # | + # @corporations_group + # |------------------ @berlin_corporation + # | + # @erlangen_corporation + # |------------------ @philisterschaft_group + # | + # @aktivitas_group + # |------------------ @fuxen_group --------- @fux_user + # | |-------------- @fuxen_subgroup + # | + # |------------------ @burschen_group ------ @burschen_protokolle_page + # |------------------ @protokolle_pages ---- @protokolle_sub_page + # | + # @officers_parent_group + # | + # @chargen_group + # | + # @senior_group + # + before do + @root_page = Page.find_root + @intranet_root_page = Page.find_intranet_root; @root_page << @intranet_root_page + @everyone_group = Group.everyone; @intranet_root_page << @everyone_group + @corporations_group = Group.corporations_parent; @everyone_group << @corporations_group + @berlin_corporation = create :corporation, name: "Berlin" + @erlangen_corporation = create :corporation, name: "Erlangen" + @philisterschaft_group = @erlangen_corporation.child_groups.create name: "Philisterschaft" + @aktivitas_group = @erlangen_corporation.child_groups.create name: "Aktivitas" + @fuxen_group = @aktivitas_group.child_groups.create name: "Fuxen" + @fuxen_subgroup = @fuxen_group.child_groups.create name: 'fuxen_subgroup' + @burschen_group = @aktivitas_group.child_groups.create name: "Burschen" + @protokolle_page = @aktivitas_group.child_pages.create title: "Protokolle" + @protokolle_sub_page = @protokolle_page.child_pages.create title: "Protokolle WS 2015/16" + @burschen_protokolle_page = @burschen_group.child_pages.create title: 'burschen_protokolle_page' + @officers_parent_group = @aktivitas_group.officers_parent + @chargen_group = @officers_parent_group.child_groups.create name: "Chargen" + @senior_group = @chargen_group.child_groups.create name: "Chargen" + @fux_user = create :user; @fuxen_group.assign_user @fux_user + end + + describe "requirements" do + describe "@everyone_group.connected_descendant_groups" do + subject { @everyone_group.connected_descendant_groups } + it { should include @corporations_group, @berlin_corporation, @erlangen_corporation, @aktivitas_group, @philisterschaft_group, @fuxen_group, @burschen_group } + it { should_not include @chargen_group, @senior_group } + it { should_not include @officers_parent_group } + end + end + + describe "#affected_nodes_after_officer_has_changed" do + subject { @aktivitas_group.affected_nodes_after_officer_has_changed } + it { should include @fuxen_group, @burschen_group } + it { should include @protokolle_page, @protokolle_sub_page } + it { should include @burschen_protokolle_page } + it { should_not include @everyone_group, @corporations_group, @erlangen_corporation, @berlin_corporation } + it { should_not include @philisterschaft_group } + it { should_not include @root_page, @intranet_root_page } + it { should_not include @officers_parent_group, @chargen_group, @senior_group } + it { should include @fux_user } + end + + describe "#affected_nodes_after_membership_has_changed" do + subject { @fuxen_group.affected_nodes_after_membership_has_changed } + it { should_not include @fuxen_group } + it { should include @aktivitas_group, @erlangen_corporation, @corporations_group, @everyone_group } + it { should_not include @burschen_group, @philisterschaft_group, @berlin_corporation } + it { should_not include @protokolle_page, @root_page, @intranet_root_page } + it { should_not include @officers_parent_group, @chargen_group, @senior_group } + it { should_not include @fux_user } + end + + describe "#affected_nodes_after_subgroup_has_changed" do + subject { @fuxen_group.affected_nodes_after_subgroup_has_changed } + it { should_not include @fuxen_group } + it { should include @aktivitas_group, @erlangen_corporation, @corporations_group, @everyone_group } + it { should_not include @burschen_group, @philisterschaft_group, @berlin_corporation } + it { should_not include @protokolle_page, @root_page, @intranet_root_page } + it { should_not include @officers_parent_group, @chargen_group, @senior_group } + it { should_not include @fux_user } + it { should_not include @fuxen_subgroup } + end + +end \ No newline at end of file diff --git a/spec/models/concerns/user_memberships_spec.rb b/spec/models/concerns/user_memberships_spec.rb new file mode 100644 index 000000000..557eaac80 --- /dev/null +++ b/spec/models/concerns/user_memberships_spec.rb @@ -0,0 +1,131 @@ +require 'spec_helper' + +describe UserMemberships do + + # @indirect_group + # |------------ @group + # | |------ @user1 + # | |------ @user2 + # | + # |------------ @group2 + # + before do + @group = create(:group) + @user1 = create(:user); @group.assign_user(@user1) + @user2 = create(:user); @group.assign_user(@user2) + @user = @user1 + @membership1 = Membership.where(user: @user1, group: @group).first + @membership2 = Membership.where(user: @user2, group: @group).first + @indirect_group = @group.parent_groups.create + @indirect_membership1 = Membership.where(user: @user1, group: @indirect_group).first + @indirect_membership2 = Membership.where(user: @user2, group: @indirect_group).first + @group2 = @indirect_group.child_groups.create + end + + describe "(Memberships)" do + describe "#memberships" do + subject { @user1.memberships } + it { should include @membership1 } + it { should include @indirect_membership1 } + it "should not include invalidated memberships" do + @membership1.invalidate at: 10.minutes.ago + subject { should_not include @membership1 } + end + it "should not include invalidated indirect memberships" do + @membership1.invalidate at: 10.minutes.ago + subject { should_not include @indirect_membership1 } + end + end + + describe "#direct_memberships" do + subject { @user1.direct_memberships } + it { should include @membership1 } + it { should_not include @indirect_membership1 } + end + + describe "#indirect_memberships" do + subject { @user1.indirect_memberships } + it { should include @indirect_membership1 } + it { should_not include @membership1 } + end + + describe "#memberships_in(group)" do + describe "for the user being a direct member" do + subject { @user.memberships_in @group } + it { should be_kind_of MembershipCollection } + it { should include @membership1 } + end + describe "for the user being an indirect member" do + subject { @user.memberships_in @indirect_group } + it { should be_kind_of MembershipCollection } + it { should include @indirect_membership1 } + end + end + + describe "#membership_in(group)" do + describe "for the user being a direct member" do + subject { @user.membership_in @group } + it { should == @membership1 } + end + describe "for the user being an indirect member" do + subject { @user.membership_in @indirect_group } + it { should == @indirect_membership1 } + end + end + + describe "#member_of?(group) [defined in UserRoles]" do + describe "for the user being direct member" do + subject { @user.member_of? @group} + it { should == true } + end + describe "for the user being indirect member" do + subject { @user.member_of? @indirect_group } + it { should == true } + end + describe "for the user not being a member" do + subject { @user.member_of? @group2 } + it { should == false } + end + end + end + + describe "(Groups)" do + # + # @indirect_group + # |------------ @group + # | |------ @user1 # @membership1 + # | |------ @user2 + # | + # |------------ @group2 + # |------ @user1 + # + before do + Membership.create user: @user1, group: @group2 + end + + describe "#groups" do + subject { @user1.groups } + it { should include @group } + it { should include @indirect_group } + describe "when a direct membership has been invalidated" do + before { @membership1.invalidate at: 10.minutes.ago } + it { should_not include @group } + it { should include @group2 } + it { should include @indirect_group } + end + end + + describe "#direct_groups" do + subject { @user.direct_groups } + it { should include @group } + it { should_not include @indirect_group } + end + + describe "#indirect_groups" do + subject { @user.indirect_groups } + it { should include @indirect_group } + it { should_not include @group } + its(:count) { should == 1 } + end + end +end \ No newline at end of file diff --git a/spec/models/concerns/user_roles_spec.rb b/spec/models/concerns/user_roles_spec.rb index f37fd5562..2e9c89909 100644 --- a/spec/models/concerns/user_roles_spec.rb +++ b/spec/models/concerns/user_roles_spec.rb @@ -2,7 +2,7 @@ describe User do - before do + before do @user = create( :user ) @user.save end @@ -10,53 +10,59 @@ # Roles # ========================================================================================== - + describe "#role_for" do - before do - @object = create( :page ) - @object.create_main_admins_parent_group - @sub_object = create( :group ); @sub_object.parent_pages << @object - @sub_sub_object = create( :user ); @sub_sub_object.parent_groups << @sub_object - end - subject { @user.role_for @object } - context "for the user being not related to the object" do - it { should == nil } - end - context "for the user being a member of the object" do + context "for pages" do before do - @group = create( :group ) - @group.child_users << @user - @object.child_groups << @group + @object = create( :page ) + @object.create_main_admins_parent_group + @sub_object = create( :group ); @sub_object.parent_pages << @object + @sub_sub_object = create( :user ); @sub_sub_object.parent_groups << @sub_object + end + subject { @user.role_for @object } + + context "for the user being not related to the object" do + it { should == nil } + end + context "for the user being an admin of the object" do + before { @object.admins << @user } + it { should == :admin } + end + context "for the user being a main_admin of the object" do + before { @object.main_admins << @user } + it { should == :main_admin } + end + context "for the object being not structureable" do + before { @object = "This is a string." } + it { should == nil } + end + context "for descendant objects of administrated objects" do + before { @object.admins << @user } + it "should return the inherited role" do + @user.role_for( @object ).should == :admin + @user.role_for( @sub_object ).should == :admin + @user.role_for( @sub_sub_object ).should == :admin + end end - it { should == :member } - end - context "for the user being an admin of the object" do - before { @object.admins << @user } - it { should == :admin } - end - context "for the user being a main_admin of the object" do - before { @object.main_admins << @user } - it { should == :main_admin } - end - context "for the object being not structureable" do - before { @object = "This is a string." } - it { should == nil } end - context "for descendant objects of administrated objects" do - before { @object.admins << @user } - it "should return the inherited role" do - @user.role_for( @object ).should == :admin - @user.role_for( @sub_object ).should == :admin - @user.role_for( @sub_sub_object ).should == :admin + context "for groups" do + before do + @object = create(:group) + end + subject { @user.role_for @object } + + context "for the user being a member of the object" do + before { @object << @user } + it { should == :member } end end end - + # Admins # ------------------------------------------------------------------------------------------ - + describe "#admin_of" do - before do + before do @group = create( :group, name: "Directly Administrated Group" ) @group.find_or_create_admins_parent_group @group.admins_parent.child_users << @user @@ -64,7 +70,7 @@ subject { @user.admin_of } it { should == @user.administrated_objects } end - + describe "#admin_of?" do before do @group = create( :group, name: "Directly Administrated Group" ) @@ -103,7 +109,7 @@ it { should == false } end end - + describe "#directly_administrated_objects" do before do @group = create( :group, name: "Directly Administrated Group" ) @@ -118,7 +124,7 @@ end end end - + describe "#administrated_objects" do before do @group = create( :group, name: "Administrated Group" ) @@ -145,43 +151,39 @@ end end end - + # Main Admins # ------------------------------------------------------------------------------------------ - + describe "#main_admin_of?" do before do - @page = create( :page ) + @group = create(:group) end - subject { @user.main_admin_of? @page } + subject { @user.main_admin_of? @group } context "for the main_admins_parent_group existing" do - before { @page.create_main_admins_parent_group } + before { @group.create_main_admins_parent_group } context "for the user being a main admin of the object" do - before { @page.main_admins << @user } + before { @group.main_admins << @user } it { should == true } end context "for the user being just a regular admin of the object" do - before { @page.admins << @user } + before { @group.admins << @user } it { should == false } end context "for the user being just a regular member of the object" do - before do - @group = create( :group ) - @group.child_users << @user - @page.child_groups << @group - end + before { @group << @user } it "should be false" do - @user.member_of?( @page ).should be_true # just to make sure + @user.member_of?(@group).should be_true # just to make sure subject.should == false end end end end - - + + # Guest Status # ========================================================================================== - + describe "#guest_of?" do before { @group = create( :group ) } subject { @user.guest_of? @group } @@ -196,11 +198,11 @@ it { should == true } end end - - + + # Developers # ========================================================================================== - + describe "#developer?" do subject { @user.developer? } describe "for no developers group existing" do @@ -234,5 +236,5 @@ end end end - + end \ No newline at end of file diff --git a/spec/models/corporation_spec.rb b/spec/models/corporation_spec.rb index cfc7b61ec..a6b1149a4 100644 --- a/spec/models/corporation_spec.rb +++ b/spec/models/corporation_spec.rb @@ -60,11 +60,11 @@ @another_corporation = create( :corporation ) @user = create( :user ) - @first_membership = UserGroupMembership.create( user: @user, group: @first_corporation ) + @first_membership = Membership.create(user: @user, group: @first_corporation) @first_membership.valid_from = 1.year.ago @first_membership.save - @second_membership = UserGroupMembership.create( user: @user, group: @second_corporation ) + @second_membership = Membership.create(user: @user, group: @second_corporation) end describe "for the corporation the user has joined first" do subject { @first_corporation.is_first_corporation_this_user_has_joined?( @user ) } diff --git a/spec/models/dag_link_spec.rb b/spec/models/dag_link_spec.rb deleted file mode 100644 index 0a2c1008c..000000000 --- a/spec/models/dag_link_spec.rb +++ /dev/null @@ -1,48 +0,0 @@ -require 'spec_helper' - -# The dag link functionality is tested extensively in the corresponding `acts-as-dag` gem. -# This test is just to make sure that the integration is propery done. Therefore, some basic scenarios are tested here. -# -# We use the Page model here to represent the dag's node objects, since it's a relatively simple model, which is already -# present in the database. If the Page model should become more extensive in the future, it's recommended to refactor -# this test to use a new model, perhaps defined in the test itself. -# -describe "Page (DagLinkNode)" do - - def setup_pages - @page = FactoryGirl.create( :page ) - @parent = FactoryGirl.create( :page ) - @grandfather = FactoryGirl.create( :page ) - @page.parent_pages << @parent - @parent.parent_pages << @grandfather - end - - before { setup_pages } - - describe "#ancestors" do - it "should return all ancestors, not only the parents" do - @page.ancestors.should include( @parent, @grandfather ) - end - end - - describe "#descendants" do - it "should return all descendants, not only the children" do - @grandfather.descendants.should include( @parent, @page ) - end - end - - describe "#parents" do - it "should return only the parents rather than all ancestors" do - @page.parents.should include( @parent ) - @page.parents.should_not include( @grandfather ) - end - end - - describe "#children" do - it "should return only the children rather than all descendants" do - @grandfather.children.should include( @parent ) - @grandfather.children.should_not include( @page ) - end - end - -end diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb index baedad8d0..e4606c068 100644 --- a/spec/models/event_spec.rb +++ b/spec/models/event_spec.rb @@ -55,8 +55,8 @@ @event.groups.should include @group, @another_group end end - - + + # Contact People and Attendees # ========================================================================================== @@ -76,7 +76,7 @@ @event.contact_people.should_not include @user end end - + describe "#attendees" do subject { @event.attendees } before { @user = create :user } @@ -105,7 +105,7 @@ # ========================================================================================== describe ".upcoming" do - before do + before do @upcoming_event = create( :event, start_at: 5.hours.from_now ) @recent_event = create(:event, start_at: 2.days.ago, end_at: 2.days.ago + 2.hours) @recent_event_today = create(:event, start_at: Date.today.to_datetime.change(hour: 0, min: 5)) @@ -140,8 +140,8 @@ end end - describe ".direct" do - # group_a + describe ".direct [removed]" do + # group_a # |----- event_0 # |----- group_b # | |------ event_1 @@ -159,15 +159,11 @@ end it "should list direct events" do @group_a.events.should include @event_0, @event_1, @event_2 - @group_a.events.direct.should include @event_0 - @group_a.events.direct.should_not include @event_1 - end - it "should commute with .find_all_by_group" do - Event.find_all_by_group( @group_a ).direct.to_a.should == - Event.direct.find_all_by_group( @group_a ).to_a + @group_a.child_events.should include @event_0 + @group_a.child_events.should_not include @event_1 end end - + # Finder Methods # ========================================================================================== @@ -196,12 +192,12 @@ describe ".find_all_by_groups" do before do - @group1 = create( :group ) - @event1 = @group1.events.create( :start_at => 5.hours.from_now ) - @group2 = create( :group ) - @event2 = @group2.events.create( :start_at => 2.hours.from_now ) - @group3 = create( :group ) - @event3 = @group3.events.create + @group1 = create :group + @event1 = @group1.child_events.create start_at: 5.hours.from_now + @group2 = create :group + @event2 = @group2.child_events.create start_at: 2.hours.from_now + @group3 = create :group + @event3 = @group3.child_events.create end subject { Event.find_all_by_groups( [ @group1, @group2 ] ) } it "should return the events of the given groups" do @@ -214,11 +210,11 @@ subject.first.start_at.should < subject.last.start_at end end - - + + # Structure # ========================================================================================== - + # The following DAG structure should be possible in the model layer (bug fix). # # @corporation @@ -241,7 +237,7 @@ @user = create :user @group = create :group @group.assign_user @user - + @event = Event.new @event.name ||= I18n.t(:enter_name_of_event_here) @event.start_at ||= Time.zone.now.change(hour: 20, min: 15) @@ -249,19 +245,19 @@ @event.parent_groups << @group @event.contact_people_group.assign_user @user end - - + + describe "#destroy" do subject { @event.destroy } - + it "should destroy the contact people and attendees groups as well" do @contact_people_group = @event.contact_people_group @attendees_group = @event.attendees_group - + subject Group.exists?(id: @contact_people_group.id).should == false Group.exists?(id: @attendees_group.id).should == false end end - + end diff --git a/spec/models/graph_performance_spec.rb b/spec/models/graph_performance_spec.rb new file mode 100644 index 000000000..43682b3c9 --- /dev/null +++ b/spec/models/graph_performance_spec.rb @@ -0,0 +1,276 @@ +# This test creates and moves some groups in order to +# determine the graph performance. +# +# This is the same test as: +# https://github.com/fiedl/neo4j_ancestry_vs_acts_as_dag/blob/master/spec/performance_spec.rb +# https://github.com/fiedl/neo4j_gem_test/blob/master/spec/performance_spec.rb +# +require 'spec_helper' +require 'table-formatter' + +if ENV['CI'] != 'travis' + + class TestGraph + def initialize(params) + @number_of_groups = params[:number_of_groups] + @number_of_users = params[:number_of_users] + end + + def create_groups + @groups = (1..@number_of_groups).map { |n| Group.create(name: "Group #{n}") } + end + def groups + @groups + end + + def create_parent_group + @parent_group = Group.create name: "Parent Group" + end + def parent_group + @parent_group + end + + def create_ancestor_group + @ancestor_group = Group.create name: "Ancestor Group" + end + def ancestor_group + @ancestor_group + end + + def add_users_to_groups + groups.each do |group| + (1..@number_of_users).each { |n| group.child_users << FactoryGirl.create(:user) } + end + end + + def move_groups_into_parent_group + groups.each do |group| + parent_group.child_groups << group + end + end + def move_parent_group_into_ancestor_group + ancestor_group.child_groups << parent_group + end + def remove_the_link_of_the_ancestor_group + ancestor_group.child_groups.destroy(parent_group) + end + + + def number_of_users_of_the_last_group + groups.last.descendant_users.count + end + def number_of_parent_group_child_groups + parent_group.child_groups.count + end + def number_of_ancestor_group_descendant_groups + ancestor_group.descendant_groups.count + end + def number_of_ancestor_group_members + ancestor_group.descendant_users.count + end + def number_of_parent_group_ancestor_groups + parent_group.ancestor_groups.count + end + + def find_ancestor_group_descendants + ancestor_group.descendants.to_a + end + def find_ancestor_group_members + ancestor_group.members.to_a + end + def find_connected_descendant_groups + ancestor_group.connected_descendant_groups.to_a + end + + def clear_graph + DagLink.delete_all + end + end + + class Neo4jTestGraph < TestGraph + def number_of_ancestor_group_descendant_groups + ancestor_group.neo_node.query_as(:self).match("self-[*]->(g:Group)").pluck(:g).count + end + def number_of_parent_group_ancestor_groups + parent_group.neo_node.query_as(:self).match("self<-[*]-(n)").pluck(:n).count + end + + def find_ancestor_group_descendants + ancestor_group.neo_node.query_as(:self).match("self-[*]->(n)").pluck(:n).collect { |n| n.to_active_record } + end + def find_ancestor_group_members + User.find(ancestor_group.neo_node.query_as(:self).match("self-[*]->(u:User)").pluck('u.active_record_id')) + end + + def clear_db + # clear_model_memory_caches + Neo4j::Session.current._query('MATCH (n) OPTIONAL MATCH (n)-[r]-() DELETE n,r') + end + end + + class ConnectedGroupsTestGraph < TestGraph + def number_of_users_of_the_last_group + groups.last.members.count + end + def number_of_ancestor_group_members + ancestor_group.members.count + end + end + + describe "graph performance: " do + + $number_of_groups = 10 + $number_of_users = 2 + + before :each do + clear_db + end + + let(:graph) { + params = {number_of_groups: $number_of_groups, number_of_users: $number_of_users} + if defined?(Neo4j) + Neo4jTestGraph.new(params) + elsif defined?(StructureableConnectedGroups) + ConnectedGroupsTestGraph.new(params) + else + TestGraph.new(params) + end + } + + specify "creating #{$number_of_groups} groups" do + benchmark { graph.create_groups } + graph.groups.count.should == $number_of_groups + end + + specify "adding #{$number_of_users} users to each of the #{$number_of_groups} groups" do + graph.create_groups + benchmark { graph.add_users_to_groups } + graph.number_of_users_of_the_last_group.should == $number_of_users + end + + specify "moving #{$number_of_groups} groups into a parent group" do + graph.create_groups + graph.create_parent_group + benchmark { graph.move_groups_into_parent_group } + graph.number_of_parent_group_child_groups.should == $number_of_groups + end + + specify "moving the group structure into an ancestor group" do + graph.create_groups + graph.create_parent_group + graph.move_groups_into_parent_group + graph.create_ancestor_group + benchmark { graph.move_parent_group_into_ancestor_group } + graph.number_of_ancestor_group_descendant_groups.should == $number_of_groups + 1 + end + + specify "moving the groups with users into an ancestor group" do + graph.create_groups + graph.add_users_to_groups + graph.create_parent_group + graph.move_groups_into_parent_group + graph.create_ancestor_group + benchmark { graph.move_parent_group_into_ancestor_group } + graph.number_of_ancestor_group_descendant_groups.should == $number_of_groups + 1 + graph.number_of_ancestor_group_members.should == $number_of_groups * $number_of_users + end + + specify "removing the link to the ancestor group" do + graph.create_groups + graph.add_users_to_groups + graph.create_parent_group + graph.move_groups_into_parent_group + graph.create_ancestor_group + graph.move_parent_group_into_ancestor_group + benchmark { graph.remove_the_link_of_the_ancestor_group } + graph.number_of_ancestor_group_descendant_groups.should == 0 + end + + specify "destroying the ancestor group" do + graph.create_groups + graph.add_users_to_groups + graph.create_parent_group + graph.move_groups_into_parent_group + graph.create_ancestor_group + graph.move_parent_group_into_ancestor_group + benchmark { graph.ancestor_group.destroy } + graph.number_of_parent_group_ancestor_groups.should == 0 + end + + specify "finding all #{$number_of_groups * $number_of_users} members" do + graph.create_groups + graph.add_users_to_groups + graph.create_parent_group + graph.move_groups_into_parent_group + graph.create_ancestor_group + graph.move_parent_group_into_ancestor_group + benchmark { graph.find_ancestor_group_members } + benchmark("second run: finding all #{$number_of_groups * $number_of_users} members") { graph.find_ancestor_group_members } + graph.find_ancestor_group_members.count.should == $number_of_groups * $number_of_users + graph.find_ancestor_group_members.first.should be_kind_of User + end + + specify "finding all connected descendant groups" do + graph.create_groups + graph.add_users_to_groups + graph.create_parent_group + graph.move_groups_into_parent_group + graph.create_ancestor_group + graph.move_parent_group_into_ancestor_group + benchmark { graph.find_connected_descendant_groups } + benchmark("second run: all connected descendant groups") { graph.find_connected_descendant_groups } + graph.find_connected_descendant_groups.count.should == $number_of_groups + 1 + end + + specify "creating 10 events for ancestor group" do + graph.create_groups + graph.add_users_to_groups + graph.create_parent_group + graph.move_groups_into_parent_group + graph.create_ancestor_group + graph.move_parent_group_into_ancestor_group + benchmark do + 10.times { graph.ancestor_group.child_events.create } + end + graph.ancestor_group.events.count.should == 10 + end + + after(:all) do + print_results + end + + $results = [] + def benchmark(description = nil) + duration_in_seconds = Benchmark.realtime { + yield + }.round(4) + + description ||= RSpec.current_example.metadata[:description] if RSpec.respond_to? :current_example # rspec 3 + description ||= example.description # rspec 2 + duration = "#{duration_in_seconds} seconds" + + $results << [description, "#{duration_in_seconds.to_s} s"] + print "#{description}: #{duration}.\n".blue + end + + def print_results + print "\n\n## Results for #{ENV['BACKEND']}\n\n".blue.bold + + print "$number_of_groups = #{$number_of_groups}\n".blue + print "$number_of_users = #{$number_of_users}\n\n".blue + + print results_table.blue.bold + end + def results_table + t = TableFormatter.new + t.source = $results + t.labels = ['Description', 'Duration'] + t.display.to_s + end + + def clear_db + graph.clear_graph + end + + end +end \ No newline at end of file diff --git a/spec/models/group_collection_spec.rb b/spec/models/group_collection_spec.rb new file mode 100644 index 000000000..f27850dfb --- /dev/null +++ b/spec/models/group_collection_spec.rb @@ -0,0 +1,80 @@ +require 'spec_helper' + +describe GroupCollection do + + # + # group1 --- page1 --- group2 --- group3 --- user1 + # | + # |------- user2 + # + before do + @group1 = create :group, name: 'group1' + @page1 = @group1.child_pages.create title: 'page1' + @group2 = @page1.child_groups.create name: 'group2' + @group3 = @group2.child_groups.create name: 'group3' + @user1 = create :user; @group3 << @user1 + @user2 = create :user; @group1 << @user2 + + @membership_collection = Membership.where(user: @user1) + @group_collection = GroupCollection.new(memberships: @membership_collection) + end + + describe "#to_a" do + subject { @group_collection.to_a } + it { should be_kind_of Array } + it { should include @group3, @group2 } + it { should_not include @group1, @page1, @user2 } + end + + describe "#count" do + subject { @group_collection.count } + it { should == 2 } + end + + describe "#flagged" do + before { @group3.add_flag :test_flag } + subject { @group_collection.flagged(:test_flag) } + it { should be_kind_of GroupCollection } + its(:count) { should == 1 } + its(:to_a) { should include @group3 } + its(:to_a) { should_not include @group2 } + its(:to_a) { should_not include @group1, @page1, @user2 } + end + + describe "(validity range scopes)" do + # + # group1 --- page1 --- group2 --- group3 --- user1 + # | | + # |------- user2 |------ user3 + # | + # |---- group4 --(past)-- user3 + # + before do + @group4 = @group1.child_groups.create name: 'group4' + @user3 = create :user, last_name: 'user3'; @group4 << @user3; @group3 << @user3 + Membership.where(user: @user3, group: @group4).first.invalidate at: 1.week.ago + + @membership_collection = Membership.where(user: @user3) + @group_collection = GroupCollection.new(memberships: @membership_collection) + end + + describe "#now" do + subject { @group_collection.now } + it { should include @group3, @group2 } + it { should_not include @group4, @group1 } + end + + describe "#with_past" do + subject { @group_collection.with_past } + it { should include @group3, @group2 } + it { should include @group4, @group1 } + end + + describe "#past" do + subject { @group_collection.past } + it { should_not include @group3, @group2 } + it { should include @group4, @group1 } + end + + end +end \ No newline at end of file diff --git a/spec/models/group_mixins/guests_spec.rb b/spec/models/group_mixins/guests_spec.rb index 9a80eec4a..930480bda 100644 --- a/spec/models/group_mixins/guests_spec.rb +++ b/spec/models/group_mixins/guests_spec.rb @@ -8,7 +8,7 @@ describe "guests_parent_group" do before do - @container_group = create( :group ) + @container_group = create( :group ) @container_subgroup = create( :group ) # this is to test if subgroup's guests are NOT listed @container_subgroup.parent_groups << @container_group @guests_parent = @container_group.create_guests_parent_group @@ -45,7 +45,7 @@ subject.should include( @guests_sub1 ) end it "should NOT find the guests of the container group's subgroups" do - subject.should_not include( @guests_sub2 ) + subject.should_not include( @guests_sub2 ) end end @@ -59,7 +59,7 @@ describe "if the group does not have a guests_parent group" do subject { @other_group.find_guest_users } it "should still return an empty array" do - subject.should == [] + subject.should.to_a == [] end end end @@ -67,7 +67,7 @@ subject { @container_group } its( :guests_parent ) { should == @guests_parent } its( :guests_parent! ) { should == @guests_parent } - + its( :guests ) { should == @container_group.find_guest_users } end diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index 56eef0778..ac8868e77 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -48,16 +48,21 @@ # Associated Objects # ========================================================================================== - + # Workflows # ------------------------------------------------------------------------------------------ describe "(Workflows)" do + # + # @group + # |---- @subgroup --- @subworkflow + # |---- @workflow + # before do @group = create( :group ) @subgroup = create( :group ) @subgroup.parent_groups << @group - + @workflow = create( :workflow ) @workflow.parent_groups << @group @subworkflow = create( :workflow ) @@ -90,12 +95,12 @@ # ------------------------------------------------------------------------------------------ describe "(Events)" do - before do + before do @group = create( :group ) @subgroup = @group.child_groups.create - @upcoming_events = [ @group.events.create( start_at: 5.hours.from_now ), - @subgroup.events.create( start_at: 5.hours.from_now ) ] - @recent_events = [ @group.events.create( start_at: 2.days.ago ) ] + @upcoming_events = [ @group.child_events.create( start_at: 5.hours.from_now ), + @subgroup.child_events.create( start_at: 5.hours.from_now ) ] + @recent_events = [ @group.child_events.create( start_at: 2.days.ago ) ] @unrelated_events = [ create( :event ) ] end @@ -115,42 +120,6 @@ end - # Users - # ------------------------------------------------------------------------------------------ - - describe "(Users)" do - - before do - @user = create( :user ) - @group = create( :group ) - @subgroup = create( :group ); @group.child_groups << @subgroup - end - - describe "#descendant_users" do - describe "for usual groups" do - before { @user.parent_groups << @subgroup } - subject { @group.descendant_users } - - it "should return all descendant users, including the users of the subgroups" do - subject.should include( @user ) - end - end - end - - describe "#child_users" do - describe "for usual groups" do - before { @user.parent_groups << @group } - subject { @group.child_users } - - it "should return all child users" do - subject.should include( @user ) - end - end - end - - end - - # Groups # ------------------------------------------------------------------------------------------ @@ -160,7 +129,7 @@ @name_match = "Group Name" @group = create( :group ) @group1 = create( :group, :name => @name_match ); @group1.parent_groups << @group - @group2 = create( :group, :name => "Other #{@name_match}" ); @group2.parent_groups << @group1 + @group2 = create( :group, :name => "Other #{@name_match}" ); @group2.parent_groups << @group1 @group3 = create( :group, :name => @name_match ); @group3.parent_groups << @group2 @matching_groups = [ @group1, @group3 ] @not_matching_groups = [ @group2 ] @@ -187,7 +156,7 @@ describe "for the group being a child of a corporation" do before { @group.parent_groups << @corporation } it "should return the parent" do - subject.should == @corporation + subject.should == @corporation end end describe "for the group being a descendant of a corporation" do @@ -205,7 +174,7 @@ end end end - + describe "#corporation?" do subject { @group.corporation? } describe "for the group being a corporation" do @@ -224,7 +193,7 @@ it { should == false } end end - + describe '#leaf_groups' do subject { @group.leaf_groups } describe 'for the group being a corporation' do @@ -248,7 +217,7 @@ @group_b = @group.child_groups.create @status_3 = @group_b.child_groups.create end - it 'should contain all status groups' do + it 'should contain all status groups' do should include(@status_1) should include(@status_2) should include(@status_3) @@ -288,7 +257,7 @@ @group = create(:corporation) @group.cached(:leaf_groups) wait_for_cache - + # The creation of this group structure should reset the cache. @status_1 = @group.child_groups.create @group_a = @group.child_groups.create @@ -308,7 +277,7 @@ describe "#<<" do before { @group = create(:group) } subject { @group << @object_to_add } - + describe "(user)" do before do @user = create(:user) @@ -324,7 +293,7 @@ UserGroupMembership.with_invalid.find_by_user_and_group(@user, @group).valid_from.should > 1.second.ago end end - + describe "(group)" do before do @subgroup = create(:group) diff --git a/spec/models/membership_collection_spec.rb b/spec/models/membership_collection_spec.rb new file mode 100644 index 000000000..458df7204 --- /dev/null +++ b/spec/models/membership_collection_spec.rb @@ -0,0 +1,390 @@ +require 'spec_helper' + +describe MembershipCollection do + + # Example: + # + # group1 --- page1 --- group2 --- group3 --- user1 + # | + # |------- user2 + # + before do + @group1 = create :group, name: 'group1' + @page1 = @group1.child_pages.create title: 'page1' + @group2 = @page1.child_groups.create name: 'group2' + @group3 = @group2.child_groups.create name: 'group3' + @user1 = create :user; @group3 << @user1 + @user2 = create :user; @group1 << @user2 + end + + describe "#where" do + describe "Membership.where(user: ..., group: ...)" do + describe "for a direct membership" do + subject { Membership.where(user: @user1, group: @group3) } + its(:count) { should == 1 } + its('first.user') { should == @user1 } + its('first.group') { should == @group3 } + its('first.direct?') { should == true } + end + describe "for an indirect membership" do + subject { Membership.where(user: @user1, group: @group2) } + its(:count) { should == 1 } + its('first.user') { should == @user1 } + its('first.group') { should == @group2 } + its('first.direct?') { should == false } + end + end + + describe "Membership.where(user: @user1)" do + # + # page1 -------| + # group4 --- group2 --- group3 --- user1 + # + before { @group4 = @group2.parent_groups.create name: 'group4' } + subject { Membership.where(user: @user1) } + it { should be_kind_of MembershipCollection } + its(:to_a) { should be_kind_of Array } + its(:first) { should be_kind_of Membership } + its(:count) { should == 3 } + it "should not perform too many queries" do + # (User, Group, DagLink, Flag) for the direct links + # (Group, Flag, Flag) for each generation hop, in this case, 3 hops, without caching. + count_queries(13) { subject.to_a }.should <= 13 # scales with number of hops + end + describe "when connected groups are already cached" do + before { @user1.connected_ancestor_groups } + it "should not perform too many queries" do + # (User, Group, DagLink, Flag) for the direct links + # (Group, Flag) for the indirect connected groups + count_queries(6) { subject.to_a }.should <= 6 # does not scale with number of hops + end + end + end + + describe "Membership.where(group: ...)" do + # + # group2 --- group3 --- user1 + # + describe "for groups that have direct members" do + describe ".where(group: @group3)" do + subject { Membership.where(group: @group3) } + it { should be_kind_of MembershipCollection } + its(:to_a) { should be_kind_of Array } + its(:first) { should be_kind_of Membership } + its(:count) { should == 1 } + its('direct.count') { should == 1 } + its('first.user') { should == @user1 } + its('first.group') { should == @group3 } + end + end + + describe "for groups that have indirect members" do + describe "Membership.where(group: @group2)" do + subject { Membership.where(group: @group2) } + it { should be_kind_of MembershipCollection } + its(:to_a) { should be_kind_of Array } + its(:first) { should be_kind_of Membership } + its(:count) { should == 1 } + its('direct.count') { should == 0 } + its('first.user') { should == @user1 } + its('first.group') { should == @group2 } + end + end + end + + describe "for user and group" do + describe "when the link is direct" do + subject { Membership.where(group: @group3, user: @user1) } + it { should be_kind_of MembershipCollection } + its(:to_a) { should be_kind_of Array } + its(:first) { should be_kind_of Membership } + its(:count) { should == 1 } + its('direct.count') { should == 1 } + end + + describe "when the link is not direct" do + subject { Membership.where(group: @group2, user: @user1) } + it { should be_kind_of MembershipCollection } + its(:to_a) { should be_kind_of Array } + its(:first) { should be_kind_of Membership } + its(:count) { should == 1 } + its('direct.count') { should == 0 } + end + + describe "when there are direct and indirect memberships" do + # @indirect_group + # |------------ @group + # | |------ @user1 + # | |------ @user2 + # | + # |------------ @group2 + # + before do + @group = create(:group, name: 'group') + @user1 = create(:user); @group.assign_user(@user1) + @user2 = create(:user); @group.assign_user(@user2) + @user = @user1 + @membership1 = Membership.where(user: @user1, group: @group).first + @membership2 = Membership.where(user: @user2, group: @group).first + @indirect_group = @group.parent_groups.create name: 'indirect_group' + @indirect_membership1 = Membership.where(user: @user1, group: @indirect_group).first + @indirect_membership2 = Membership.where(user: @user2, group: @indirect_group).first + @group2 = @indirect_group.child_groups.create + end + describe "when the selector group is a direct group" do + subject { Membership.where(group: @group, user: @user) } + its(:count) { should == 1 } + its(:first) { should == @membership1 } + its(:first) { should be_kind_of Membership } + its('first.group') { should == @group } + its('first.user') { should == @user } + end + describe "when the selector group is an indirect group" do + subject { Membership.where(group: @indirect_group, user: @user) } + its(:count) { should == 1 } + its(:first) { should == @indirect_membership1 } + its(:first) { should be_kind_of Membership } + its('first.group') { should == @indirect_group } + its('first.user') { should == @user } + end + end + end + end + + describe "#direct" do + it "reduces the scope to direct memberships" do + Membership.where(user: @user1).direct.count.should == 1 + end + + it "should be interchangable" do + Membership.where(user: @user1).direct.to_a.should == Membership.direct.where(user: @user1).to_a + end + end + + describe "#uniq" do + # + # @group1 --- @subgroup1 ------ + # | | + # |------- @subgroup2 --- @user1 + # + before do + @group1 = create :group, name: 'group1' + @subgroup1 = @group1.child_groups.create name: 'subgroup1' + @subgroup2 = @group1.child_groups.create name: 'subgroup2' + @user1 = create :user; @subgroup1 << @user1; @subgroup2 << @user1 + end + subject { Membership.where(user: @user1).uniq } + its(:count) { should == Membership.where(user: @user1).count - 1 } + end + + describe "#first_per_group" do + # If a user has two memberships in a group, differing in the validity range, + # this filter selects the first, i.e. earliest, membership for each group. + # + # @group1 --- @subgroup1 ------ + # | | + # |------- @subgroup2 --- @user1 + # + before do + @group1 = create :group, name: 'group1' + @subgroup1 = @group1.child_groups.create name: 'subgroup1' + @subgroup2 = @group1.child_groups.create name: 'subgroup2' + @user1 = create :user; @subgroup1 << @user1; @subgroup2 << @user1 + @time1 = 1.year.ago; @time2 = 6.months.ago; @time3 = 2.months.ago; @time4 = nil + Membership.where(user: @user1, group: @subgroup1).first.update_attributes valid_from: @time1, valid_to: @time2 + Membership.where(user: @user1, group: @subgroup2).first.update_attributes valid_from: @time3, valid_to: @time4 + end + describe "with past memberships" do + subject { Membership.where(group: @group1).first_per_group } + it { should be_kind_of MembershipCollection } + its(:count) { should == 1 } + its('first.valid_from.to_i') { should == @time1.to_i } + end + describe "only current memberships" do + subject { Membership.where(group: @group1).now.first_per_group } + it { should be_kind_of MembershipCollection } + its(:count) { should == 1 } + its('first.valid_from.to_i') { should == @time3.to_i } + end + end + + describe "#groups" do + # group1 --- page1 --- group2 --- group3 --- user1 + # | + # |------- user2 + before { @membership_collection = @user1.memberships } + subject { @membership_collection.groups } + it { should be_kind_of GroupCollection } + it { should include @group2, @group3 } + describe "for #direct" do + before { @membership_collection = @membership_collection.direct } + it { should_not include @group2 } + end + describe "for #indirect" do + before { @membership_collection = @membership_collection.indirect } + it { should_not include @group1 } + end + end + + describe "#destroy_all" do + # Example: + # + # group1 --- page1 --- group2 --- group3 --- user1 + # | + # |------- user2 + # |-------group4 --- user1 (past membership) + # + before do + @group4 = @group1.child_groups.create name: 'group4' + @group4 << @user1 + Membership.where(user: @user1, group: @group4).first.invalidate at: 10.days.ago + + @memberships = Membership.where(user: @user1) + end + describe "for #now" do + subject { @memberships.now.destroy_all; Membership.where(user: @user1).groups } + it { should include @group1, @group4 } + it { should_not include @group3, @group2 } + end + + describe "for #past" do + subject { @memberships.past.destroy_all; Membership.where(user: @user1).groups } + it { should_not include @group1, @group4 } + it { should include @group3, @group2 } + end + + describe "for #with_past" do + subject { @memberships.with_past.destroy_all; Membership.where(user: @user1).groups } + it { should_not include @group1, @group4 } + it { should_not include @group3, @group2 } + end + end + + describe "#join_validity_ranges_of_indirect_memberships" do + # Join the validity ranges of indirect memberships. + # + # @group1 + # |------- @subgroup1 -----| + # |------- @subgroup2 --- @user1 + # + # First, user1 joins subgroup1, then moves to subgroup2. + # + # |-----------| first indirect membership in @group1 + # |--------- second indirect membership in @group2 + # |--------------------- joined indirect membership + # + before do + @group1 = create :group, name: 'group1' + @subgroup1 = @group1.child_groups.create name: 'subgroup1' + @subgroup2 = @group1.child_groups.create name: 'subgroup2' + @user1 = create :user; @subgroup1 << @user1; @subgroup2 << @user1 + @time1 = 1.year.ago; @time2 = 6.months.ago; @time3 = 2.months.ago; @time4 = nil + Membership.where(user: @user1, group: @subgroup1).first.update_attributes valid_from: @time1, valid_to: @time2 + Membership.where(user: @user1, group: @subgroup2).first.update_attributes valid_from: @time3, valid_to: @time4 + end + describe "for a group" do + subject { Membership.where(group: @group1).join_validity_ranges_of_indirect_memberships } + it { should be_kind_of MembershipCollection } + it "should join the indirect memberships, i.e. count only one membership, not two" do + subject.count.should == 1 + end + its('first.valid_from.to_i') { should == @time1.to_i } + describe "for a right-open interval" do + before do + @time4 = nil + Membership.where(user: @user1, group: @subgroup2).first.update_attributes valid_from: @time3, valid_to: @time4 + end + its('first.valid_from.to_i') { should == @time1.to_i } + its('first.valid_to.to_i') { should == @time4.to_i } + end + describe "for a left-open interval" do + before do + @time1 = nil + @time4 = 1.day.ago + Membership.where(user: @user1, group: @subgroup1).first.update_attributes valid_from: @time1, valid_to: @time2 + Membership.where(user: @user1, group: @subgroup2).first.update_attributes valid_from: @time3, valid_to: @time4 + end + its('first.valid_from.to_i') { should == @time1.to_i } + its('first.valid_to.to_i') { should == @time4.to_i } + end + describe "for a closed interval" do + before do + @time4 = 1.day.ago + Membership.where(user: @user1, group: @subgroup2).first.update_attributes valid_from: @time3, valid_to: @time4 + end + its('first.valid_from.to_i') { should == @time1.to_i } + its('first.valid_to.to_i') { should == @time4.to_i } + end + end + describe "for a user" do + subject { Membership.where(user: @user1).join_validity_ranges_of_indirect_memberships } + it { should be_kind_of MembershipCollection } + it "should join the indirect memberships, i.e. count only one membership, not two" do + subject.count.should == 3 + subject.indirect.count.should == 1 + end + describe "for a right-open interval" do + before do + @time4 = nil + Membership.where(user: @user1, group: @subgroup2).first.update_attributes valid_from: @time3, valid_to: @time4 + end + its('indirect.first.valid_from.to_i') { should == @time1.to_i } + its('indirect.first.valid_to.to_i') { should == @time4.to_i } + end + describe "for a left-open interval" do + before do + @time1 = nil + @time4 = 1.day.ago + Membership.where(user: @user1, group: @subgroup1).first.update_attributes valid_from: @time1, valid_to: @time2 + Membership.where(user: @user1, group: @subgroup2).first.update_attributes valid_from: @time3, valid_to: @time4 + end + its('indirect.first.valid_from.to_i') { should == @time1.to_i } + its('indirect.first.valid_to.to_i') { should == @time4.to_i } + end + describe "for a closed interval" do + before do + @time4 = 1.day.ago + Membership.where(user: @user1, group: @subgroup2).first.update_attributes valid_from: @time3, valid_to: @time4 + end + its('indirect.first.valid_from.to_i') { should == @time1.to_i } + its('indirect.first.valid_to.to_i') { should == @time4.to_i } + end + end + describe "for user and group" do + subject { Membership.where(user: @user1, group: @group1).join_validity_ranges_of_indirect_memberships } + it { should be_kind_of MembershipCollection } + it "should join the indirect memberships, i.e. count only one membership, not two" do + subject.count.should == 1 + subject.indirect.count.should == 1 + end + describe "for a right-open interval" do + before do + @time4 = nil + Membership.where(user: @user1, group: @subgroup2).first.update_attributes valid_from: @time3, valid_to: @time4 + end + its('first.valid_from.to_i') { should == @time1.to_i } + its('first.valid_to.to_i') { should == @time4.to_i } + end + describe "for a left-open interval" do + before do + @time1 = nil + @time4 = 1.day.ago + Membership.where(user: @user1, group: @subgroup1).first.update_attributes valid_from: @time1, valid_to: @time2 + Membership.where(user: @user1, group: @subgroup2).first.update_attributes valid_from: @time3, valid_to: @time4 + end + its('first.valid_from.to_i') { should == @time1.to_i } + its('first.valid_to.to_i') { should == @time4.to_i } + end + describe "for a closed interval" do + before do + @time4 = 1.day.ago + Membership.where(user: @user1, group: @subgroup2).first.update_attributes valid_from: @time3, valid_to: @time4 + end + its('first.valid_from.to_i') { should == @time1.to_i } + its('first.valid_to.to_i') { should == @time4.to_i } + end + end + + end + +end \ No newline at end of file diff --git a/spec/models/membership_spec.rb b/spec/models/membership_spec.rb new file mode 100644 index 000000000..a822e956c --- /dev/null +++ b/spec/models/membership_spec.rb @@ -0,0 +1,120 @@ +require 'spec_helper' + +describe Membership do + + # Example: + # + # group1 --- page1 --- group2 --- group3 --- user1 + # | + # |------- user2 + # + before do + @group1 = create :group, name: 'group1' + @page1 = @group1.child_pages.create title: 'page1' + @group2 = @page1.child_groups.create name: 'group2' + @group3 = @group2.child_groups.create name: 'group3' + @user1 = create :user; @group3 << @user1 + @user2 = create :user; @group1 << @user2 + end + + describe ".find" do + subject { Membership.find(@dag_link_id) } + # + # group2 --- group3 --- user1 + # + describe "for a direct membership" do + before { @dag_link_id = Membership.where(user: @user1, group: @group3).first.dag_link.id } + it { should be_kind_of Membership } + its(:user) { should == @user1 } + its(:group) { should == @group3 } + its(:direct?) { should be_true } + it "should perform only 4 queries: DagLink, User, Group and Flags" do + count_queries(4) { subject }.should <= 4 + end + end + describe "for a non-existent dag link" do + before { @dag_link_id = DagLink.pluck(:id).max + 5 } + it "should raise an error" do + expect { subject }.to raise_error + end + end + end + + + describe "#save!" do + subject { @membership.save! } + + describe "for direct memberships" do + before { @membership = Membership.where(user: @user2, group: @group1).first } + it "should save a changed valid_from and valid_to attributes" do + @membership.valid_from = @time1 = 2.months.ago + @membership.valid_to = @time2 = 1.month.ago + subject + @membership.reload.valid_from.to_i.should == @time1.to_i + @membership.reload.valid_to.to_i.should == @time2.to_i + @membership.dag_link.reload.valid_from.to_i.should == @time1.to_i + @membership.dag_link.reload.valid_to.to_i.should == @time2.to_i + end + end + + describe "for indirect memberships" do + before { @membership = Membership.where(user: @user1, group: @group2).first } + specify "requirements" do + @membership.user.should == @user1 + @membership.group.should == @group2 + end + it "should raise an error, since indirect memberships are non-persistent objects" do + expect { subject }.to raise_error + end + end + end + + describe ".create" do + # + # @group1 --- @subgroup1 @user1 + # + before do + @group1 = create :group, name: 'group1' + @subgroup1 = @group1.child_groups.create name: 'subgroup1' + @user1 = create :user + end + describe "creating a direct membership" do + subject { Membership.create group: @group1, user: @user1 } + it "should create the corresponding dag link" do + @user1.links_as_child.count.should == 0 + subject + @user1.links_as_child.count.should == 1 + @user1.links_as_child.first.ancestor.should == @group1 + end + it { should be_kind_of Membership } + its(:group) { should == @group1 } + its(:user) { should == @user1 } + + describe "if the membership already exists indirectly" do + before { Membership.create group: @subgroup1, user: @user1 } + it "should create the corresponding dag link" do + @user1.links_as_child.count.should == 1 + subject + @user1.links_as_child.count.should == 2 + @user1.links_as_child.last.ancestor.should == @group1 + end + it { should be_kind_of Membership } + its(:group) { should == @group1 } + its(:user) { should == @user1 } + end + end + + describe "creating a direct membership with validity range" do + before { @time1 = 1.month.ago; @time2 = 10.days.ago } + subject { Membership.create group: @group1, user: @user1, valid_from: @time1, valid_to: @time2 } + it { should be_kind_of Membership } + its(:group) { should == @group1 } + its(:user) { should == @user1 } + its('valid_from.to_i') { should == @time1.to_i } + its('valid_to.to_i') { should == @time2.to_i } + its('dag_link.valid_from.to_i') { should == @time1.to_i } + its('dag_link.valid_to.to_i') { should == @time2.to_i } + end + end + +end \ No newline at end of file diff --git a/spec/models/status_group_membership_spec.rb b/spec/models/status_group_membership_spec.rb index 1d7fced0d..128d72838 100644 --- a/spec/models/status_group_membership_spec.rb +++ b/spec/models/status_group_membership_spec.rb @@ -1,350 +1,351 @@ -require 'spec_helper' - -describe StatusGroupMembership do - - # Alias Methods for Delegated Methods - # ========================================================================================== - - describe "#promoted_by_workflow" do - before do - @workflow = create( :workflow ) - @membership = create( :status_group_membership ) - end - subject { @membership.promoted_by_workflow } - describe "if one has been assigned" do - before do - @membership.promoted_by_workflow = @workflow - end - it "should return the workflow that has been associated" do - subject.should == @workflow - end - it "should persist" do - @membership.save - @reloaded_membership = StatusGroupMembership.find( @membership.id ) - @reloaded_membership.promoted_by_workflow.should == @workflow - @reloaded_membership = StatusGroupMembership - .find_by_user_and_group( @membership.user, @membership.group ) - @reloaded_membership.promoted_by_workflow.should == @workflow - end - it "should be an alias of #workflow" do - subject.should == @membership.workflow - end - end - describe "if none has been assigned" do - it { should == nil } - end - end - - describe "#promoted_on_event" do - before do - @event = create( :event ) - @membership = create( :status_group_membership ) - end - subject { @membership.promoted_on_event } - describe "if one has been assigned" do - before { @membership.promoted_on_event = @event } - it { should == @event } - it "should persist" do - @membership.save - @reloaded_membership = StatusGroupMembership.find( @membership.id ) - @reloaded_membership.promoted_on_event.should == @event - @reloaded_membership = StatusGroupMembership - .find_by_user_and_group( @membership.user, @membership.group ) - @reloaded_membership.promoted_on_event.should == @event - end - it "should be an alias of #event" do - subject.should == @membership.event - end - end - describe "if none has been assigned" do - it { should == nil } - end - end - - describe "#event_by_name" do - before do - @membership = create( :status_group_membership ) - end - subject { @membership.event_by_name } - describe "for existing event" do - before do - @event = create( :event ) - @membership.event = @event - end - it { should == @event.name } - end - describe "if no event is assigned" do - it { should == nil } - end - end - describe "#event_by_name=" do - before do - @membership = create( :status_group_membership ) - end - describe "for an existing event" do - before { @event = create( :event ) } - subject { @membership.event_by_name = @event.name } - it "should assign the event" do - @membership.event.should == nil - subject - @membership.event.should == @event - end - end - describe "for a new event" do - subject { @membership.event_by_name = "A New Event" } - it "should create the event" do - @membership.event.should == nil - subject - @membership.event.name.should == "A New Event" - end - it "should persist" do - subject - @membership.save - @reloaded_membership = StatusGroupMembership.find( @membership.id ) - @reloaded_membership.event.should == @membership.event - @reloaded_membership.event.name.should == "A New Event" - end - it "should copy the date from the membership" do - subject - @membership.event.start_at.to_date.should == @membership.created_at.to_date - end - describe "for the membership having a corporation" do - before do - @corporation = create( :corporation ) - @corporation.child_groups << @membership.group - end - it "should association the corporation with the new event" do - subject - @membership.event.group.becomes( Group ).should == @corporation.becomes( Group ) - end - end - end - describe "for an empty string" do - before do - @membership.event_by_name = "some prefilled event" - end - subject { @membership.event_by_name = "" } - it "should set the event to nil" do - subject - @membership.event.should == nil - end - end - end - - - - # Finder Methods - # ========================================================================================== - - class SomeCorporationDerivative < Corporation - # This is just a dummy. The main app could invent a class inherited from Corporation. - # Some methods need to work with them as well as with the original Corporation class. - end - - # @corporation - # |------- @intermediate_group - # |------------ @status_group - # | |--------- @user - # | - # |------------ @second_status_group - # - describe "Finder Methods: " do - before do - @corporation = create( :corporation ) - @intermediate_group = create( :group, name: "Not a Status Group" ) - @status_group = create( :group, name: "Status Group" ) - - @intermediate_group.parent_groups << @corporation - @status_group.parent_groups << @intermediate_group - @user = create( :user ) - @status_group.assign_user @user - - @membership = UserGroupMembership.find_by_user_and_group( @user, @status_group ) - .becomes( StatusGroupMembership ) - @intermediate_group_membership = UserGroupMembership - .find_by_user_and_group( @user, @intermediate_group ).becomes StatusGroupMembership - - @second_status_group = @intermediate_group.child_groups.create(name: "Second Status Group") - - @other_corporation = create(:corporation_with_status_groups) - @membership_in_other_corporation = @other_corporation.status_groups.first.assign_user(@user) - .becomes(StatusGroupMembership) - - @other_user = create(:user) - @membership_of_other_user = @status_group.assign_user(@other_user).becomes(StatusGroupMembership) - end - - describe ".find_all_by_corporation" do - subject { StatusGroupMembership.find_all_by_corporation( @corporation ) } - it "should be chainable, i.e. return an ActiveRecord::Relation object" do - subject.should be_kind_of ActiveRecord::Relation - end - it "should return the membership of the descendant_users in their status groups" do - subject.should include @membership - end - it "should work for corporation derivatives as well" do - @corporation_derivative = @corporation.becomes SomeCorporationDerivative - expect { StatusGroupMembership.find_all_by_corporation( @corporation_derivative ) } - .not_to raise_error - end - it "should not return memberships in intermediate groups" do - # this behavior might be changed by the main app. - subject.should_not include @intermediate_group_membership - end - end - - describe ".find_all_by_user" do - subject { StatusGroupMembership.find_all_by_user( @user ) } - it "should be chainable, i.e. return an ActiveRecord::Relation object" do - subject.should be_kind_of ActiveRecord::Relation - end - it "should return the memberships of the user in his status groups" do - subject.should include @membership - end - it "should not list memberships of the user in non-status groups" do - @non_status_membership = UserGroupMembership - .find_by_user_and_group( @user, @corporation ) - subject.should_not include @non_status_membership - end - it "should not return memberships in intermediate groups" do - # this behavior might be changed by the main app. - subject.should_not include @intermediate_group_membership - end - it "should return current memberships, but not expired memberships" do - subject.should include @membership - @membership.invalidate at: 2.minutes.ago - StatusGroupMembership.find_all_by_user( @user ).should_not include @membership - end - end - - describe ".find_all_by_user.now" do - subject { StatusGroupMembership.find_all_by_user( @user ).now } - it "should return current memberships, but not expired memberships" do - subject.should include @membership - @membership.invalidate at: 2.minutes.ago - StatusGroupMembership.find_all_by_user( @user ).now.should_not include @membership - end - end - - describe ".find_all_by_user.now_and_in_the_past" do - subject { StatusGroupMembership.find_all_by_user( @user ).now_and_in_the_past } - it "should return current memberships and expired ones" do - subject.should include @membership - @membership.invalidate at: 2.minutes.ago - StatusGroupMembership.find_all_by_user( @user ).now_and_in_the_past - .should include @membership - end - end - - describe ".find_all_by_user.in_the_past" do - subject { StatusGroupMembership.find_all_by_user( @user ).in_the_past } - it "should return only expired memberships" do - subject.should_not include @membership - @membership.invalidate at: 2.minutes.ago - StatusGroupMembership.find_all_by_user( @user ).in_the_past - .should include @membership - end - end - - describe ".find_all_by_user_and_corporation" do - subject { StatusGroupMembership.find_all_by_user_and_corporation( @user, @corporation ) } - it "should return the memberships of the user in the status groups of the corporation" do - subject.should include @membership - end - it "should not return memberships in other corporations" do - subject.should_not include @membership_in_other_corporation - end - it "should not return memberships of other users" do - subject.should_not include @membership_of_other_user - end - end - - # @corporation - # |------- @intermediate_group - # |------------ @status_group - # | |--------- (@user) - # | - # |------------ @second_status_group - # |--------- @user - # - describe ".now_and_in_the_past.find_all_by_user_and_corporation" do - before do - @membership.update_attribute(:valid_from, 1.year.ago) - @second_membership = @membership.move_to(@second_status_group, at: 20.days.ago) - .becomes(StatusGroupMembership) - end - subject { StatusGroupMembership.now_and_in_the_past.find_all_by_user_and_corporation(@user, @corporation) } - specify "prelims" do - @user.should be_kind_of User - @corporation.reload.should be_kind_of Corporation - @corporation.descendants.should include @intermediate_group, @status_group, @second_status_group, @user - @intermediate_group.reload.descendants.should include @status_group, @second_status_group, @user - @status_group.reload.descendants.should include @user - @second_status_group.reload.descendants.should include @user - @corporation.members.should include @user - @user.should be_member_of @corporation - @membership.valid_to.to_date.should == 20.days.ago.to_date - end - it { should include @second_membership } - it { should include @membership } - it { should_not include @intermediate_group_membership } - end - - end - - # Status Workflow in model layer (bug fix) - # ========================================================================================== - - describe "(status workflow scenario)" do - - before do - @user = create(:user) - @corporation = create(:corporation_with_status_groups) - @status_groups = @corporation.status_groups - - @first_status_group = @status_groups.first - @second_status_group = @status_groups.second - - @first_status_group.assign_user @user - - @first_promotion_workflow = create( :promotion_workflow, name: 'First Promotion', - :remove_from_group_id => @first_status_group.id, - :add_to_group_id => @second_status_group.id ) - @first_promotion_workflow.parent_groups << @first_status_group - end - - def status_groups_of_user_and_corporation - StatusGroupMembership.now_and_in_the_past.find_all_by_user_and_corporation(@user, @corporation) - end - def first_status_group_membership - StatusGroupMembership.with_invalid.find_by_user_and_group(@user, @first_status_group) - end - def second_status_group_membership - StatusGroupMembership.with_invalid.find_by_user_and_group(@user, @second_status_group) - end - - describe "prelims" do - specify "first_status_group_membership should find the membership even if invalidated" do - first_status_group_membership.should_not == nil - first_status_group_membership.invalidate at: 1.hour.ago - first_status_group_membership.should_not == nil - end - end - - describe "executing the first promotion workflow" do - subject { @first_promotion_workflow.execute(user_id: @user.id); @user.reload } - it "should add the second status group to the user's status groups" do - status_groups_of_user_and_corporation.should_not include second_status_group_membership - subject - status_groups_of_user_and_corporation.should include second_status_group_membership - end - it "should not remove the first status group from the user's status groups" do - status_groups_of_user_and_corporation.should include first_status_group_membership - subject - status_groups_of_user_and_corporation.should include first_status_group_membership - end - end - - end - -end +# require 'spec_helper' +# +# describe StatusGroupMembership do +# +# # Alias Methods for Delegated Methods +# # ========================================================================================== +# +# describe "#promoted_by_workflow" do +# before do +# @workflow = create( :workflow ) +# @membership = create( :status_group_membership ) +# end +# subject { @membership.promoted_by_workflow } +# describe "if one has been assigned" do +# before do +# @membership.promoted_by_workflow = @workflow +# end +# it "should return the workflow that has been associated" do +# subject.should == @workflow +# end +# it "should persist" do +# @membership.save +# @reloaded_membership = StatusGroupMembership.find( @membership.id ) +# @reloaded_membership.promoted_by_workflow.should == @workflow +# @reloaded_membership = StatusGroupMembership +# .find_by_user_and_group( @membership.user, @membership.group ) +# @reloaded_membership.promoted_by_workflow.should == @workflow +# end +# it "should be an alias of #workflow" do +# subject.should == @membership.workflow +# end +# end +# describe "if none has been assigned" do +# it { should == nil } +# end +# end +# +# describe "#promoted_on_event" do +# before do +# @event = create( :event ) +# @membership = create( :status_group_membership ) +# end +# subject { @membership.promoted_on_event } +# describe "if one has been assigned" do +# before { @membership.promoted_on_event = @event } +# it { should == @event } +# it "should persist" do +# @membership.save +# @reloaded_membership = StatusGroupMembership.find( @membership.id ) +# @reloaded_membership.promoted_on_event.should == @event +# @reloaded_membership = StatusGroupMembership +# .find_by_user_and_group( @membership.user, @membership.group ) +# @reloaded_membership.promoted_on_event.should == @event +# end +# it "should be an alias of #event" do +# subject.should == @membership.event +# end +# end +# describe "if none has been assigned" do +# it { should == nil } +# end +# end +# +# describe "#event_by_name" do +# before do +# @membership = create( :status_group_membership ) +# end +# subject { @membership.event_by_name } +# describe "for existing event" do +# before do +# @event = create( :event ) +# @membership.event = @event +# end +# it { should == @event.name } +# end +# describe "if no event is assigned" do +# it { should == nil } +# end +# end +# describe "#event_by_name=" do +# before do +# @membership = create( :status_group_membership ) +# end +# describe "for an existing event" do +# before { @event = create( :event ) } +# subject { @membership.event_by_name = @event.name } +# it "should assign the event" do +# @membership.event.should == nil +# subject +# @membership.event.should == @event +# end +# end +# describe "for a new event" do +# subject { @membership.event_by_name = "A New Event" } +# it "should create the event" do +# @membership.event.should == nil +# subject +# @membership.event.name.should == "A New Event" +# end +# it "should persist" do +# subject +# @membership.save +# @reloaded_membership = StatusGroupMembership.find( @membership.id ) +# @reloaded_membership.event.should == @membership.event +# @reloaded_membership.event.name.should == "A New Event" +# end +# it "should copy the date from the membership" do +# subject +# @membership.event.start_at.to_date.should == @membership.created_at.to_date +# end +# describe "for the membership having a corporation" do +# before do +# @corporation = create( :corporation ) +# @corporation.child_groups << @membership.group +# end +# it "should association the corporation with the new event" do +# subject +# @membership.event.group.becomes( Group ).should == @corporation.becomes( Group ) +# end +# end +# end +# describe "for an empty string" do +# before do +# @membership.event_by_name = "some prefilled event" +# end +# subject { @membership.event_by_name = "" } +# it "should set the event to nil" do +# subject +# @membership.event.should == nil +# end +# end +# end +# +# +# +# # Finder Methods +# # ========================================================================================== +# +# class SomeCorporationDerivative < Corporation +# # This is just a dummy. The main app could invent a class inherited from Corporation. +# # Some methods need to work with them as well as with the original Corporation class. +# end +# +# # @corporation +# # |------- @intermediate_group +# # |------------ @status_group +# # | |--------- @user +# # | +# # |------------ @second_status_group +# # +# describe "Finder Methods: " do +# before do +# @corporation = create( :corporation ) +# @intermediate_group = create( :group, name: "Not a Status Group" ) +# @status_group = create( :group, name: "Status Group" ) +# +# @intermediate_group.parent_groups << @corporation +# @status_group.parent_groups << @intermediate_group +# @user = create( :user ) +# @status_group.assign_user @user +# +# @membership = UserGroupMembership.find_by_user_and_group( @user, @status_group ) +# .becomes( StatusGroupMembership ) +# @intermediate_group_membership = UserGroupMembership +# .find_by_user_and_group( @user, @intermediate_group ).becomes StatusGroupMembership +# +# @second_status_group = @intermediate_group.child_groups.create(name: "Second Status Group") +# +# @other_corporation = create(:corporation_with_status_groups) +# @membership_in_other_corporation = @other_corporation.status_groups.first.assign_user(@user) +# .becomes(StatusGroupMembership) +# +# @other_user = create(:user) +# @membership_of_other_user = @status_group.assign_user(@other_user).becomes(StatusGroupMembership) +# end +# +# describe ".find_all_by_corporation" do +# subject { StatusGroupMembership.find_all_by_corporation( @corporation ) } +# it "should be chainable, i.e. return an ActiveRecord::Relation object" do +# subject.should be_kind_of ActiveRecord::Relation +# end +# it "should return the membership of the descendant_users in their status groups" do +# subject.should include @membership +# end +# it "should work for corporation derivatives as well" do +# @corporation_derivative = @corporation.becomes SomeCorporationDerivative +# expect { StatusGroupMembership.find_all_by_corporation( @corporation_derivative ) } +# .not_to raise_error +# end +# it "should not return memberships in intermediate groups" do +# # this behavior might be changed by the main app. +# subject.should_not include @intermediate_group_membership +# end +# end +# +# describe ".find_all_by_user" do +# subject { StatusGroupMembership.find_all_by_user( @user ) } +# it "should be chainable, i.e. return an ActiveRecord::Relation object" do +# subject.should be_kind_of ActiveRecord::Relation +# end +# it "should return the memberships of the user in his status groups" do +# subject.should include @membership +# end +# it "should not list memberships of the user in non-status groups" do +# @non_status_membership = UserGroupMembership +# .find_by_user_and_group( @user, @corporation ) +# subject.should_not include @non_status_membership +# end +# it "should not return memberships in intermediate groups" do +# # this behavior might be changed by the main app. +# subject.should_not include @intermediate_group_membership +# end +# it "should return current memberships, but not expired memberships" do +# subject.should include @membership +# @membership.invalidate at: 2.minutes.ago +# StatusGroupMembership.find_all_by_user( @user ).should_not include @membership +# end +# end +# +# describe ".find_all_by_user.now" do +# subject { StatusGroupMembership.find_all_by_user( @user ).now } +# it "should return current memberships, but not expired memberships" do +# subject.should include @membership +# @membership.invalidate at: 2.minutes.ago +# StatusGroupMembership.find_all_by_user( @user ).now.should_not include @membership +# end +# end +# +# describe ".find_all_by_user.now_and_in_the_past" do +# subject { StatusGroupMembership.find_all_by_user( @user ).now_and_in_the_past } +# it "should return current memberships and expired ones" do +# subject.should include @membership +# @membership.invalidate at: 2.minutes.ago +# StatusGroupMembership.find_all_by_user( @user ).now_and_in_the_past +# .should include @membership +# end +# end +# +# describe ".find_all_by_user.in_the_past" do +# subject { StatusGroupMembership.find_all_by_user( @user ).in_the_past } +# it "should return only expired memberships" do +# subject.should_not include @membership +# @membership.invalidate at: 2.minutes.ago +# StatusGroupMembership.find_all_by_user( @user ).in_the_past +# .should include @membership +# end +# end +# +# describe ".find_all_by_user_and_corporation" do +# subject { StatusGroupMembership.find_all_by_user_and_corporation( @user, @corporation ) } +# it "should return the memberships of the user in the status groups of the corporation" do +# subject.should include @membership +# end +# it "should not return memberships in other corporations" do +# subject.should_not include @membership_in_other_corporation +# end +# it "should not return memberships of other users" do +# subject.should_not include @membership_of_other_user +# end +# end +# +# # @corporation +# # |------- @intermediate_group +# # |------------ @status_group +# # | |--------- (@user) +# # | +# # |------------ @second_status_group +# # |--------- @user +# # +# describe ".now_and_in_the_past.find_all_by_user_and_corporation" do +# before do +# @membership.update_attribute(:valid_from, 1.year.ago) +# @second_membership = @membership.move_to(@second_status_group, at: 20.days.ago) +# .becomes(StatusGroupMembership) +# end +# subject { StatusGroupMembership.now_and_in_the_past.find_all_by_user_and_corporation(@user, @corporation) } +# specify "prelims" do +# @user.should be_kind_of User +# @corporation.reload.should be_kind_of Corporation +# @corporation.descendants.should include @intermediate_group, @status_group, @second_status_group, @user +# @intermediate_group.reload.descendants.should include @status_group, @second_status_group, @user +# @status_group.reload.descendants.should include @user +# @second_status_group.reload.descendants.should include @user +# @corporation.members.should include @user +# @user.should be_member_of @corporation +# @membership.valid_to.to_date.should == 20.days.ago.to_date +# end +# it { should include @second_membership } +# it { should include @membership } +# it { should_not include @intermediate_group_membership } +# end +# +# end +# +# # Status Workflow in model layer (bug fix) +# # ========================================================================================== +# +# describe "(status workflow scenario)" do +# +# before do +# @user = create(:user) +# @corporation = create(:corporation_with_status_groups) +# @status_groups = @corporation.status_groups +# +# @first_status_group = @status_groups.first +# @second_status_group = @status_groups.second +# +# @first_status_group.assign_user @user +# +# @first_promotion_workflow = create( :promotion_workflow, name: 'First Promotion', +# :remove_from_group_id => @first_status_group.id, +# :add_to_group_id => @second_status_group.id ) +# @first_promotion_workflow.parent_groups << @first_status_group +# end +# +# def status_groups_of_user_and_corporation +# StatusGroupMembership.now_and_in_the_past.find_all_by_user_and_corporation(@user, @corporation) +# end +# def first_status_group_membership +# StatusGroupMembership.with_invalid.find_by_user_and_group(@user, @first_status_group) +# end +# def second_status_group_membership +# StatusGroupMembership.with_invalid.find_by_user_and_group(@user, @second_status_group) +# end +# +# describe "prelims" do +# specify "first_status_group_membership should find the membership even if invalidated" do +# first_status_group_membership.should_not == nil +# first_status_group_membership.invalidate at: 1.hour.ago +# first_status_group_membership.should_not == nil +# end +# end +# +# describe "executing the first promotion workflow" do +# subject { @first_promotion_workflow.execute(user_id: @user.id); @user.reload } +# it "should add the second status group to the user's status groups" do +# status_groups_of_user_and_corporation.should_not include second_status_group_membership +# subject +# status_groups_of_user_and_corporation.should include second_status_group_membership +# end +# it "should not remove the first status group from the user's status groups" do +# status_groups_of_user_and_corporation.should include first_status_group_membership +# subject +# status_groups_of_user_and_corporation.should include first_status_group_membership +# end +# end +# +# end +# +# end +# \ No newline at end of file diff --git a/spec/models/user_group_membership_mixins/validity_range_for_indirect_memberships_spec.rb b/spec/models/user_group_membership_mixins/validity_range_for_indirect_memberships_spec.rb deleted file mode 100644 index f0f560f4f..000000000 --- a/spec/models/user_group_membership_mixins/validity_range_for_indirect_memberships_spec.rb +++ /dev/null @@ -1,296 +0,0 @@ -require 'spec_helper' - -describe UserGroupMembershipMixins::ValidityRangeForIndirectMemberships do - - # Memberships: - # - # *-----------------(c)--------------------* - # | - # |--------------------| - # | | - # *-------(a)--------* | - # *---------(b)---------* - # - # _________________________________________________________ - # t1 t2 t3 time --> - # - # Structure: - # - # indirect_group ........................... (c) - # |---------- direct_group ........... (a), (b) - # |--------- user - # - before do - @user = create(:user) - @indirect_group = create(:group) - @direct_group_a = @indirect_group.child_groups.create - @direct_group_b = @indirect_group.child_groups.create - @direct_group_a.assign_user @user - @indirect_membership = UserGroupMembership.find_by_user_and_group(@user, @indirect_group) - @direct_membership_a = UserGroupMembership.find_by_user_and_group(@user, @direct_group_a) - @direct_membership_b = @direct_membership_a.move_to_group(@direct_group_b) - @t1 = 2.hours.ago - @t2 = 1.hour.ago - @t3 = nil - @direct_membership_a.update_attribute(:valid_from, @t1) - @direct_membership_a.update_attribute(:valid_to, @t2) - @direct_membership_b.update_attribute(:valid_from, @t2) - @direct_membership_b.update_attribute(:valid_to, @t3) - @indirect_membership.delete_cache - - @direct_membership_a.reload - @direct_membership_b.reload - @indirect_membership.reload - end - - specify "preliminaries" do - @direct_membership_a.valid_from.to_i.should < @direct_membership_b.valid_from.to_i - - #@direct_membership_a.valid_to.should < @direct_membership_b.valid_to - end - - - # Validity Range Attributes - # ==================================================================================================== - - describe "#valid_from" do - subject { @indirect_membership.valid_from } - it "should be the valid_from attribute of the earliest direct membership" do - subject.to_i.should == @direct_membership_a.valid_from.to_i - end - end - describe "#valid_from=" do - before { @time = 30.minutes.ago } - subject { @indirect_membership.valid_from = @time } - it "should set the valid_from attribute of the earliset direct membership" do - subject - @indirect_membership.save - @direct_membership_a.reload.valid_from.to_i.should == @time.to_i - end - end - - describe "#valid_to" do - subject { @indirect_membership.valid_to } - it "should be the valid_to attribute of the latest direct membership" do - subject.to_i.should == @direct_membership_b.valid_to.to_i - end - end - describe "#valid_to=" do - before { @time = 30.minutes.ago } - subject { @indirect_membership.valid_to = @time } - it "should set the valid_to addtirbute of the last direct membership" do - subject - @indirect_membership.save - @direct_membership_b.reload.valid_to.to_i.should == @time.to_i - end - end - - describe "#earliest_direct_membership" do - subject { @indirect_membership.earliest_direct_membership } - it { should == @direct_membership_a } - end - - describe "#latest_direct_membership" do - subject { @indirect_membership.latest_direct_membership } - it { should == @direct_membership_b } - end - - describe "#recalculate_validity_range_from_direct_memberships" do - before do - @t1 = 10.hours.ago; @t2 = 8.hours.ago; @t3 = 37.minutes.ago - @direct_membership_a.update_attribute(:valid_from, @t1) - @direct_membership_a.update_attribute(:valid_to, @t2) - @direct_membership_b.update_attribute(:valid_from, @t2) - @direct_membership_b.update_attribute(:valid_to, @t3) - end - subject { @indirect_membership.reload.recalculate_validity_range_from_direct_memberships; @indirect_membership.reload } - it "should make the indirect validity range match the direct memberships' combined range" do - subject - @indirect_membership.valid_from.to_i.should == @t1.to_i - @indirect_membership.valid_to.to_i.should == @t3.to_i - end - it "should write the indirect ranges to the database" do - subject - @indirect_membership.read_attribute(:valid_from).to_i.should == @t1.to_i - @indirect_membership.read_attribute(:valid_to).to_i.should == @t3.to_i - end - it "should persist in the graph" do - subject - DagLink.find(@indirect_membership.id).valid_from.to_i.should == @t1.to_i - DagLink.find(@indirect_membership.id).valid_to.to_i.should == @t3.to_i - end - describe "for the earliest valid_from being nil" do - before { @direct_membership_a.update_attribute(:valid_from, nil) } - specify "prelims" do - @reloaded_direct_membership_a = UserGroupMembership.with_invalid.find(@direct_membership_a.id) - @reloaded_direct_membership_a.read_attribute(:valid_from).should == nil - end - it "should set the indirect valid_from to nil" do - subject - @indirect_membership.read_attribute(:valid_from).should == nil - end - end - describe "(bug fix: reproducing status group membership scenario)" do - # @corporation - # |------- @intermediate_group - # |------------ @status_group - # | |--------- (@user) - # | - # |------------ @second_status_group - # |--------- @user - before do - @corporation = create( :corporation, name: "Corporation" ) - @intermediate_group = create( :group, name: "Not a Status Group" ) - @status_group = create( :group, name: "Status Group" ) - @intermediate_group.parent_groups << @corporation - @status_group.parent_groups << @intermediate_group - @user = create( :user ) - @status_group.assign_user @user - @membership = UserGroupMembership.find_by_user_and_group( @user, @status_group ) - @intermediate_group_membership = UserGroupMembership - .find_by_user_and_group( @user, @intermediate_group ) - @second_status_group = @intermediate_group.child_groups.create(name: "Second Status Group") - @membership.update_attribute(:valid_from, 1.year.ago) - @corpo_membership = UserGroupMembership.find_by_user_and_group(@user, @corporation) - end - specify "prelims" do - @user.should be_kind_of User - @corporation.reload.should be_kind_of Corporation - @corporation.descendants.should include @intermediate_group, @status_group, @second_status_group, @user - @intermediate_group.reload.descendants.should include @status_group, @second_status_group, @user - @status_group.reload.descendants.should include @user - end - describe "promoting the membership" do - subject { @second_membership = @membership.move_to(@second_status_group, at: 20.day.ago) } - # @membership valid_from: 1.year.ago, valid_to: 20.days.ago - # @second_membership valid_from: 20.days.ago, valid_to: nil - # @corpo_membership valid_from: 1.year.ago, valid_to: nil - before { subject } - - it "should update the valid_from and valid_to of the indirect membership" do - @corpo_membership.read_attribute(:valid_from).to_date.should == 1.year.ago.to_date - @corpo_membership.read_attribute(:valid_to).should == nil - end - it "should update the validity range persistent" do - @reloaded_corpo_membership = UserGroupMembership.find(@corpo_membership.id) - @reloaded_corpo_membership.should == @corpo_membership - @reloaded_corpo_membership.valid_from.to_date.should == 1.year.ago.to_date - @reloaded_corpo_membership.valid_to.should == nil - end - it "should update the valid_from and valid_to of the corresponding graph link" do - @link = DagLink.find(@corpo_membership.id) - @link.valid_from.to_date.should == 1.year.ago.to_date - @link.valid_to.should == nil - end - it "should update the graph structure" do - @second_status_group.reload.descendants.should include @user - @corporation.reload.descendants.should include @user - end - it "should update the corporation members correctly" do - @corporation.members.should include @user - @user.should be_member_of @corporation - end - specify "the corporation members should match the memberships in number" do - @corporation.memberships.count.should > 0 - @corporation.memberships.count.should == @corporation.members.count - end - specify "the indirect membership should be included in the memberships associated with the corporation" do - @corporation.memberships.should include @corpo_membership - end - it "should make no difference if the validity range is forcefully updated" do - @corpo_membership.read_attribute(:valid_from).to_date.should == 1.year.ago.to_date - @corpo_membership.read_attribute(:valid_to).should == nil - - @membership.update_attribute(:valid_from, 1.year.ago) - @membership.update_attribute(:valid_to, 20.days.ago) - @second_membership.update_attribute(:valid_from, 20.days.ago) - @second_membership.update_attribute(:valid_to, nil) - - @corpo_membership = UserGroupMembership.find(@corpo_membership.id) - @corpo_membership.read_attribute(:valid_from).to_date.should == 1.year.ago.to_date - @corpo_membership.read_attribute(:valid_to).should == nil - end - end - end - end - - - # Invalidation - # ==================================================================================================== - - describe "#make_invalid" do - subject { @indirect_membership.make_invalid } - it "should raise an error" do - expect { subject }.to raise_error - end - end - describe "#invalidate" do - subject { @indirect_membership.invalidate } - it "should raise an error" do - expect { subject }.to raise_error - end - end - - - # Validity Check - # ==================================================================================================== - - describe "#valid_at?(time)" do - subject { @indirect_membership.valid_at? @time_to_check } - specify "preliminaries" do - @indirect_membership.earliest_direct_membership.valid_from.to_i.should == @t1.to_i - @indirect_membership.earliest_direct_membership.valid_to.to_i.should == @t2.to_i - @indirect_membership.latest_direct_membership.valid_from.to_i.should == @t2.to_i - @indirect_membership.latest_direct_membership.valid_to.should == @t3 - end - it "should return false before the early direct membership" do - @time_to_check = 3.hours.ago - subject.should == false - end - it "should return true for the duration of the early direct membership" do - @time_to_check = 1.5.hours.ago - subject.should == true - end - it "should return true for the duration of the late direct membership" do - @time_to_check = 0.5.hours.ago - subject.should == true - end - end - - - # Temporal scopes - # ==================================================================================================== - - describe "#at_time" do - subject { UserGroupMembership.find_all_by_user(@user).at_time(30.minutes.ago) } - specify "preliminaries" do - @direct_membership_a.valid_from.to_i.should == @t1.to_i - @direct_membership_a.valid_to.to_i.should == @t2.to_i - @direct_membership_b.valid_from.to_i.should == @t2.to_i - @direct_membership_b.valid_to.should == @t3 - @indirect_membership.valid_from.to_i.should == @t1.to_i - @indirect_membership.valid_to.should == @t3 - # @indirect_membership.read_attribute(:valid_from).to_i.should == @t1.to_i - # @indirect_membership.read_attribute(:valid_to).should == @t3 - end - it "should find the direct membership" do - subject.should include @direct_membership_b - end - it "should find the indirect membership as well" do - subject.should include @indirect_membership - end - end - - describe "#only_valid" do - subject { UserGroupMembership.only_valid.find_all_by_user(@user) } - it "should find the valid indirect memberships" do - subject.should include @indirect_membership - end - it "should not find the invalid indirect memberships" do - @direct_membership_b.invalidate at: 20.minutes.ago - subject.should_not include @indirect_membership - end - end - - -end diff --git a/spec/models/user_group_membership_mixins/validity_range_spec.rb b/spec/models/user_group_membership_mixins/validity_range_spec.rb deleted file mode 100644 index f4225d7c0..000000000 --- a/spec/models/user_group_membership_mixins/validity_range_spec.rb +++ /dev/null @@ -1,293 +0,0 @@ -require 'spec_helper' - -describe UserGroupMembershipMixins::ValidityRange do - - before do - @user = create(:user) - @group = create(:group) - @membership = UserGroupMembership.create(user: @user, group: @group) - @membership.reload - end - - specify "preliminaries" do - @membership.should_not be_changed - @membership.id.should be_kind_of Integer - @membership.should be_kind_of UserGroupMembership - end - - describe "#valid_from" do - subject { @membership.valid_from } - it { should be_kind_of Time } - it "should be set to the created_at date by default" do - subject.to_i.should > @membership.created_at.to_i-2 - subject.to_i.should < @membership.created_at.to_i+2 - end - end - describe "#valid_to" do - subject { @membership.valid_to } - describe "being unset" do - it { should == nil } - end - describe "being set" do - before { @membership.valid_to = 1.hour.ago } - it { should be_kind_of Time } - end - end - - describe "#valid_from_localized_date" do - subject { @membership.valid_from_localized_date } - describe "if no valid_from given" do - before { @membership.valid_from = nil } - it { should == "" } - end - describe "if a datetime given" do - before do - @time = "1.1.2013 12:30 UTC".to_datetime - @membership.valid_from = @time - end - it { should == "01.01.2013" } - end - end - describe "#valid_from_localized_date=" do - describe "setting a date string" do - subject { @membership.valid_from_localized_date = "1.1.2013" } - it "should set the correct date" do - subject - @membership.valid_from.to_date.should == "1.1.2013".to_date - end - end - describe "setting an empty string" do - subject { @membership.valid_from_localized_date = "" } - it "should set valid_from to nil" do - subject - @membership.valid_from.should == nil - end - end - describe "setting an invalid date" do - subject { @membership.valid_from_localized_date = "FOO BAR" } - it "should raise an error" do - expect { subject }.to raise_error - end - end - end - - describe "#make_invalid" do - describe "with time argument" do - before { @time = 1.hour.ago } - subject { @membership.make_invalid(@time) } - it "should set the valid_to argument to the given time" do - @membership.valid_to.should == nil - subject - @membership.valid_to.should == @time - end - it "should mark the membership as invalid" do - @membership.currently_valid?.should == true - subject - @membership.currently_valid?.should == false - end - it "should return the membership" do - subject.should == @membership - end - describe "with 'at: time' argument" do - subject { @membership.make_invalid at: @time } - it "should set the valid_to argument to the given time" do - @membership.valid_to.should == nil - subject - @membership.valid_to.should == @time - end - end - end - describe "without argument" do - subject { @membership.make_invalid } - it "should set the end of the validity to the current time" do - @membership.valid_to.should == nil - subject - @membership.valid_to.to_i.should > Time.zone.now.to_i-2 - @membership.valid_to.to_i.should < Time.zone.now.to_i+2 - end - end - end - - describe "#invalidate" do - describe "with time argument" do - before { @time = 1.hour.ago } - subject { @membership.invalidate(@time) } - it "should set the valid_to argument to the given time" do - @membership.valid_to.should == nil - subject - @membership.valid_to.should == @time - end - it "should mark the membership as invalid" do - @membership.currently_valid?.should == true - subject - @membership.currently_valid?.should == false - end - it "should return the membership" do - subject.should == @membership - end - describe "with 'at: time' argument" do - subject { @membership.invalidate at: @time } - it "should set the valid_to argument to the given time" do - @membership.valid_to.should == nil - subject - @membership.valid_to.should == @time - end - end - end - describe "without argument" do - subject { @membership.invalidate } - it "should set the end of the validity to the current time" do - @membership.valid_to.should == nil - subject - @membership.valid_to.to_i.should == Time.zone.now.to_i - end - end - end - - describe "#currently_valid?" do - subject { @membership.currently_valid? } - it "should check whether the membership is valid in terms of the validity range at present time" do - @membership.currently_valid?.should == true - @membership.invalidate - @membership.currently_valid?.should == false - end - end - describe "#valid_at?(time)" do - before do - @time = 1.hour.ago - @membership.update_attribute(:valid_from, @time) - end - subject { @membership.valid_at? @time } - it "should check whether the membership is valid in terms of the validity range at the given time" do - @membership.valid_at?(@time).should == true - @membership.invalidate at: (@time - 1.hour) - @membership.valid_at?(@time).should == false - end - end - - describe "(temporal scopes)" do - before do - @valid_membership = @membership - @valid_membership.update_attribute(:valid_from, 2.hours.ago) - @group2 = create(:group) - @time = 1.hour.ago - @invalid_membership = UserGroupMembership.create(user: @user, group: @group2) - @invalid_membership.valid_from = 2.hours.ago - @invalid_membership.invalidate(@time) - @query = UserGroupMembership.find_all_by_user(@user) - end - - describe "#at_time" do - subject { @query.at_time(@time + 1.minute) } - it "should limit the search to match the validity range" do - subject.should include @valid_membership - subject.should_not include @invalid_membership - end - end - - describe "#only_valid" do - subject { @query.only_valid } - it "should return only memberships that are currently valid" do - subject.should include @valid_membership - subject.should_not include @invalid_membership - end - end - - describe "#only_invalid" do - subject { @query.only_invalid } - it "should return only memberships that are currently invalid" do - subject.should_not include @valid_membership - subject.should include @invalid_membership - end - end - - describe "#with_invalid" do - subject { @query.with_invalid } - it "should return both valid and invalid memberships" do - subject.should include @valid_membership - subject.should include @invalid_membership - end - end - - describe "(by default)" do - subject { @query } - it "should return only currently valid memberships" do - subject.should include @valid_membership - subject.should_not include @invalid_membership - end - end - - describe "#now" do - subject { @query.now } - it "should return only memberships that are currently valid" do - subject.should include @valid_membership - subject.should_not include @invalid_membership - end - end - describe "#in_the_past" do - subject { @query.in_the_past } - it "should return only memberships that are currently invalid" do - subject.should_not include @valid_membership - subject.should include @invalid_membership - end - end - describe "#now_and_in_the_past" do - subject { @query.now_and_in_the_past } - it "should return both valid and invalid memberships" do - subject.should include @valid_membership - subject.should include @invalid_membership - end - end - end - - describe "(validity range constraints)" do - # - # @time @now - # ====================================================> time - # |------------------------------------> @membership1 - # |-----------------------> @membership2 - # |-----------| @membership3 - # ----------------------------------------------------> @membership4 - # - # - before do - @user1 = create :user - @user2 = create :user - @user3 = create :user - @user4 = create :user - @time = 1.year.ago - @now = Time.zone.now - @membership1 = @group.assign_user @user1, at: @time - 1.day - @membership2 = @group.assign_user @user2, at: @time + 1.day - @membership3 = @group.assign_user @user3, at: @time + 1.day; @membership3.invalidate at: 1.month.ago - @membership4 = @group.assign_user @user4; @membership4.update_attribute(:valid_from, nil) - end - specify 'prelims' do - @membership4.valid_from.should == nil - @membership4.valid_to.should == nil - @group.memberships.last.id.should == @membership4.id - @group.memberships.last.valid_from.should == nil - end - describe ".now_and_in_the_past.started_after(time)" do - subject { @group.memberships.now_and_in_the_past.started_after(@time) } - it { should include @membership2 } - it { should include @membership3 } - it { should_not include @membership1 } - it { should_not include @membership4 } - end - describe ".started_after(time)" do - subject { @group.memberships.started_after(@time) } - it { should include @membership2 } - it { should_not include @membership3 } - it { should_not include @membership1 } - it { should_not include @membership4 } - end - describe "to_a.started_after(time)" do - subject { @group.memberships.to_a.started_after(@time) } - it { should include @membership2 } - it { should_not include @membership3 } - it { should_not include @membership1 } - it { should_not include @membership4 } - end - end -end diff --git a/spec/models/user_group_membership_spec.rb b/spec/models/user_group_membership_spec.rb deleted file mode 100644 index df0be5f79..000000000 --- a/spec/models/user_group_membership_spec.rb +++ /dev/null @@ -1,428 +0,0 @@ -require 'spec_helper' - -describe UserGroupMembership do - - before do - @group = Group.create( name: "Group 1" ) - @super_group = Group.create( name: "Parent Group of Groups 1 and 2" ) - @other_group = Group.create( name: "Group 2" ) - @group.parent_groups << @super_group - @other_group.parent_groups << @super_group - @other_user = create(:user) - @user = User.create( first_name: "John", last_name: "Doe", :alias => "j.doe" ) - end - - it "should allow to create example group and user and the group structure" do - @user.should_not == nil - @group.should_not == nil - @super_group.should_not == nil - end - - def create_membership - UserGroupMembership.create( user: @user, group: @group ) - end - - def find_membership - UserGroupMembership.find_by( user: @user, group: @group ) - end - - def find_membership_now_and_in_the_past - UserGroupMembership.find_all_by( user: @user, group: @group ).now_and_in_the_past.first - end - - def find_indirect_membership - UserGroupMembership.find_by( user: @user, group: @super_group ) - end - - def find_indirect_membership_now_and_in_the_past - UserGroupMembership.find_all_by( user: @user, group: @super_group ).now_and_in_the_past.first - end - - def create_other_membership - UserGroupMembership.create( user: @user, group: @other_group ) - end - - def create_another_membership - UserGroupMembership.create( user: @other_user, group: @group ) - end - - def find_other_membership - UserGroupMembership.find_by( user: @user, group: @other_group ) - end - - def find_other_membership_now_and_in_the_past - UserGroupMembership.find_all_by( user: @user, group: @other_group).now_and_in_the_past.first - end - - def create_memberships - create_membership - create_other_membership - create_another_membership - # the indirect membership is created implicitly, becuase @group and @super_group are already connected. - end - - # Creation Class Method - # ==================================================================================================== - - describe ".create" do - it "should create a link between parent and child" do - UserGroupMembership.create( user: @user, group: @group ) - @user.parents.should include( @group ) - end - it "should raise an error if argument is missing" do - expect { UserGroupMembership.create( user: @user ) }.to raise_error RuntimeError - expect { UserGroupMembership.create( group: @group ) }.to raise_error RuntimeError - end - it "should be able to identify a user by its 'user_title'" do - UserGroupMembership.create( user_title: @user.title, group_id: @group.id ) - @user.parents.should include @group - end - end - - # Finder Class Methods - # ==================================================================================================== - - describe "Finder Method" do - before { create_memberships } - - describe ".find_all_by" do - it "should find all memberships for a user" do - UserGroupMembership.find_all_by( user: @user ).should include( find_membership ) - UserGroupMembership.find_all_by( user: @user ).should include( find_indirect_membership ) - end - it "should find all memberships for a group" do - UserGroupMembership.find_all_by( group: @group ).should include( find_membership ) - end - it "should not find memberships that are invalid at the present time" do - find_membership.update_attribute(:valid_to, 1.hour.ago) - UserGroupMembership.find_all_by( user: @user ) - .should_not include( find_membership_now_and_in_the_past ) - UserGroupMembership.find_all_by( user: @user ) - .should include find_other_membership - end - it "should be able to identify users by 'user_title'" do - UserGroupMembership.find_all_by( user_title: @user.title ).each do |membership| - membership.user_id.should == @user.id - end - end - end - describe ".find_all_by.now_and_in_the_past" do - before { find_membership.make_invalid } - it "should find all memberships, including the ones that are invalid at the present time" do - UserGroupMembership.find_all_by( user: @user ).now_and_in_the_past - .should include( find_membership_now_and_in_the_past, find_indirect_membership, find_other_membership ) - end - end - - describe ".find_by" do - it "should be the same as .find_by_all.first" do - UserGroupMembership.find_by( user: @user, group: @group ).should == - UserGroupMembership.find_all_by( user: @user, group: @group ).first - end - end - - describe ".find_by_user_and_group" do - it "should find the right membership" do - UserGroupMembership.find_by_user_and_group( @user, @group ).should == find_membership - end - end - - describe ".find_all_by_user" do - it "should find the right memberships" do - UserGroupMembership.find_all_by_user( @user ).should include( find_membership ) - end - end - - describe ".find_all_by_group" do - it "should find the right memberships" do - UserGroupMembership.find_all_by_group( @group ).should include( find_membership ) - end - end - end - - describe "#== ( other_membership ), i.e. euality relation, " do - it "should return true if the two objects represent the same membership" do - membership = create_membership - same_membership = find_membership - membership.should == same_membership - end - end - - - # Access Methods to Associated User and Group - # ==================================================================================================== - - describe "Access Method to Assiciation" do - before { @membership = create_membership } - subject { @membership } - - describe "#user" do - its(:user) { should == @user } - end - describe "#user=" do - subject { @membership.user = @other_user } - it "should assign a user to the membership" do - @membership.user.should == @user - subject - @membership.user.should == @other_user - end - end - describe "#user_id" do - subject { @membership.user_id } - it { should == @user.id } - end - describe "#user_title" do - subject { @membership.user_title } - it { should == @user.title } - end - describe "#user_title=" do - subject { @membership.user_title = @other_user.title } - it "should assign the user matching the title to the membership" do - @membership.user.should == @user - subject - @membership.user.should == @other_user - end - end - describe "#group" do - its(:group) { should == @group } - end - describe "#group_id" do - subject { @membership.group_id } - it { should == @group.id } - end - end - - - # Associated Corporation - # ==================================================================================================== - - # corporation - # |-------- group - # |---( membership )---- user - # - describe "#corporation" do - describe "for the group having a corporation" do - before do - @corporation = create( :corporation ) - @group = @corporation.child_groups.create - @user = create( :user ) - @group.assign_user @user - end - subject { UserGroupMembership.find_by_user_and_group( @user, @group ).corporation } - it { should == @corporation } - end - describe "for the group not having a corporation" do - before do - @group = create( :group ) - @user = create( :user ) - @group.assign_user @user - end - subject { UserGroupMembership.find_by_user_and_group( @user, @group ).corporation } - it { should == nil } - end - describe "for the group being a corporation" do - before do - @corporation = create( :corporation ) - @user = create( :user ) - @corporation.assign_user @user - end - subject { UserGroupMembership.find_by_user_and_group( @user, @corporation ).corporation } - it { should == @corporation } - end - end - - - - # Access Methods to Associated Direct Memberships - # ==================================================================================================== - - describe "#direct_memberships" do - before {create_memberships } - describe "for a direct membership" do - subject { find_membership } - it "should include only itself (the direct membership)" do - subject.direct_memberships.should == [ subject ] - end - end - describe "for an indirect membership" do - subject { find_indirect_membership } - it "should include the direct membership" do - subject.direct_memberships.should include( find_membership ) - end - end - end - - describe "#direct_memberships_now_and_in_the_past" do - before { create_memberships } - it "should return an ActiveRecord::Relation, i.e. be chainable" do - find_membership.direct_memberships_now_and_in_the_past.kind_of?( ActiveRecord::Relation ).should be_true - end - # it "should be the same as #direct_memberships.now_and_in_the_past" do - # find_indirect_membership.direct_memberships_now_and_in_the_past.should == - # find_indirect_membership.direct_memberships.now_and_in_the_past - # end - describe "for a direct membership" do - it "should include itself (the direct membership)" do - find_membership.direct_memberships_now_and_in_the_past.should include( find_membership ) - end - end - describe "for an indirect membership" do - it "should include the direct membership" do - find_indirect_membership.direct_memberships_now_and_in_the_past.should include( find_membership ) - end - end - end - - describe "#direct_groups" do - before { create_memberships } - describe "for a direct membership" do - it "should return an array containing only the own group" do - find_membership.direct_groups.should == [ find_membership.group ] - end - end - describe "for an indirect membership" do - it "should return an array containing the direct group" do - find_indirect_membership.direct_groups.should == [ find_membership.group, find_other_membership.group ] - end - end - end - - - # Access Methods to Associated Indirect Memberships - # ==================================================================================================== - - describe "#indirect_memberships" do - before do - @membership = UserGroupMembership.create(user: @user, group: @group) - @indirect_membership = find_indirect_membership - end - subject { @membership.indirect_memberships } - it { should include find_indirect_membership } - it { should_not include find_membership } - describe "for invalidated memberships" do - before do - @membership.update_attribute(:valid_from, 2.hours.ago) - @membership.update_attribute(:valid_to, 1.hour.ago) - end - it "should still find the indirect memberships" do - subject.should include @indirect_membership - end - end - end - - - # More Tests for Indirect Memberships - # ==================================================================================================== - - describe "Indirect Membership" do - - before do - @sub_group = Group.create( name: "Sub Group" ) - @sub_group.parent_groups << @group - @user.parent_groups << @sub_group - @membership = UserGroupMembership.find_by_user_and_group( @user, @sub_group ) - @indirect_membership = UserGroupMembership.find_by_user_and_group( @user, @group ) - end - - subject { @indirect_membership } - - it "should have the same validity range (valid_from) as the direct membership" do - @indirect_membership.valid_from.should == @membership.valid_from - end - - it "should have the same validity range (valid_to) as the direct membership" do - @indirect_membership.valid_to.should == @membership.valid_to - end - - it "should also effect the direct membership on change of the valid_from date" do - new_time = 1.hour.ago - @membership.valid_from = new_time - @membership.save - @indirect_membership.valid_from.to_i.should == new_time.to_i - end - - it "should also effect the direct membership on change of the valid_to date" do - new_time = Time.current + 1.hour - @membership.invalidate - @membership.valid_to = new_time - @membership.save - @indirect_membership.reload - @indirect_membership.valid_to.to_i.should == new_time.to_i - end - - it "should be effected by the direct membership on change of the valid_from date" do - new_time = 1.hour.ago - @indirect_membership.valid_from = new_time - @indirect_membership.save - @membership.reload - @membership.valid_from.to_i.should == new_time.to_i - end - - it "should be effected by the direct membership on change of the valid_to date" do - new_time = Time.current + 1.hour - @membership.invalidate # need to archive the *direct* membership, ... - @indirect_membership.reload - @indirect_membership.valid_to = new_time # but can change the time of the *indirect*. - @indirect_membership.save - @membership.reload - @membership.valid_to.to_i.should == new_time.to_i - end - end - - # Methods to Change the Membership - # ==================================================================================================== - - describe "#move_to_group( group )" do - before do - create_membership - find_membership.move_to_group( @other_group ) - time_travel 2.seconds - end - it "should hide old direct membership" do - find_membership.should == nil - end - it "should create a new membership between the user and the given group" do - find_other_membership.should_not == nil - end - end - - - # Destroy - # ========================================================================================== - - describe "#destroy" do - describe "for nested structures (bug fix)" do - # - # @corporation - # |-------- @status_1 ---------------- @user | p - # |-------- @group_a | r - # | |------- @status_2 ---- @user | o - # | | m - # |-------- @group_b | o - # |------- @status_3 ---- @user | t - # V e - before do - @user = create(:user) - @corporation = create(:corporation) - @status_1 = @corporation.child_groups.create - @group_a = @corporation.child_groups.create - @status_2 = @group_a.child_groups.create - @group_b = @corporation.child_groups.create - @status_3 = @group_b.child_groups.create - @membership_1 = @status_1.assign_user @user, at: 1.year.ago - @membership_2 = @membership_1.promote_to @status_2, at: 10.minutes.ago - @membership_3 = @membership_2.promote_to @status_3, at: 2.minutes.ago - end - subject do - @user.parent_groups.each do |group| - UserGroupMembership.with_invalid.find_by_user_and_group(@user, group).destroy - end - end - it "should not raise an error (bug fix)" do - expect { subject }.not_to raise_error - end - end - end - - -end diff --git a/spec/models/user_mixins/memberships_spec.rb b/spec/models/user_mixins/memberships_spec.rb deleted file mode 100644 index b0ea90c9a..000000000 --- a/spec/models/user_mixins/memberships_spec.rb +++ /dev/null @@ -1,138 +0,0 @@ -require 'spec_helper' - -describe UserMixins::Memberships do - - # @indirect_group - # |------------ @group - # | |------ @user1 - # | |------ @user2 - # | - # |------------ @group2 - # - before do - @group = create(:group) - @user1 = create(:user); @group.assign_user(@user1) - @user2 = create(:user); @group.assign_user(@user2) - @user = @user1 - @membership1 = UserGroupMembership.find_by(user: @user1, group: @group) - @membership2 = UserGroupMembership.find_by(user: @user2, group: @group) - @indirect_group = @group.parent_groups.create - @indirect_membership1 = UserGroupMembership.find_by(user: @user1, group: @indirect_group) - @indirect_membership2 = UserGroupMembership.find_by(user: @user2, group: @indirect_group) - @group2 = @indirect_group.child_groups.create - end - - - # User Group Memberships - # ========================================================================================== - - describe "#memberships" do - subject { @user1.memberships } - it { should include @membership1 } - it { should include @indirect_membership1 } - it "should not include invalidated memberships" do - @membership1.invalidate at: 10.minutes.ago - subject { should_not include @membership1 } - end - it "should not include invalidated indirect memberships" do - @membership1.invalidate at: 10.minutes.ago - subject { should_not include @indirect_membership1 } - end - end - - describe "#direct_memberships" do - subject { @user1.direct_memberships } - it { should include @membership1 } - it { should_not include @indirect_membership1 } - end - - describe "#indirect_memberships" do - subject { @user1.indirect_memberships } - it { should include @indirect_membership1 } - it { should_not include @membership1 } - end - - - describe "#membership_in( group )" do - describe "for the user being a direct member" do - subject { @user.membership_in @group } - it { should == @membership1 } - end - describe "for the user being an indirect member" do - subject { @user.membership_in @indirect_group } - it { should == @indirect_membership1 } - end - end - - describe "#member_of?( group )" do - describe "for the user being direct member" do - subject { @user.member_of? @group} - it { should == true } - end - describe "for the user being indirect member" do - subject { @user.member_of? @indirect_group } - it { should == true } - end - describe "for the user not being a member" do - subject { @user.member_of? @group2 } - it { should == false } - end - end - - - # Groups the user is member of - # ========================================================================================== - - describe "#groups" do - subject { @user1.groups } - it { should include @group } - it { should include @indirect_group } - it "should not include groups of invalidated memberships" do - @membership1.invalidate at: 10.minutes.ago - subject.should_not include @group - subject.should_not include @indirect_group - end - end - describe "#groups << group" do - subject { @user.groups << @group2 } - it "should assign the user to the given group" do - @user.should_not be_in @group2.members - subject - @user.should be_in @group2.members - @user.should be_in @group2.direct_members - end - end - describe "#groups.destroy(group)" do - describe "for the membership being direct" do - subject { @user.groups.destroy(@group) } - it "should remove the user from the members list" do - @user1.should be_in @group.members - subject - @user1.should_not be_in @group.members - end - it "should remove the membership permanently" do - subject - UserGroupMembership.with_invalid.find_by_user_and_group(@user1, @group).should == nil - end - end - describe "for the membership being indirect" do - subject { @user.groups.destroy(@indirect_group) } - it "should raise an error" do - expect { subject }.to raise_error - end - end - end - - describe "#direct_groups" do - subject { @user.direct_groups } - it { should include @group } - it { should_not include @indirect_group } - end - - describe "#indirect_groups" do - subject { @user.indirect_groups } - it { should include @indirect_group } - it { should_not include @group } - end - -end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 3becd3215..c6d7f577a 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -708,8 +708,8 @@ it "should include the groups the user is an indirect member of" do subject.should include( Group.everyone ) end - it "should return all ancestor groups" do - subject.should == @user.ancestor_groups + it "should return all connected ancestor groups" do + subject.collect(&:id).sort.should == @user.connected_ancestor_groups.collect(&:id).sort end end @@ -767,7 +767,7 @@ @subgroup = create( :group ); @subgroup.parent_groups << @corporationE @user.save - @first_membership_E = StatusGroupMembership.create( user: @user, group: @corporationE.status_groups.first ) + @first_membership_E = Membership.create(user: @user, group: @corporationE.status_groups.first) @user.parent_groups << @subgroup @user.reload end @@ -780,7 +780,7 @@ @user.cached(:corporations) wait_for_cache - first_membership_S = StatusGroupMembership.create( user: @user, group: @corporationS.status_groups.first ) + first_membership_S = Membership.create(user: @user, group: @corporationS.status_groups.first) first_membership_S.update_attributes(valid_from: "2010-05-01".to_datetime) @user.reload end @@ -791,7 +791,7 @@ @user.cached(:corporations) wait_for_cache - first_membership_H = StatusGroupMembership.create( user: @user, group: @corporationH.guests_parent ) + first_membership_H = Membership.create(user: @user, group: @corporationH.guests_parent) first_membership_H.update_attributes(valid_from: "2010-05-01".to_datetime) @user.reload end @@ -802,7 +802,7 @@ @user.cached(:corporations) former_group = @corporationE.child_groups.create former_group.add_flag :former_members_parent - second_membership_E = StatusGroupMembership.create( user: @user, group: former_group ) + second_membership_E = Membership.create(user: @user, group: former_group) second_membership_E.update_attributes(valid_from: "2014-05-01".to_datetime) @first_membership_E.update_attributes(valid_to: "2014-05-01".to_datetime) @user.reload @@ -819,7 +819,7 @@ @subgroup = create( :group ); @subgroup.parent_groups << @corporationE @user.save - @first_membership_E = StatusGroupMembership.create( user: @user, group: @corporationE.status_groups.first ) + @first_membership_E = Membership.create(user: @user, group: @corporationE.status_groups.first) @user.parent_groups << @subgroup @user.reload end @@ -831,7 +831,7 @@ end context "when user entered corporation S" do before do - first_membership_S = StatusGroupMembership.create( user: @user, group: @corporationS.status_groups.first ) + first_membership_S = Membership.create(user: @user, group: @corporationS.status_groups.first) first_membership_S.update_attributes(valid_from: "2010-05-01".to_datetime) @user.reload end @@ -839,7 +839,7 @@ end context "when user entered corporation H as guest" do before do - first_membership_H = StatusGroupMembership.create( user: @user, group: @corporationH.guests_parent ) + first_membership_H = Membership.create(user: @user, group: @corporationH.guests_parent) first_membership_H.update_attributes(valid_from: "2010-05-01".to_datetime) @user.reload end @@ -849,7 +849,7 @@ before do former_group = @corporationE.child_groups.create former_group.add_flag :former_members_parent - second_membership_E = StatusGroupMembership.create( user: @user, group: former_group ) + second_membership_E = Membership.create(user: @user, group: former_group) second_membership_E.update_attributes(valid_from: "2014-05-01".to_datetime) @first_membership_E.update_attributes(valid_to: "2014-05-01".to_datetime) @user.reload @@ -874,7 +874,7 @@ @subgroup = create( :group ); @subgroup.parent_groups << @corporationE @user.save - @first_membership_E = StatusGroupMembership.create( user: @user, group: @corporationE.status_groups.first ) + @first_membership_E = Membership.create(user: @user, group: @corporationE.status_groups.first) @user.parent_groups << @subgroup @user.reload end @@ -887,7 +887,7 @@ @user.cached(:current_corporations) wait_for_cache - first_membership_S = StatusGroupMembership.create( user: @user, group: @corporationS.status_groups.first ) + first_membership_S = Membership.create(user: @user, group: @corporationS.status_groups.first) first_membership_S.update_attributes(valid_from: "2010-05-01".to_datetime) @user.reload end @@ -896,7 +896,7 @@ context "when user entered corporation H as guest" do before do @user.cached(:current_corporations) - first_membership_H = StatusGroupMembership.create( user: @user, group: @corporationH.guests_parent ) + first_membership_H = Membership.create(user: @user, group: @corporationH.guests_parent) first_membership_H.update_attributes(valid_from: "2010-05-01".to_datetime) @user.reload end @@ -909,7 +909,7 @@ former_group = @corporationE.child_groups.create former_group.add_flag :former_members_parent - second_membership_E = StatusGroupMembership.create( user: @user, group: former_group ) + second_membership_E = Membership.create(user: @user, group: former_group) second_membership_E.update_attributes(valid_from: "2014-05-01".to_datetime) @first_membership_E.update_attributes(valid_to: "2014-05-01".to_datetime) @user.reload @@ -992,7 +992,7 @@ @corporation = create( :corporation_with_status_groups ) @status_group = @corporation.status_groups.first @status_group.assign_user @user - @status_group_membership = StatusGroupMembership.find_by_user_and_group(@user, @status_group) + @status_group_membership = Membership.where(user: @user, group: @status_group).first end subject { @user.current_status_membership_in(@corporation) } @@ -1006,20 +1006,41 @@ # ------------------------------------------------------------------------------------------ describe "#memberships" do + # Join the validity ranges of indirect memberships. + # + # @group1 + # |------- @subgroup1 -----| <--- past membership + # |------- @subgroup2 --- @user1 + # + # First, user1 joins subgroup1, then moves to subgroup2. + # + # |-----------| first indirect membership in @group1 + # |--------- second indirect membership in @group2 + # |--------------------- joined indirect membership + # before do - @group = create( :group ) - @group.child_users << @user - @membership = UserGroupMembership.find_by( user: @user, group: @group ) - end - subject { @user.memberships } - it "should return an array of the user's memberships" do - subject.should == [ @membership ] - end - it "should be the same as UserGroupMembership.find_all_by_user" do - subject.should == UserGroupMembership.find_all_by_user( @user ) + @group1 = create :group, name: 'group1' + @subgroup1 = @group1.child_groups.create name: 'subgroup1' + @subgroup2 = @group1.child_groups.create name: 'subgroup2' + @user1 = create :user; @subgroup1 << @user1; @subgroup2 << @user1 + @time1 = 1.year.ago; @time2 = 6.months.ago; @time3 = 2.months.ago; @time4 = nil + Membership.where(user: @user1, group: @subgroup1).first.update_attributes valid_from: @time1, valid_to: @time2 + Membership.where(user: @user1, group: @subgroup2).first.update_attributes valid_from: @time3, valid_to: @time4 + + @membership1 = Membership.where(user: @user1, group: @group1).last + @submembership1 = Membership.where(user: @user1, group: @subgroup1).first # past membership + @submembership2 = Membership.where(user: @user1, group: @subgroup2).first + end + subject { @user1.memberships } + specify "prelims" do + @membership1.should be_kind_of Membership + @submembership2.should be_kind_of Membership + @submembership1.should be_kind_of Membership end - it "should allow to chain other ActiveRelation scopes, like `only_valid`" do - subject.only_valid.should == [ @membership ] + it { should be_kind_of MembershipCollection } + it "should only include current memberships per default" do + subject.should include @membership1, @submembership2 + subject.count.should == 2 end end @@ -1134,18 +1155,16 @@ time_travel 2.seconds end describe "(leaving an event)" do - # TODO: We need multiple dag links between two nodes! before { @event_or_group = @event; subject } specify { @event.attendees.should_not include @user} specify { @event.attendees_group.members.should_not include @user } - specify { @event.attendees_group.child_users.should_not include @user } + specify { @event.attendees_group.child_users.should include @user } end describe "(leaving a group)" do before { @event_or_group = @group; subject } - # TODO: We need multiple dag links between two nodes! - # specify { @group.members.should_not include @user } - # specify { @group.members.former.should include @user } - # specify { @group.child_users.should include @user } + specify { @group.members.should_not include @user } + specify { @group.members.former.should include @user } + specify { @group.child_users.should include @user } end end @@ -1339,10 +1358,10 @@ subject { @user.group_flags } describe "for the user being hidden" do before { @user.hidden = true } - it { should include 'hidden_users' } + it { should include :hidden_users } end describe "for the user not being hidden" do - it { should_not include 'hidden_users' } + it { should_not include :hidden_users } end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index febd941c9..873ad31fa 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -206,6 +206,19 @@ # Call `debug` to enter pry. # config.include Debug + + # This counts the number of queries performed within the block. + # + # Usage: + # + # expected_count = 10 + # count = count_queries(expected_count) do + # SomeModel.first + # end + # + # If the count does not match the expected_count, the queries are printed out. + # + config.include CountQueries # Devise test helper for controller tests config.include Devise::TestHelpers, :type => :controller @@ -318,6 +331,7 @@ # I18n.default_locale = :de I18n.locale = :de + TEST_TIMEZONE = "Berlin" # Request Host diff --git a/spec/support/count_queries.rb b/spec/support/count_queries.rb new file mode 100644 index 000000000..ef17797f9 --- /dev/null +++ b/spec/support/count_queries.rb @@ -0,0 +1,41 @@ +require 'colored' +module CountQueries + + # This counts the number of queries performed within the block. + # + # Usage: + # + # c = count_queries do + # SomeModel.first + # end + # + # See also: http://stackoverflow.com/a/22388177/2066546 + # + def count_queries(expected_count = nil, &block) + queries = collect_queries(&block) + + # Output the queries if the count does not match. + if expected_count && queries.count != expected_count + queries.each do |query| + print query.yellow + "\n\n" + end + end + + return queries.count + end + + def collect_queries(&block) + queries = [] + + counter_f = ->(name, started, finished, unique_id, payload) { + unless payload[:name].in? %w[ CACHE SCHEMA ] + queries << payload[:sql] + end + } + + ActiveSupport::Notifications.subscribed(counter_f, "sql.active_record", &block) + + return queries + end + +end \ No newline at end of file diff --git a/your_platform.gemspec b/your_platform.gemspec index 5fdd38c8e..ec410ecbb 100644 --- a/your_platform.gemspec +++ b/your_platform.gemspec @@ -21,7 +21,7 @@ Gem::Specification.new do |s| s.files = Dir["{app,config,db,lib,vendor}/**/*"] + ["Rakefile", "README.md"] s.test_files = Dir["spec/**/*"] - # Dependencies + # Dependencies # -------------------------------------------------------------------------------- # Rails and Rails Additions @@ -34,8 +34,8 @@ Gem::Specification.new do |s| s.add_dependency "bundler", ">= 1.9.4" s.add_dependency 'web-console', '>= 2.1.3' - - # JavaScript + + # JavaScript s.add_dependency "jquery-rails", '>= 3.1.3', '<= 4.0.4' # Fix version due to datatables issue (http://stackoverflow.com/a/31150030/2066546) s.add_dependency "jquery-ui-rails", '~> 4.2.0' # MIT, GPL2 s.add_dependency "autosize-rails" # autosize textbox @@ -48,18 +48,16 @@ Gem::Specification.new do |s| # Data Structures # Retry transactions: Rescue from deadlocks. s.add_dependency 'transaction_retry' - # DAG Structure, https://github.com/resgraph/acts-as-dag - s.add_dependency 'acts-as-dag', '>= 2.5.7' # MIT License s.add_dependency 'acts_as_tree' # MIT License - + # Caching s.add_dependency 'redis-rails' - + # Workers s.add_dependency 'foreman' s.add_dependency 'sidekiq', '>= 3.4.2' s.add_dependency 'sidekiq-limit_fetch' - + # Authentification s.add_dependency 'devise', '>= 2.2.5' # MIT License @@ -69,7 +67,7 @@ Gem::Specification.new do |s| s.add_dependency 'cancan' # MIT License # To use ActiveModel has_secure_password (password encryption) - s.add_dependency 'bcrypt', '>= 3.0.1' # MIT License + s.add_dependency 'bcrypt', '>= 3.0.1' # MIT License # Settings s.add_dependency 'rails-settings-cached' @@ -80,7 +78,7 @@ Gem::Specification.new do |s| s.add_dependency 'redcarpet', '>= 3.3.2' # for Markdown # MIT License s.add_dependency 'gemoji', '>= 2.1.0' s.add_dependency 'auto_html', '>= 1.6.4' - + # Layout: Twitter Bootstrap s.add_dependency 'font-awesome-rails', '~> 4.3.0' # fix bootstrap to 3.3.3 due to icon issue: @@ -89,7 +87,7 @@ Gem::Specification.new do |s| # In Place Editing s.add_dependency 'best_in_place', '>= 2.1.0' # MIT License - + # Geo Coding s.add_dependency 'geocoder' # MIT License s.add_dependency 'gmaps4rails', '2.0.2' # CURRENTLY ONLY THE FORK WORKS FOR US # MIT License @@ -112,52 +110,55 @@ Gem::Specification.new do |s| # Hide slim breadcrumb elements until user hovers the separator s.add_dependency 'slim_breadcrumb', '>= 0.0.3' # MIT License - + # Workflow Kit s.add_dependency 'workflow_kit', '~> 0.0.7' # MIT License # View Helpers - s.add_dependency 'phony' + s.add_dependency 'phony' s.add_dependency 'will_paginate', '> 3.0' s.add_dependency 'jquery-datatables-rails', '~> 3.1.1' - + # JavaScript s.add_dependency 'turbolinks', '>= 2.5.3' s.add_dependency 'jquery-turbolinks' s.add_dependency 'turboboost' - + # Client-Side Validations s.add_dependency 'judge' - + # Metrics s.add_dependency 'fnordmetric' # MIT License s.add_dependency 'rack-mini-profiler', '>= 0.9.0.pre' # MIT License - + # Activity Feed s.add_dependency 'public_activity', '~> 1.4.1' # MIT License - + # XLS Export s.add_dependency 'to_xls' - + # PDF Export s.add_dependency 'prawn' - + # ICS Export (iCal) s.add_dependency 'icalendar' - + # Gamification s.add_dependency 'merit' - + # Dummy Data Generation s.add_dependency 'faker' - + + # Console + s.add_dependency "table-formatter" + # Fixes # https://github.com/eventmachine/eventmachine/issues/509 s.add_dependency 'eventmachine', '>= 1.0.7' # https://github.com/lautis/uglifier/pull/86 - s.add_dependency 'uglifier', '>= 2.7.2' + s.add_dependency 'uglifier', '>= 2.7.2' - # Development Dependencies + # Development Dependencies # -------------------------------------------------------------------------------- s.add_development_dependency "rspec-rails", "2.10.0"