Repository: data-for-change/anyway

Issue:
Add severity text to widgets
Split to severity with localized text of severity 
accident_count_by_accident_year (severity of accidents)
injured_count_by_accident_year (severity of injured)

@MichalOren FYI


Discussion:
Hi @atalyaalon , 
The current data should be replaced by a dict with severity levels as keys and values the relevant items from current data?
@ziv17 what do you think?
Here we had for example text separated from count- perhaps we should keep it as better practice? Not having translations as keys - since then do need to differentiate them (killed should be in red - and the FE needs to know how many killed using the key - in any language
Also here: https://github.com/hasadna/anyway/blob/dev/static/data/widgets/accident_severity_by_cross_location.json
We will soon need to decide on templates we want to use - and we'll need to adjust best practices.
What do you think?
Hi @atalyaalon , 
We want two levels of grouping? The severity should be the outer level or the inner?
> 
> 
> @ziv17 what do you think?
> Here we had for example text separated from count- perhaps we should keep it as better practice? Not having translations as keys - since then do need to differentiate them (killed should be in red - and the FE needs to know how many killed using the key - in any language

Hi @atalyaalon , I don't understand
> 
> 
> Also here: https://github.com/hasadna/anyway/blob/dev/static/data/widgets/accident_severity_by_cross_location.json
> We will soon need to decide on templates we want to use - and we'll need to adjust best practices.
> What do you think?

Here too, I do not understand.
@ziv17 following our discussion - the proper terminology is category for the bar in the x-axis (years in our case) and series to color dimension (severity in our case)
So we can create this template in our case (let me know what you think):

```
{
    "name": accident_count_by_accident_year,
    "data": {
                "items": {
                            "series1":
                                    {"category1": { "name": 2018,
                                                              "value": 3,
                                                     },
                                     "category2": { "name": 2019,
                                                              "value": 2,
                                                     },
                                     "category3": { "name": 2020,
                                                              "value": 1,
                                                     },
                                    },
                            "series2":
                                    {"category1": { "name": 2018,
                                                              "value": 30,
                                                     },
                                     "category2": { "name": 2019,
                                                              "value": 20,
                                                     },
                                     "category3": { "name": 2020,
                                                              "value": 10,
                                                     },
                                    },
                            "series3":
                                    {"category1": { "name": 2018,
                                                              "value": 300,
                                                     },
                                     "category2": { "name": 2019,
                                                              "value": 200,
                                                     },
                                     "category3": { "name": 2020,
                                                              "value": 100,
                                                     },
                                    }

                        },
              "text": {
                          "title": "כמות התאונות לפי שנה במקטע כניסה למצפה שלם - צומת שדי תרומות"
                           "labels": {
                                            "series1": { "name": "fatal",
                                                              "value": "קטלנית",
                                                     },
                                            "series2": { "name": "severe",
                                                              "value": "קשה",
                                                     },
                                            "series3": { "name": "light",
                                                              "value": "קלה",
                                                     },
                                      }
                        }
            }
    "meta": {
        "rank": 8,
        "information": ""
        type: "stacked_bar"
    }
}
```
@ziv17 also - when count of accidents/injured in specific year and severity is 0 - we should still display the category.
Thanks!
@atalyaalon OK. Why are we naming the first level `label`, and the second level `category`? Both are attributes of accident.
> 
> 
> @ziv17 what do you think?
> Here we had for example text separated from count- perhaps we should keep it as better practice? Not having translations as keys - since then do need to differentiate them (killed should be in red - and the FE needs to know how many killed using the key - in any language

@atalyaalon  I think it is a good idea to not use translated text as labels, and not use it in any logic.
It will be even better not to use the English string, but a code, or `Enum`, and only translate it just before it is sent outside.
@ziv17 sounds good! Go for it.

Resolving patch:
diff --git a/anyway/backend_constants.py b/anyway/backend_constants.py
--- a/anyway/backend_constants.py
+++ b/anyway/backend_constants.py
@@ -1,5 +1,7 @@
 from enum import Enum
-from typing import List
+from typing import List, Iterable
+from flask_babel import _
+# noinspection PyProtectedMember
 
 
 class BackEndConstants(object):
@@ -18,44 +20,6 @@ class BackEndConstants(object):
 
     ALL_AGE_GROUPS_LIST = list(range(1, AGE_GROUPS_NUMBER + 1)) + [99]
 
-    # This is a type for the field 'injury_severity' in the table 'involved_markers_hebrew'
-    class InjurySeverity:
-        DEAD = 1
-        SEVERE = 2
-        LIGHT = 3
-
-    # This is a type for the 'accident_severity' table field name
-    class AccidentSeverity:
-        FATAL = 1
-        SEVERE = 2
-        LIGHT = 3
-
-    class AccidentType:
-        PEDESTRIAN_INJURY = 1
-        COLLISION_OF_FRONT_TO_SIDE = 2
-        COLLISION_OF_FRONT_TO_REAR_END = 3
-        COLLISION_OF_SIDE_TO_SIDE_LATERAL = 4
-        HEAD_ON_FRONTAL_COLLISION = 5
-        COLLISION_WITH_A_STOPPED_NON_PARKED_VEHICLE = 6
-        COLLISION_WITH_A_PARKED_VEHICLE = 7
-        COLLISION_WITH_AN_INANIMATE_OBJECT = 8
-        SWERVING_OFF_THE_ROAD_OR_ONTO_THE_PAVEMENT = 9
-        OVERTURNED_VEHICLE = 10
-        SKID = 11
-        INJURY_OF_A_PASSENGER_IN_A_VEHICLE = 12
-        A_FALL_FROM_A_MOVING_VEHICLE = 13
-        FIRE = 14
-        OTHER = 15
-        COLLISION_OF_REAR_END_TO_FRONT = 17
-        COLLISION_OF_REAR_END_TO_SIDE = 18
-        COLLISION_WITH_AN_ANIMAL = 19
-        DAMAGE_CAUSED_BY_A_FALLING_LOAD_OFF_A_VEHICLE = 20
-
-    class DriverType:
-        PROFESSIONAL_DRIVER = 1
-        PRIVATE_VEHICLE_DRIVER = 2
-        OTHER_DRIVER = 3
-
     # This class should be correlated with the Roles table
     class Roles2Names(Enum):
         Admins = "admins"
@@ -120,3 +84,114 @@ class ResolutionCategories(Enum):
 
 
 BE_CONST = BackEndConstants()
+
+
+class LabeledCode(Enum):
+
+    def get_label(self) -> str:
+        return type(self).labels()[self]
+
+    @classmethod
+    def codes(cls: Iterable) -> List[int]:
+        if isinstance(cls, Iterable):
+            return [a.value for a in cls]
+        else:
+            raise NotImplementedError(f'{cls}: needs to be derived from Enum')
+
+    @classmethod
+    def labels(cls):
+        return {}
+
+
+# This is a type for the field 'injury_severity' in the table 'involved_markers_hebrew'
+class InjurySeverity(LabeledCode):
+    KILLED = 1
+    SEVERE_INJURED = 2
+    LIGHT_INJURED = 3
+
+    @classmethod
+    def labels(cls):
+        return {
+            InjurySeverity.KILLED: "killed",
+            InjurySeverity.SEVERE_INJURED: "severe injured",
+            InjurySeverity.LIGHT_INJURED: "light injured",
+        }
+
+
+_("killed")
+_("severe injured")
+_("light injured")
+
+# This is a type for the 'accident_severity' table field name
+class AccidentSeverity(LabeledCode):
+    FATAL = 1
+    SEVERE = 2
+    LIGHT = 3
+
+    @classmethod
+    def labels(cls):
+        return {
+            AccidentSeverity.FATAL: "fatal",
+            AccidentSeverity.SEVERE: "severe",
+            AccidentSeverity.LIGHT: "light",
+        }
+
+
+class AccidentType(LabeledCode):
+    PEDESTRIAN_INJURY = 1
+    COLLISION_OF_FRONT_TO_SIDE = 2
+    COLLISION_OF_FRONT_TO_REAR_END = 3
+    COLLISION_OF_SIDE_TO_SIDE_LATERAL = 4
+    HEAD_ON_FRONTAL_COLLISION = 5
+    COLLISION_WITH_A_STOPPED_NON_PARKED_VEHICLE = 6
+    COLLISION_WITH_A_PARKED_VEHICLE = 7
+    COLLISION_WITH_AN_INANIMATE_OBJECT = 8
+    SWERVING_OFF_THE_ROAD_OR_ONTO_THE_PAVEMENT = 9
+    OVERTURNED_VEHICLE = 10
+    SKID = 11
+    INJURY_OF_A_PASSENGER_IN_A_VEHICLE = 12
+    A_FALL_FROM_A_MOVING_VEHICLE = 13
+    FIRE = 14
+    OTHER = 15
+    COLLISION_OF_REAR_END_TO_FRONT = 17
+    COLLISION_OF_REAR_END_TO_SIDE = 18
+    COLLISION_WITH_AN_ANIMAL = 19
+    DAMAGE_CAUSED_BY_A_FALLING_LOAD_OFF_A_VEHICLE = 20
+
+    @classmethod
+    def labels(cls):
+        return {
+            AccidentType.PEDESTRIAN_INJURY: "Pedestrian injury",
+            AccidentType.COLLISION_OF_FRONT_TO_SIDE: "Collision of front to side",
+            AccidentType.COLLISION_OF_FRONT_TO_REAR_END: "Collision of front to rear-end",
+            AccidentType.COLLISION_OF_SIDE_TO_SIDE_LATERAL: "Collision of side to side (lateral)",
+            AccidentType.HEAD_ON_FRONTAL_COLLISION: "Head-on frontal collision",
+            AccidentType.COLLISION_WITH_A_STOPPED_NON_PARKED_VEHICLE: "Collision with a stopped non-parked vehicle",
+            AccidentType.COLLISION_WITH_A_PARKED_VEHICLE: "Collision with a parked vehicle",
+            AccidentType.COLLISION_WITH_AN_INANIMATE_OBJECT: "Collision with an inanimate object",
+            AccidentType.SWERVING_OFF_THE_ROAD_OR_ONTO_THE_PAVEMENT: "Swerving off the road or onto the pavement",
+            AccidentType.OVERTURNED_VEHICLE: "Overturned vehicle",
+            AccidentType.SKID: "Skid",
+            AccidentType.INJURY_OF_A_PASSENGER_IN_A_VEHICLE: "Injury of a passenger in a vehicle",
+            AccidentType.A_FALL_FROM_A_MOVING_VEHICLE: "A fall from a moving vehicle",
+            AccidentType.FIRE: "Fire",
+            AccidentType.OTHER: "Other",
+            AccidentType.COLLISION_OF_REAR_END_TO_FRONT: "Collision of rear-end to front",
+            AccidentType.COLLISION_OF_REAR_END_TO_SIDE: "Collision of rear-end to side",
+            AccidentType.COLLISION_WITH_AN_ANIMAL: "Collision with an animal",
+            AccidentType.DAMAGE_CAUSED_BY_A_FALLING_LOAD_OFF_A_VEHICLE: "Damage caused by a falling load off a vehicle",
+        }
+
+
+class DriverType(LabeledCode):
+    PROFESSIONAL_DRIVER = 1
+    PRIVATE_VEHICLE_DRIVER = 2
+    OTHER_DRIVER = 3
+
+    @classmethod
+    def labels(cls):
+        return {
+            DriverType.PROFESSIONAL_DRIVER: "professional_driver",
+            DriverType.PRIVATE_VEHICLE_DRIVER: "private_vehicle_driver",
+            DriverType.OTHER_DRIVER: "other_driver",
+        }
diff --git a/anyway/infographics_dictionaries.py b/anyway/infographics_dictionaries.py
--- a/anyway/infographics_dictionaries.py
+++ b/anyway/infographics_dictionaries.py
@@ -1,64 +1,18 @@
-from anyway.backend_constants import BE_CONST
-
 # noinspection PyProtectedMember
 from flask_babel import _
 
-english_driver_type_dict = {
-    BE_CONST.DriverType.PROFESSIONAL_DRIVER: "professional_driver",
-    BE_CONST.DriverType.PRIVATE_VEHICLE_DRIVER: "private_vehicle_driver",
-    BE_CONST.DriverType.OTHER_DRIVER: "other_driver",
-}
-
 head_on_collisions_comparison_dict = dict(
     head_to_head_collision="התנגשות חזית בחזית", others="אחרות", head_to_head="חזיתיות"
 )
-english_accident_severity_dict = {
-    BE_CONST.AccidentSeverity.FATAL: "fatal",
-    BE_CONST.AccidentSeverity.SEVERE: "severe",
-    BE_CONST.AccidentSeverity.LIGHT: "light",
-}
-
-hebrew_accident_severity_dict = {
-    BE_CONST.AccidentSeverity.FATAL: "קטלנית",
-    BE_CONST.AccidentSeverity.SEVERE: "קשה",
-    BE_CONST.AccidentSeverity.LIGHT: "קלה",
-}
-
-english_accident_type_dict = {
-    BE_CONST.AccidentType.PEDESTRIAN_INJURY: "Pedestrian injury",
-    BE_CONST.AccidentType.COLLISION_OF_FRONT_TO_SIDE: "Collision of front to side",
-    BE_CONST.AccidentType.COLLISION_OF_FRONT_TO_REAR_END: "Collision of front to rear-end",
-    BE_CONST.AccidentType.COLLISION_OF_SIDE_TO_SIDE_LATERAL: "Collision of side to side (lateral)",
-    BE_CONST.AccidentType.HEAD_ON_FRONTAL_COLLISION: "Head-on frontal collision",
-    BE_CONST.AccidentType.COLLISION_WITH_A_STOPPED_NON_PARKED_VEHICLE: "Collision with a stopped non-parked vehicle",
-    BE_CONST.AccidentType.COLLISION_WITH_A_PARKED_VEHICLE: "Collision with a parked vehicle",
-    BE_CONST.AccidentType.COLLISION_WITH_AN_INANIMATE_OBJECT: "Collision with an inanimate object",
-    BE_CONST.AccidentType.SWERVING_OFF_THE_ROAD_OR_ONTO_THE_PAVEMENT: "Swerving off the road or onto the pavement",
-    BE_CONST.AccidentType.OVERTURNED_VEHICLE: "Overturned vehicle",
-    BE_CONST.AccidentType.SKID: "Skid",
-    BE_CONST.AccidentType.INJURY_OF_A_PASSENGER_IN_A_VEHICLE: "Injury of a passenger in a vehicle",
-    BE_CONST.AccidentType.A_FALL_FROM_A_MOVING_VEHICLE: "A fall from a moving vehicle",
-    BE_CONST.AccidentType.FIRE: "Fire",
-    BE_CONST.AccidentType.OTHER: "Other",
-    BE_CONST.AccidentType.COLLISION_OF_REAR_END_TO_FRONT: "Collision of rear-end to front",
-    BE_CONST.AccidentType.COLLISION_OF_REAR_END_TO_SIDE: "Collision of rear-end to side",
-    BE_CONST.AccidentType.COLLISION_WITH_AN_ANIMAL: "Collision with an animal",
-    BE_CONST.AccidentType.DAMAGE_CAUSED_BY_A_FALLING_LOAD_OFF_A_VEHICLE: "Damage caused by a falling load off a vehicle",
-}
 
-english_injury_severity_dict = {
-    BE_CONST.InjurySeverity.DEAD: "fatal",
-    BE_CONST.InjurySeverity.SEVERE: "severe",
-    BE_CONST.InjurySeverity.LIGHT: "light",
-}
 
 # temporary until we'll create translations
-class smart_dict(dict):
+class SmartDict(dict):
     def __missing__(self, key):
         return key
 
 
-segment_dictionary = smart_dict()
+segment_dictionary = SmartDict()
 
 # bogus calls to gettext to get pybabel extract to recognize the strings
 _("fatal")
diff --git a/anyway/infographics_utils.py b/anyway/infographics_utils.py
--- a/anyway/infographics_utils.py
+++ b/anyway/infographics_utils.py
@@ -5,6 +5,7 @@
 import os
 import copy
 from functools import lru_cache
+from collections import defaultdict
 from typing import Optional, Dict, List, Union, Any, Type, Callable
 
 # noinspection PyUnresolvedReferences
@@ -20,18 +21,15 @@
 
 # noinspection PyProtectedMember
 from flask_babel import _
-from anyway.backend_constants import BE_CONST
+from anyway.backend_constants import (
+    BE_CONST, LabeledCode, InjurySeverity, AccidentSeverity, DriverType, AccidentType
+)
 from anyway.models import NewsFlash, AccidentMarkerView, InvolvedMarkerView, VehicleMarkerView
 from anyway.parsers import resolution_dict
 from anyway.app_and_db import db
 from anyway.infographics_dictionaries import (
-    english_driver_type_dict,
     head_on_collisions_comparison_dict,
-    english_accident_severity_dict,
-    english_accident_type_dict,
     segment_dictionary,
-    english_injury_severity_dict,
-    hebrew_accident_severity_dict,
 )
 from anyway.parsers import infographics_data_cache_updater
 from anyway.utilities import parse_age_from_range
@@ -241,6 +239,8 @@ def get_accident_count_by_severity(location_info, start_time, end_time):
 
 # adding calls to _() for pybabel extraction
 _("Fatal, severe and light accidents count in the specified location.")
+_("Fatal, severe and light injured count in the specified years, split by injury severity")
+_("Fatal, severe and light accidents count in the specified years, split by accident severity")
 
 
 @register
@@ -278,7 +278,7 @@ def prepare_table(location_info, start_time, end_time):
         )
         # Add casualties
         for accident in accidents:
-            accident["type"] = english_accident_type_dict[accident["accident_type"]]
+            accident["type"] = AccidentType(accident["accident_type"]).get_label()
             dt = accident["accident_timestamp"].to_pydatetime()
             accident["date"] = dt.strftime("%d/%m/%y")
             accident["hour"] = dt.strftime("%H:%M")
@@ -301,9 +301,7 @@ def prepare_table(location_info, start_time, end_time):
                 accident["id"],
                 accident["provider_code"],
             )
-            accident["accident_severity"] = english_accident_severity_dict[
-                accident["accident_severity"]
-            ]
+            accident["accident_severity"] = AccidentSeverity(accident["accident_severity"]).get_label()
         return accidents
 
     @staticmethod
@@ -360,14 +358,14 @@ def get_most_severe_accidents(table_obj, filters, start_time, end_time, limit=10
             table_obj, filters, entities, start_time, end_time, limit
         )
         for item in items:
-            item["accident_severity"] = hebrew_accident_severity_dict[item["accident_severity"]]
+            item["accident_severity"] = _(AccidentSeverity(item["accident_severity"]).get_label())
         return items
 
     @staticmethod
     def localize_items(request_params: RequestParams, items: Dict) -> Dict:
         for item in items["data"]["items"]:
             try:
-                item["accident_type"] = _(english_accident_type_dict[item["accident_type"]])
+                item["accident_type"] = _(AccidentType(item["accident_type"]).get_label())
             except KeyError:
                 logging.exception(
                     f"MostSevereAccidentsWidget.localize_items: Exception while translating {item}."
@@ -413,9 +411,10 @@ def generate_items(self) -> None:
     def get_head_to_head_stat(self) -> Dict:
         news_flash = self.request_params.news_flash_obj
         road_data = {}
+
         filter_dict = {
             "road_type": BE_CONST.ROAD_TYPE_NOT_IN_CITY_NOT_IN_INTERSECTION,
-            "accident_severity": BE_CONST.AccidentSeverity.FATAL,
+            "accident_severity": AccidentSeverity.FATAL.value,  # pylint: disable=no-member
         }
         all_roads_data = get_accidents_stats(
             table_obj=AccidentMarkerView,
@@ -443,10 +442,12 @@ def get_head_to_head_stat(self) -> Dict:
             )
 
         road_sums = self.sum_count_of_accident_type(
-            road_data, BE_CONST.AccidentType.HEAD_ON_FRONTAL_COLLISION
+            # pylint: disable=no-member
+            road_data, AccidentType.HEAD_ON_FRONTAL_COLLISION.value
         )
         all_roads_sums = self.sum_count_of_accident_type(
-            all_roads_data, BE_CONST.AccidentType.HEAD_ON_FRONTAL_COLLISION
+            # pylint: disable=no-member
+            all_roads_data, AccidentType.HEAD_ON_FRONTAL_COLLISION.value
         )
 
         res = {
@@ -550,8 +551,10 @@ def __init__(self, request_params: RequestParams):
     def generate_items(self) -> None:
         accidents_heat_map_filters = self.request_params.location_info.copy()
         accidents_heat_map_filters["accident_severity"] = [
-            BE_CONST.AccidentSeverity.FATAL,
-            BE_CONST.AccidentSeverity.SEVERE,
+            # pylint: disable=no-member
+            AccidentSeverity.FATAL.value,
+            # pylint: disable=no-member
+            AccidentSeverity.SEVERE.value,
         ]
         self.items = AccidentsHeatMapWidget.get_accidents_heat_map(
             filters=accidents_heat_map_filters,
@@ -589,19 +592,38 @@ def __init__(self, request_params: RequestParams):
         super().__init__(request_params, type(self).name)
         self.rank = 8
         self.text = {
-            "title": "כמות התאונות לפי שנה במקטע "
-            + self.request_params.location_info["road_segment_name"]
+            # "title" will be set in localize_items()
+            "labels": gen_entity_labels(AccidentSeverity)
         }
+        self.information = "Fatal, severe and light accidents count in the specified years, split by accident severity"
 
     def generate_items(self) -> None:
-        self.items = get_accidents_stats(
+
+        res = get_accidents_stats(
             table_obj=AccidentMarkerView,
             filters=self.request_params.location_info,
-            group_by="accident_year",
+            group_by=("accident_severity", "accident_year"),
             count="accident_year",
             start_time=self.request_params.start_time,
-            end_time=self.request_params.end_time,
+            end_time=self.request_params.end_time
         )
+        try:
+            self.items = add_empty_keys_to_gen_two_level_dict(
+                res,
+                AccidentSeverity.codes(),
+                list(range(self.request_params.start_time.year,
+                           self.request_params.end_time.year + 1))
+            )
+        except Exception as e:
+            logging.exception(f'failed to add empty keys to {res}', e)
+
+    @staticmethod
+    def localize_items(request_params: RequestParams, items: Dict) -> Dict:
+        items["data"]["text"]["title"] =\
+            _("Number of accidents, by year, splitted by accident severity, in segment")\
+            + " "\
+            + segment_dictionary[request_params.location_info["road_segment_name"]]
+        return items
 
 
 @register
@@ -612,19 +634,37 @@ def __init__(self, request_params: RequestParams):
         super().__init__(request_params, type(self).name)
         self.rank = 9
         self.text = {
-            "title": "נפגעים בתאונות במקטע "
-            + self.request_params.location_info["road_segment_name"]
+            # "title" will be set in localize_items()
+            "labels": gen_entity_labels(InjurySeverity)
         }
+        self.information = "Fatal, severe and light injured count in the specified years, split by injury severity"
 
     def generate_items(self) -> None:
-        self.items = get_accidents_stats(
+        res = get_accidents_stats(
             table_obj=InvolvedMarkerView,
             filters=get_injured_filters(self.request_params.location_info),
-            group_by="accident_year",
+            group_by=("injury_severity", "accident_year"),
             count="accident_year",
             start_time=self.request_params.start_time,
             end_time=self.request_params.end_time,
         )
+        try:
+            self.items = add_empty_keys_to_gen_two_level_dict(
+                res,
+                InjurySeverity.codes(),
+                list(range(self.request_params.start_time.year,
+                           self.request_params.end_time.year + 1))
+            )
+        except Exception as e:
+            logging.exception(f'failed to add empty keys to {res}', e)
+
+    @staticmethod
+    def localize_items(request_params: RequestParams, items: Dict) -> Dict:
+        items["data"]["text"]["title"] =\
+            _("Number of injured in accidents, per year, splitted by severity, in segment")\
+            + " "\
+            + segment_dictionary[request_params.location_info["road_segment_name"]]
+        return items
 
 
 @register
@@ -771,8 +811,8 @@ def filter_and_group_injured_count_per_age_group(request_params: RequestParams):
             .filter(
                 InvolvedMarkerView.injury_severity.in_(
                     [
-                        BE_CONST.InjurySeverity.DEAD,
-                        BE_CONST.InjurySeverity.SEVERE,
+                        InjurySeverity.KILLED.value,  # pylint: disable=no-member
+                        InjurySeverity.SEVERE_INJURED.value,  # pylint: disable=no-member
                     ]
                 )
             )
@@ -797,7 +837,7 @@ def defaultdict_int_factory() -> Callable:
 
         for row in query:
             age_range = row.age_group
-            injury_name = english_injury_severity_dict[row.injury_severity]
+            injury_name = InjurySeverity(row.injury_severity).get_label()
             count = row.count
 
             # The age groups in the DB are not the same age groups in the Widget - so we need to merge some of the groups
@@ -845,7 +885,7 @@ def __init__(self, request_params: RequestParams):
     def generate_items(self) -> None:
         self.items = {"image_src": "vision_zero_2_plus_1"}
 
-    def get_frontal_accidents_in_past_year(self) -> int:
+    def get_frontal_accidents_in_past_year(self) -> Optional[int]:
         news_flash = self.request_params.news_flash_obj
         road_data = {}
         filter_dict = {
@@ -869,14 +909,14 @@ def get_frontal_accidents_in_past_year(self) -> int:
             )
 
             road_sums = self.sum_count_of_accident_type(
-                road_data, BE_CONST.AccidentType.HEAD_ON_FRONTAL_COLLISION
+                # pylint: disable=no-member
+                road_data, AccidentType.HEAD_ON_FRONTAL_COLLISION.value
             )
 
-            frontal_accidents_severe_fatal_past_year = road_sums
-            return frontal_accidents_severe_fatal_past_year
+            return road_sums
 
     @staticmethod
-    def sum_count_of_accident_type(data: Dict, acc_type: int) -> Dict:
+    def sum_count_of_accident_type(data: Dict, acc_type: int) -> int:
         given = sum([d["count"] for d in data if d["accident_type"] == acc_type])
         return given
 
@@ -919,17 +959,20 @@ def count_accidents_by_driver_type(request_params):
             vehicle_type, count = item["involve_vehicle_type"], int(item["count"])
             if vehicle_type in VehicleCategory.PROFESSIONAL_DRIVER.get_codes():
                 driver_types[
-                    english_driver_type_dict[BE_CONST.DriverType.PROFESSIONAL_DRIVER]
+                    # pylint: disable=no-member
+                    DriverType.PROFESSIONAL_DRIVER.get_label()
                 ] += count
             elif vehicle_type in VehicleCategory.PRIVATE_DRIVER.get_codes():
                 driver_types[
-                    english_driver_type_dict[BE_CONST.DriverType.PRIVATE_VEHICLE_DRIVER]
+                    # pylint: disable=no-member
+                    DriverType.PRIVATE_VEHICLE_DRIVER.get_label()
                 ] += count
             elif (
                 vehicle_type in VehicleCategory.LIGHT_ELECTRIC.get_codes()
                 or vehicle_type in VehicleCategory.OTHER.get_codes()
             ):
-                driver_types[english_driver_type_dict[BE_CONST.DriverType.OTHER_DRIVER]] += count
+                # pylint: disable=no-member
+                driver_types[DriverType.OTHER_DRIVER.get_label()] += count
         output = [
             {"driver_type": driver_type, "count": count}
             for driver_type, count in driver_types.items()
@@ -1266,7 +1309,8 @@ def motorcycle_accidents_vs_all_accidents(
             .filter(InvolvedMarkerView.road_type.in_(BE_CONST.NON_CITY_ROAD_TYPES))
             .filter(
                 InvolvedMarkerView.accident_severity.in_(
-                    [BE_CONST.AccidentSeverity.FATAL, BE_CONST.AccidentSeverity.SEVERE]
+                    # pylint: disable=no-member
+                    [AccidentSeverity.FATAL.value, AccidentSeverity.SEVERE.value]
                 )
             )
             .group_by(location_label, vehicle_label)
@@ -1497,7 +1541,7 @@ def localize_items(request_params: RequestParams, items: Dict) -> Dict:
         for item in items["data"]["items"]:
             try:
                 item[VehicleMarkerView.accident_type.name] = _(
-                    english_accident_type_dict[item["accident_type"]]
+                    AccidentType(item["accident_type"]).get_label()
                 )
             except KeyError:
                 logging.exception(
@@ -1554,8 +1598,20 @@ def get_accidents_stats(
     # get stats
     query = get_query(table_obj, filters, start_time, end_time)
     if group_by:
-        query = query.group_by(group_by)
-        query = query.with_entities(group_by, func.count(count))
+        if isinstance(group_by, tuple):
+            if len(group_by) == 2:
+                query = query.group_by(*group_by)
+                query = query.with_entities(*group_by, func.count(count))
+                dd = query.all()
+                res = retro_dictify(dd)
+                return res
+            else:
+                err_msg = f'get_accidents_stats: {group_by}: Only a string or a tuple of two are valid for group_by'
+                logging.error(err_msg)
+                raise Exception(err_msg)
+        else:
+            query = query.group_by(group_by)
+            query = query.with_entities(group_by, func.count(count))
     df = pd.read_sql_query(query.statement, query.session.bind)
     df.rename(columns={"count_1": "count"}, inplace=True)  # pylint: disable=no-member
     df.columns = [c.replace("_hebrew", "") for c in df.columns]
@@ -1564,6 +1620,30 @@ def get_accidents_stats(
     )
 
 
+def add_empty_keys_to_gen_two_level_dict(d, level_1_values: List[Any], level_2_values: List[Any],
+                                         default_level_3_value: int = 0) -> Dict[Any, Dict[Any, int]]:
+    for v1 in level_1_values:
+        if v1 not in d:
+            d[v1] = {}
+        for v2 in level_2_values:
+            if v2 not in d[v1]:
+                d[v1][v2] = default_level_3_value
+    return d
+
+
+# noinspection Mypy
+def retro_dictify(indexable) -> Dict[Any, Dict[Any, Any]]:
+    d = defaultdict(dict)
+    for row in indexable:
+        here = d
+        for elem in row[:-2]:
+            if elem not in here:
+                here[elem] = defaultdict(lambda : 0)
+            here = here[elem]
+        here[row[-2]] = row[-1]
+    return d
+
+
 def get_injured_filters(location_info):
     new_filters = {}
     for curr_filter, curr_values in location_info.items():
@@ -1584,9 +1664,10 @@ def get_most_severe_accidents_with_entities(
         BE_CONST.CBS_ACCIDENT_TYPE_1_CODE,
         BE_CONST.CBS_ACCIDENT_TYPE_3_CODE,
     ]
+    # pylint: disable=no-member
     filters["accident_severity"] = [
-        BE_CONST.AccidentSeverity.FATAL,
-        BE_CONST.AccidentSeverity.SEVERE,
+        AccidentSeverity.FATAL.value,
+        AccidentSeverity.SEVERE.value,
     ]
     query = get_query(table_obj, filters, start_time, end_time)
     query = query.with_entities(*entities)
@@ -1683,7 +1764,8 @@ def sum_road_accidents_by_specific_type(road_data, field_name):
 def convert_roads_fatal_accidents_to_frontend_view(data_dict):
     data_list = []
     for key, value in data_dict.items():
-        if key == BE_CONST.AccidentType.HEAD_ON_FRONTAL_COLLISION:
+        # pylint: disable=no-member
+        if key == AccidentType.HEAD_ON_FRONTAL_COLLISION.value:
             data_list.append(
                 {"desc": head_on_collisions_comparison_dict["head_to_head"], "count": value}
             )
@@ -1705,6 +1787,15 @@ def get_latest_accident_date(table_obj, filters):
     return (df.to_dict(orient="records"))[0].get("max_1")  # pylint: disable=no-member
 
 
+def gen_entity_labels(entity: Type[LabeledCode]) -> dict:
+    res = {}
+    for e in entity:
+        label = e.get_label()
+        res[e.value] = {"name": label,
+                        "localized_name": _(label)}
+    return res
+
+
 # noinspection PyArgumentList
 def generate_widgets(request_params: RequestParams, to_cache: bool = True) -> List[Widget]:
     widgets = []


Test patch: