diff --git a/Gemfile.lock b/Gemfile.lock
index f33419243c46e98e47e526a7d42666346b68aee4..efa92a34d47faccb9f7145389826952d43e179f8 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -28,35 +28,35 @@ GIT
 GEM
   remote: https://rubygems.org/
   specs:
-    actioncable (7.1.2)
-      actionpack (= 7.1.2)
-      activesupport (= 7.1.2)
+    actioncable (7.1.3)
+      actionpack (= 7.1.3)
+      activesupport (= 7.1.3)
       nio4r (~> 2.0)
       websocket-driver (>= 0.6.1)
       zeitwerk (~> 2.6)
-    actionmailbox (7.1.2)
-      actionpack (= 7.1.2)
-      activejob (= 7.1.2)
-      activerecord (= 7.1.2)
-      activestorage (= 7.1.2)
-      activesupport (= 7.1.2)
+    actionmailbox (7.1.3)
+      actionpack (= 7.1.3)
+      activejob (= 7.1.3)
+      activerecord (= 7.1.3)
+      activestorage (= 7.1.3)
+      activesupport (= 7.1.3)
       mail (>= 2.7.1)
       net-imap
       net-pop
       net-smtp
-    actionmailer (7.1.2)
-      actionpack (= 7.1.2)
-      actionview (= 7.1.2)
-      activejob (= 7.1.2)
-      activesupport (= 7.1.2)
+    actionmailer (7.1.3)
+      actionpack (= 7.1.3)
+      actionview (= 7.1.3)
+      activejob (= 7.1.3)
+      activesupport (= 7.1.3)
       mail (~> 2.5, >= 2.5.4)
       net-imap
       net-pop
       net-smtp
       rails-dom-testing (~> 2.2)
-    actionpack (7.1.2)
-      actionview (= 7.1.2)
-      activesupport (= 7.1.2)
+    actionpack (7.1.3)
+      actionview (= 7.1.3)
+      activesupport (= 7.1.3)
       nokogiri (>= 1.8.5)
       racc
       rack (>= 2.2.4)
@@ -64,15 +64,15 @@ GEM
       rack-test (>= 0.6.3)
       rails-dom-testing (~> 2.2)
       rails-html-sanitizer (~> 1.6)
-    actiontext (7.1.2)
-      actionpack (= 7.1.2)
-      activerecord (= 7.1.2)
-      activestorage (= 7.1.2)
-      activesupport (= 7.1.2)
+    actiontext (7.1.3)
+      actionpack (= 7.1.3)
+      activerecord (= 7.1.3)
+      activestorage (= 7.1.3)
+      activesupport (= 7.1.3)
       globalid (>= 0.6.0)
       nokogiri (>= 1.8.5)
-    actionview (7.1.2)
-      activesupport (= 7.1.2)
+    actionview (7.1.3)
+      activesupport (= 7.1.3)
       builder (~> 3.1)
       erubi (~> 1.11)
       rails-dom-testing (~> 2.2)
@@ -82,25 +82,25 @@ GEM
       activemodel (>= 5.2.0)
       activestorage (>= 5.2.0)
       activesupport (>= 5.2.0)
-    activejob (7.1.2)
-      activesupport (= 7.1.2)
+    activejob (7.1.3)
+      activesupport (= 7.1.3)
       globalid (>= 0.3.6)
-    activemodel (7.1.2)
-      activesupport (= 7.1.2)
-    activerecord (7.1.2)
-      activemodel (= 7.1.2)
-      activesupport (= 7.1.2)
+    activemodel (7.1.3)
+      activesupport (= 7.1.3)
+    activerecord (7.1.3)
+      activemodel (= 7.1.3)
+      activesupport (= 7.1.3)
       timeout (>= 0.4.0)
-    activestorage (7.1.2)
-      actionpack (= 7.1.2)
-      activejob (= 7.1.2)
-      activerecord (= 7.1.2)
-      activesupport (= 7.1.2)
+    activestorage (7.1.3)
+      actionpack (= 7.1.3)
+      activejob (= 7.1.3)
+      activerecord (= 7.1.3)
+      activesupport (= 7.1.3)
       marcel (~> 1.0)
     activestorage-scaleway-service (1.0.1)
       activestorage
       aws-sdk-s3
-    activesupport (7.1.2)
+    activesupport (7.1.3)
       base64
       bigdecimal
       concurrent-ruby (~> 1.0, >= 1.0.2)
@@ -123,8 +123,8 @@ GEM
     autoprefixer-rails (10.4.16.0)
       execjs (~> 2)
     aws-eventstream (1.3.0)
-    aws-partitions (1.880.0)
-    aws-sdk-core (3.190.2)
+    aws-partitions (1.883.0)
+    aws-sdk-core (3.190.3)
       aws-eventstream (~> 1, >= 1.3.0)
       aws-partitions (~> 1, >= 1.651.0)
       aws-sigv4 (~> 1.8)
@@ -140,7 +140,7 @@ GEM
       aws-eventstream (~> 1, >= 1.0.2)
     base64 (0.2.0)
     bcrypt (3.1.20)
-    bigdecimal (3.1.5)
+    bigdecimal (3.1.6)
     bindex (0.8.1)
     bootsnap (1.17.1)
       msgpack (~> 1.2)
@@ -152,7 +152,7 @@ GEM
       rails (>= 3.1)
     breadcrumbs_on_rails (4.1.0)
       railties (>= 5.0)
-    bugsnag (6.26.1)
+    bugsnag (6.26.3)
       concurrent-ruby (~> 1.0)
     builder (3.2.4)
     byebug (11.1.3)
@@ -307,7 +307,7 @@ GEM
     image_processing (1.12.2)
       mini_magick (>= 4.9.5, < 5)
       ruby-vips (>= 2.0.17, < 3)
-    io-console (0.7.1)
+    io-console (0.7.2)
     irb (1.11.1)
       rdoc
       reline (>= 0.4.2)
@@ -370,7 +370,7 @@ GEM
       nokogiri (~> 1.13)
     mini_magick (4.12.0)
     mini_mime (1.1.5)
-    minitest (5.21.1)
+    minitest (5.21.2)
     msgpack (1.7.2)
     multi_xml (0.6.0)
     multipart-post (2.3.0)
@@ -423,7 +423,7 @@ GEM
       time
       uri
     orm_adapter (0.5.0)
-    parser (3.3.0.4)
+    parser (3.3.0.5)
       ast (~> 2.4.1)
       racc
     pexels (0.5.0)
@@ -436,33 +436,33 @@ GEM
     puma (6.4.2)
       nio4r (~> 2.0)
     racc (1.7.3)
-    rack (2.2.8)
+    rack (3.0.8)
     rack-mini-profiler (2.3.4)
       rack (>= 1.2.0)
-    rack-protection (3.2.0)
+    rack-protection (4.0.0)
       base64 (>= 0.1.0)
-      rack (~> 2.2, >= 2.2.4)
-    rack-session (1.0.2)
-      rack (< 3)
+      rack (>= 3.0.0, < 4)
+    rack-session (2.0.0)
+      rack (>= 3.0.0)
     rack-test (2.1.0)
       rack (>= 1.3)
-    rackup (1.0.0)
-      rack (< 3)
-      webrick
-    rails (7.1.2)
-      actioncable (= 7.1.2)
-      actionmailbox (= 7.1.2)
-      actionmailer (= 7.1.2)
-      actionpack (= 7.1.2)
-      actiontext (= 7.1.2)
-      actionview (= 7.1.2)
-      activejob (= 7.1.2)
-      activemodel (= 7.1.2)
-      activerecord (= 7.1.2)
-      activestorage (= 7.1.2)
-      activesupport (= 7.1.2)
+    rackup (2.1.0)
+      rack (>= 3)
+      webrick (~> 1.8)
+    rails (7.1.3)
+      actioncable (= 7.1.3)
+      actionmailbox (= 7.1.3)
+      actionmailer (= 7.1.3)
+      actionpack (= 7.1.3)
+      actiontext (= 7.1.3)
+      actionview (= 7.1.3)
+      activejob (= 7.1.3)
+      activemodel (= 7.1.3)
+      activerecord (= 7.1.3)
+      activestorage (= 7.1.3)
+      activesupport (= 7.1.3)
       bundler (>= 1.15.0)
-      railties (= 7.1.2)
+      railties (= 7.1.3)
     rails-autocomplete (2.0.1)
       rails (>= 4.0)
     rails-dom-testing (2.2.0)
@@ -475,9 +475,9 @@ GEM
     rails-i18n (7.0.8)
       i18n (>= 0.7, < 2)
       railties (>= 6.0.0, < 8)
-    railties (7.1.2)
-      actionpack (= 7.1.2)
-      activesupport (= 7.1.2)
+    railties (7.1.3)
+      actionpack (= 7.1.3)
+      activesupport (= 7.1.3)
       irb
       rackup (>= 1.0.0)
       rake (>= 12.2)
