From 6f09124ee05109ce1c9163aa033392fbcd456521 Mon Sep 17 00:00:00 2001 From: Luis M Rodriguez-R Date: Thu, 14 May 2026 17:41:00 +0200 Subject: [PATCH 1/5] Push all Phase 2 files to the rebased branch --- app/controllers/names/base_controller.rb | 134 +++++++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 app/controllers/names/base_controller.rb diff --git a/app/controllers/names/base_controller.rb b/app/controllers/names/base_controller.rb new file mode 100644 index 00000000..3e50c247 --- /dev/null +++ b/app/controllers/names/base_controller.rb @@ -0,0 +1,134 @@ +# frozen_string_literal: true + +# Base controller for Names-related controllers. +# Contains shared logic, before_actions, and private methods. +class Names::BaseController < ApplicationController + before_action :set_tutorial + + # Authentication and authorization methods + before_action :authenticate_user!, only: %i[observe unobserve observing] + before_action :authenticate_contributor!, only: %i[new create claim] + before_action :authenticate_admin!, only: %i[demote temporary_editable] + before_action :authenticate_curator!, only: %i[unranked unknown_proposal submitted endorsed draft return validate endorse edit_redirect] + before_action :authenticate_owner_or_curator!, only: %i[unclaim new_correspondence transfer_user transfer_user_commit] + before_action :authenticate_can_edit!, only: %i[edit destroy proposed_in not_validly_proposed_in emended_in assigned_in corrigendum_in corrigendum_orphan corrigendum edit_description edit_rank edit_notes edit_etymology autofill_etymology edit_parent] + before_action :authenticate_can_edit_type!, only: %i[edit_type] + before_action :authenticate_can_edit_validated!, only: %i[update edit_links] + + # Setup methods + before_action :set_name_and_notifications, only: %i[show] + before_action :set_name, only: %i[edit update destroy network wiki proposed_in not_validly_proposed_in emended_in assigned_in corrigendum_in corrigendum_orphan corrigendum edit_description edit_rank edit_notes edit_etymology edit_links edit_type edit_redirect autofill_etymology edit_parent return validate endorse claim unclaim demote temporary_editable transfer_user transfer_user_commit new_correspondence observe unobserve quality_checks] + + private + + # Use callbacks to share common setup or constraints between actions + def set_name_and_notifications + if set_name + current_user + &.unseen_notifications + &.where(notifiable: @name) + &.update(seen: true) + end + end + + def set_name + @name = Name.find(params[:id]) + + if @name&.can_view?(current_user, cookies[:reviewer_token]) + @register = @name.try(:register) + true + else + render 'hidden' + false + end + end + + def set_tutorial + return if params[:tutorial].blank? + @tutorial = Tutorial.find(params[:tutorial]) + end + + def authenticate_owner_or_curator! + unless current_user.try(:curator?) || @name.user?(current_user) + flash[:alert] = 'User is not the owner of the name' + redirect_to(@name) + end + end + + def authenticate_can_edit_validated! + unless @name.can_edit_validated?(current_user) + flash[:alert] = 'User cannot edit this aspect of the name' + redirect_to(@name) + end + end + + def authenticate_can_edit_type! + unless @name.can_edit_type?(current_user) + flash[:alert] = 'User cannot edit the nomenclatural type' + redirect_to(@name) + end + end + + def authenticate_can_edit! + unless @name.can_edit?(current_user) + flash[:alert] = 'User cannot edit name' + redirect_to(@name) + end + end + + # Never trust parameters from the scary internet, only allow the white list through + def name_params + fields = [] + if @name.can_edit_validated?(current_user) + fields += %i[ + notes ncbi_taxonomy lpsn_url gtdb_accession algaebase_species + algaebase_taxonomy wikispecies_entry + ] + unless @name.type? + fields += %i[ + nomenclatural_type_type nomenclatural_type_id + nomenclatural_type_entry + ] + end + end + + if @name.can_edit?(current_user) + fields += %i[ + name rank description syllabication syllabication_reviewed + nomenclatural_type_type nomenclatural_type_id nomenclatural_type_entry + etymology_text register genome_strain + ] + etymology_pars + end + + fields << :redirect if current_user.try(:curator?) + + @name_params ||= params.require(:name).permit(*fields.uniq) + end + + def etymology_pars + Name.etymology_particles.map do |i| + Name.etymology_fields.map { |j| :"etymology_#{i}_#{j}" } + end.flatten + end + + def change_status(fun, success_msg, *extra_opts) + if @name.send(fun, *extra_opts) + flash[:notice] = success_msg + else + flash[:alert] = @name.status_alert + end + redirect_to(@name) + rescue ActiveRecord::RecordInvalid => inv + flash['alert'] = + 'An unexpected error occurred while updating the name: ' + + inv.record.errors.map { |e| "#{e.attribute} #{e.message}" }.to_sentence + redirect_to(inv.record) + end + + def add_automatic_correspondence(message) + NameCorrespondence.new( + message: message, notify: '0', automatic: true, + user: current_user, name: @name + ).save + end +end From 8ce1fd05ef6203af8060ab753d4e68baf2ed97d6 Mon Sep 17 00:00:00 2001 From: Luis M Rodriguez-R Date: Thu, 14 May 2026 17:46:22 +0200 Subject: [PATCH 2/5] Push Names:: controllers to the rebased branch --- app/controllers/names/editing_controller.rb | 55 ++++++ app/controllers/names/filtering_controller.rb | 144 ++++++++++++++ app/controllers/names/names_controller.rb | 185 ++++++++++++++++++ app/controllers/names/network_controller.rb | 15 ++ .../names/publications_controller.rb | 71 +++++++ app/controllers/names/status_controller.rb | 45 +++++ .../names/user_actions_controller.rb | 73 +++++++ app/controllers/names/utility_controller.rb | 57 ++++++ app/controllers/names/wiki_controller.rb | 11 ++ 9 files changed, 656 insertions(+) create mode 100644 app/controllers/names/editing_controller.rb create mode 100644 app/controllers/names/filtering_controller.rb create mode 100644 app/controllers/names/names_controller.rb create mode 100644 app/controllers/names/network_controller.rb create mode 100644 app/controllers/names/publications_controller.rb create mode 100644 app/controllers/names/status_controller.rb create mode 100644 app/controllers/names/user_actions_controller.rb create mode 100644 app/controllers/names/utility_controller.rb create mode 100644 app/controllers/names/wiki_controller.rb diff --git a/app/controllers/names/editing_controller.rb b/app/controllers/names/editing_controller.rb new file mode 100644 index 00000000..b20960e8 --- /dev/null +++ b/app/controllers/names/editing_controller.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +# Controller for editing actions on names. +class Names::EditingController < Names::BaseController + # GET /names/1/edit + def edit + end + + # GET /names/1/edit_description + def edit_description + end + + # GET /names/1/edit_notes + def edit_notes + end + + # GET /names/1/edit_rank + def edit_rank + end + + # GET /names/1/edit_links + def edit_links + end + + # GET /names/1/edit_type + def edit_type + unless @name.rank? + flash[:alert] = 'You must define the rank before the type material' + redirect_to(@name) + end + end + + # GET /names/1/edit_redirect + def edit_redirect + end + + # GET /names/1/autofill_etymology + def autofill_etymology + @name.autofill_etymology + render :edit_etymology + end + + # GET /names/1/edit_etymology + def edit_etymology + end + + # GET /names/1/edit_parent + def edit_parent + if @name.placement + redirect_to(edit_placement_path(@name.placement)) + else + redirect_to(new_placement_path(@name)) + end + end +end diff --git a/app/controllers/names/filtering_controller.rb b/app/controllers/names/filtering_controller.rb new file mode 100644 index 00000000..3afe333f --- /dev/null +++ b/app/controllers/names/filtering_controller.rb @@ -0,0 +1,144 @@ +# frozen_string_literal: true + +# Controller for filtering and listing names by status, user, or type. +class Names::FilteringController < Names::BaseController + # GET /names/user + # GET /names/user?user=abc + def user + @user = current_user + if params[:user] && current_user&.admin? + @user = User.find_by(username: params[:user]) + end + params[:status] ||= 'all' + index(where: { created_by: @user }) + render :index + end + + # GET /names/observing + def observing + user = current_user + if params[:user] && current_user.admin? + user = User.find_by(username: params[:user]) + end + @title = 'Names with active alerts' + @status = 'user' + @names = user.observing_names.reverse_order + index + render :index + end + + # GET /names/submitted + def submitted + @submitted = true + @status = 'submitted' + index(status: 10) + render :index + end + + # GET /names/endorsed + def endorsed + @endorsed = true + @status = 'endorsed' + index(status: 12) + render :index + end + + # GET /names/draft + def draft + @draft = true + @status = 'draft' + index(status: 5) + render :index + end + + # GET /names/unranked + def unranked + @names = Name.where(rank: nil).order(created_at: :asc) + @names = @names.paginate(page: params[:page], per_page: 30) + end + + # GET /names/unknown_proposal + def unknown_proposal + @names = Name.where(proposed_in: nil).where('name LIKE ?', 'Candidatus %').order(created_at: :asc) + @names = @names.paginate(page: params[:page], per_page: 30) + end + + # GET /type-genomes + # GET /type-genomes.json + def type_genomes + @names = Name.where(status: 15, nomenclatural_type_type: :Genome) + .reorder(priority_date: :desc, updated_at: :desc) + .paginate(page: params[:page], per_page: 50) + @crumbs = [['Genomes', genomes_path], 'Type'] + end + + private + + # Reuse the index logic from Names::MainController + def index(opts = {}) + @user ||= nil + @submitted ||= false + @endorsed ||= false + @draft ||= false + @sort ||= params[:sort] || 'date' + @status ||= params[:status] || 'public' + @status = 'ICNafp' if @status == 'ICN' + @title ||= [ + @status == 'all' ? nil : @status.gsub(/^\S/, &:upcase), + 'Names', + @user.present? ? "by #{@user.username}" : nil + ].compact.join(' ') + opts[:rank] = params[:rank] if params[:rank].present? + + opts[:status] ||= + case @status.to_s.downcase + when 'public'; Name.public_status + when 'automated'; 0 + when 'seqcode'; 15 + when 'icnp'; 20 + when 'icnafp'; 25 + when 'valid'; Name.valid_status + end + + @names ||= + case @sort.to_s.downcase + when 'date' + if opts[:status] == 15 + Name.order(validated_at: :desc) + else + Name.order(created_at: :desc) + end + when 'citations' + Name + .left_joins(:publication_names).group(:id) + .order('COUNT(publication_names.id) DESC') + else + @sort = 'alphabetically' + Name.order(name: :asc) + end + @names = @names.where(redirect: nil) + @names = @names.where(status: opts[:status]) if opts[:status] + @names = @names.where(rank: opts[:rank]) if opts[:rank] + @names = @names.where(opts[:where]) if opts[:where] + @names = @names.paginate(page: params[:page], per_page: 30) + + @count = @names.count + @count = @count.size if @count.is_a? Hash + @crumbs = [['Names', names_path]] + if @user.present? + bu = "by #{@user.username}" + if @status == 'all' + @crumbs << bu + else + @crumbs << [bu, names_path(user: @user.username)] + @crumbs << @status.gsub(/^\S/, &:upcase) + end + else + if @status == 'public' + @crumbs[0] = 'Names' + else + @crumbs << @status.gsub(/^\S/, &:upcase) + end + end + end +end diff --git a/app/controllers/names/names_controller.rb b/app/controllers/names/names_controller.rb new file mode 100644 index 00000000..ef4c43f1 --- /dev/null +++ b/app/controllers/names/names_controller.rb @@ -0,0 +1,185 @@ +# frozen_string_literal: true + +# Controller for core CRUD operations on Names. +class Names::NamesController < Names::BaseController + # GET /names/autocomplete.json?q=Maco + # GET /names/autocomplete.json?q=Allo&rank=genus + def autocomplete + name = params[:q].downcase + rank = params[:rank]&.downcase + @names = + Name.where('LOWER(name) LIKE ?', "#{name}%") + .or(Name.where('LOWER(name) LIKE ?', "% #{name}%")) + .limit(20) + @names = @names.where(rank: rank) if rank + end + + # GET /names + # GET /names.json + def index(opts = {}) + return user if params[:user].present? && !opts[:where].present? + + @user ||= nil + @submitted ||= false + @endorsed ||= false + @draft ||= false + @sort ||= params[:sort] || 'date' + @status ||= params[:status] || 'public' + @status = 'ICNafp' if @status == 'ICN' + @title ||= [ + @status == 'all' ? nil : @status.gsub(/^\S/, &:upcase), + 'Names', + @user.present? ? "by #{@user.username}" : nil + ].compact.join(' ') + opts[:rank] = params[:rank] if params[:rank].present? + + opts[:status] ||= + case @status.to_s.downcase + when 'public'; Name.public_status + when 'automated'; 0 + when 'seqcode'; 15 + when 'icnp'; 20 + when 'icnafp'; 25 + when 'valid'; Name.valid_status + end + + @names ||= + case @sort.to_s.downcase + when 'date' + if opts[:status] == 15 + Name.order(validated_at: :desc) + else + Name.order(created_at: :desc) + end + when 'citations' + Name + .left_joins(:publication_names).group(:id) + .order('COUNT(publication_names.id) DESC') + else + @sort = 'alphabetically' + Name.order(name: :asc) + end + @names = @names.where(redirect: nil) + @names = @names.where(status: opts[:status]) if opts[:status] + @names = @names.where(rank: opts[:rank]) if opts[:rank] + @names = @names.where(opts[:where]) if opts[:where] + @names = @names.paginate(page: params[:page], per_page: 30) + + @count = @names.count + @count = @count.size if @count.is_a? Hash + @crumbs = [['Names', names_path]] + if @user.present? + bu = "by #{@user.username}" + if @status == 'all' + @crumbs << bu + else + @crumbs << [bu, names_path(user: @user.username)] + @crumbs << @status.gsub(/^\S/, &:upcase) + end + else + if @status == 'public' + @crumbs[0] = 'Names' + else + @crumbs << @status.gsub(/^\S/, &:upcase) + end + end + end + + # GET /names/1 + # GET /names/1.json + # GET /names/1.pdf + def show + if @name.redirect.present? && !params[:no_redirect] + flash[:info] = 'Redirected from ' + @name.name + redirect_to(name_url(@name.redirect, format: params[:format])) + return + end + + @publication_names = + @name.publication_names_ordered + .paginate(page: params[:page], per_page: 10) + @oldest_publication = @name.publications.last + @crumbs = [['Names', names_path], @name.abbr_name] + respond_to do |format| + format.html + format.json + format.pdf do + response.set_header('Link', '<%s>; rel="canonical"' % url_for(@name)) + render( + template: 'names/show_pdf.html.erb', + pdf: "#{@name.name} | SeqCode Registry", + header: { html: { template: 'layouts/_pdf_header' } }, + footer: { html: { template: 'layouts/_pdf_footer' } }, + page_size: 'A4' + ) + end + end + end + + # GET /names/new + def new + @name = Name.new + end + + # GET /names/1/edit + def edit + end + + # POST /names + # POST /names.json + def create + @name = Name.new(status: 5, created_by: current_user) + @name.assign_attributes(name_params) + + respond_to do |format| + if @name.save + @name.add_observer(current_user) + format.html { redirect_to @name, notice: 'Name was successfully created' } + format.json { render :show, status: :created, location: @name } + else + format.html { render :new } + format.json { render json: @name.errors, status: :unprocessable_entity } + end + end + end + + # PATCH/PUT /names/1 + # PATCH/PUT /names/1.json + def update + name_params[:syllabication_reviewed] = true if name_params[:syllabication] + name_params[:register] = nil if name_params[:register]&.==('') + + if name_params[:nomenclatural_type_type]&.==('Name') + name_params[:genome_strain] = nil + end + + if params[:edit].==('redirect') + if name_params[:redirect].present? + name_params[:redirect] = Name.find_by(name: name_params[:redirect]) + name_params[:status] = @name.status <= 15 ? 0 : @name.status + else + name_params[:redirect] = nil + end + end + + respond_to do |format| + if @name.update(name_params) + format.html { redirect_to(params[:return_to] || @name, notice: 'Name was successfully updated') } + format.json { render(:show, status: :ok, location: @name) } + else + format.html { render(name_params[:name] ? :edit : :edit_etymology) } + format.json { render(json: @name.errors, status: :unprocessable_entity) } + end + end + end + + # DELETE /names/1 + # DELETE /names/1.json + def destroy + @name.destroy + respond_to do |format| + format.html { redirect_to names_url, notice: 'Name was successfully destroyed' } + format.json { head :no_content } + end + end +end diff --git a/app/controllers/names/network_controller.rb b/app/controllers/names/network_controller.rb new file mode 100644 index 00000000..e3f088ad --- /dev/null +++ b/app/controllers/names/network_controller.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +# Controller for network-related actions on names. +class Names::NetworkController < Names::BaseController + # GET /names/1/network + def network + respond_to do |format| + format.html + format.json do + @nodes = @name.network_nodes + @edges = @name.network_edges + end + end + end +end diff --git a/app/controllers/names/publications_controller.rb b/app/controllers/names/publications_controller.rb new file mode 100644 index 00000000..e974cdb5 --- /dev/null +++ b/app/controllers/names/publications_controller.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +# Controller for publication-related actions on names. +class Names::PublicationsController < Names::BaseController + # POST /names/1/proposed_in/2 + # POST /names/1/proposed_in/2?not=true + def proposed_in + @publication = + params[:not] ? nil : Publication.where(id: params[:publication_id]).first + @name.update(proposed_in: @publication) + redirect_back(fallback_location: @name) + end + + # POST /names/1/not_validly_proposed_in/2 + # POST /names/1/not_validly_proposed_in/2?not=true + def not_validly_proposed_in + @name.publication_names + .where(publication_id: params[:publication_id]) + .update(not_valid_proposal: !params[:not]) + redirect_back(fallback_location: @name) + end + + # GET /names/1/corrigendum_in + # GET /names/1/corrigendum_in?publication_id=2 + def corrigendum_in + @corrigendum_in_old = @name.corrigendum_in + @publication = Publication.where(id: params[:publication_id]).first + @name.corrigendum_in = @publication + end + + # POST /names/1/assigned_in/2 + # POST /names/1/assigned_in/2?not=true + def assigned_in + @publication = + params[:not] ? nil : Publication.where(id: params[:publication_id]).first + @name.update(assigned_in: @publication) + @name.placement.try(:update, publication: @publication) + redirect_back(fallback_location: @name) + end + + # POST /names/1/corrigendum + def corrigendum + par = params[:delete_corrigenda] ? + { corrigendum_in_id: nil, corrigendum_from: nil } : + params.require(:name).permit( + :name, :corrigendum_in_id, :corrigendum_from, :corrigendum_kind + ) + if @name.update(par) + flash[:notice] = params[:delete_corrigenda] ? + 'Corrigendum removed successfully' : + 'Corrigendum successfully registered' + redirect_to(name_path(@name)) + elsif params[:delete_corrigenda] + flash[:alert] = 'Corrigendum could not be removed' + redirect_to(name_path(@name)) + else + flash.now[:alert] = 'There was an issue registering the corrigendum' + params[:publication_id] = par[:corrigendum_in_id] + render :corrigendum_in + end + end + + # POST /names/1/emended_in/2 + # POST /names/1/emended_in/2?not=true + def emended_in + @name.publication_names + .where(publication_id: params[:publication_id]) + .update(emends: !params[:not]) + redirect_back(fallback_location: @name) + end +end diff --git a/app/controllers/names/status_controller.rb b/app/controllers/names/status_controller.rb new file mode 100644 index 00000000..e520326d --- /dev/null +++ b/app/controllers/names/status_controller.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +# Controller for status-related actions on names. +class Names::StatusController < Names::BaseController + # POST /names/1/return + def return + change_status(:return, 'Name returned to author', current_user) + end + + # POST /names/1/validate + def validate + change_status( + :validate, 'Name successfully validated', current_user, params[:code] + ) + end + + # POST /names/1/endorse + def endorse + change_status(:endorse, 'Name successfully endorsed', current_user) + end + + # POST /names/1/claim + def claim + change_status(:claim, 'Name successfully claimed', current_user) + end + + # POST /names/1/unclaim + def unclaim + change_status(:unclaim, 'Name successfully unclaimed', current_user) + end + + # POST /names/1/demote + def demote + change_status(:demote, 'Name successfully demoted', current_user) + end + + # POST /names/1/temporary_editable + def temporary_editable + to_time = DateTime.now + (params[:stop] ? 0 : 10.minutes) + unless @name.update_column(:temporary_editable_at, to_time) + flash[:alert] = 'Impossible to temporary update name' + end + redirect_to(@name) + end +end diff --git a/app/controllers/names/user_actions_controller.rb b/app/controllers/names/user_actions_controller.rb new file mode 100644 index 00000000..dfde74c2 --- /dev/null +++ b/app/controllers/names/user_actions_controller.rb @@ -0,0 +1,73 @@ +# frozen_string_literal: true + +# Controller for user-related actions on names. +class Names::UserActionsController < Names::BaseController + # GET /names/1/transfer_user + def transfer_user + end + + # POST /names/1/transfer_user + def transfer_user_commit + old_user = @name.user + username = params.require(:name)[:user] + user = User.find_by_email_or_username(username) + + if !user.present? + flash[:alert] = 'The user could not be found' + render :transfer_user + elsif @name.transfer(current_user, user) + add_automatic_correspondence( + 'SeqCode Register transferred from %s to %s' % [ + old_user&.username, user&.username + ] + ) + flash[:notice] = + 'Name successfully transferred to the new user' + redirect_to(@name) + else + flash[:alert] = + 'The list has not been transferred due to a failed check: ' + + @name.status_alert + render :transfer_user + end + end + + # GET /names/1/observe + def observe + @name.add_observer(current_user) + if params[:from] && RedirectSafely.safe?(params[:from]) + redirect_to(params[:from]) + else + redirect_back(fallback_location: @name) + end + end + + # GET /names/1/unobserve + def unobserve + @name.observers.delete(current_user) + if params[:from] && RedirectSafely.safe?(params[:from]) + redirect_to(params[:from]) + else + redirect_back(fallback_location: @name) + end + end + + # POST /names/1/new_correspondence + def new_correspondence + @name_correspondence = NameCorrespondence.new( + params.require(:name_correspondence).permit(:message, :notify) + ) + unless @name_correspondence.message.empty? + @name_correspondence.user = current_user + @name_correspondence.name = @name + if @name_correspondence.save + @name.add_observer(current_user) + @name.register.try(:unsnooze_curation!) + flash[:notice] = 'Correspondence recorded' + else + flash[:alert] = 'An unexpected error occurred with the correspondence' + end + end + redirect_to(@tutorial || @name) + end +end diff --git a/app/controllers/names/utility_controller.rb b/app/controllers/names/utility_controller.rb new file mode 100644 index 00000000..04cfef00 --- /dev/null +++ b/app/controllers/names/utility_controller.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +# Controller for utility actions on names. +class Names::UtilityController < Names::BaseController + # GET /names/autocomplete.json?q=Maco + # GET /names/autocomplete.json?q=Allo&rank=genus + def autocomplete + name = params[:q].downcase + rank = params[:rank]&.downcase + @names = + Name.where('LOWER(name) LIKE ?', "#{name}%") + .or(Name.where('LOWER(name) LIKE ?', "% #{name}%")) + .limit(20) + @names = @names.where(rank: rank) if rank + end + + # GET /names/linkout.xml + # GET /names/1/linkout.xml + def linkout + @provider_id = Rails.configuration.try(:linkout_provider_id) + unless @provider_id.present? + @provider_id = 'Define using config.linkout_provider_id' + end + + @names = + params[:id] ? + Name.where(id: params[:id]) : + Name.where('status >= 15') + .where.not(ncbi_taxonomy: nil) + .order(created_at: :asc) + @names = + @names.paginate( + page: params[:page] || 1, per_page: params[:per_page] || 10 + ) + end + + # GET /names/etymology_sandbox + def etymology_sandbox + @name = Name.new(name: params[:name] || '') + end + + # GET /names/syllabify?name=Abc + def syllabify + @name = Name.new(name: params[:name] || '') + @syllabification = @name.guess_syllabication + end + + # GET /names/1/quality_checks + def quality_checks + @crumbs = [ + ['Names', names_path], + [@name.abbr_name, @name], + 'Quality Checks' + ] + render('quality_checks', layout: !params[:content].present?) + end +end diff --git a/app/controllers/names/wiki_controller.rb b/app/controllers/names/wiki_controller.rb new file mode 100644 index 00000000..f004879d --- /dev/null +++ b/app/controllers/names/wiki_controller.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +# Controller for wiki-related actions on names. +class Names::WikiController < Names::BaseController + # GET /names/1/wiki + def wiki + @crumbs = [['Names', names_path], [@name.abbr_name, @name], 'Wiki source'] + @name.check_wikispecies if current_user # Force re-check for logged users + @name_page = :wiki + end +end From a176678659fd36b81798a29f17daafe3b5c0616b Mon Sep 17 00:00:00 2001 From: Luis M Rodriguez-R Date: Thu, 14 May 2026 17:48:37 +0200 Subject: [PATCH 3/5] Push service objects and documentation to the rebased branch From 9a3c19b34148d341a772b9a6dfa0eda749cf702e Mon Sep 17 00:00:00 2001 From: Luis M Rodriguez-R Date: Thu, 14 May 2026 17:49:55 +0200 Subject: [PATCH 4/5] Push updated routes.rb to the rebased branch --- config/routes.rb | 90 ++++++++++++++++++++++++------------------------ 1 file changed, 45 insertions(+), 45 deletions(-) diff --git a/config/routes.rb b/config/routes.rb index feb29713..b9b4ea11 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -49,63 +49,63 @@ end # Names - # --> Index - get 'type-genomes(.:format)' => 'names#type_genomes', as: :name_type_genomes # --> Standard resources - resources(:names) do + resources(:names, controller: 'names/main') do collection do # --> Index - get :submitted - get :endorsed - get :draft - get :user - get :observing - get :linkout + get :type_genomes, controller: 'names/filtering' + # --> Filtering + get :submitted, controller: 'names/filtering' + get :endorsed, controller: 'names/filtering' + get :draft, controller: 'names/filtering' + get :user, controller: 'names/filtering' + get :observing, controller: 'names/filtering' + get :linkout, controller: 'names/utility' # --> User utilities - get :autocomplete - get :etymology_sandbox - get :syllabify + get :autocomplete, controller: 'names/utility' + get :etymology_sandbox, controller: 'names/utility' + get :syllabify, controller: 'names/utility' # --> Curator utilities - get :unranked - get :unknown_proposal + get :unranked, controller: 'names/filtering' + get :unknown_proposal, controller: 'names/filtering' end member do # --> Display name - get :network - get :linkout - get :wiki - get :quality_checks + get :network, controller: 'names/network' + get :linkout, controller: 'names/utility' + get :wiki, controller: 'names/wiki' + get :quality_checks, controller: 'names/utility' # --> Edit name - get :edit_description - get :edit_type - get :edit_etymology - get :autofill_etymology - get :edit_notes - get :edit_rank - get :edit_links - get :edit_redirect - post :return - post :validate - post :endorse - post :temporary_editable + get :edit_description, controller: 'names/editing' + get :edit_type, controller: 'names/editing' + get :edit_etymology, controller: 'names/editing' + get :autofill_etymology, controller: 'names/editing' + get :edit_notes, controller: 'names/editing' + get :edit_rank, controller: 'names/editing' + get :edit_links, controller: 'names/editing' + get :edit_redirect, controller: 'names/editing' + post :return, controller: 'names/status' + post :validate, controller: 'names/status' + post :endorse, controller: 'names/status' + post :temporary_editable, controller: 'names/status' # --> Edit user relationship to name - get :observe - get :unobserve - post :claim - post :unclaim - post :demote - get :transfer_user - post :transfer_user, action: :transfer_user_commit + get :observe, controller: 'names/user_actions' + get :unobserve, controller: 'names/user_actions' + post :claim, controller: 'names/status' + post :unclaim, controller: 'names/status' + post :demote, controller: 'names/status' + get :transfer_user, controller: 'names/user_actions' + post :transfer_user, action: :transfer_user_commit, controller: 'names/user_actions' # --> Edit name relationships - get :edit_parent - post :new_correspondence - post :proposed_in, path: '/proposed_in/:publication_id' - get :corrigendum_in - post :corrigendum - post :emended_in, path: '/emended_in/:publication_id' - post :assigned_in, path: '/assigned_in/:publication_id' + get :edit_parent, controller: 'names/editing' + post :new_correspondence, controller: 'names/user_actions' + post :proposed_in, path: '/proposed_in/:publication_id', controller: 'names/publications' + get :corrigendum_in, controller: 'names/publications' + post :corrigendum, controller: 'names/publications' + post :emended_in, path: '/emended_in/:publication_id', controller: 'names/publications' + post :assigned_in, path: '/assigned_in/:publication_id', controller: 'names/publications' post :not_validly_proposed_in, - path: '/not_validly_proposed_in/:publication_id' + path: '/not_validly_proposed_in/:publication_id', controller: 'names/publications' end resources :pseudonyms end From 71d09c6356f939eed154c90f37bc0572d0b9747b Mon Sep 17 00:00:00 2001 From: Luis M Rodriguez-R Date: Thu, 14 May 2026 17:50:48 +0200 Subject: [PATCH 5/5] Remove the original names_controller.rb as it has been split into Names:: controllers --- app/controllers/names_controller.rb | 687 ---------------------------- 1 file changed, 687 deletions(-) delete mode 100644 app/controllers/names_controller.rb diff --git a/app/controllers/names_controller.rb b/app/controllers/names_controller.rb deleted file mode 100644 index 545f7f2b..00000000 --- a/app/controllers/names_controller.rb +++ /dev/null @@ -1,687 +0,0 @@ -class NamesController < ApplicationController - before_action(:set_tutorial) - before_action(:set_name_and_notifications, only: %i[show]) - before_action( - :set_name, - only: %i[ - edit update destroy network wiki - proposed_in not_validly_proposed_in emended_in assigned_in - corrigendum_in corrigendum_orphan corrigendum - edit_description edit_rank edit_notes edit_etymology edit_links edit_type - edit_redirect autofill_etymology edit_parent - return validate endorse claim unclaim demote temporary_editable - transfer_user transfer_user_commit - new_correspondence observe unobserve quality_checks - ] - ) - before_action( - :authenticate_can_edit!, - only: %i[ - edit destroy - proposed_in not_validly_proposed_in emended_in assigned_in - corrigendum_in corrigendum_orphan corrigendum - edit_description edit_rank edit_notes edit_etymology - autofill_etymology edit_parent - ] - ) - before_action(:authenticate_can_edit_type!, only: [:edit_type]) - before_action( - :authenticate_owner_or_curator!, - only: %i[unclaim new_correspondence transfer_user transfer_user_commit] - ) - before_action(:authenticate_contributor!, only: %i[new create claim]) - before_action(:authenticate_admin!, only: %i[demote temporary_editable]) - before_action( - :authenticate_curator!, - only: %i[ - unranked unknown_proposal submitted endorsed draft - return validate endorse edit_redirect - ] - ) - before_action(:authenticate_user!, only: %i[observe unobserve observing]) - before_action(:authenticate_can_edit_validated!, only: %i[update edit_links]) - - # GET /names/autocomplete.json?q=Maco - # GET /names/autocomplete.json?q=Allo&rank=genus - def autocomplete - name = params[:q].downcase - rank = params[:rank]&.downcase - @names = - Name.where('LOWER(name) LIKE ?', "#{name}%") - .or(Name.where('LOWER(name) LIKE ?', "% #{name}%")) - .limit(20) - @names = @names.where(rank: rank) if rank - end - - # GET /names - # GET /names.json - def index(opts = {}) - return user if params[:user].present? && !opts[:where].present? - - @user ||= nil - @submitted ||= false - @endorsed ||= false - @draft ||= false - @sort ||= params[:sort] || 'date' - @status ||= params[:status] || 'public' - @status = 'ICNafp' if @status == 'ICN' - @title ||= [ - @status == 'all' ? nil : @status.gsub(/^\S/, &:upcase), - 'Names', - @user.present? ? "by #{@user.username}" : nil - ].compact.join(' ') - opts[:rank] = params[:rank] if params[:rank].present? - - opts[:status] ||= - case @status.to_s.downcase - when 'public'; Name.public_status - when 'automated'; 0 - when 'seqcode'; 15 - when 'icnp'; 20 - when 'icnafp'; 25 - when 'valid'; Name.valid_status - end - - @names ||= - case @sort.to_s.downcase - when 'date' - if opts[:status] == 15 - Name.order(validated_at: :desc) - else - Name.order(created_at: :desc) - end - when 'citations' - Name - .left_joins(:publication_names).group(:id) - .order('COUNT(publication_names.id) DESC') - else - @sort = 'alphabetically' - Name.order(name: :asc) - end - @names = @names.where(redirect: nil) - @names = @names.where(status: opts[:status]) if opts[:status] - @names = @names.where(rank: opts[:rank]) if opts[:rank] - @names = @names.where(opts[:where]) if opts[:where] - @names = @names.paginate(page: params[:page], per_page: 30) - - @count = @names.count - @count = @count.size if @count.is_a? Hash - @crumbs = [['Names', names_path]] - if @user.present? - bu = "by #{@user.username}" - if @status == 'all' - @crumbs << bu - else - @crumbs << [bu, names_path(user: @user.username)] - @crumbs << @status.gsub(/^\S/, &:upcase) - end - else - if @status == 'public' - @crumbs[0] = 'Names' - else - @crumbs << @status.gsub(/^\S/, &:upcase) - end - end - end - - # GET /type-genomes - # GET /type-genomes.json - def type_genomes - @names = Name.where(status: 15, nomenclatural_type_type: :Genome) - .reorder(priority_date: :desc, updated_at: :desc) - .paginate(page: params[:page], per_page: 50) - @crumbs = [['Genomes', genomes_path], 'Type'] - end - - # GET /names/user - # GET /names/user?user=abc - def user - @user = current_user - if params[:user] && current_user&.admin? - @user = User.find_by(username: params[:user]) - end - params[:status] ||= 'all' - index(where: { created_by: @user }) - render(:index) - end - - # GET /names/observing - def observing - user = current_user - if params[:user] && current_user.admin? - user = User.find_by(username: params[:user]) - end - @title = 'Names with active alerts' - @status = 'user' - @names = user.observing_names.reverse_order - index - render(:index) - end - - # GET /names/submitted - def submitted - @submitted = true - @status = 'submitted' - index(status: 10) - render(:index) - end - - # GET /names/endorsed - def endorsed - @endorsed = true - @status = 'endorsed' - index(status: 12) - render(:index) - end - - # GET /names/draft - def draft - @draft = true - @status = 'draft' - index(status: 5) - render(:index) - end - - # GET /names/etymology_sandbox - def etymology_sandbox - @name = Name.new(name: params[:name] || '') - end - - # GET /names/syllabify?name=Abc - def syllabify - @name = Name.new(name: params[:name] || '') - @syllabification = @name.guess_syllabication - end - - # GET /names/1 - # GET /names/1.json - # GET /names/1.pdf - def show - if @name.redirect.present? && !params[:no_redirect] - flash[:info] = 'Redirected from ' + @name.name - redirect_to(name_url(@name.redirect, format: params[:format])) - return - end - - @publication_names = - @name.publication_names_ordered - .paginate(page: params[:page], per_page: 10) - @oldest_publication = @name.publications.last - @crumbs = [['Names', names_path], @name.abbr_name] - respond_to do |format| - format.html - format.json - format.pdf do - response.set_header('Link', '<%s>; rel="canonical"' % url_for(@name)) - render( - template: 'names/show_pdf.html.erb', - pdf: "#{@name.name} | SeqCode Registry", - header: { html: { template: 'layouts/_pdf_header' } }, - footer: { html: { template: 'layouts/_pdf_footer' } }, - page_size: 'A4' - ) - end - end - end - - # GET /names/linkout.xml - # GET /names/1/linkout.xml - def linkout - @provider_id = Rails.configuration.try(:linkout_provider_id) - unless @provider_id.present? - @provider_id = 'Define using config.linkout_provider_id' - end - - @names = - params[:id] ? - Name.where(id: params[:id]) : - Name.where('status >= 15') - .where.not(ncbi_taxonomy: nil) - .order(created_at: :asc) - @names = - @names.paginate( - page: params[:page] || 1, per_page: params[:per_page] || 10 - ) - end - - # GET /names/1/network - def network - respond_to do |format| - format.html - format.json do - @nodes = @name.network_nodes - @edges = @name.network_edges - end - end - end - - # GET /names/1/wiki - def wiki - @crumbs = [['Names', names_path], [@name.abbr_name, @name], 'Wiki source'] - @name.check_wikispecies if current_user # Force re-check for logged users - @name_page = :wiki - end - - # GET /names/new - def new - @name = Name.new - end - - # GET /names/1/edit - def edit - end - - # GET /names/1/edit_description - def edit_description - end - - # GET /names/1/edit_notes - def edit_notes - end - - # GET /names/1/edit_rank - def edit_rank - end - - # GET /names/1/edit_links - def edit_links - end - - # GET /names/1/edit_type - def edit_type - unless @name.rank? - flash[:alert] = 'You must define the rank before the type material' - redirect_to(@name) - end - end - - # GET /names/1/edit_redirect - def edit_redirect - end - - # GET /names/1/autofill_etymology - def autofill_etymology - @name.autofill_etymology - render(:edit_etymology) - end - - # POST /names - # POST /names.json - def create - @name = Name.new(status: 5, created_by: current_user) - @name.assign_attributes(name_params) - - respond_to do |format| - if @name.save - @name.add_observer(current_user) - format.html { redirect_to @name, notice: 'Name was successfully created' } - format.json { render :show, status: :created, location: @name } - else - format.html { render :new } - format.json { render json: @name.errors, status: :unprocessable_entity } - end - end - end - - # PATCH/PUT /names/1 - # PATCH/PUT /names/1.json - def update - name_params[:syllabication_reviewed] = true if name_params[:syllabication] - name_params[:register] = nil if name_params[:register]&.==('') - - if name_params[:nomenclatural_type_type]&.==('Name') - name_params[:genome_strain] = nil - end - - if params[:edit].==('redirect') - if name_params[:redirect].present? - name_params[:redirect] = Name.find_by(name: name_params[:redirect]) - name_params[:status] = @name.status <= 15 ? 0 : @name.status - else - name_params[:redirect] = nil - end - end - - respond_to do |format| - if @name.update(name_params) - format.html { redirect_to(params[:return_to] || @name, notice: 'Name was successfully updated') } - format.json { render(:show, status: :ok, location: @name) } - else - format.html { render(name_params[:name] ? :edit : :edit_etymology) } - format.json { render(json: @name.errors, status: :unprocessable_entity) } - end - end - end - - # DELETE /names/1 - # DELETE /names/1.json - def destroy - @name.destroy - respond_to do |format| - format.html { redirect_to names_url, notice: 'Name was successfully destroyed' } - format.json { head :no_content } - end - end - - # GET /names/unranked - def unranked - @names = Name.where(rank: nil).order(created_at: :asc) - @names = @names.paginate(page: params[:page], per_page: 30) - end - - # GET /names/unknown_proposal - def unknown_proposal - @names = Name.where(proposed_in: nil).where('name LIKE ?', 'Candidatus %').order(created_at: :asc) - @names = @names.paginate(page: params[:page], per_page: 30) - end - - # POST /names/1/proposed_in/2 - # POST /names/1/proposed_in/2?not=true - def proposed_in - @publication = - params[:not] ? nil : Publication.where(id: params[:publication_id]).first - @name.update(proposed_in: @publication) - redirect_back(fallback_location: @name) - end - - # POST /names/1/not_validly_proposed_in/2 - # POST /names/1/not_validly_proposed_in/2?not=true - def not_validly_proposed_in - @name.publication_names - .where(publication_id: params[:publication_id]) - .update(not_valid_proposal: !params[:not]) - redirect_back(fallback_location: @name) - end - - # GET /names/1/corrigendum_in - # GET /names/1/corrigendum_in?publication_id=2 - def corrigendum_in - @corrigendum_in_old = @name.corrigendum_in - @publication = Publication.where(id: params[:publication_id]).first - @name.corrigendum_in = @publication - end - - # POST /names/1/assigned_in/2 - # POST /names/1/assigned_in/2?not=true - def assigned_in - @publication = - params[:not] ? nil : Publication.where(id: params[:publication_id]).first - @name.update(assigned_in: @publication) - @name.placement.try(:update, publication: @publication) - redirect_back(fallback_location: @name) - end - - # POST /names/1/corrigendum - def corrigendum - par = params[:delete_corrigenda] ? - { corrigendum_in_id: nil, corrigendum_from: nil } : - params.require(:name).permit( - :name, :corrigendum_in_id, :corrigendum_from, :corrigendum_kind - ) - if @name.update(par) - flash[:notice] = params[:delete_corrigenda] ? - 'Corrigendum removed successfully' : - 'Corrigendum successfully registered' - redirect_to(name_path(@name)) - elsif params[:delete_corrigenda] - flash[:alert] = 'Corrigendum could not be removed' - redirect_to(name_path(@name)) - else - flash.now[:alert] = 'There was an issue registering the corrigendum' - params[:publication_id] = par[:corrigendum_in_id] - render(:corrigendum_in) - end - end - - # POST /names/1/emended_in/2 - # POST /names/1/emended_in/2?not=true - def emended_in - @name.publication_names - .where(publication_id: params[:publication_id]) - .update(emends: !params[:not]) - redirect_back(fallback_location: @name) - end - - # GET /names/1/edit_parent - def edit_parent - if @name.placement - redirect_to(edit_placement_path(@name.placement)) - else - redirect_to(new_placement_path(@name)) - end - end - - # POST /names/1/return - def return - change_status(:return, 'Name returned to author', current_user) - end - - # POST /names/1/validate - def validate - change_status( - :validate, 'Name successfully validated', current_user, params[:code] - ) - end - - # POST /names/1/endorse - def endorse - change_status(:endorse, 'Name successfully endorsed', current_user) - end - - # POST /names/1/claim - def claim - change_status(:claim, 'Name successfully claimed', current_user) - end - - # POST /names/1/unclaim - def unclaim - change_status(:unclaim, 'Name successfully claimed', current_user) - end - - # GET /names/1/transfer_user - def transfer_user - end - - # POST /names/1/transfer_user - def transfer_user_commit - old_user = @name.user - username = params.require(:name)[:user] - user = User.find_by_email_or_username(username) - - if !user.present? - flash[:alert] = 'The user could not be found' - render :transfer_user - elsif @name.transfer(current_user, user) - add_automatic_correspondence( - 'SeqCode Register transferred from %s to %s' % [ - old_user&.username, user&.username - ] - ) - flash[:notice] = - 'Name successfully transferred to the new user' - redirect_to(@name) - else - flash[:alert] = - 'The list has not been transferred due to a failed check: ' + - @name.status_alert - render :transfer_user - end - end - - # POST /names/1/demote - def demote - change_status(:demote, 'Name successfully demoted', current_user) - end - - # POST /names/1/temporary_editable - def temporary_editable - to_time = DateTime.now + (params[:stop] ? 0 : 10.minutes) - unless @name.update_column(:temporary_editable_at, to_time) - flash[:alert] = 'Impossible to temporary update name' - end - redirect_to(@name) - end - - # POST /names/1/new_correspondence - def new_correspondence - @name_correspondence = NameCorrespondence.new( - params.require(:name_correspondence).permit(:message, :notify) - ) - unless @name_correspondence.message.empty? - @name_correspondence.user = current_user - @name_correspondence.name = @name - if @name_correspondence.save - @name.add_observer(current_user) - @name.register.try(:unsnooze_curation!) - flash[:notice] = 'Correspondence recorded' - else - flash[:alert] = 'An unexpected error occurred with the correspondence' - end - end - redirect_to(@tutorial || @name) - end - - # GET /names/1/observe - def observe - @name.add_observer(current_user) - if params[:from] && RedirectSafely.safe?(params[:from]) - redirect_to(params[:from]) - else - redirect_back(fallback_location: @name) - end - end - - # GET /names/1/unobserve - def unobserve - @name.observers.delete(current_user) - if params[:from] && RedirectSafely.safe?(params[:from]) - redirect_to(params[:from]) - else - redirect_back(fallback_location: @name) - end - end - - # GET /names/1/quality_checks - def quality_checks - @crumbs = [ - ['Names', names_path], - [@name.abbr_name, @name], - 'Quality Checks' - ] - render('quality_checks', layout: !params[:content].present?) - end - - private - - # Use callbacks to share common setup or constraints between actions - def set_name_and_notifications - if set_name - current_user - &.unseen_notifications - &.where(notifiable: @name) - &.update(seen: true) - end - end - - def set_name - @name = Name.find(params[:id]) - - if @name&.can_view?(current_user, cookies[:reviewer_token]) - @register = @name.try(:register) - true - else - render 'hidden' - false - end - end - - def set_tutorial - return if params[:tutorial].blank? - @tutorial = Tutorial.find(params[:tutorial]) - end - - def authenticate_owner_or_curator! - unless current_user.try(:curator?) || @name.user?(current_user) - flash[:alert] = 'User is not the owner of the name' - redirect_to(@name) - end - end - - def authenticate_can_edit_validated! - unless @name.can_edit_validated?(current_user) - flash[:alert] = 'User cannot edit this aspect of the name' - redirect_to(@name) - end - end - - def authenticate_can_edit_type! - unless @name.can_edit_type?(current_user) - flash[:alert] = 'User cannot edit the nomenclatural type' - redirect_to(@name) - end - end - - def authenticate_can_edit! - unless @name.can_edit?(current_user) - flash[:alert] = 'User cannot edit name' - redirect_to(@name) - end - end - - # Never trust parameters from the scary internet, only allow the white list - # through - def name_params - fields = [] - if @name.can_edit_validated?(current_user) - fields += %i[ - notes ncbi_taxonomy lpsn_url gtdb_accession algaebase_species - algaebase_taxonomy wikispecies_entry - ] - unless @name.type? - fields += %i[ - nomenclatural_type_type nomenclatural_type_id - nomenclatural_type_entry - ] - end - end - - if @name.can_edit?(current_user) - fields += %i[ - name rank description syllabication syllabication_reviewed - nomenclatural_type_type nomenclatural_type_id nomenclatural_type_entry - etymology_text register genome_strain - ] + etymology_pars - end - - fields << :redirect if current_user.try(:curator?) - - @name_params ||= params.require(:name).permit(*fields.uniq) - end - - def etymology_pars - Name.etymology_particles.map do |i| - Name.etymology_fields.map { |j| :"etymology_#{i}_#{j}" } - end.flatten - end - - def change_status(fun, success_msg, *extra_opts) - if @name.send(fun, *extra_opts) - flash[:notice] = success_msg - else - flash[:alert] = @name.status_alert - end - redirect_to(@name) - rescue ActiveRecord::RecordInvalid => inv - flash['alert'] = - 'An unexpected error occurred while updating the name: ' + - inv.record.errors.map { |e| "#{e.attribute} #{e.message}" }.to_sentence - redirect_to(inv.record) - end - - def add_automatic_correspondence(message) - NameCorrespondence.new( - message: message, notify: '0', automatic: true, - user: current_user, name: @name - ).save - end -end