diff --git a/FEATURES/121023 b/FEATURES/121023
new file mode 100644
index 0000000000000000000000000000000000000000..6b47029120b6b294fccc3f7dee27895078c2081e
--- /dev/null
+++ b/FEATURES/121023
@@ -0,0 +1,10 @@
+        '121023' =>
+            ['Label' => $this->_('Ajout du filtre ville dans la boite Agenda'),
+             'Desc' => 'Possibilité de filtrer les évenements par ville',
+             'Image' => '',
+             'Video' => '',
+             'Category' => 'Agenda',
+             'Right' => function($feature_description, $user) {return true;},
+             'Wiki' => 'http://wiki.bokeh-library-portal.org/index.php?title=Param%C3%A9trer_le_module_Calendrier#Selection_de_filtres',
+             'Test' => '',
+             'Date' => '2020-11-12'],
\ No newline at end of file
diff --git a/VERSIONS_WIP/121023 b/VERSIONS_WIP/121023
new file mode 100644
index 0000000000000000000000000000000000000000..6764bdb1ef94f5ccacd2989cedd8b1249162cf29
--- /dev/null
+++ b/VERSIONS_WIP/121023
@@ -0,0 +1 @@
+ - ticket #121023 : Agenda : possibilité de filtrer les évenements par ville
\ No newline at end of file
diff --git a/library/Class/Article.php b/library/Class/Article.php
index b67073652a8f1b19a3e66629f807e15fea50b2c4..afc750af2698fa7fc184064d6d6073128af2b538 100644
--- a/library/Class/Article.php
+++ b/library/Class/Article.php
@@ -293,6 +293,19 @@ class ArticleLoader extends Storm_Model_Loader {
   }
 
 