@@ -499,7 +499,7 @@ GEM
       actionpack (>= 5.2)
       railties (>= 5.2)
     rexml (3.2.6)
-    roo (2.10.0)
+    roo (2.10.1)
       nokogiri (~> 1)
       rubyzip (>= 1.3.0, < 3.0.0)
     rotp (6.3.0)
@@ -552,10 +552,11 @@ GEM
       simplecov_json_formatter (~> 0.1)
     simplecov-html (0.12.3)
     simplecov_json_formatter (0.1.4)
-    sinatra (3.2.0)
+    sinatra (4.0.0)
       mustermann (~> 3.0)
-      rack (~> 2.2, >= 2.2.4)
-      rack-protection (= 3.2.0)
+      rack (>= 3.0.0, < 4)
+      rack-protection (= 4.0.0)
+      rack-session (>= 2.0.0, < 3)
       tilt (~> 2.0)
     snaky_hash (2.0.1)
       hashie
@@ -716,4 +717,4 @@ RUBY VERSION
    ruby 3.3.0p0
 
 BUNDLED WITH
-   2.5.4
+   2.5.3
diff --git a/app/controllers/admin/administration/locations_controller.rb b/app/controllers/admin/administration/locations_controller.rb
new file mode 100644
index 0000000000000000000000000000000000000000..3cb94a4abdf118a3ae40f3695cd8969dd0d37995
--- /dev/null
+++ b/app/controllers/admin/administration/locations_controller.rb
@@ -0,0 +1,68 @@
+class Admin::Administration::LocationsController < Admin::Administration::ApplicationController
+  load_and_authorize_resource class: Administration::Location,
+                              through: :current_university
+
+  def index
+    breadcrumb
+  end
+
+  def show
+    breadcrumb
+  end
+
+  def static
+    @about = @location
+    @website = @location.websites&.first
+    render_as_plain_text
+  end
+
+  def new
+    breadcrumb
+  end
+
+  def edit
+    breadcrumb
+    add_breadcrumb t('edit')
+  end
+
+  def create
+    @location.university = current_university
+    if @location.save
+      redirect_to [:admin, @location],
+                  notice: t('admin.successfully_created_html', model: @location.to_s)
+    else
+      breadcrumb
+      render :new, status: :unprocessable_entity
+    end
+  end
+
+  def update
+    if @location.update(location_params)
+      redirect_to [:admin, @location],
+                  notice: t('admin.successfully_updated_html', model: @location.to_s)
+    else
+      breadcrumb
+      add_breadcrumb t('edit')
+      render :edit, status: :unprocessable_entity
+    end
+  end
+
+  def destroy
+    @location.destroy
+    redirect_to admin_education_locations_url,
+                notice: t('admin.successfully_destroyed_html', model: @location.to_s)
+  end
+
+  private
+
+  def breadcrumb
+    super
+    add_breadcrumb Administration::Location.model_name.human(count: 2), admin_administration_locations_path
+    breadcrumb_for @location
+  end
+
+  def location_params
+    params.require(:administration_location)
+          .permit(:name, :address, :zipcode, :city, :country, :url, :phone, school_ids: [], program_ids: [])
+  end
+end
diff --git a/app/models/administration.rb b/app/models/administration.rb
index 364c98970f7677d08c1cf3c1adc5ee5eaedb689e..82053de20c9d2836e7d4d26d78ad8eb1608aa328 100644
--- a/app/models/administration.rb
+++ b/app/models/administration.rb
@@ -8,6 +8,7 @@ module Administration
 
   def self.parts
     [
+      [Administration::Location, :admin_administration_locations_path],
       [Administration::Qualiopi, :admin_administration_qualiopi_criterions_path],
     ]
   end
diff --git a/app/models/administration/location.rb b/app/models/administration/location.rb
new file mode 100644
index 0000000000000000000000000000000000000000..8908cd91886441cd8e8cf3d7ff3f0dca4fe9ca8c
--- /dev/null
+++ b/app/models/administration/location.rb
@@ -0,0 +1,119 @@
+# == Schema Information
+#
+# Table name: administration_locations
+#
+#  id            :uuid             not null, primary key
+#  address       :string
+#  city          :string
+#  country       :string
+#  latitude      :float
+#  longitude     :float
+#  name          :string
+#  phone         :string
+#  slug          :string
+#  summary       :text
+#  url           :string
+#  zipcode       :string
+#  created_at    :datetime         not null
+#  updated_at    :datetime         not null
+#  university_id :uuid             not null, indexed
+#
+# Indexes
+#
+#  index_administration_locations_on_university_id  (university_id)
+#
+# Foreign Keys
+#
+#  fk_rails_bfeca0e4b1  (university_id => universities.id)
+#
+class Administration::Location < ApplicationRecord
+  include AsIndirectObject
+  include Contentful
+  include Sanitizable
+  include Sluggable
+  include WebsitesLinkable
+  include WithBlobs
+  include WithCountry
+  include WithGitFiles
+  include WithGeolocation
+  include WithPermalink
+  include WithUniversity
+
+  has_and_belongs_to_many :schools,
+                          class_name: 'Education::School',
+                          foreign_key: :education_school_id,
+                          association_foreign_key: :administration_location_id
+                          alias_method :education_schools, :schools
+  has_and_belongs_to_many :programs,
+                          class_name: 'Education::Program',
+                          foreign_key: :education_program_id,
+                          association_foreign_key: :administration_location_id
+                          alias_method :education_programs, :programs
+  has_many                :diplomas, 
+                          -> { distinct },
+                          through: :programs,
+                          source: :diploma
+                          alias_method :education_diplomas, :diplomas
+             
+
+  scope :ordered, -> { order(:name) }
+
+  validates :name, :address, :city, :zipcode, :country, presence: true
+
+  def to_s
+    "#{name}"
+  end
+
+  def git_path(website)
+    "#{git_path_content_prefix(website)}locations/#{slug}/_index.html" if for_website?(website)
+  end
+
+  def dependencies
+    active_storage_blobs +
+    contents_dependencies +
+    programs +
+    schools
+  end
+
+  def references
+    []
+  end
+
+  # WebsitesLinkable
+
+  def has_administrators?
+    # TODO les administrateurs du site
+    false
+  end
+
+  def has_researchers?
+    # TODO les chercheurs du site
+    false
+  end
+
+  def has_teachers?
+    # TODO les enseignants du site
+    false
+  end
+
+  def has_education_programs?
+    programs.any?
+  end
+
+  def has_education_diplomas?
+    diplomas.any?
+  end
+
+  def has_research_papers?
+    false
+  end
+
+  def has_research_volumes?
+    false
+  end
+
+  def has_administration_locations?
+    # Un site (location) n'a pas de site (location) dépendant
+    false
+  end
+end
diff --git a/app/models/communication/extranet/document/category.rb b/app/models/communication/extranet/document/category.rb
index 16cdc262a308fd180eab5f7c19edcd6e30c5a081..a34ab07dc246b71b2428cee799d053d4b4628e05 100644
--- a/app/models/communication/extranet/document/category.rb
+++ b/app/models/communication/extranet/document/category.rb
@@ -22,7 +22,7 @@
 #  fk_rails_76e327b90f  (extranet_id => communication_extranets.id)
 #
 class Communication::Extranet::Document::Category < ApplicationRecord
-  include WithSlug
+  include Sluggable
   include WithUniversity
 
   belongs_to :extranet, class_name: 'Communication::Extranet'
diff --git a/app/models/communication/extranet/document/kind.rb b/app/models/communication/extranet/document/kind.rb
index 6c993fc15020b7451c04b6e6788a4e080d0c9b10..3d574467df8a3b768e52b6e33e2f4a64b12a862e 100644
--- a/app/models/communication/extranet/document/kind.rb
+++ b/app/models/communication/extranet/document/kind.rb
@@ -22,7 +22,7 @@
 #  fk_rails_2a55cf899a  (university_id => universities.id)
 #
 class Communication::Extranet::Document::Kind < ApplicationRecord
-  include WithSlug
+  include Sluggable
   include WithUniversity
 
   belongs_to :extranet, class_name: 'Communication::Extranet'
diff --git a/app/models/communication/extranet/post.rb b/app/models/communication/extranet/post.rb
index b8c6fe37c7e7fb7d899e2b6f17d30625df76d07f..a3cf3bc06705c439b1a2a4f9d64a7c879108f573 100644
--- a/app/models/communication/extranet/post.rb
+++ b/app/models/communication/extranet/post.rb
@@ -36,11 +36,11 @@
 class Communication::Extranet::Post < ApplicationRecord
   include Contentful
   include Sanitizable
+  include Sluggable
   include WithAccessibility
   include WithFeaturedImage
   include WithPublication
   include WithPermalink
