diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index b8b357820..d1c858952 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -18,7 +18,32 @@ @import "font-awesome-sprockets"; @import "font-awesome"; @import "mixins/mixins"; -@import "*"; + +// Local +@import "about"; +@import "account"; +@import "activities"; +@import "autocomplete"; +@import "bootstrap-tweaks"; +@import "collaborations"; +@import "collection"; +@import "curators"; +@import "events"; +@import "external-resources"; +@import "footer"; +@import "forms"; +@import "headers"; +@import "helpers"; +@import "home"; +@import "icons"; +@import "learning-paths"; +@import "masonry"; +@import "nodes"; +@import "showcase"; +@import "toggle"; +@import "workflows"; + +// Vendor @import "cytoscape-panzoom"; @import "jquery.simplecolorpicker"; @import "eonasdan-bootstrap-datetimepicker"; @@ -1049,6 +1074,24 @@ td.day .calendar-text { } } +.theme-selector { + display: flex; + gap: 10px; +} + +.theme-select-option { + flex-grow: 1; + label { + border: 5px solid; + padding: 10px 20px; + border-radius: 5px; + display: block; + + hr { + border-width: 10px; + } + } +} .btn-oidc-only { font-size: 36px; padding: 4px; diff --git a/app/assets/stylesheets/learning-paths.scss b/app/assets/stylesheets/learning-paths.scss index 692c64557..f90730a63 100644 --- a/app/assets/stylesheets/learning-paths.scss +++ b/app/assets/stylesheets/learning-paths.scss @@ -38,7 +38,7 @@ } } -.learning-path-item { +.block-item { text-align: center; border-image-slice: 80; border-image-width: 80px; diff --git a/app/controllers/collections_controller.rb b/app/controllers/collections_controller.rb index 99a394bfd..7809339e9 100644 --- a/app/controllers/collections_controller.rb +++ b/app/controllers/collections_controller.rb @@ -114,7 +114,7 @@ def destroy @collection.create_activity :destroy, owner: current_user @collection.destroy respond_to do |format| - format.html { redirect_to collections_url, notice: 'Collection was successfully destroyed.' } + format.html { redirect_to collections_path, notice: 'Collection was successfully destroyed.' } format.json { head :no_content } end end diff --git a/app/controllers/content_providers_controller.rb b/app/controllers/content_providers_controller.rb index 854f29053..4897919bc 100644 --- a/app/controllers/content_providers_controller.rb +++ b/app/controllers/content_providers_controller.rb @@ -86,7 +86,7 @@ def destroy @content_provider.create_activity :destroy, owner: current_user @content_provider.destroy respond_to do |format| - format.html { redirect_to content_providers_url, notice: 'Content Provider was successfully destroyed.' } + format.html { redirect_to content_providers_path, notice: 'Content Provider was successfully destroyed.' } format.json { head :no_content } end end diff --git a/app/controllers/events_controller.rb b/app/controllers/events_controller.rb index cf1a0e811..413e78523 100644 --- a/app/controllers/events_controller.rb +++ b/app/controllers/events_controller.rb @@ -187,7 +187,7 @@ def destroy @event.create_activity :destroy, owner: current_user @event.destroy respond_to do |format| - format.html { redirect_to events_url, notice: 'Event was successfully destroyed.' } + format.html { redirect_to events_path, notice: 'Event was successfully destroyed.' } format.json { head :no_content } end end diff --git a/app/controllers/learning_path_topics_controller.rb b/app/controllers/learning_path_topics_controller.rb index 5fdf24169..7489e14fc 100644 --- a/app/controllers/learning_path_topics_controller.rb +++ b/app/controllers/learning_path_topics_controller.rb @@ -68,7 +68,7 @@ def destroy @learning_path_topic.create_activity :destroy, owner: current_user @learning_path_topic.destroy respond_to do |format| - format.html { redirect_to learning_path_topics_url, notice: 'Topic was successfully destroyed.' } + format.html { redirect_to learning_path_topics_path, notice: 'Topic was successfully destroyed.' } # format.json { head :no_content } end end diff --git a/app/controllers/learning_paths_controller.rb b/app/controllers/learning_paths_controller.rb index 83d91005a..a99105e8d 100644 --- a/app/controllers/learning_paths_controller.rb +++ b/app/controllers/learning_paths_controller.rb @@ -85,7 +85,7 @@ def destroy @learning_path.create_activity :destroy, owner: current_user @learning_path.destroy respond_to do |format| - format.html { redirect_to learning_paths_url, notice: 'Learning path was successfully destroyed.' } + format.html { redirect_to learning_paths_path, notice: 'Learning path was successfully destroyed.' } format.json { head :no_content } end end diff --git a/app/controllers/materials_controller.rb b/app/controllers/materials_controller.rb index 4d9a54723..0322f4adc 100644 --- a/app/controllers/materials_controller.rb +++ b/app/controllers/materials_controller.rb @@ -132,7 +132,7 @@ def destroy @material.create_activity :destroy, owner: current_user @material.destroy respond_to do |format| - format.html { redirect_to materials_url, notice: 'Material was successfully destroyed.' } + format.html { redirect_to materials_path, notice: 'Material was successfully destroyed.' } format.json { head :no_content } end end diff --git a/app/controllers/nodes_controller.rb b/app/controllers/nodes_controller.rb index cf942a5cb..331a4a8dc 100644 --- a/app/controllers/nodes_controller.rb +++ b/app/controllers/nodes_controller.rb @@ -79,7 +79,7 @@ def destroy @node.create_activity :destroy, owner: current_user @node.destroy respond_to do |format| - format.html { redirect_to nodes_url, notice: 'Node was successfully destroyed.' } + format.html { redirect_to nodes_path, notice: 'Node was successfully destroyed.' } format.json { head :no_content } end end diff --git a/app/controllers/sources_controller.rb b/app/controllers/sources_controller.rb index 33142098e..cd35bf435 100644 --- a/app/controllers/sources_controller.rb +++ b/app/controllers/sources_controller.rb @@ -24,7 +24,7 @@ def show # GET /sources/new def new - authorize Source + authorize @content_provider, :create_source? @source = @content_provider.sources.build end @@ -36,9 +36,10 @@ def edit # POST /sources # POST /sources.json def create - authorize Source + authorize @content_provider, :create_source? @source = @content_provider.sources.build(source_params) @source.user = current_user + @source.space = current_space respond_to do |format| if @source.save @@ -145,13 +146,12 @@ def set_source def set_content_provider @content_provider = @source.content_provider if @source @content_provider ||= ContentProvider.friendly.find(params[:content_provider_id]) - authorize @content_provider, :manage? end # Never trust parameters from the scary internet, only allow the white list through. def source_params permitted = [:url, :method, :token, :default_language, :enabled] - permitted << :approval_status if policy(Source).approve? + permitted << :approval_status if policy(@source || Source).approve? permitted << :content_provider_id if policy(Source).index? params.require(:source).permit(permitted) diff --git a/app/controllers/spaces_controller.rb b/app/controllers/spaces_controller.rb index 5d58c0512..8d118e303 100644 --- a/app/controllers/spaces_controller.rb +++ b/app/controllers/spaces_controller.rb @@ -65,7 +65,7 @@ def destroy @space.create_activity :destroy, owner: current_user @space.destroy respond_to do |format| - format.html { redirect_to spaces_url, notice: 'Space was successfully deleted.' } + format.html { redirect_to spaces_path, notice: 'Space was successfully deleted.' } end end diff --git a/app/controllers/workflows_controller.rb b/app/controllers/workflows_controller.rb index b2b306558..937f1bc2f 100644 --- a/app/controllers/workflows_controller.rb +++ b/app/controllers/workflows_controller.rb @@ -87,7 +87,7 @@ def destroy @workflow.create_activity :destroy, owner: current_user @workflow.destroy respond_to do |format| - format.html { redirect_to workflows_url, notice: 'Workflow was successfully destroyed.' } + format.html { redirect_to workflows_path, notice: 'Workflow was successfully destroyed.' } format.json { head :no_content } end end diff --git a/app/mailers/curation_mailer.rb b/app/mailers/curation_mailer.rb index 399ad2a70..a8679dbdc 100644 --- a/app/mailers/curation_mailer.rb +++ b/app/mailers/curation_mailer.rb @@ -17,7 +17,8 @@ def source_requires_approval(source, user) @user = user @source = source subject = "#{TeSS::Config.site['title_short']} source \"#{@source.title}\" requires approval" - mail(subject:, to: User.with_role('admin').map(&:email)) do |format| + space = @source.space || Space.default + mail(subject:, to: space.administrators.map(&:email)) do |format| format.html format.text end diff --git a/app/models/default_space.rb b/app/models/default_space.rb index 3f1f8144d..f0680d3d1 100644 --- a/app/models/default_space.rb +++ b/app/models/default_space.rb @@ -56,4 +56,8 @@ def learning_path_topics def default? true end + + def administrators + User.with_role('admin') + end end diff --git a/app/models/source.rb b/app/models/source.rb index 381e5da0a..889bf44a0 100644 --- a/app/models/source.rb +++ b/app/models/source.rb @@ -2,6 +2,7 @@ class Source < ApplicationRecord include LogParameterChanges include HasTestJob include HasAssociatedNodes + include InSpace APPROVAL_STATUS = { 0 => :not_approved, diff --git a/app/models/user.rb b/app/models/user.rb index 44b713043..dfb7df1a4 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -381,6 +381,10 @@ def has_space_role?(space, role) space_roles.where(key: role, space: space).any? end + def has_role_in_any_space?(role) + space_roles.where(key: role).any? + end + protected def reassign_resources(new_owner = User.get_default_user) diff --git a/app/policies/application_policy.rb b/app/policies/application_policy.rb index 4bf15e15a..895ab57b3 100644 --- a/app/policies/application_policy.rb +++ b/app/policies/application_policy.rb @@ -16,6 +16,8 @@ def initialize(context, record) @user = context.user @request = context.request @record = record + @space = nil + @space = record.space if record.respond_to?(:space) end def index? @@ -24,14 +26,10 @@ def index? def show? true - # scope.where(:id => record.id).exists? end def create? - # Only admin, scraper_user or curator roles can create - #@user.has_role?(:admin) or @user.has_role?(:scraper_user) or @user.has_role?(:curator) - # Any registered user user can create - @user && !@user.role.blank? + @user end def new? @@ -52,19 +50,11 @@ def destroy? # "manage" isn't actually an action, but the "destroy?" and "update?" policies delegate to this method. def manage? - @user && @user.is_admin? - end - - def request_is_api?(request) - return false if request.nil? - return ((request.post? or request.put? or request.patch?) and request.format.json?) + @user&.is_admin? end def curators_and_admin - @user && ( - @user.has_role?(:curator) || - @user.has_role?(:admin) || - @user.has_role?(:scraper_user)) + user_has_role?(:curator, :admin, :scraper_user) end def scope @@ -84,4 +74,22 @@ def resolve end end + private + + def request_is_api? + !!@request && ((@request.post? || @request.put? || @request.patch?) && @request.format.json?) + end + + def scraper? + request_is_api? && @user&.has_role?(:scraper_user) + end + + # Check if the user has any of the given roles. + # If we're in a space, also check they have any of those roles in the context of the space. + def user_has_role?(*roles) + return false if @user.nil? + roles.any? { |r| @user.has_role?(r) } || + (@space && roles.any? { |r| @user.has_space_role?(@space, r) }) + end + end diff --git a/app/policies/content_provider_policy.rb b/app/policies/content_provider_policy.rb index 9325015af..0e2cc2dbf 100644 --- a/app/policies/content_provider_policy.rb +++ b/app/policies/content_provider_policy.rb @@ -1,3 +1,6 @@ class ContentProviderPolicy < ScrapedResourcePolicy - + def create_source? + (TeSS::Config.feature['user_source_creation'] && manage?) || + user_has_role?(:admin, :curator) + end end diff --git a/app/policies/learning_path_policy.rb b/app/policies/learning_path_policy.rb index ae6e287c5..ca1a2b210 100644 --- a/app/policies/learning_path_policy.rb +++ b/app/policies/learning_path_policy.rb @@ -9,7 +9,7 @@ def update? end def manage? - curators_and_admin || @user&.has_role?(:learning_path_curator) + curators_and_admin || user_has_role?(:learning_path_curator) end def create? diff --git a/app/policies/learning_path_topic_policy.rb b/app/policies/learning_path_topic_policy.rb index e393948d5..a5a523e94 100644 --- a/app/policies/learning_path_topic_policy.rb +++ b/app/policies/learning_path_topic_policy.rb @@ -5,7 +5,7 @@ def update? end def manage? - curators_and_admin || @user&.has_role?(:learning_path_curator) + curators_and_admin || user_has_role?(:learning_path_curator) end def create? diff --git a/app/policies/node_policy.rb b/app/policies/node_policy.rb index 774006a57..96853b402 100644 --- a/app/policies/node_policy.rb +++ b/app/policies/node_policy.rb @@ -2,26 +2,11 @@ class NodePolicy < ApplicationPolicy def create? # Only admin, scraper_user, curator or node_curator roles can create - @user && (@user.has_role?(:admin) || @user.has_role?(:scraper_user) || @user.has_role?(:curator) || @user.has_role?(:node_curator)) + user_has_role?(:admin, :curator, :node_curator) || scraper? end def manage? - return false unless @user - return true if @user.is_admin? - - if request_is_api?(@request) #is this an API action - allow scraper_user roles only - if @user.has_role?(:scraper_user) #and @user.is_owner?(@record) # check ownership - return true - else - return false - end - end - - if @user.has_role?(:curator) || @user.has_role?(:node_curator) || @user.is_owner?(@record) - return true - else - return false - end + user_has_role?(:admin, :curator, :node_curator) || scraper? || @user&.is_owner?(@record) end end diff --git a/app/policies/resource_policy.rb b/app/policies/resource_policy.rb index 4a36b1789..0de44e133 100644 --- a/app/policies/resource_policy.rb +++ b/app/policies/resource_policy.rb @@ -10,7 +10,7 @@ def create? end def manage? - super || (@user && (@user.is_owner?(@record) || (request_is_api?(@request) && @user.has_role?(:scraper_user)))) + super || (@user&.is_owner?(@record) || scraper?) end end diff --git a/app/policies/scraped_resource_policy.rb b/app/policies/scraped_resource_policy.rb index bd716259e..c28afd262 100644 --- a/app/policies/scraped_resource_policy.rb +++ b/app/policies/scraped_resource_policy.rb @@ -3,7 +3,7 @@ class ScrapedResourcePolicy < ResourcePolicy def manage? - super || (@user && @user.is_curator?) || is_content_provider_editor? + super || (@user&.is_curator?) || is_content_provider_editor? end private diff --git a/app/policies/source_policy.rb b/app/policies/source_policy.rb index b21620790..d56933235 100644 --- a/app/policies/source_policy.rb +++ b/app/policies/source_policy.rb @@ -13,15 +13,11 @@ def index? end def create? - if TeSS::Config.feature['user_source_creation'] - super - else - administration? - end + administration? end def approve? - @user && @user.has_role?(:admin) + user_has_role?(:admin) end def request_approval? diff --git a/app/policies/space_policy.rb b/app/policies/space_policy.rb index 1d5197e12..c2ce876c4 100644 --- a/app/policies/space_policy.rb +++ b/app/policies/space_policy.rb @@ -1,7 +1,7 @@ class SpacePolicy < ApplicationPolicy def create? - @user && @user.has_role?(:admin) + @user&.has_role?(:admin) end def edit? @@ -13,7 +13,7 @@ def update? end def manage? - @user && @user.is_admin? + @user&.is_admin? end end diff --git a/app/policies/user_policy.rb b/app/policies/user_policy.rb index 827506f4f..fadaf44d3 100644 --- a/app/policies/user_policy.rb +++ b/app/policies/user_policy.rb @@ -12,13 +12,13 @@ def show? def create? # Do not allow creations via API and only admin role can create users - !request_is_api?(request) && @user && @user.is_admin? + !request_is_api? && @user&.is_admin? end def manage? # Do not allow updates via API # Only admin role can update other users or the users themselves - !request_is_api?(request) && (@user && (@user == @record || @user.is_admin?)) + !request_is_api? && (@user && (@user == @record || @user.is_admin?)) end def change_token? diff --git a/app/views/devise/shared/_links.html.erb b/app/views/devise/shared/_links.html.erb index e7120aed2..8051e325f 100644 --- a/app/views/devise/shared/_links.html.erb +++ b/app/views/devise/shared/_links.html.erb @@ -4,7 +4,8 @@ <%= link_to t('devise.links.log_in'), new_session_path(resource_name) %> <% end -%> - <%- if devise_mapping.registerable? && controller_name != 'registrations' %> + <%- if devise_mapping.registerable? && controller_name != 'registrations' && + TeSS::Config.feature['registration'] && !from_blocked_country? %>
  • <%= t('devise.links.new_to', title: TeSS::Config.site['title_short']) %> <%= link_to t('devise.links.register'), new_registration_path(resource_name) %>
  • <% end -%> diff --git a/app/views/layouts/_header.html.erb b/app/views/layouts/_header.html.erb index b6c8defe3..5eff2026d 100644 --- a/app/views/layouts/_header.html.erb +++ b/app/views/layouts/_header.html.erb @@ -51,7 +51,8 @@ { feature: 'collections', link: collections_path }, { feature: 'trainers', link: trainers_path }, { feature: 'content_providers', link: content_providers_path }, - { feature: 'nodes', link: nodes_path } + { feature: 'nodes', link: nodes_path }, + { feature: 'spaces', link: spaces_path } ].select do |t| t[:feature] == 'about' || TeSS::Config.feature[t[:feature]] end.sort_by do |t| diff --git a/app/views/layouts/_user_menu.html.erb b/app/views/layouts/_user_menu.html.erb index fae85f8da..2830bb043 100644 --- a/app/views/layouts/_user_menu.html.erb +++ b/app/views/layouts/_user_menu.html.erb @@ -16,16 +16,22 @@ <% end %> - <% if current_user.is_curator? || current_user.is_admin? %> + <% is_admin = current_user.is_admin? %> + <% is_curator = current_user.is_curator? %> + <% is_current_space_admin = !Space.current_space.default? && current_user.has_space_role?(Space.current_space, :admin) %> + <% if is_admin || is_curator || is_current_space_admin %> - + <% if is_admin || is_curator %> + + <% end %> + <% if TeSS::Config.feature['sources'] %> <% end %> - <% unless TeSS::Config.feature['disabled'].include?('topics') %> + + <% if !TeSS::Config.feature['disabled'].include?('topics') && (is_admin || is_curator) %> <% end %> - - + <% end %> + + <% if is_admin %> + + + <% if TeSS::Config.feature['spaces'] %> + <% end %> - + <% end %> + + <% if TeSS::Config.feature['spaces'] && (is_admin || is_current_space_admin) && !Space.current_space.default? %> + + <% end %> <% end %> diff --git a/app/views/learning_paths/_learning_path.html.erb b/app/views/learning_paths/_learning_path.html.erb index 3abdb502c..64c87a277 100644 --- a/app/views/learning_paths/_learning_path.html.erb +++ b/app/views/learning_paths/_learning_path.html.erb @@ -1,4 +1,4 @@ -
  • +
  • <%= link_to learning_path, class: 'link-overlay' do %> <%= item_order_badge(collection_item) if defined? collection_item %> diff --git a/app/views/search/common/_facet_sidebar.html.erb b/app/views/search/common/_facet_sidebar.html.erb index 72a7534ee..167d2e123 100644 --- a/app/views/search/common/_facet_sidebar.html.erb +++ b/app/views/search/common/_facet_sidebar.html.erb @@ -52,6 +52,13 @@ Variable that should be available <%= render partial: 'search/common/facet_sidebar_boolean_filter', locals: { facet: facet, resources: resources } %> <% end %> + <% resource_name = resource_type.model_name.human.downcase.pluralize %> + <%= render partial: 'search/common/facet_sidebar_boolean_filter', + locals: { facet_field: 'across_all_spaces', + count: '-', + enable_text: t('sidebar.filter.values.show_cross_space', resource: resource_name), + disable_text: t('sidebar.filter.values.hide_cross_space', resource: resource_name) } %> + <% if resource_type.name == 'Event' %> <%= render partial: 'search/common/facet_sidebar_boolean_filter', locals: { facet_field: 'include_expired', diff --git a/app/views/search/common/_facet_sidebar_curation.html.erb b/app/views/search/common/_facet_sidebar_curation.html.erb index a30431886..0a9d7587a 100644 --- a/app/views/search/common/_facet_sidebar_curation.html.erb +++ b/app/views/search/common/_facet_sidebar_curation.html.erb @@ -1,7 +1,5 @@ <% show_last_scraped_filter = resource_type.method_defined?(:last_scraped) %> <% show_hidden_filter = resource_type.method_defined?(:user_requires_approval?) %> -<% show_cross_space_filter = current_user&.is_admin? %> -<% resource_name = resource_type.model_name.human.downcase.pluralize %> <% if show_last_scraped_filter || show_hidden_filter %> <% end %> diff --git a/app/views/spaces/_form.html.erb b/app/views/spaces/_form.html.erb index 21acb3d70..aa2de5cbf 100644 --- a/app/views/spaces/_form.html.erb +++ b/app/views/spaces/_form.html.erb @@ -7,7 +7,7 @@ <%= f.input :description, as: :markdown_area, input_html: { rows: '10' } %> - <%= f.input :theme, collection: TeSS::Config.themes.keys, selected: @space.theme || 'default' %> + <%= render partial: 'theme_selector', locals: { form: f } %>
    <%= render partial: 'common/image_form', locals: { form: f } %> diff --git a/app/views/spaces/_space.html.erb b/app/views/spaces/_space.html.erb index 907cbb1ae..4b1157500 100644 --- a/app/views/spaces/_space.html.erb +++ b/app/views/spaces/_space.html.erb @@ -1,21 +1,24 @@ +<% theme_colour = TeSS::Config.themes[space.theme]&.dig('primary') %> <% cache(space, expires_in: 6.hours) do %>
  • - <%= link_to space, class: 'link-overlay' do %> + <%= link_to space, class: 'link-overlay', style: (theme_colour ? "border-color: #{theme_colour}" : nil) do %> <%= image_tag get_image_url_for(space), class: "media-image listing_image pull-right" %> -

    +

    "> <%= space.title %>

    - <%# if space.materials.length > 0 %> - - <%# end %> + <%= space.url %> - <%# if space.events.length > 0 %> - - - - - <%# end %> + <% if space.materials.length > 0 %> +
    <%= pluralize(space.materials.length, 'training material') %>
    + <% end %> + + <% if space.events.length > 0 %> +
    + <%= pluralize(space.events.not_finished.length, 'event') %> + (<%= pluralize(space.events.finished.length, 'past event') %>) +
    + <% end %> <% end %>
  • <% end %> diff --git a/app/views/spaces/_theme_selector.html.erb b/app/views/spaces/_theme_selector.html.erb new file mode 100644 index 000000000..75ef41e5b --- /dev/null +++ b/app/views/spaces/_theme_selector.html.erb @@ -0,0 +1,18 @@ +
    + +
    + <% TeSS::Config.themes.each do |theme, colours| %> + <% id = "space_theme_#{theme}" %> + <% primary = colours['primary'] %> + <% secondary = colours['secondary'] %> +
    + <%= label_tag(id, class: 'radio_buttons optional', style: "border-color: #{primary};") do %> + <%= form.radio_button(:theme, theme, id: id) %> + <%= theme.titleize %> +
    +
    + <% end %> +
    + <% end %> +
    +
    diff --git a/app/views/spaces/show.html.erb b/app/views/spaces/show.html.erb index ada8f3925..9b3599c26 100644 --- a/app/views/spaces/show.html.erb +++ b/app/views/spaces/show.html.erb @@ -27,7 +27,7 @@
    - <%= display_attribute(@space, :host) %> + <%= display_attribute(@space, :url) %>
    <%= render_markdown @space.description %> diff --git a/app/views/static/home/_space_welcome.html.erb b/app/views/static/home/_space_welcome.html.erb index 37788ea4d..2be0a1005 100644 --- a/app/views/static/home/_space_welcome.html.erb +++ b/app/views/static/home/_space_welcome.html.erb @@ -2,7 +2,7 @@ <%# Welcome text %>

    <%= current_space.title %>

    -
    +
    <%= render_markdown current_space.description %>
    diff --git a/config/locales/en.yml b/config/locales/en.yml index 6662563cf..d5650cfb0 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -905,7 +905,7 @@ en: show_broken_links: 'Show %{resource} with broken links' hide_broken_links: 'Hide %{resource} with broken links' show_cross_space: 'Show %{resource} from all spaces' - hide_cross_space: 'Hide %{resource} from all spaces' + hide_cross_space: 'Only show %{resource} from current space' hidden: values: show_hidden: 'Show hidden items' @@ -940,6 +940,7 @@ en: log_out: 'Log out' log_out_confirm: 'Are you sure you want to log out?' spaces_admin: 'Manage spaces' + current_space_admin: 'Manage current space' activity: empty: No activities recorded. deleted_owner: Someone diff --git a/config/tess.example.yml b/config/tess.example.yml index 3aaf14395..3f497d934 100644 --- a/config/tess.example.yml +++ b/config/tess.example.yml @@ -126,7 +126,7 @@ default: &default # The order in which the tabs appear (if feature enabled) tab_order: ['about', 'events', 'materials', 'elearning_materials', 'workflows', 'collections', 'trainers', 'content_providers', 'nodes'] # The tabs that should be collapsed under the "Directory" tab. Can be left blank to hide it. - directory_tabs: ['trainers', 'content_providers', 'nodes'] + directory_tabs: ['trainers', 'content_providers', 'nodes', 'spaces'] # The order in with the 'about us' tabs appear about_us_tab_order: ['tess_club', 'contact', 'team', 'funding', 'acknowledgements', 'cite'] n_provider_ids: 5 @@ -264,7 +264,7 @@ default: &default primary: '#047eaa' # Blue by default secondary: '#f47d21' # Orange by default green: - primary: '529d00' + primary: '#529d00' secondary: '#829d30' blue: primary: '#024552' diff --git a/db/migrate/20250807085314_add_space_id_to_sources.rb b/db/migrate/20250807085314_add_space_id_to_sources.rb new file mode 100644 index 000000000..4dbbef59a --- /dev/null +++ b/db/migrate/20250807085314_add_space_id_to_sources.rb @@ -0,0 +1,5 @@ +class AddSpaceIdToSources < ActiveRecord::Migration[7.2] + def change + add_reference :sources, :space, foreign_key: true + end +end diff --git a/db/schema.rb b/db/schema.rb index 7354d68e5..769407996 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -482,7 +482,9 @@ t.integer "approval_status" t.datetime "updated_at" t.string "default_language" + t.bigint "space_id" t.index ["content_provider_id"], name: "index_sources_on_content_provider_id" + t.index ["space_id"], name: "index_sources_on_space_id" t.index ["user_id"], name: "index_sources_on_user_id" end @@ -667,6 +669,7 @@ add_foreign_key "node_links", "nodes" add_foreign_key "nodes", "users" add_foreign_key "sources", "content_providers" + add_foreign_key "sources", "spaces" add_foreign_key "sources", "users" add_foreign_key "space_roles", "spaces" add_foreign_key "space_roles", "users" diff --git a/lib/facets.rb b/lib/facets.rb index 686316428..a02a711b6 100644 --- a/lib/facets.rb +++ b/lib/facets.rb @@ -140,7 +140,7 @@ def elixir(scope, value, _) def across_all_spaces(scope, value, user) sunspot_scoped(scope) do - all_spaces = (user&.is_admin? && value) + all_spaces = value with(:space_id, Space.current_space.id) unless all_spaces end end diff --git a/lib/ingestors/ingestor.rb b/lib/ingestors/ingestor.rb index 1fa4fa7be..7f9106e9a 100644 --- a/lib/ingestors/ingestor.rb +++ b/lib/ingestors/ingestor.rb @@ -164,6 +164,7 @@ def write_resources(type, resources, user, provider, source: nil) # check for matched events resource.user_id ||= user.id resource.content_provider_id ||= provider.id + resource.space_id ||= source&.space_id existing_resource = find_existing(type, resource) update = existing_resource diff --git a/test/controllers/sources_controller_test.rb b/test/controllers/sources_controller_test.rb index 8e8ca85e1..a00707c69 100644 --- a/test/controllers/sources_controller_test.rb +++ b/test/controllers/sources_controller_test.rb @@ -203,6 +203,28 @@ class SourcesControllerTest < ActionController::TestCase assert_response :success end + test 'space admin should get edit for source in their space' do + sign_in users(:space_admin) + source = sources(:unapproved_source) + space = spaces(:plants) + source.space = space + source.save! + + get :edit, params: { id: source } + assert_response :success + end + + test 'space admin should not get edit for source in another space' do + sign_in users(:space_admin) + source = sources(:unapproved_source) + space = spaces(:astro) + source.space = space + source.save! + + get :edit, params: { id: source } + assert_response :forbidden + end + # CREATE Tests test 'public should not create source' do assert_no_difference 'Source.count' do @@ -436,9 +458,48 @@ class SourcesControllerTest < ActionController::TestCase assert source.reload.approved? end + test 'space admin can approve source in their space' do + sign_in users(:space_admin) + source = sources(:unapproved_source) + space = spaces(:plants) + source.space = space + source.save! + refute source.approved? + + patch :update, params: { id: source, source: { approval_status: 'approved' } } + + assert_redirected_to source_path(assigns(:source)) + assert source.reload.approved? + end + + test 'space admin cannot approve source in other space' do + sign_in users(:space_admin) + source = sources(:unapproved_source) + space = spaces(:astro) + source.space = space + source.save! + refute source.approved? + + patch :update, params: { id: source, source: { approval_status: 'approved' } } + + assert_response :forbidden + refute source.reload.approved? + end + test 'regular user cannot approve source' do - sign_in @user + sign_in users(:another_regular_user) + source = sources(:unapproved_source) + refute source.approved? + + patch :update, params: { id: source, source: { approval_status: 'approved' } } + + assert_response :forbidden + refute source.reload.approved? + end + + test 'source owner cannot approve source' do source = sources(:unapproved_source) + sign_in source.user refute source.approved? patch :update, params: { id: source, source: { approval_status: 'approved' } } diff --git a/test/controllers/static_controller_test.rb b/test/controllers/static_controller_test.rb index 13b254422..7da77d111 100644 --- a/test/controllers/static_controller_test.rb +++ b/test/controllers/static_controller_test.rb @@ -16,7 +16,9 @@ class StaticControllerTest < ActionController::TestCase 'collections': true, 'content_providers': true, 'trainers': true, - 'nodes': true } + 'nodes': true, + 'spaces': true + } with_settings(feature: features) do get :home @@ -45,7 +47,9 @@ class StaticControllerTest < ActionController::TestCase 'collections': false, 'content_providers': false, 'trainers': false, - 'nodes': false } + 'nodes': false, + 'spaces': false + } with_settings(feature: features) do get :home @@ -151,7 +155,9 @@ class StaticControllerTest < ActionController::TestCase 'collections': true, 'content_providers': true, 'trainers': true, - 'nodes': true } + 'nodes': true, + 'spaces': true + } with_settings(feature: features, site: { tab_order: %w[materials events], directory_tabs: [] }) do get :home diff --git a/test/mailers/curation_mailer_test.rb b/test/mailers/curation_mailer_test.rb index d3062701c..b16627e3c 100644 --- a/test/mailers/curation_mailer_test.rb +++ b/test/mailers/curation_mailer_test.rb @@ -249,4 +249,30 @@ class CurationMailerTest < ActionMailer::TestCase end end end + + test 'source approval requests go to administrators' do + source = sources(:unapproved_source) + user = source.user + assert_nil source.space + email = CurationMailer.source_requires_approval(source, user) + + admins = User.with_role('admin') + assert admins.any? + assert_equal admins.map(&:email).sort, email.to.sort + end + + test 'source in space approval requests go to space administrators' do + source = sources(:unapproved_source) + space = spaces(:plants) + source.space = space + source.save! + user = source.user + assert source.space + email = CurationMailer.source_requires_approval(source, user) + + space_admins = space.administrators + assert_equal 1, space_admins.length + assert_equal 1, email.to.length + assert_equal 'plantboss@example.com', email.to.first + end end diff --git a/test/unit/ingestors/ingestor_test.rb b/test/unit/ingestors/ingestor_test.rb index 28ad68760..7b1bd13c3 100644 --- a/test/unit/ingestors/ingestor_test.rb +++ b/test/unit/ingestors/ingestor_test.rb @@ -89,7 +89,7 @@ class IngestorTest < ActiveSupport::TestCase assert_equal(event.language, 'de') end - test 'does not set event language when languare and source default language missing' do + test 'does not set event language when language and source default language missing' do user = users(:scraper_user) provider = content_providers(:portal_provider) @@ -187,4 +187,28 @@ class IngestorTest < ActiveSupport::TestCase end end end + + test 'sets event space from source space' do + user = users(:scraper_user) + provider = content_providers(:portal_provider) + space = spaces(:plants) + + @source = Source.create!(url: 'https://somewhere.com/stuff', method: 'bioschemas', + enabled: true, approval_status: 'approved', + space: space, content_provider: provider, user: users(:admin)) + + ingestor = Ingestors::Ingestor.new + + ingestor.instance_variable_set(:@events, + [OpenStruct.new(url: 'https://some-course.ca', + title: 'Some course', + start: '2021-01-31 13:00:00', + end:'2021-01-31 14:00:00')]) + assert_difference('provider.events.count', 1) do + ingestor.write(user, provider, source: @source) + end + event = Event.find_by(title: 'Some course') + assert_equal space, event.space + end + end