+  protected function _byPlaceTown($town) {
+    if (!$town || '' == $town)
+      return $this;
+    $this->_select
+      ->join('lieux',
+             'lieux.ID = cms_article.ID_LIEU',
+             array())
+      ->where('trim(lieux.VILLE) like ?', $town);
+
+    return $this;
+  }
+
+
   protected function _byCustomFields($custom_fields) {
     foreach ($custom_fields as $id => $value) {
       $this->_select
@@ -342,6 +355,7 @@ class ArticleLoader extends Storm_Model_Loader {
             'events_only' => false, // filtre que les évènements,
             'published' => true, // seulement les articles dont les date de debut / fin incluent le jour en cours,
             'id_lieu' => null,  // id du lieu Class_Lieu
+            'place_town' => null,
             'display_mode' => 'Title',
             'custom_fields' => []];
   }
@@ -388,6 +402,7 @@ class ArticleLoader extends Storm_Model_Loader {
       ->_publishedNow()
       ->_byIdBib($this->_id_bib)
       ->_byIdLieu($this->_id_lieu)
+      ->_byPlaceTown($preferences['place_town'])
       ->_whereSelectionIn($this->_id_articles, $this->_id_categories)
       ->_whereEventDateIn($this->_event_date)
       ->_whereEventStartAfter($this->_event_start_after)
diff --git a/library/Class/Calendar.php b/library/Class/Calendar.php
index 89401fd4f37b847711aaf8677d5b6309fa14dcc6..a04851a259374d56ac21aaa7f067d2f3c23809ac 100644
--- a/library/Class/Calendar.php
+++ b/library/Class/Calendar.php
@@ -154,11 +154,17 @@ class Class_Calendar {
 
   protected function _loadArticles($extra_prefs) {
     $id_lieu = $this->getPlaceParam();
-
-    if ( (0 === strpos($this->id_module, 'library_')) && (!$id_lieu))
+    $town = $this->_getParam('place_town', null);
+    if ( (0 === strpos($this->id_module, 'library_'))
+        && (!$id_lieu)
+        && !$town)
       return [];
+    $prefs = [];
+    if ($town)
+      $prefs['place_town'] = $town;
 
-    $prefs = array_merge(['display_order' => (isset($this->preferences['display_order'])
+    $prefs = array_merge($prefs,
+                         ['display_order' => (isset($this->preferences['display_order'])
                                               ? $this->preferences['display_order']
                                               : $this->preferences['order']),
                           'id_categorie' => $this->_getCategoriesIds(),
diff --git a/library/Class/Systeme/ModulesAccueil/Calendrier.php b/library/Class/Systeme/ModulesAccueil/Calendrier.php
index 72c58a72981996cdf3e492cfb53c4d8018114c71..a9b9e9436daba78c847bf64564951132ec515674 100644
--- a/library/Class/Systeme/ModulesAccueil/Calendrier.php
+++ b/library/Class/Systeme/ModulesAccueil/Calendrier.php
@@ -62,7 +62,8 @@ class Class_Systeme_ModulesAccueil_Calendrier extends Class_Systeme_ModulesAccue
   public function getAvailableFilters() {
     $available_filters = ['day' => $this->_('Date'),
                           'date' => $this->_('Mois'),
-                          'place' => $this->_('Lieu')];
+                          'place' => $this->_('Lieu'),
+                          'place_town' => $this->_('Ville')];
 
     $custom_fields = Class_CustomField_Model::getModel('Article')->getFields();
 
diff --git a/library/Class/Systeme/ModulesAccueil/Library.php b/library/Class/Systeme/ModulesAccueil/Library.php
index 3e21bc8a629ed9b9d22375842dfe37ea6a407638..81bbeeb507fc37b51ef8af78fbee35b3a84fd862 100644
--- a/library/Class/Systeme/ModulesAccueil/Library.php
+++ b/library/Class/Systeme/ModulesAccueil/Library.php
@@ -37,6 +37,7 @@ class Class_Systeme_ModulesAccueil_Library extends Class_Systeme_ModulesAccueil_
 
     FILTER_OPENING = 'opening',
     FILTER_TOWN = 'town',
+    FILTER_PLACE_TOWN = 'place_town',
     FILTER_TERRITORY = 'territory',
     FILTER_SEARCH = 'search',
 
diff --git a/library/ZendAfi/View/Helper/Filters/Element.php b/library/ZendAfi/View/Helper/Filters/Element.php
index fe9047a62a9e987e7535e5474773cf578c27f326..da0a37f8d3fd55c118bc92a7eae5c3c34b6ea87b 100644
--- a/library/ZendAfi/View/Helper/Filters/Element.php
+++ b/library/ZendAfi/View/Helper/Filters/Element.php
@@ -33,7 +33,7 @@ abstract class ZendAfi_View_Helper_Filters_Element extends ZendAfi_View_Helper_B
     if (preg_match('/^custom_field_(\d+)/', $filter, $matches))
       return (new ZendAfi_View_Helper_Filters_Element_CustomField($matches[1]))->setFilter($filter);
 
-    $class = 'ZendAfi_View_Helper_Filters_Element_' . ucfirst($filter);
+    $class = 'ZendAfi_View_Helper_Filters_Element_' . Storm_Inflector::camelize($filter);
 
     if (!class_exists($class))
       $class = 'ZendAfi_View_Helper_Filters_Element_Null';
diff --git a/library/ZendAfi/View/Helper/Filters/Element/PlaceTown.php b/library/ZendAfi/View/Helper/Filters/Element/PlaceTown.php
new file mode 100644
index 0000000000000000000000000000000000000000..5df7ff3e399edd25bb43f51277de8ca353dfaceb
--- /dev/null
+++ b/library/ZendAfi/View/Helper/Filters/Element/PlaceTown.php
@@ -0,0 +1,39 @@
+<?php
+/**
+ * Copyright (c) 2012, Agence Française Informatique (AFI). All rights reserved.
+ *
+ * BOKEH is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE as published by
+ * the Free Software Foundation.
+ *
+ * There are special exceptions to the terms and conditions of the AGPL as it
+ * is applied to this software (see README file).
+ *
+ * BOKEH is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+ *
+ * You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE
+ * along with BOKEH; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA
+ */
+
+class ZendAfi_View_Helper_Filters_Element_PlaceTown extends ZendAfi_View_Helper_Filters_Element {
+  public function __construct($custom_field_id = null) {
+    $this->_custom_field_id = Class_Systeme_ModulesAccueil_Library::FILTER_PLACE_TOWN;
+  }
+
+
+  public function elements() {
+    $towns = array_unique(
+                          (new Storm_Model_Collection(Class_Lieu::findAllBy(['order' => 'ville'])))
+                          ->collect('ville')
+                          ->getArrayCopy());
+
+    $towns = array_map(function($town)
+                       { return trim($town);}, $towns);
+
+    return array_combine($towns, $towns);
+  }
+}
diff --git a/tests/application/modules/admin/controllers/WidgetControllerTest.php b/tests/application/modules/admin/controllers/WidgetControllerTest.php
index fe5968f206f3bb0c77d221e60bb367e99666128e..886bdb16bf508b8e77fea4aa295162336c92fa2b 100644
--- a/tests/application/modules/admin/controllers/WidgetControllerTest.php
+++ b/tests/application/modules/admin/controllers/WidgetControllerTest.php
@@ -382,8 +382,8 @@ class WidgetControllerCalendarTest extends WidgetControllerDispatchWidgetConfigu
 
 
   /** @test */
-  public function secondListShouldContainsTwoElements() {
-    $this->assertXPathCount('//div[@id="input_enabled_filters"]/div[2]/ul/li', 2);
+  public function secondListShouldContainsThreeElements() {
+    $this->assertXPathCount('//div[@id="input_enabled_filters"]/div[2]/ul/li', 3);
   }
 
 
diff --git a/tests/scenarios/Templates/TemplatesWidgetTest.php b/tests/scenarios/Templates/TemplatesWidgetTest.php
index 244f990d4e07549810afe506c4123edf977dfe5a..cc76092f5ff2ccda7749f4c04203755b6b4ef99a 100644
--- a/tests/scenarios/Templates/TemplatesWidgetTest.php
+++ b/tests/scenarios/Templates/TemplatesWidgetTest.php
@@ -43,6 +43,7 @@ class TemplatesWidgetsNewsletterTest extends TemplatesIntonationTestCase {
     $this->assertXpath('//input[@name="titre"][@type="text"]');
   }
 
+
   /** @test */
   public function buttonDisableNewsletterShouldBePresent() {
     $this->fixture('Class_Newsletter',
@@ -118,11 +119,38 @@ abstract class TemplatesWidgetRenderAllTestCase extends TemplatesIntonationTestC
 
     $this->fixture('Class_Bib',
                    ['id' => 1,
-                    'libelle' => 'Annecy']);
+                    'libelle' => 'Annecy',
+                    'id_lieu' => 11,
+                    'ville' => 'Grand Annecy']);
 
     $this->fixture('Class_Bib',
                    ['id' => 45,
-                    'libelle' => 'Cran']);
+                    'libelle' => 'Cran',
+                    'id_lieu' => 145,
+                    'ville' => 'Grand Annecy']);
+
+    $this->fixture('Class_Lieu',
+                   ['id' => 11,
+                    'libelle' => 'Centre culturel',
+                    'ville' => 'Grand Annecy'
+                   ]);
+
+    $this->fixture('Class_Lieu',
+                   ['id' => 145,
+                    'libelle' => 'Mediatheque',
+                    'ville' => 'Grand Annecy '
+                   ]);
+
+    $this->fixture('Class_Lieu',
+                   ['id' => 146,
+                    'libelle' => 'Ludotheque',
+                    'ville' => 'Faverges'
+                   ]);
+
+    Class_Bib::newInstanceWithId(46, ['libelle' => 'Seythenex',
+                                      'ville' => 'Faverges',
+                                      'id_lieu' => 146])
+      ->save();
   }
 }
 
@@ -164,6 +192,14 @@ class TemplatesWidgetRenderAllContainersTest extends TemplatesWidgetRenderAllTes
 class TemplatesWidgetRenderAllAgendaTest extends TemplatesWidgetRenderAllTestCase {
   public function setUp() {
     parent::setUp();
+
+    Class_Profil::find(72)
+      ->setCfgAccueil(['modules' =>
+                       ['21' => ['division' => 3,
+                                 'type_module' => 'CALENDAR',
+                                 'preferences' => ['rss' => 1,
+                                                   'enabled_filters' => 'place;place_town']]]]);
+
     $this->dispatch('/opac/widget/render-all/profile_id/72/widget_id/21');
   }
 
@@ -184,6 +220,109 @@ class TemplatesWidgetRenderAllAgendaTest extends TemplatesWidgetRenderAllTestCas
   public function renderWidgetAgendaShouldContainsVersionLinkForArticleVacance() {
     $this->assertXPathContentContains('//a[contains(@href, "/admin/cms/version/id/78")]', 'Historique');
   }
+
+
+  /** @test */
+  public function filterPlaceShouldBeDisplayed() {
+    $this->assertXPathContentContains('//div[contains(@class, "filters")]//button[contains(@class, "button_Lieu")]', 'Lieu');
+  }
+
+
+  /** @test */
+  public function filterTownShouldBeDisplayed() {
+    $this->assertXPathContentContains('//div[contains(@class, "filters")]//button[contains(@class, "button_Ville")]', 'Ville');
+  }
+
+
+  /** @test */
+  public function townGrandAnnecyShouldBeSelectable() {
+    $this->assertXPathContentContains('//div[contains(@class, "filters")]//li//a[contains(@href, "/cms/render-all/id_module/21/place_town/Grand+Annecy")]', 'Grand Annecy');
+  }
+}
+
+
+
+
+class TemplatesWidgetFilterTest extends TemplatesWidgetRenderAllTestCase {
+
+  public function setUp() {
+    parent::setUp();
+    Intonation_View_CalendarContent::setTimeSource(new TimeSourceForTest('2020-11-12 23:34:00'));
+    Class_Profil::find(1)
+      ->setCfgAccueil(['modules' =>
+                       ['51' => ['division' => 3,
+                                 'type_module' => 'CALENDAR',
+                                 'preferences' => ['enabled_filters' => 'place;place_town',
+                                                   'layout' => 'wall'
+                                 ]]]]);
+
+    Class_Article::newInstanceWithId(78,
+                                     ['titre' => 'L\'été',
+                                      'contenu' => 'À la mer',
+                                      'id_lieu' => 145])
+      ->save();
+    Class_Article::newInstanceWithId(79,
+                                     ['titre' => 'L\'hiver',
+                                      'contenu' => 'À la montagne',
+                                      'id_lieu' => 146])
+      ->save();
+
+    Class_Article::getLoader()
+      ->whenCalled('getArticlesByPreferences')
+      ->with(['display_order' => 'EventDebut',
+              'id_categorie' => '',
+              'events_only' => true,
+              'event_date' => '2020-11',
+              'id_bib' => 0,
+              'id_lieu' => '',
+              'place_town' => 'Grand Annecy',
+              'custom_fields' => [],
+              'published' => true,
+              'event_end_after' => '2020-11-12'])
+      ->answers([Class_Article::find(78)])
+      ->whenCalled('getArticlesByPreferences')
+      ->with(['display_order' => 'EventDebut',
+              'id_categorie' => '',
+              'events_only' => true,
+              'event_date' => '2020-11',
+              'id_bib' => 0,
+              'id_lieu' => '',
+              'place_town' => 'Grand Annecy',
+              'custom_fields' => [],
+              'published' => true
+              ])
+      ->answers([Class_Article::find(78)])
+      ->whenCalled('getArticlesByPreferences')
+      ->with(['display_order' => 'EventDebut',
+              'id_categorie' => '',
+              'events_only' => true,
+              'event_date' => '',
+              'id_bib' => 0,
+              'id_lieu' => '',
+              'place_town' => 'Grand Annecy',
+              'custom_fields' => [],
+              'published' => true,
+              'event_start_after' => '2020-11',
+              'event_end_after' => '',
+              'limit' => 3
+              ])
+      ->answers([Class_Article::find(78)])
+      ->beStrict();
+
+    $this->dispatch('/cms/calendar/id_profil/1/id_module/51/place_town/Grand+Annecy');
+  }
+
+
+  /** @test */
+  public function eventLEteShouldBeDisplayedWithTownGrandAnnecySelected() {
+    $this->assertXPathContentContains('//div', 'L\'été', $this->_response->getBody());
+  }
+
+
+  /** @test */
+  public function eventAlaMontagneShouldNotBeDisplayedWithTownGrandAnnecySelected() {
+    $this->assertNotXPathContentContains('//div', 'L\'hiver');
+  }
 }
 
 
@@ -494,6 +633,7 @@ class TemplatesWidgetSearchToggleStyleTest extends TemplatesIntonationTestCase {
 
 
 
+
 class TemplatesImageWidgetTest extends TemplatesIntonationTestCase {
 
   /** @test */
@@ -567,6 +707,7 @@ class TemplatesCreditsWidgetTest extends TemplatesIntonationTestCase {
 
 
 
+
 class TemplatesScrollWidgetTest extends TemplatesIntonationTestCase {
 
   /** @test */
@@ -758,6 +899,7 @@ class TemplateRenderWidgetTest extends TemplatesIntonationTestCase {
 
 
 
+
 class TemplatesDispatchLibraryWidgetTest extends TemplatesIntonationTestCase {
 
   public function setUp() {
@@ -979,6 +1121,7 @@ class TemplatesDispatchDomainWidgetTest extends TemplatesIntonationTestCase {
 
 
 
+
 class TemplatesDispatchIntonationWithDomainWidgetTest extends TemplatesIntonationTestCase {
 
   /** @test */
@@ -995,6 +1138,7 @@ class TemplatesDispatchIntonationWithDomainWidgetTest extends TemplatesIntonatio
 
 
 
+
 class TemplatesDispatchAdminWidgetEditActionTest extends TemplatesIntonationTestCase {
 
   /** @test */
@@ -1025,7 +1169,6 @@ class TemplatesDispatchAdminWidgetEditActionTest extends TemplatesIntonationTest
   }
 
 
-
   /** @test */
   public function customCssClassesShouldHaveBeenSaved() {
     $this->postDispatch('/admin/widget/edit-action/id/recherche_resultat_simple/id_profil/72',