-  include WithSlug
   include WithUniversity
 
   belongs_to :author, class_name: 'University::Person', optional: true
diff --git a/app/models/communication/extranet/post/category.rb b/app/models/communication/extranet/post/category.rb
index 560a5e574c529f6dd65d053df68689e1ec887cbf..f1849195325583213db073184ac18d5e137de856 100644
--- a/app/models/communication/extranet/post/category.rb
+++ b/app/models/communication/extranet/post/category.rb
@@ -22,7 +22,7 @@
 #  fk_rails_e53c2a25fc  (extranet_id => communication_extranets.id)
 #
 class Communication::Extranet::Post::Category < ApplicationRecord
-  include WithSlug
+  include Sluggable
   include WithUniversity
 
   belongs_to :extranet, class_name: 'Communication::Extranet'
diff --git a/app/models/communication/website/agenda/category.rb b/app/models/communication/website/agenda/category.rb
index a067f66dec6d8780d418a6001671380dc83e1f86..049f0b88f5603e01782321f4fddd4ee594a5f0ce 100644
--- a/app/models/communication/website/agenda/category.rb
+++ b/app/models/communication/website/agenda/category.rb
@@ -39,15 +39,15 @@ class Communication::Website::Agenda::Category < ApplicationRecord
   include AsDirectObject
   include Contentful
   include Sanitizable
+  include Sluggable
   include WithBlobs
   include WithFeaturedImage
   include WithMenuItemTarget
   include WithPermalink
   include WithPosition
-  include WithSlug
   include WithTranslations
   include WithUniversity
-
+  
   belongs_to              :parent,
                           class_name: 'Communication::Website::Agenda::Category',
                           optional: true
diff --git a/app/models/communication/website/agenda/event.rb b/app/models/communication/website/agenda/event.rb
index 6e58e6bd8347ae6344b69a859ddb36929fe8ec57..d30cb3a71fc133f1b97fd68180e9faad0f22508f 100644
--- a/app/models/communication/website/agenda/event.rb
+++ b/app/models/communication/website/agenda/event.rb
@@ -45,6 +45,7 @@ class Communication::Website::Agenda::Event < ApplicationRecord
   include AsDirectObject
   include Contentful
   include Sanitizable
+  include Sluggable
   include WithAccessibility
   include WithBlobs
   include WithCal
@@ -52,7 +53,6 @@ class Communication::Website::Agenda::Event < ApplicationRecord
   include WithFeaturedImage
   include WithMenuItemTarget
   include WithPermalink
-  include WithSlug
   include WithTime
   include WithTranslations
   include WithTree
diff --git a/app/models/communication/website/page/administration_location.rb b/app/models/communication/website/page/administration_location.rb
new file mode 100644
index 0000000000000000000000000000000000000000..a9d54d590c833367e87c9ba4fd8ba3e891cf5ea1
--- /dev/null
+++ b/app/models/communication/website/page/administration_location.rb
@@ -0,0 +1,31 @@
+class Communication::Website::Page::AdministrationLocation < Communication::Website::Page
+
+  def is_necessary_for_website?
+    website.about && website.about&.respond_to?(:administration_locations)
+  end
+
+  def editable_width?
+    false
+  end
+
+  def full_width_by_default?
+    true
+  end
+
+  def full_width
+    true
+  end
+
+  def dependencies
+    super +
+    [website.config_default_languages] +
+    website.administration_locations
+  end
+
+  protected
+
+  def current_git_path
+    @current_git_path ||= "#{git_path_prefix}locations/_index.html"
+  end
+
+end
diff --git a/app/models/communication/website/page/with_type.rb b/app/models/communication/website/page/with_type.rb
index 49083795f1da4274c18008a991556781997b2753..2f9a6a534478f6d3f7a79298bceb00e89ef4be9d 100644
--- a/app/models/communication/website/page/with_type.rb
+++ b/app/models/communication/website/page/with_type.rb
@@ -20,6 +20,8 @@ module Communication::Website::Page::WithType
       Communication::Website::Page::ResearchVolume,
       Communication::Website::Page::ResearchPaper,
       Communication::Website::Page::ResearchHalPublication,
+      # Administration
+      Communication::Website::Page::AdministrationLocation,
       # People facets
       Communication::Website::Page::Administrator,
       Communication::Website::Page::Author,
diff --git a/app/models/communication/website/permalink/location.rb b/app/models/communication/website/permalink/location.rb
new file mode 100644
index 0000000000000000000000000000000000000000..ad2b0358cf4ff537cd5e248d3bc1eca7a8218e9b
--- /dev/null
+++ b/app/models/communication/website/permalink/location.rb
@@ -0,0 +1,14 @@
+class Communication::Website::Permalink::Location < Communication::Website::Permalink
+  def self.required_in_config?(website)
+    website.has_administration_locations?
+  end
+
+  def self.static_config_key
+    :locations
+  end
+
+  # /campus/:slug/
+  def self.pattern_in_website(website, language)
+    "/#{website.special_page(Communication::Website::Page::AdministrationLocation, language: language).slug_with_ancestors}/:slug/"
+  end
+end
diff --git a/app/models/communication/website/post.rb b/app/models/communication/website/post.rb
index 89bd6d00f40805c4db70123f61bdd446d72c0775..5bcb5863e37cbfa5f92d00715bd114e3981e8e00 100644
--- a/app/models/communication/website/post.rb
+++ b/app/models/communication/website/post.rb
@@ -42,6 +42,7 @@ class Communication::Website::Post < ApplicationRecord
   include AsDirectObject
   include Contentful
   include Sanitizable
+  include Sluggable # We override slug_unavailable? method
   include WithAccessibility
   include WithBlobs
   include WithDuplication
@@ -49,7 +50,6 @@ class Communication::Website::Post < ApplicationRecord
   include WithMenuItemTarget
   include WithPermalink
   include WithPublication
-  include WithSlug # We override slug_unavailable? method
   include WithTranslations
   include WithUniversity
 
diff --git a/app/models/communication/website/post/category.rb b/app/models/communication/website/post/category.rb
index d8e19451c9082500aa75ea8375ff6f31898b848f..eb21c8709084d88e95a252f5d00e775d3e662b47 100644
--- a/app/models/communication/website/post/category.rb
+++ b/app/models/communication/website/post/category.rb
@@ -44,12 +44,12 @@ class Communication::Website::Post::Category < ApplicationRecord
   include AsDirectObject
   include Contentful
   include Sanitizable
+  include Sluggable # We override slug_unavailable? method
   include WithBlobs
   include WithFeaturedImage
   include WithMenuItemTarget
   include WithPermalink
   include WithPosition
-  include WithSlug # We override slug_unavailable? method
   include WithTranslations
   include WithTree
   include WithUniversity
diff --git a/app/models/communication/website/with_associated_objects.rb b/app/models/communication/website/with_associated_objects.rb
index 5e1fb4721d63aeaae299bf52f4122034554206d3..639bd2c7dcff61f109bc210066bd74d150b8e3fa 100644
--- a/app/models/communication/website/with_associated_objects.rb
+++ b/app/models/communication/website/with_associated_objects.rb
@@ -62,6 +62,10 @@ module Communication::Website::WithAssociatedObjects
     has_education_programs? ? about.published_programs : Education::Program.none
   end
 
+  def administration_locations
+    has_administration_locations? ? about.administration_locations : Administration::Location.none
+  end
+
   def research_volumes
     has_research_volumes? ? about.published_volumes : Research::Journal::Volume.none
   end
@@ -122,6 +126,10 @@ module Communication::Website::WithAssociatedObjects
     about && about.has_education_programs?
   end
 
+  def has_administration_locations?
+    about && about.has_administration_locations?
+  end
+
   def has_research_papers?
     about && about.has_research_papers?
   end
diff --git a/app/models/concerns/with_slug.rb b/app/models/concerns/sluggable.rb
similarity index 98%
rename from app/models/concerns/with_slug.rb
rename to app/models/concerns/sluggable.rb
index fe4de4072802c13240bd55a1a3c5b144f32e9d72..14f9cb4efbb9c07d9ddaabc2f3ca7e74b7157c53 100644
--- a/app/models/concerns/with_slug.rb
+++ b/app/models/concerns/sluggable.rb
@@ -1,4 +1,4 @@
-module WithSlug
+module Sluggable
   extend ActiveSupport::Concern
 
   included do
diff --git a/app/models/concerns/aboutable.rb b/app/models/concerns/websites_linkable.rb
similarity index 53%
rename from app/models/concerns/aboutable.rb
rename to app/models/concerns/websites_linkable.rb
index 5a9f8541b9ed4abdb3e77a0297e4773a94d0e47e..891cc062e87ea62dd733c5366f587f1f353e2585 100644
--- a/app/models/concerns/aboutable.rb
+++ b/app/models/concerns/websites_linkable.rb
@@ -1,4 +1,10 @@
-module Aboutable
+# Means there might be a website about this object
+# https://iut-perigueux.u-bordeaux.fr about a location
+# https://www.iut.u-bordeaux-montaigne.fr about a school
+# https://mmibordeaux.com about a program
+# https://www.degrowthjournal.org about a journal
+# hthttps://mica.u-bordeaux-montaigne.fr about a laboratory
+module WebsitesLinkable
   extend ActiveSupport::Concern
 
   def has_administrators?
