diff --git a/app/controllers/admin/notes_controller.rb b/app/controllers/admin/notes_controller.rb
new file mode 100644
index 00000000000..4c0310ac55e
--- /dev/null
+++ b/app/controllers/admin/notes_controller.rb
@@ -0,0 +1,53 @@
+class Admin::NotesController < AdminController
+ include Admin::TagHelper
+ include TranslatableParams
+
+ def new
+ @note = Note.new
+ @note.build_all_translations
+ end
+
+ def create
+ @note = Note.new(note_params)
+ if @note.save
+ notice = 'Note successfully created.'
+ redirect_to admin_note_parent_path(@note), notice: notice
+ else
+ @note.build_all_translations
+ render :new
+ end
+ end
+
+ def edit
+ @note = Note.find(params[:id])
+ @note.build_all_translations
+ end
+
+ def update
+ @note = Note.find(params[:id])
+ if @note.update(note_params)
+ notice = 'Note successfully updated.'
+ redirect_to admin_note_parent_path(@note), notice: notice
+ else
+ @note.build_all_translations
+ render :edit
+ end
+ end
+
+ def destroy
+ @note = Note.find(params[:id])
+ @note.destroy
+ notice = 'Note successfully destroyed.'
+ redirect_to admin_note_parent_path(@note), notice: notice
+ end
+
+ private
+
+ def note_params
+ translatable_params(
+ params.require(:note),
+ translated_keys: [:locale, :body],
+ general_keys: [:notable_id, :notable_type]
+ )
+ end
+end
diff --git a/app/models/note.rb b/app/models/note.rb
new file mode 100644
index 00000000000..13ed304fc8c
--- /dev/null
+++ b/app/models/note.rb
@@ -0,0 +1,24 @@
+# == Schema Information
+# Schema version: 20220720085105
+#
+# Table name: notes
+#
+# id :bigint not null, primary key
+# notable_type :string
+# notable_id :bigint
+# notable_tag :string
+# created_at :datetime not null
+# updated_at :datetime not null
+# body :text
+#
+
+class Note < ApplicationRecord
+ include AdminColumn
+
+ translates :body
+ include Translatable
+
+ belongs_to :notable, polymorphic: true
+
+ validates :body, presence: true
+end
diff --git a/app/views/admin/notes/_form.html.erb b/app/views/admin/notes/_form.html.erb
new file mode 100644
index 00000000000..f22f4e245b3
--- /dev/null
+++ b/app/views/admin/notes/_form.html.erb
@@ -0,0 +1,27 @@
+<%= foi_error_messages_for :note %>
+
+
+
+
+
+ <% @note.ordered_translations.each do |translation| %>
+ <% if AlaveteliLocalization.default_locale?(translation.locale) %>
+ <%= fields_for('note', @note) do |t| %>
+ <%= render partial: 'locale_fields', locals: { t: t, locale: translation.locale } %>
+ <% end %>
+ <% else %>
+ <%= f.fields_for(:translations, translation, child_index: translation.locale) do |t| %>
+ <%= render partial: 'locale_fields', locals: { t: t, locale: translation.locale } %>
+ <% end %>
+ <% end %>
+ <% end %>
+
+
diff --git a/app/views/admin/notes/_locale_fields.html.erb b/app/views/admin/notes/_locale_fields.html.erb
new file mode 100644
index 00000000000..ebc0b5dcc17
--- /dev/null
+++ b/app/views/admin/notes/_locale_fields.html.erb
@@ -0,0 +1,16 @@
+
+
+ <%= t.hidden_field :locale, :value => locale %>
+
+ <%= t.label :body, class: 'control-label' %>
+
+ <% if AlaveteliLocalization.default_locale?(locale) && t.object.errors[:body].any? %>
+
+ <% end %>
+ <%= t.text_area :body, class: 'span6', rows: 10 %>
+ <% if AlaveteliLocalization.default_locale?(locale) && t.object.errors[:body].any? %>
+
+ <%end %>
+
+
+
diff --git a/app/views/admin/notes/_note.html.erb b/app/views/admin/notes/_note.html.erb
new file mode 100644
index 00000000000..0607754d93c
--- /dev/null
+++ b/app/views/admin/notes/_note.html.erb
@@ -0,0 +1,24 @@
+
+
+
+ <%= link_to chevron_right, "##{dom_id(note)}", data: { toggle: 'collapse', parent: 'notes' } %>
+ <%= link_to(note.body, edit_admin_note_path(note), title: 'view full details') %>
+
+
+
+ <%= render_tag note.notable_tag if note.notable_tag %>
+
+
+ <%= tag.div id: dom_id(note), class: 'item-detail accordion-body collapse row' do %>
+ <% note.for_admin_column do |name, value, type| %>
+
+
+ <%= name %>
+
+
+ <%= admin_value(value) %>
+
+
+ <% end %>
+ <% end %>
+
diff --git a/app/views/admin/notes/edit.erb b/app/views/admin/notes/edit.erb
new file mode 100644
index 00000000000..0ad11316c60
--- /dev/null
+++ b/app/views/admin/notes/edit.erb
@@ -0,0 +1,21 @@
+
+
+<%= form_for [:admin, @note], class: 'form form-horizontal' do |f| %>
+ <%= render partial: 'form', locals: { f: f } %>
+
+ <%= submit_tag 'Save', class: 'btn btn-success' %>
+
+<% end %>
+
+<%= form_tag [:admin, @note], class: 'form form-inline', method: 'delete' do %>
+ <%= submit_tag 'Destroy note',
+ class: 'btn btn-danger',
+ data: { confirm: 'Are you sure? This is irreversible.' } %>
+ (this is permanent!)
+<% end %>
diff --git a/app/views/admin/notes/new.html.erb b/app/views/admin/notes/new.html.erb
new file mode 100644
index 00000000000..3dce3850c25
--- /dev/null
+++ b/app/views/admin/notes/new.html.erb
@@ -0,0 +1,14 @@
+
+
+<%= form_for [:admin, @note], class: 'form form-horizontal' do |f| %>
+ <%= render partial: 'form', locals: { f: f } %>
+
+ <%= submit_tag 'Create', class: 'btn btn-success' %>
+
+<% end %>
diff --git a/config/routes.rb b/config/routes.rb
index 81b44e1c3f6..0c03b659a62 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -485,6 +485,16 @@
end
####
+ #### AdminNote controller
+ namespace :admin do
+ resources :notes, except: [:index, :show]
+ end
+
+ direct :admin_note_parent do |note|
+ admin_general_index_path
+ end
+ ####
+
#### AdminPublicBody controller
scope '/admin', :as => 'admin' do
resources :bodies,
diff --git a/db/migrate/20220720085105_create_notes.rb b/db/migrate/20220720085105_create_notes.rb
new file mode 100644
index 00000000000..5e50edb3b82
--- /dev/null
+++ b/db/migrate/20220720085105_create_notes.rb
@@ -0,0 +1,19 @@
+class CreateNotes < ActiveRecord::Migration[6.1]
+ def change
+ create_table :notes do |t|
+ t.references :notable, polymorphic: true
+ t.string :notable_tag
+ t.timestamps
+ end
+
+ reversible do |dir|
+ dir.up do
+ Note.create_translation_table!(body: :text)
+ end
+
+ dir.down do
+ Note.drop_translation_table!
+ end
+ end
+ end
+end
diff --git a/spec/controllers/admin/notes_controller_spec.rb b/spec/controllers/admin/notes_controller_spec.rb
new file mode 100644
index 00000000000..6e5134004bd
--- /dev/null
+++ b/spec/controllers/admin/notes_controller_spec.rb
@@ -0,0 +1,153 @@
+require 'spec_helper'
+
+RSpec.describe Admin::NotesController do
+ before(:each) { basic_auth_login(@request) }
+
+ describe 'GET new' do
+ before { get :new }
+
+ it 'returns a successful response' do
+ expect(response).to be_successful
+ end
+
+ it 'assigns the note' do
+ expect(assigns[:note]).to be_a(Note)
+ end
+
+ it 'renders the correct template' do
+ expect(response).to render_template(:new)
+ end
+ end
+
+ describe 'POST #create' do
+ before do
+ post :create, params: params
+ end
+
+ context 'on a successful create' do
+ let(:params) do
+ { note: { body: 'New body' } }
+ end
+
+ it 'assigns the note' do
+ expect(assigns[:note]).to be_a(Note)
+ end
+
+ it 'creates the note' do
+ expect(assigns[:note].body).to eq('New body')
+ end
+
+ it 'sets a notice' do
+ expect(flash[:notice]).to eq('Note successfully created.')
+ end
+
+ it 'redirects to the general index' do
+ expect(response).to redirect_to(admin_general_index_path)
+ end
+ end
+
+ context 'on an unsuccessful create' do
+ let(:params) do
+ { note: { body: '' } }
+ end
+
+ it 'assigns the note' do
+ expect(assigns[:note]).to be_a(Note)
+ end
+
+ it 'does not create the note' do
+ expect(assigns[:note]).to be_new_record
+ end
+
+ it 'renders the form again' do
+ expect(response).to render_template(:new)
+ end
+ end
+ end
+
+ describe 'GET edit' do
+ let!(:note) { FactoryBot.create(:note) }
+
+ before { get :edit, params: { id: note.id } }
+
+ it 'returns a successful response' do
+ expect(response).to be_successful
+ end
+
+ it 'assigns the note' do
+ expect(assigns[:note]).to eq(note)
+ end
+
+ it 'renders the correct template' do
+ expect(response).to render_template(:edit)
+ end
+ end
+
+ describe 'PATCH #update' do
+ let!(:note) { FactoryBot.create(:note) }
+
+ before do
+ patch :update, params: params
+ end
+
+ context 'on a successful update' do
+ let(:params) do
+ { id: note.id, note: { body: 'New body' } }
+ end
+
+ it 'assigns the note' do
+ expect(assigns[:note]).to eq(note)
+ end
+
+ it 'updates the note' do
+ expect(note.reload.body).to eq('New body')
+ end
+
+ it 'sets a notice' do
+ expect(flash[:notice]).to eq('Note successfully updated.')
+ end
+
+ it 'redirects to the general index' do
+ expect(response).to redirect_to(admin_general_index_path)
+ end
+ end
+
+ context 'on an unsuccessful update' do
+ let(:params) do
+ { id: note.id, note: { body: '' } }
+ end
+
+ it 'assigns the note' do
+ expect(assigns[:note]).to eq(note)
+ end
+
+ it 'does not update the note' do
+ expect(note.reload.body).not_to be_blank
+ end
+
+ it 'renders the form again' do
+ expect(response).to render_template(:edit)
+ end
+ end
+ end
+
+ describe 'DELETE #destroy' do
+ let!(:note) { FactoryBot.create(:note) }
+
+ it 'destroys the note' do
+ allow(Note).to receive(:find).and_return(note)
+ expect(note).to receive(:destroy)
+ delete :destroy, params: { id: note.id }
+ end
+
+ it 'sets a notice' do
+ delete :destroy, params: { id: note.id }
+ expect(flash[:notice]).to eq('Note successfully destroyed.')
+ end
+
+ it 'redirects to the general index' do
+ delete :destroy, params: { id: note.id }
+ expect(response).to redirect_to(admin_general_index_path)
+ end
+ end
+end
diff --git a/spec/factories/notes.rb b/spec/factories/notes.rb
new file mode 100644
index 00000000000..291dc8a5421
--- /dev/null
+++ b/spec/factories/notes.rb
@@ -0,0 +1,23 @@
+# == Schema Information
+# Schema version: 20220720085105
+#
+# Table name: notes
+#
+# id :bigint not null, primary key
+# notable_type :string
+# notable_id :bigint
+# notable_tag :string
+# created_at :datetime not null
+# updated_at :datetime not null
+# body :text
+#
+
+FactoryBot.define do
+ factory :note do
+ body { 'Test note' }
+
+ trait :for_public_body do
+ association :notable, factory: :public_body
+ end
+ end
+end
diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb
new file mode 100644
index 00000000000..952605250fc
--- /dev/null
+++ b/spec/models/note_spec.rb
@@ -0,0 +1,48 @@
+# == Schema Information
+# Schema version: 20220720085105
+#
+# Table name: notes
+#
+# id :bigint not null, primary key
+# notable_type :string
+# notable_id :bigint
+# notable_tag :string
+# created_at :datetime not null
+# updated_at :datetime not null
+# body :text
+#
+
+require 'spec_helper'
+
+RSpec.describe Note, type: :model do
+ let(:note) { FactoryBot.build(:note) }
+
+ describe 'validations' do
+ specify { expect(note).to be_valid }
+
+ it 'requires body' do
+ note.body = nil
+ expect(note).not_to be_valid
+ end
+ end
+
+ describe 'translations' do
+ before { note.save! }
+
+ it 'adds translated body' do
+ expect(note.body_translations).to_not include(es: 'body')
+ AlaveteliLocalization.with_locale(:es) { note.body = 'body' }
+ expect(note.body_translations).to include(es: 'body')
+ end
+ end
+
+ describe 'associations' do
+ context 'when info request cited' do
+ let(:note) { FactoryBot.build(:note, :for_public_body) }
+
+ it 'belongs to a public body via polymorphic notable' do
+ expect(note.notable).to be_a PublicBody
+ end
+ end
+ end
+end