@@ -28,4 +34,8 @@ module Aboutable
   def has_research_volumes?
     raise NotImplementedError
   end
+
+  def has_administration_locations?
+    raise NotImplementedError
+  end
 end
diff --git a/app/models/education/diploma.rb b/app/models/education/diploma.rb
index 655948b20e8a82ed045527aa11304c7a2cd3efce..74244ebc514b2eb954aaac6a4133000c24de007d 100644
--- a/app/models/education/diploma.rb
+++ b/app/models/education/diploma.rb
@@ -27,9 +27,9 @@ class Education::Diploma < ApplicationRecord
   include AsIndirectObject
   include Contentful
   include Sanitizable
+  include Sluggable
   include WithGitFiles
   include WithPermalink
-  include WithSlug
   include WithUniversity
 
   has_many :programs, dependent: :nullify
diff --git a/app/models/education/program.rb b/app/models/education/program.rb
index b8e2c446d5b5afcf81aec038d980bea7f1a1bd1c..e636a892ca1036b8c8f8c019a52bfaef4b5482f4 100644
--- a/app/models/education/program.rb
+++ b/app/models/education/program.rb
@@ -51,10 +51,11 @@
 #  fk_rails_ec1f16f607  (parent_id => education_programs.id)
 #
 class Education::Program < ApplicationRecord
-  include Aboutable
   include AsIndirectObject
   include Contentful
   include Sanitizable
+  include Sluggable
+  include WebsitesLinkable
   include WithAccessibility
   include WithAlumni
   include WithBlobs
@@ -62,11 +63,11 @@ class Education::Program < ApplicationRecord
   include WithFeaturedImage
   include WithGitFiles
   include WithInheritance
+  include WithLocations
   include WithMenuItemTarget
   include WithPermalink
   include WithPosition
   include WithSchools
-  include WithSlug
   include WithTeam
   include WithTree
   include WithUniversity
@@ -147,6 +148,7 @@ class Education::Program < ApplicationRecord
   def dependencies
     active_storage_blobs +
     contents_dependencies +
+    locations +
     university_people_through_involvements.map(&:teacher) +
     university_people_through_role_involvements.map(&:administrator) +
     [diploma]
@@ -166,7 +168,7 @@ class Education::Program < ApplicationRecord
   end
 
   #####################
-  # Aboutable methods #
+  # WebsitesLinkable methods #
   #####################
   def has_administrators?
     university_people_through_role_involvements.any? ||
diff --git a/app/models/education/program/with_locations.rb b/app/models/education/program/with_locations.rb
new file mode 100644
index 0000000000000000000000000000000000000000..9c03f1e9c4ebc878677116c361535886fed776a5
--- /dev/null
+++ b/app/models/education/program/with_locations.rb
@@ -0,0 +1,15 @@
+module Education::Program::WithLocations
+  extend ActiveSupport::Concern
+
+  included do
+    has_and_belongs_to_many :administration_locations,
+                            class_name: 'Administration::Location',
+                            foreign_key: :administration_location_id,
+                            association_foreign_key: :education_program_id
+                            alias_method :locations, :administration_locations
+  end
+
+  def has_administration_locations?
+    locations.any?
+  end
+end
diff --git a/app/models/education/school.rb b/app/models/education/school.rb
index 9941eb6814f472623dcfd99f20bc50ab1e37eaf9..1c5de7eb0d150f52662916ccd06d12de803a4181 100644
--- a/app/models/education/school.rb
+++ b/app/models/education/school.rb
@@ -25,17 +25,17 @@
 #  fk_rails_e01b37a3ad  (university_id => universities.id)
 #
 class Education::School < ApplicationRecord
-  include Aboutable
   include AsIndirectObject
   include Sanitizable
+  include WebsitesLinkable
   include WithAlumni
   include WithBlobs
   include WithCountry
   include WithGitFiles
+  include WithLocations
   include WithPrograms # must come before WithAlumni and WithTeam
   include WithTeam
-
-  belongs_to  :university
+  include WithUniversity
 
   # 'websites' might override the same method defined in WithWebsites, so we use the full name
   has_many    :communication_websites,
@@ -75,12 +75,13 @@ class Education::School < ApplicationRecord
     active_storage_blobs +
     programs +
     diplomas +
+    locations +
     administrators.map(&:administrator) +
     researchers.map(&:researcher)
   end
 
   #####################
-  # Aboutable methods #
+  # WebsitesLinkable methods
   #####################
 
   def has_research_papers?
diff --git a/app/models/education/school/with_locations.rb b/app/models/education/school/with_locations.rb
new file mode 100644
index 0000000000000000000000000000000000000000..ac62adfdeced3ae2b1a4881ad7eb47f995dcaf69
--- /dev/null
+++ b/app/models/education/school/with_locations.rb
@@ -0,0 +1,15 @@
+module Education::School::WithLocations
+  extend ActiveSupport::Concern
+
+  included do
+    has_and_belongs_to_many :administration_locations,
+                            class_name: 'Administration::Location',
+                            foreign_key: :administration_location_id,
+                            association_foreign_key: :education_school_id
+                            alias_method :locations, :administration_locations
+  end
+
+  def has_administration_locations?
+    locations.any?
+  end
+end
diff --git a/app/models/research/hal/publication.rb b/app/models/research/hal/publication.rb
index 7f01e14ee7e21cc639fa92dfc963080abb204a12..62e543fe86d77005396687a7ba4e68d88327d619 100644
--- a/app/models/research/hal/publication.rb
+++ b/app/models/research/hal/publication.rb
@@ -30,10 +30,10 @@
 class Research::Hal::Publication < ApplicationRecord
   include AsIndirectObject
   include Sanitizable
+  include Sluggable
   include WithCitations
   include WithGitFiles
   include WithPermalink
-  include WithSlug
 
   has_and_belongs_to_many :researchers,
                           class_name: 'University::Person',
diff --git a/app/models/research/journal.rb b/app/models/research/journal.rb
index adb4d33c798ea53c516127de0188a5eae3405945..508d25bc21a28a6d902eff1c24a6cce89b2d9c67 100644
--- a/app/models/research/journal.rb
+++ b/app/models/research/journal.rb
@@ -21,9 +21,9 @@
 #
 class Research::Journal < ApplicationRecord
   include AsIndirectObject
-  include Aboutable
   include Favoritable
   include Sanitizable
+  include WebsitesLinkable
   include WithGitFiles
   include WithUniversity
 
@@ -86,7 +86,7 @@ class Research::Journal < ApplicationRecord
   end
 
   #####################
-  # Aboutable methods #
+  # WebsitesLinkable methods #
   #####################
   def has_administrators?
     false
@@ -107,6 +107,10 @@ class Research::Journal < ApplicationRecord
   def has_education_diplomas?
     false
   end
+  
+  def has_administration_locations?
+    false
+  end
 
   def has_research_papers?
     published_papers.published.any?
diff --git a/app/models/research/journal/paper.rb b/app/models/research/journal/paper.rb
index 83946342a9364979794d2a0f95d834fa0da5cf6f..db56fc0a68d3846301e8829ff0bf7a72633de6bb 100644
--- a/app/models/research/journal/paper.rb
+++ b/app/models/research/journal/paper.rb
@@ -45,15 +45,15 @@
 #
 class Research::Journal::Paper < ApplicationRecord
   include AsIndirectObject
+  include Contentful
   include Sanitizable
+  include Sluggable
   include WithBlobs
-  include Contentful
   include WithCitations
   include WithGitFiles
   include WithPermalink
   include WithPosition
   include WithPublication
-  include WithSlug
   include WithUniversity
 
   has_summernote :bibliography
diff --git a/app/models/research/journal/paper/kind.rb b/app/models/research/journal/paper/kind.rb
index 4ac1d90d922fa238103851e2c6a6cc4cae694e2a..26044c9bc237de927406af09e83f3fde0e10be51 100644
--- a/app/models/research/journal/paper/kind.rb
+++ b/app/models/research/journal/paper/kind.rb
@@ -24,8 +24,8 @@
 class Research::Journal::Paper::Kind < ApplicationRecord
   include AsIndirectObject
   include Sanitizable
+  include Sluggable
   include WithGitFiles
-  include WithSlug
   include WithUniversity
 
   belongs_to :journal, class_name: 'Research::Journal'
diff --git a/app/models/research/journal/volume.rb b/app/models/research/journal/volume.rb
index c2efddbb93dee263e2d580fdb20bbd009238a3b2..a47e6789f34597bb4167c0a5e96b39eff9ac144d 100644
--- a/app/models/research/journal/volume.rb
+++ b/app/models/research/journal/volume.rb
@@ -33,12 +33,12 @@
 class Research::Journal::Volume < ApplicationRecord
   include AsIndirectObject
   include Sanitizable
+  include Sluggable
   include WithBlobs
   include WithFeaturedImage
   include WithGitFiles
   include WithPermalink
   include WithPublication
-  include WithSlug
   include WithUniversity
 
   has_summernote :text
diff --git a/app/models/research/laboratory.rb b/app/models/research/laboratory.rb
index 0a091ec6ca552eb0c1a54ab2c5509e3b31cc13fe..29eebf3199adc39ff69660f02616466bf7c2a252 100644
--- a/app/models/research/laboratory.rb
+++ b/app/models/research/laboratory.rb
@@ -21,9 +21,9 @@
 #  fk_rails_f61d27545f  (university_id => universities.id)
 #
 class Research::Laboratory < ApplicationRecord
-  include Aboutable
   include AsIndirectObject
   include Sanitizable
+  include WebsitesLinkable
   include WithCountry
   include WithGitFiles
 
@@ -92,6 +92,11 @@ class Research::Laboratory < ApplicationRecord
     false
   end
 
+  # TODO en fait un laboratoire peut avoir des locations mais il faut le coder
+  def has_administration_locations?
+    false
+  end
+
   def has_research_papers?
     false
   end
diff --git a/app/models/university.rb b/app/models/university.rb
index 2789a655b08f108b736ea9a3e72e1e65db3358dd..3c6d5233cb7852a9afe2f86873a10530eb2f7c8d 100644
--- a/app/models/university.rb
+++ b/app/models/university.rb
@@ -40,12 +40,13 @@ class University < ApplicationRecord
   self.filter_attributes += [:sso_cert]
 
   # We don't include Sanitizable because too many complex attributes. We handle it below.
-  include WithPeopleAndOrganizations
+  include WithAdministration
   include WithCommunication
   include WithCountry
   include WithEducation
   include WithIdentifier
   include WithInvoice
+  include WithPeopleAndOrganizations
   include WithResearch
   include WithSso
   include WithUsers
diff --git a/app/models/university/organization.rb b/app/models/university/organization.rb
index 3d2ed70191a43be0f59aede521971f4a02822e9b..97394d6fa82c66fec0b6be29a9c7318f9e16e9d0 100644
--- a/app/models/university/organization.rb
+++ b/app/models/university/organization.rb
@@ -48,15 +48,15 @@
 #
 class University::Organization < ApplicationRecord
   include AsIndirectObject
-  include Contentful
   include Backlinkable
+  include Contentful
   include Sanitizable
+  include Sluggable
   include WithBlobs
   include WithCountry
   include WithGeolocation
   include WithGitFiles
   include WithPermalink
-  include WithSlug
   include WithTranslations
   include WithUniversity
 
diff --git a/app/models/university/person.rb b/app/models/university/person.rb
index 97b12794f2ad25ca0514c8cfe21e4d55d696cef1..6e26f2a2d58a7949d1adee796032733064b731da 100644
--- a/app/models/university/person.rb
+++ b/app/models/university/person.rb
@@ -59,6 +59,7 @@ class University::Person < ApplicationRecord
   include Backlinkable
   include Contentful
   include Sanitizable
+  include Sluggable
   include WithBlobs
   include WithCountry
   # WithRoles included before WithEducation because needed for the latter
@@ -69,7 +70,6 @@ class University::Person < ApplicationRecord
   include WithPermalink
   include WithPicture
   include WithResearch
-  include WithSlug
   include WithTranslations
   include WithUniversity
 
diff --git a/app/models/university/with_administration.rb b/app/models/university/with_administration.rb
new file mode 100644
index 0000000000000000000000000000000000000000..c19afd360860e0d22a06241a5d9602f915bdb0fd
--- /dev/null
+++ b/app/models/university/with_administration.rb
@@ -0,0 +1,10 @@
+module University::WithAdministration
+  extend ActiveSupport::Concern
+
+  included do
+    has_many  :administration_locations,
+              class_name: 'Administration::Location',
+              dependent: :destroy
+    alias_method :locations, :administration_locations
+  end
+end
diff --git a/app/services/icon.rb b/app/services/icon.rb
index b09a73897fdd87c7293b7ccbb6464f855755690f..78d817d6252c783b839ce1bc56d58fc15a1ffa6f 100644
--- a/app/services/icon.rb
+++ b/app/services/icon.rb
@@ -47,6 +47,7 @@ class Icon
   RESEARCH_HAL = RESEARCH_PUBLICATION
 
   ADMINISTRATION_CAMPUS = 'fas fa-map-marker-alt'
+  ADMINISTRATION_LOCATION = 'fas fa-map-marker-alt'
   ADMINISTRATION_ADMISSIONS = 'fas fa-door-open'
   ADMINISTRATION_INTERNSHIPS = 'fas fa-hands-helping'
   ADMINISTRATION_STATISTICS = 'fas fa-chart-bar'
diff --git a/app/views/admin/administration/locations/_form.html.erb b/app/views/admin/administration/locations/_form.html.erb
new file mode 100644
index 0000000000000000000000000000000000000000..68eb0c747f26bad2f064b4097e9c1bedc89db082
--- /dev/null
+++ b/app/views/admin/administration/locations/_form.html.erb
@@ -0,0 +1,43 @@
+<%= simple_form_for [:admin, location] do |f| %>
+  <%= f.error_notification %>
+  <%= f.error_notification message: f.object.errors[:base].to_sentence if f.object.errors[:base].present? %>
+  <div class="row mb-5">
+    <div class="col-lg-4">
+      <%= f.input :name %>
+      <%= render 'admin/application/summary/form', f: f, about: location %>
+    </div>
+    <div class="col-lg-4">
+      <%= f.input :address %>
+      <div class="row pure__row--small">
+        <div class="col-md-4">
+          <%= f.input :zipcode %>
+        </div>
+        <div class="col-md-8">
+          <%= f.input :city %>
+        </div>
+      </div>
+      <%= f.input :country, input_html: { class: 'form-select' } %>
+    </div>
+    <div class="col-lg-4">
+      <%= f.input :phone %>
+      <%= f.input :url %>
+    </div>
+  </div>
+  <div class="row">
+    <div class="col-lg-4">
+      <%= f.association :schools, 
+                        as: :check_boxes,
+                        collection: current_university.education_schools.ordered %>
+    </div>
+    <div class="col-lg-4">
+      <%= f.association :programs,
+                        as: :check_boxes,
+                        collection: collection_tree(current_university.education_programs),
+                        label_method: ->(p) { sanitize p[:label] },
+                        value_method: ->(p) { p[:id] } %>
+    </div>
+  </div>
+  <% content_for :action_bar_right do %>
+    <%= submit f %>
+  <% end %>
+<% end %>
diff --git a/app/views/admin/administration/locations/_list.html.erb b/app/views/admin/administration/locations/_list.html.erb
new file mode 100644
index 0000000000000000000000000000000000000000..c7e047114ab08300ece9119f65b1b49f678e1286
--- /dev/null
+++ b/app/views/admin/administration/locations/_list.html.erb
@@ -0,0 +1,25 @@
+<div class="table-responsive">
+  <table class="<%= table_classes %>">
+    <thead>
+      <tr>
+        <th><%= Administration::Location.human_attribute_name('name') %></th>
+        <th><%= Administration::Location.human_attribute_name('address') %></th>
+        <th></th>
+      </tr>
+    </thead>
+    <tbody>
+      <% locations.ordered.each do |location| %>
+        <tr>
+          <td><%= link_to location, [:admin, location] %></td>
+          <td><%= location.full_street_address %></td>
+          <td class="text-end">
+            <div class="btn-group" role="group">
+              <%= edit_link location %>
+              <%= destroy_link location %>
+            </div>
+          </td>
+        </tr>
+      <% end %>
+    </tbody>
+  </table>
+</div>
diff --git a/app/views/admin/administration/locations/edit.html.erb b/app/views/admin/administration/locations/edit.html.erb
new file mode 100644
index 0000000000000000000000000000000000000000..aa47ec6c2e699cbd878c4fa0c2dd78c1b657efe3
--- /dev/null
+++ b/app/views/admin/administration/locations/edit.html.erb
@@ -0,0 +1,3 @@
+<% content_for :title, @location %>
+
+<%= render 'form', location: @location %>
diff --git a/app/views/admin/administration/locations/index.html.erb b/app/views/admin/administration/locations/index.html.erb
new file mode 100644
index 0000000000000000000000000000000000000000..880744fed19a5f5a81ab1d40b640aa0259b2a633
--- /dev/null
+++ b/app/views/admin/administration/locations/index.html.erb
@@ -0,0 +1,7 @@
+<% content_for :title, Administration::Location.model_name.human(count: 2) %>
+
+<%= render 'admin/administration/locations/list', locations: @locations %>
+
+<% content_for :action_bar_right do %>
+  <%= create_link Administration::Location %>
+<% end %>
diff --git a/app/views/admin/administration/locations/new.html.erb b/app/views/admin/administration/locations/new.html.erb
new file mode 100644
index 0000000000000000000000000000000000000000..7077a263cff1225f36891018eccdd653aa9ba0cc
--- /dev/null
+++ b/app/views/admin/administration/locations/new.html.erb
@@ -0,0 +1,3 @@
+<% content_for :title, Administration::Location.model_name.human %>
+
+<%= render 'form', location: @location %>
diff --git a/app/views/admin/administration/locations/show.html.erb b/app/views/admin/administration/locations/show.html.erb
new file mode 100644
index 0000000000000000000000000000000000000000..808affcddafb8517eb65a1ef785247f2c8d6b093
--- /dev/null
+++ b/app/views/admin/administration/locations/show.html.erb
@@ -0,0 +1,70 @@
+<% content_for :title, @location %>
+
+<div class="row">
+  <div class="col-lg-4">
+    <%= osuny_panel t('metadata') do %>
+      <%= osuny_label Education::School.human_attribute_name('address') %>
+      <p>
+        <%= @location.address %><br>
+        <%= @location.zipcode %> <%= @location.city %><br>
+        <%= @location.country %>
+      </p>
+      <% if @location.phone.present? %>
+        <%= osuny_label Education::School.human_attribute_name('phone') %>
+        <p><%= @location.phone %></p>
+      <% end %>
+      <% if @location.url.present? %>
+        <%= osuny_label Administration::Location.human_attribute_name('url') %>
+        <p><%= link_to @location.url, @location.url, target: :_blank %></p>
+      <% end %>
+    <% end %>
+  </div>
+  <% if @location.schools.any? %>
+    <div class="col-lg-4">
+      <%= osuny_panel Administration::Location.human_attribute_name('schools') do %>
+        <ul class="list-unstyled">
+          <% @location.schools.ordered.each do |school| %>
+            <li><%= link_to_if can?(:read, school), school, [:admin, school] %></li>
+          <% end %>
+        </ul>
+      <% end %>
+    </div>
+  <% end %>
+
+  <% if @location.programs.any? %>
+    <div class="col-lg-4">
+      <%= osuny_panel Administration::Location.human_attribute_name('programs') do %>
+        <ul class="list-unstyled">
+          <% @location.programs.ordered.each do |program| %>
+            <li><%= link_to_if can?(:read, program), program, [:admin, program] %></li>
+          <% end %>
+        </ul>
+      <% end %>
+    </div>
+  <% end %>
+
+  <% if @location.websites.any? %>
+    <div class="col-lg-4">
+      <%= osuny_panel Administration::Location.human_attribute_name('websites') do %>
+        <ul class="list-unstyled">
+          <% @location.websites.each do |website| %>
+            <li><%= link_to website, [:admin, website] %></li>
+          <% end %>
+        </ul>
+      <% end %>
+    </div>
+  <% end %>
+</div>
+
+<%= render 'admin/communication/blocks/content/editor', about: @location %>
+
+<%= render 'admin/application/connections/list', about: @location %>
+
+<% content_for :action_bar_left do %>
+  <%= destroy_link @location %>
+  <%= static_link static_admin_administration_location_path(@location) %>
+<% end %>
+
+<% content_for :action_bar_right do %>
+  <%= edit_link @location %>
+<% end %>
diff --git a/app/views/admin/administration/locations/static.html.erb b/app/views/admin/administration/locations/static.html.erb
new file mode 100644
index 0000000000000000000000000000000000000000..0e75b24c7be59b8288a314e165deb20cb80e95a0
--- /dev/null
+++ b/app/views/admin/administration/locations/static.html.erb
@@ -0,0 +1,39 @@
+---
+title: >
+  <%= prepare_text_for_static @about.name %>
+<%= render 'admin/application/static/permalink' if @website %>
+<%= render 'admin/application/static/design', full_width: true, toc_offcanvas: true %>
+<% if @website %>
+<%= render 'admin/application/static/breadcrumbs', 
+            pages: @website.special_page(Communication::Website::Page::AdministrationLocation).ancestors_and_self,
+            current_title: @about.to_s %>
+<% end %>
+contact_details:
+<%= render 'admin/application/static/contact_detail', variable: :address, data: @about.address, kind: ContactDetails::Base %>
+<%= render 'admin/application/static/contact_detail', variable: :zipcode, data: @about.zipcode, kind: ContactDetails::Base %>
+<%= render 'admin/application/static/contact_detail', variable: :city, data: @about.city, kind: ContactDetails::Base %>
+<%= render 'admin/application/static/contact_detail', variable: :country, data: @about.country, kind: ContactDetails::Country %>
+<%= render 'admin/application/static/contact_detail', variable: :address, data: @about.address, kind: ContactDetails::Base %>
+<%= render 'admin/application/static/contact_detail', variable: :website, data: @about.url, kind: ContactDetails::Website %>
+<%= render 'admin/application/static/contact_detail', variable: :phone, data: @about.phone, kind: ContactDetails::Phone %>
+  geolocation:
+    latitude: <%= @about.latitude %>
+    longitude: <%= @about.longitude %>
+programs:
+<% @about.programs.each do |program| %>
+  - <%= program.path %>
+<% end %>
+diplomas:
+<% @about.diplomas.each do |diploma| %>
+  - name: >
+      <%= diploma.to_s %>
+    path: <%= diploma.slug %>
+    programs: <% @programs = @about.education_programs.where(diploma: diploma).root.ordered %>
+<%= render 'admin/education/programs/static_list', 
+            diploma: diploma, 
+            programs: @programs,
+            all_programs: @about.programs,
+            depth: 4 %>
+<% end %>
+<%= render 'admin/communication/blocks/content/static', about: @about %>
+---
diff --git a/app/views/admin/communication/websites/connections/_indirect_object.html.erb b/app/views/admin/communication/websites/connections/_indirect_object.html.erb
index 8c4794ce55e4b3ff70781a7dab69f3da93db8d7a..4b1a7677348312941be2d2b0a08a12165c13f0cf 100644
--- a/app/views/admin/communication/websites/connections/_indirect_object.html.erb
+++ b/app/views/admin/communication/websites/connections/_indirect_object.html.erb
@@ -1,8 +1,8 @@
 <%
-object = connection.indirect_object
-object_name = object.to_s
-object_name = object.id if object.is_a?(ActiveStorage::Blob)
 begin
+  object = connection.indirect_object
+  object_name = object.to_s
+  object_name = object.id if object.is_a?(ActiveStorage::Blob)
   object_link = url_for [:admin, object]
 rescue
 end
diff --git a/app/views/admin/education/diplomas/static.html.erb b/app/views/admin/education/diplomas/static.html.erb
index f904c860702e08d03051850f0396077450141a1c..f568f283461b2ac0a609f3ae2f648f255457fba8 100644
--- a/app/views/admin/education/diplomas/static.html.erb
+++ b/app/views/admin/education/diplomas/static.html.erb
@@ -9,7 +9,7 @@ title: >
 <% if @website
   @programs = @website.education_programs.where(diploma: @about).root.ordered %>
 programs:
-<%= render 'admin/education/diplomas/programs', diploma: @about, programs: @programs %>
+<%= render 'admin/education/programs/static_list', diploma: @about, programs: @programs %>
 <% end %>
 short_name: >
   <%= prepare_text_for_static @about.short_name %>
diff --git a/app/views/admin/education/diplomas/_programs.html.erb b/app/views/admin/education/programs/_static_list.html.erb
similarity index 52%
rename from app/views/admin/education/diplomas/_programs.html.erb
rename to app/views/admin/education/programs/_static_list.html.erb
index af697c37f81cff55735e5400d14983c09ce66e68..831d928dcf6169469e3baa361b23165137d0a78c 100644
--- a/app/views/admin/education/diplomas/_programs.html.erb
+++ b/app/views/admin/education/programs/_static_list.html.erb
@@ -6,9 +6,13 @@ indentation = ' ' * depth
 <%= indentation %>- label: >
 <%= indentation %>    <%= program.to_s %>
 <%= indentation %>  path: "<%= program.current_permalink_in_website(@website)&.path %>"
-<% children = program.children.where(diploma: diploma) %>
+<% 
+children = program.children.where(diploma: diploma)
+# Limit to a global list of programs, for example on a location
+children = children.where(id: all_programs) if all_programs
+%>
 <% if children.any? %>
 <%= indentation %>  children: 
-<%= render 'admin/education/diplomas/programs', diploma: diploma, programs: children, depth: depth + 4 %>
+<%= render 'admin/education/programs/static_list', diploma: diploma, programs: children, depth: depth + 4 %>
 <% end %>
 <% end %>
diff --git a/app/views/admin/education/programs/show/_details.html.erb b/app/views/admin/education/programs/show/_details.html.erb
index 94cdb6871b325ee024fd8ceaf57e9aed9ce6d31c..c23c4d6e1e7a204d3a03ff33400d8d5c30cacc97 100644
--- a/app/views/admin/education/programs/show/_details.html.erb
+++ b/app/views/admin/education/programs/show/_details.html.erb
@@ -10,6 +10,14 @@
         <% end %>
       </ul>
     <% end %>
+    <% if program.locations.any? %>
+      <%= osuny_label Education::Program.human_attribute_name('locations') %>
+      <ul class="list-unstyled">
+        <% program.locations.ordered.each do |location| %>
+          <li><%= link_to_if can?(:read, location), location, [:admin, location] %></li>
+        <% end %>
+      </ul>
+    <% end %>
   </div>
   <div class="col-lg-8">
     <%= osuny_label Education::Program.human_attribute_name('short_name') %>
diff --git a/app/views/admin/education/schools/show.html.erb b/app/views/admin/education/schools/show.html.erb
index 8fae9ac1ca65314010fbac5bde1050467c1b2ba5..6f4961a690385dd8ab23e20470632ebecbd916be 100644
--- a/app/views/admin/education/schools/show.html.erb
+++ b/app/views/admin/education/schools/show.html.erb
@@ -35,6 +35,18 @@
     </div>
   <% end %>
 
+  <% if @school.locations.any? %>
+    <div class="col-lg-4">
+      <%= osuny_panel Education::School.human_attribute_name('locations') do %>
+        <ul class="list-unstyled">
+          <% @school.locations.ordered.each do |location| %>
+            <li><%= link_to_if can?(:read, location), location, [:admin, location] %></li>
+          <% end %>
+        </ul>
+      <% end %>
+    </div>
+  <% end %>
+
   <% if @school.websites.any? %>
     <div class="col-xxl-4">
       <%= osuny_panel Education::School.human_attribute_name('websites') do %>
diff --git a/config/admin_navigation.rb b/config/admin_navigation.rb
index 80e6adff0f47e400554c4296282797b335a405ab..b6f9c1f8ea82def11009446fbb298701f3210473 100644
--- a/config/admin_navigation.rb
+++ b/config/admin_navigation.rb
@@ -43,7 +43,6 @@ SimpleNavigation::Configuration.run do |navigation|
       if feature_administration?
         primary.item :administration, Administration.model_name.human, admin_administration_root_path, { kind: :header, image: 'admin/administration-thumb.jpg' }
         load_from_parts Administration, primary
-        primary.item :administration_campus, 'Campus', nil, { icon: Icon::ADMINISTRATION_CAMPUS }
         primary.item :administration_admissions, 'Admissions', nil, { icon: Icon::ADMINISTRATION_ADMISSIONS }
         primary.item :administration_internship, 'Stages', nil, { icon: Icon::ADMINISTRATION_INTERNSHIPS }
         primary.item :administration_statistics, 'Statistiques', nil, { icon: Icon::ADMINISTRATION_STATISTICS }
diff --git a/config/locales/administration/en.yml b/config/locales/administration/en.yml
index f5aa929946ff61ac08de19de285d41ea7db6af2a..d2454fdafd213229852a2c617551ce6972b4a8c1 100644
--- a/config/locales/administration/en.yml
+++ b/config/locales/administration/en.yml
@@ -4,6 +4,9 @@ en:
       administration/qualiopi: Qualiopi
   activerecord:
     models:
+      administration/location:
+        one: Location
+        other: Campus
       administration/qualiopi/criterion:
         one: Criterion
         other: Criterions
@@ -11,6 +14,17 @@ en:
         one: Indicator
         other: Indicators
     attributes:
+      administration/location:
+        address: Address
+        city: City
+        country: Country
+        name: Name
+        phone: Telephone
+        programs: Programs
+        schools: Schools
+        url: Website
+        websites: Linked websites
+        zipcode: Zipcode
       administration/qualiopi/criterion:
         number: Number
         name: Name
@@ -30,5 +44,7 @@ en:
       text: Administration, in its many forms, is defined primarily by its activities in the service of the general interest. It acts in the general interest and respects the principle of legality. It is bound by the obligation of neutrality and respect for the principle of secularism. It complies with the principle of equality and guarantees everyone impartial treatment.
       source: Code des relations entre le public et l'administration
       parts:
+        location: 
+          description: Management of the various (physical) education and research sites
         qualiopi:
           description: Help in checking the conformity of the training offer with the Qualiopi standard
\ No newline at end of file
diff --git a/config/locales/administration/fr.yml b/config/locales/administration/fr.yml
index 5b57ec79688ca012b705af04a0f053d04d03df39..cd2d5d84fea1c3f921846334b082ba96257407da 100644
--- a/config/locales/administration/fr.yml
+++ b/config/locales/administration/fr.yml
@@ -4,6 +4,9 @@ fr:
       administration/qualiopi: Qualiopi
   activerecord:
     models:
+      administration/location:
+        one: Site
+        other: Campus
       administration/qualiopi/criterion:
         one: Critère
         other: Critères
@@ -11,6 +14,17 @@ fr:
         one: Indicateur
         other: Indicateurs
     attributes:
+      administration/location:
+        address: Adresse
+        city: Ville
+        country: Pays
+        name: Nom
+        phone: Téléphone
+        programs: Formations dispensées
+        schools: Écoles
+        url: Site Web
+        websites: Sites Web associés
+        zipcode: Code postal
       administration/qualiopi/criterion:
         number: Numéro
         name: Nom
@@ -30,5 +44,7 @@ fr:
       text: L'administration, sous ses multiples formes, se définit principalement par ses activités au service de l'intérêt général. L'administration agit dans l'intérêt général et respecte le principe de légalité. Elle est tenue à l'obligation de neutralité et au respect du principe de laïcité. Elle se conforme au principe d'égalité et garantit à chacun un traitement impartial.
       source: Code des relations entre le public et l'administration
       parts:
+        location: 
+          description: Gestion des différents sites (physiques) de formation et de recherche
         qualiopi:
           description: Aide à la vérification de la conformité de l'offre de formation avec la norme Qualiopi
diff --git a/config/locales/communication/en.yml b/config/locales/communication/en.yml
index 2f0971b8ade0203dec9395949866c55445643861..66df046916b8b236089482edefafb8c59d86a2b0 100644
--- a/config/locales/communication/en.yml
+++ b/config/locales/communication/en.yml
@@ -366,6 +366,9 @@ en:
           accessibility:
             slug: accessibility
             title: Accessibility
+          administration_location:
+            slug: campus
+            title: Campus
           administrator:
             slug: administrators
             title: Administrators
diff --git a/config/locales/communication/fr.yml b/config/locales/communication/fr.yml
index 099aee1bfb3e23bde05ae36e603c23930ebb4645..ed541ea417e31cc3c4b1615f9d9b5772314798e9 100644
--- a/config/locales/communication/fr.yml
+++ b/config/locales/communication/fr.yml
@@ -366,6 +366,9 @@ fr:
           accessibility:
             slug: accessibilite
             title: Accessibilité
+          administration_location:
+            slug: campus
+            title: Campus
           administrator:
             slug: equipe-administrative
             title: Équipe administrative
diff --git a/config/locales/education/en.yml b/config/locales/education/en.yml
index e79e9c9e285df376023982fabb43a7f78c52d863..38766f83388f7d5f72d3860bfed9a8b73cd842dd 100644
--- a/config/locales/education/en.yml
+++ b/config/locales/education/en.yml
@@ -53,6 +53,7 @@ en:
         initial: Initial training
         is_published: Published
         is_draft: Draft
+        locations: Campus
         main_information: Main information
         name: Name
         objectives: Objectifs
@@ -77,6 +78,7 @@ en:
         administrators: Administrators
         city: City
         country: Country
+        locations: Campus
         name: Name
         phone: Phone
         programs: Programs provided
diff --git a/config/locales/education/fr.yml b/config/locales/education/fr.yml
index 5d0ddd48383afd84ea8902fb2faed6db8cd4d854..4e2252160d136362df4cff92f0e9131ea04d8415 100644
--- a/config/locales/education/fr.yml
+++ b/config/locales/education/fr.yml
@@ -53,6 +53,7 @@ fr:
         initial: Formation initiale
         is_published: Publiée
         is_draft: Brouillon
+        locations: Campus
         main_information: Informations essentielles
         name: Nom
         objectives: Objectifs
@@ -77,6 +78,7 @@ fr:
         administrators: Équipe administrative
         city: Ville
         country: Pays
+        locations: Campus
         name: Nom
         phone: Téléphone
         programs: Formations dispensées
diff --git a/config/routes/admin/administration.rb b/config/routes/admin/administration.rb
index cd5c1ac24aaba28f21c9a2bc72150c41da993670..e4dd1801dfa37b2f85e3e2e3062da1728d400dca 100644
--- a/config/routes/admin/administration.rb
+++ b/config/routes/admin/administration.rb
@@ -1,4 +1,9 @@
 namespace :administration do
+  resources :locations do
+    member do
+      get :static
+    end
+  end
   namespace :qualiopi do
     resources :criterions, only: [:index, :show]
     resources :indicators, only: [:index, :show]
diff --git a/db/migrate/20240126081127_create_administration_locations.rb b/db/migrate/20240126081127_create_administration_locations.rb
new file mode 100644
index 0000000000000000000000000000000000000000..023a95199bb3419c200c69d64eae60321325985b
--- /dev/null
+++ b/db/migrate/20240126081127_create_administration_locations.rb
@@ -0,0 +1,19 @@
+class CreateAdministrationLocations < ActiveRecord::Migration[7.1]
+  def change
+    create_table :administration_locations, id: :uuid do |t|
+      t.references :university, null: false, foreign_key: true, type: :uuid
+      t.string :name
+      t.text :summary
+      t.string :address
+      t.string :zipcode
+      t.string :city
+      t.string :country
+      t.float :latitude
+      t.float :longitude
+      t.string :phone
+      t.string :url
+
+      t.timestamps
+    end
+  end
+end
diff --git a/db/migrate/20240126090606_create_location_join_tables.rb b/db/migrate/20240126090606_create_location_join_tables.rb
new file mode 100644
index 0000000000000000000000000000000000000000..2cb6043baf14d2e3279e72c0a2cdae6eae67cbc0
--- /dev/null
+++ b/db/migrate/20240126090606_create_location_join_tables.rb
@@ -0,0 +1,12 @@
+class CreateLocationJoinTables < ActiveRecord::Migration[7.1]
+  def change
+    create_join_table :administration_locations, :education_schools, column_options: {type: :uuid} do |t|
+      t.index [:"administration_location_id", :"education_school_id"], name: 'index_location_school'
+      t.index [:"education_school_id", :"administration_location_id"], name: 'index_school_location'
+    end
+    create_join_table :administration_locations, :education_programs, column_options: {type: :uuid} do |t|
+      t.index [:"administration_location_id", :"education_program_id"], name: 'index_location_program'
+      t.index [:"education_program_id", :"administration_location_id"], name: 'index_program_location'
+    end
+  end
+end
diff --git a/db/migrate/20240126112021_add_slug_to_administration_location.rb b/db/migrate/20240126112021_add_slug_to_administration_location.rb
new file mode 100644
index 0000000000000000000000000000000000000000..8a495db3c373800345ff62475e67c75f5092ffca
--- /dev/null
+++ b/db/migrate/20240126112021_add_slug_to_administration_location.rb
@@ -0,0 +1,5 @@
+class AddSlugToAdministrationLocation < ActiveRecord::Migration[7.1]
+  def change
+    add_column :administration_locations, :slug, :string
+  end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 5f838fbdad1e96af525ad95c76f3eedf28547e27..abd8aece95f9e7eb6959219640e23a6625d52f70 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -56,6 +56,38 @@ ActiveRecord::Schema[7.1].define(version: 2024_01_29_100647) do
     t.index ["blob_id", "variation_digest"], name: "index_active_storage_variant_records_uniqueness", unique: true
   end
 
+  create_table "administration_locations", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
+    t.uuid "university_id", null: false
+    t.string "name"
+    t.text "summary"
+    t.string "address"
+    t.string "zipcode"
+    t.string "city"
+    t.string "country"
+    t.float "latitude"
+    t.float "longitude"
+    t.string "phone"
+    t.string "url"
+    t.datetime "created_at", null: false
+    t.datetime "updated_at", null: false
+    t.string "slug"
+    t.index ["university_id"], name: "index_administration_locations_on_university_id"
+  end
+
+  create_table "administration_locations_education_programs", id: false, force: :cascade do |t|
+    t.uuid "administration_location_id", null: false
+    t.uuid "education_program_id", null: false
+    t.index ["administration_location_id", "education_program_id"], name: "index_location_program"
+    t.index ["education_program_id", "administration_location_id"], name: "index_program_location"
+  end
+
+  create_table "administration_locations_education_schools", id: false, force: :cascade do |t|
+    t.uuid "administration_location_id", null: false
+    t.uuid "education_school_id", null: false
+    t.index ["administration_location_id", "education_school_id"], name: "index_location_school"
+    t.index ["education_school_id", "administration_location_id"], name: "index_school_location"
+  end
+
   create_table "administration_qualiopi_criterions", id: :uuid, default: -> { "public.gen_random_uuid()" }, force: :cascade do |t|
     t.integer "number"
     t.text "name"
@@ -106,8 +138,8 @@ ActiveRecord::Schema[7.1].define(version: 2024_01_29_100647) do
     t.datetime "updated_at", null: false
     t.string "title"
     t.boolean "published", default: true
-    t.uuid "communication_website_id"
     t.uuid "heading_id"
+    t.uuid "communication_website_id"
     t.string "migration_identifier"
     t.index ["about_type", "about_id"], name: "index_communication_website_blocks_on_about"
     t.index ["communication_website_id"], name: "index_communication_blocks_on_communication_website_id"
@@ -419,7 +451,7 @@ ActiveRecord::Schema[7.1].define(version: 2024_01_29_100647) do
     t.index ["university_id"], name: "index_communication_website_pages_on_university_id"
   end
 
-  create_table "communication_website_permalinks", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
+  create_table "communication_website_permalinks", id: :uuid, default: -> { "public.gen_random_uuid()" }, force: :cascade do |t|
     t.uuid "university_id", null: false
     t.uuid "website_id", null: false
     t.string "about_type", null: false
@@ -1159,6 +1191,7 @@ ActiveRecord::Schema[7.1].define(version: 2024_01_29_100647) do
 
   add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id"
   add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id"
+  add_foreign_key "administration_locations", "universities"
   add_foreign_key "administration_qualiopi_indicators", "administration_qualiopi_criterions", column: "criterion_id"
   add_foreign_key "communication_block_headings", "communication_block_headings", column: "parent_id"
   add_foreign_key "communication_block_headings", "universities"
diff --git a/test/fixtures/administration/locations.yml b/test/fixtures/administration/locations.yml
new file mode 100644
index 0000000000000000000000000000000000000000..1c617dac7e7f16e9a529265b08cb444cb5df89c7
--- /dev/null
+++ b/test/fixtures/administration/locations.yml
@@ -0,0 +1,25 @@
+# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
+
+one:
+  name: MyString
+  university: default_university
+  address: MyString
+  city: MyString
+  country: MyString
+  latitude: 1.5
+  longitude: 1.5
+  phone: MyString
+  url: MyString
+  zipcode: MyString
+
+two:
+  name: MyString
+  university: default_university
+  address: MyString
+  city: MyString
+  country: MyString
+  latitude: 1.5
+  longitude: 1.5
+  phone: MyString
+  url: MyString
+  zipcode: MyString
diff --git a/test/models/communication/website/dependency_test.rb b/test/models/communication/website/dependency_test.rb
index 20f72639e472d7e91c445131a21819bbc257ed6d..7819e1b76f4625484ba221951d39670f24a8b059 100644
--- a/test/models/communication/website/dependency_test.rb
+++ b/test/models/communication/website/dependency_test.rb
@@ -70,11 +70,11 @@ class Communication::Website::DependencyTest < ActiveSupport::TestCase
     refute(destroy_obsolete_git_files_job)
     delta = website_with_github.reload.recursive_dependencies.count - dependencies_before_count
     # En ajoutant l'école, on rajoute en dépendances :
-    # - L'école, et ses formations et diplômes en cascade (3)
+    # - L'école, ses formations, diplômes et sites en cascade (4)
     # - Les catégories d'actus liés aux formations, soit la catégorie racine et la catégorie de default_program (2)
     # - Les pages "Teachers", "Administrators", "Researchers", "EducationDiplomas", "EducationPrograms" (5)
     # Donc un total de 3 + 2 + 5 = 10 dépendances
-    assert_equal 10, delta
+    assert_equal 11, delta
 
     Delayed::Job.destroy_all
 
diff --git a/test/models/university/location_test.rb b/test/models/university/location_test.rb
new file mode 100644
index 0000000000000000000000000000000000000000..ba4f84b9d61c9671a8b90179cc13cbd766956727
--- /dev/null
+++ b/test/models/university/location_test.rb
@@ -0,0 +1,7 @@
+require "test_helper"
+
+class University::LocationTest < ActiveSupport::TestCase
+  # test "the truth" do
+  #   assert true
+  # end
+end