From d2981688f32b41d9ed94e11b85dbbf129ce59128 Mon Sep 17 00:00:00 2001
From: Remco Tukker <remco.tukker@geant.org>
Date: Thu, 14 Sep 2023 15:57:03 +0200
Subject: [PATCH 1/2] added datamodels for policy, connected users, and network
 questions

---
 .../background_task/parse_excel_data.py       |   2 +-
 compendium_v2/db/model.py                     | 347 ++++++++++++++++-
 compendium_v2/db/model_enums.py               |  88 +++++
 .../8ff7260ad48f_add_presentation_models.py   | 359 ++++++++++++++++++
 .../publishers/survey_publisher_2022.py       |   2 +-
 .../publishers/survey_publisher_v2.py         |  26 +-
 test/test_survey_publisher_2022.py            |   8 +-
 test/test_survey_publisher_v2.py              |  29 +-
 8 files changed, 838 insertions(+), 23 deletions(-)
 create mode 100644 compendium_v2/db/model_enums.py
 create mode 100644 compendium_v2/migrations/versions/8ff7260ad48f_add_presentation_models.py

diff --git a/compendium_v2/background_task/parse_excel_data.py b/compendium_v2/background_task/parse_excel_data.py
index aba3ef6f..69612055 100644
--- a/compendium_v2/background_task/parse_excel_data.py
+++ b/compendium_v2/background_task/parse_excel_data.py
@@ -2,7 +2,7 @@ import logging
 import openpyxl
 import os
 
-from compendium_v2.db.model import FeeType
+from compendium_v2.db.model_enums import FeeType
 from compendium_v2.environment import setup_logging
 
 setup_logging()
diff --git a/compendium_v2/db/model.py b/compendium_v2/db/model.py
index a9d92279..5464188b 100644
--- a/compendium_v2/db/model.py
+++ b/compendium_v2/db/model.py
@@ -3,24 +3,44 @@ from __future__ import annotations
 
 import logging
 from decimal import Decimal
-from enum import Enum
 from typing import List, Optional
-from typing_extensions import Annotated
+from typing_extensions import Annotated, TypedDict
 
 from sqlalchemy import String, JSON
 from sqlalchemy.orm import Mapped, mapped_column, relationship
 from sqlalchemy.schema import ForeignKey
 
 from compendium_v2.db import db
+from compendium_v2.db.model_enums import CarryMechanism, CommarcialChargingLevel, UserCategory, ServiceCategory, \
+    ConnectivityCoverage, ConnectionMethod, YesNoPlanned, MonitoringMethod, CommercialConnectivityCoverage, FeeType
 
 
 logger = logging.getLogger(__name__)
 
+
 str128 = Annotated[str, 128]
 str128_pk = Annotated[str, mapped_column(String(128), primary_key=True)]
 str256_pk = Annotated[str, mapped_column(String(256), primary_key=True)]
 int_pk = Annotated[int, mapped_column(primary_key=True)]
 int_pk_fkNREN = Annotated[int, mapped_column(ForeignKey("nren.id"), primary_key=True)]
+user_category_pk = Annotated[UserCategory, mapped_column(primary_key=True)]
+json_str_list = Annotated[List[str], mapped_column(JSON)]
+
+ExternalConnection = TypedDict(
+    'ExternalConnection',
+    {
+        'link_name': str,
+        'capacity': Optional[Decimal],
+        'from_organization': str,
+        'to_organization': str,
+        'interconnection_method': Optional[ConnectionMethod]
+    }
+)
+
+RemoteCampus = TypedDict(
+    'RemoteCampus',
+    {'country': str, 'local_r_and_e_connection': Optional[bool]}
+)
 
 
 # Unfortunately flask-sqlalchemy doesnt fully support DeclarativeBase yet.
@@ -60,14 +80,6 @@ class FundingSource(db.Model):
     other: Mapped[Decimal]
 
 
-class FeeType(Enum):
-    flat_fee = "flat_fee"
-    usage_based_fee = "usage_based_fee"
-    combination = "combination"
-    no_charge = "no_charge"
-    other = "other"
-
-
 class ChargingStructure(db.Model):
     __tablename__ = 'charging_structure'
     nren_id: Mapped[int_pk_fkNREN]
@@ -140,8 +152,321 @@ class TrafficVolume(db.Model):
 
 class InstitutionURLs(db.Model):
     __tablename__ = 'institution_urls'
+    nren_id: Mapped[int_pk_fkNREN]
+    nren: Mapped[NREN] = relationship(lazy='joined')
+    year: Mapped[int_pk]
+    urls: Mapped[json_str_list]
+
+
+class CentralProcurement(db.Model):
+    __tablename__ = 'central_procurement'
+    nren_id: Mapped[int_pk_fkNREN]
+    nren: Mapped[NREN] = relationship(lazy='joined')
+    year: Mapped[int_pk]
+    central_procurement: Mapped[bool]
+    amount: Mapped[Optional[Decimal]]
+
+
+class ServiceManagement(db.Model):
+    __tablename__ = 'service_management'
+    nren_id: Mapped[int_pk_fkNREN]
+    nren: Mapped[NREN] = relationship(lazy='joined')
+    year: Mapped[int_pk]
+    service_management_framework: Mapped[Optional[bool]]
+    service_level_targets: Mapped[Optional[bool]]
+
+
+class ServiceUserTypes(db.Model):
+    __tablename__ = 'service_user_types'
+    nren_id: Mapped[int_pk_fkNREN]
+    nren: Mapped[NREN] = relationship(lazy='joined')
+    year: Mapped[int_pk]
+    user_category: Mapped[user_category_pk]
+    service_category: Mapped[ServiceCategory]
+
+
+class EOSCListings(db.Model):
+    __tablename__ = 'eosc_listings'
+    nren_id: Mapped[int_pk_fkNREN]
+    nren: Mapped[NREN] = relationship(lazy='joined')
+    year: Mapped[int_pk]
+    service_names: Mapped[json_str_list]
+
+
+class Standards(db.Model):
+    __tablename__ = 'standards'
+    nren_id: Mapped[int_pk_fkNREN]
+    nren: Mapped[NREN] = relationship(lazy='joined')
+    year: Mapped[int_pk]
+    audits: Mapped[Optional[bool]]
+    audit_specifics: Mapped[str]
+    business_continuity_plans: Mapped[Optional[bool]]
+    business_continuity_plans_specifics: Mapped[str]
+    crisis_management_procedure: Mapped[Optional[bool]]
+
+
+class CrisisExcercises(db.Model):
+    __tablename__ = 'crisis_excercises'
+    nren_id: Mapped[int_pk_fkNREN]
+    nren: Mapped[NREN] = relationship(lazy='joined')
+    year: Mapped[int_pk]
+    exercise_descriptions: Mapped[json_str_list]
+
+
+class SecurityControls(db.Model):
+    __tablename__ = 'security_controls'
+    nren_id: Mapped[int_pk_fkNREN]
+    nren: Mapped[NREN] = relationship(lazy='joined')
+    year: Mapped[int_pk]
+    security_control_descriptions: Mapped[json_str_list]
+
+
+class ConnectedProportion(db.Model):
+    __tablename__ = 'connected_proportion'
+    nren_id: Mapped[int_pk_fkNREN]
+    nren: Mapped[NREN] = relationship(lazy='joined')
+    year: Mapped[int_pk]
+    user_category: Mapped[user_category_pk]
+    coverage: Mapped[Optional[ConnectivityCoverage]]
+    number_connected: Mapped[Optional[int]]
+    market_share: Mapped[Optional[Decimal]]
+    users_served: Mapped[Optional[int]]
+
+
+class ConnectivityLevel(db.Model):
+    __tablename__ = 'connectivity_level'
+    nren_id: Mapped[int_pk_fkNREN]
+    nren: Mapped[NREN] = relationship(lazy='joined')
+    year: Mapped[int_pk]
+    user_category: Mapped[user_category_pk]
+    typical_speed: Mapped[Optional[int]]
+    highest_speed: Mapped[Optional[int]]
+    highest_speed_proportion: Mapped[Optional[Decimal]]
+
+
+class ConnectionCarrier(db.Model):
+    __tablename__ = 'connection_carrier'
+    nren_id: Mapped[int_pk_fkNREN]
+    nren: Mapped[NREN] = relationship(lazy='joined')
+    year: Mapped[int_pk]
+    user_category: Mapped[user_category_pk]
+    carry_mechanism: Mapped[CarryMechanism]
+
+
+class ConnectivityLoad(db.Model):
+    __tablename__ = 'connectivity_load'
+    nren_id: Mapped[int_pk_fkNREN]
+    nren: Mapped[NREN] = relationship(lazy='joined')
+    year: Mapped[int_pk]
+    user_category: Mapped[user_category_pk]
+    average_load_from_institutions: Mapped[Optional[int]]
+    average_load_to_institutions: Mapped[Optional[int]]
+    peak_load_from_institutions: Mapped[Optional[int]]
+    peak_load_to_institutions: Mapped[Optional[int]]
+
+
+class ConnectivityGrowth(db.Model):
+    __tablename__ = 'connectivity_growth'
+    nren_id: Mapped[int_pk_fkNREN]
+    nren: Mapped[NREN] = relationship(lazy='joined')
+    year: Mapped[int_pk]
+    user_category: Mapped[user_category_pk]
+    growth: Mapped[Decimal]
+
+
+class CommercialConnectivity(db.Model):
+    __tablename__ = 'commercial_connectivity'
+    nren_id: Mapped[int_pk_fkNREN]
+    nren: Mapped[NREN] = relationship(lazy='joined')
+    year: Mapped[int_pk]
+    commercial_r_and_e: Mapped[Optional[CommercialConnectivityCoverage]]
+    commercial_general: Mapped[Optional[CommercialConnectivityCoverage]]
+    commercial_collaboration: Mapped[Optional[CommercialConnectivityCoverage]]
+    commercial_service_provider: Mapped[Optional[CommercialConnectivityCoverage]]
+    university_spin_off: Mapped[Optional[CommercialConnectivityCoverage]]
+
+
+class CommercialChargingLevel(db.Model):
+    __tablename__ = 'commercial_charging_level'
+    nren_id: Mapped[int_pk_fkNREN]
+    nren: Mapped[NREN] = relationship(lazy='joined')
+    year: Mapped[int_pk]
+    collaboration: Mapped[Optional[CommarcialChargingLevel]]
+    service_supplier: Mapped[Optional[CommarcialChargingLevel]]
+    direct_peering: Mapped[Optional[CommarcialChargingLevel]]
+
+
+class RemoteCampuses(db.Model):
+    __tablename__ = 'remote_campuses'
+    nren_id: Mapped[int_pk_fkNREN]
+    nren: Mapped[NREN] = relationship(lazy='joined')
+    year: Mapped[int_pk]
+    remote_campus_connectivity: Mapped[bool]
+    connections: Mapped[List[RemoteCampus]] = mapped_column(JSON)
+
+
+class DarkFibreLease(db.Model):
+    __tablename__ = 'dark_fibre_lease'
+    nren_id: Mapped[int_pk_fkNREN]
+    nren: Mapped[NREN] = relationship(lazy='joined')
+    year: Mapped[int_pk]
+    iru_or_lease: Mapped[bool]
+    fibre_length_in_country: Mapped[Optional[int]]
+    fibre_length_outside_country: Mapped[Optional[int]]
+    iru_duration: Mapped[Optional[Decimal]]
+
+
+class DarkFibreInstalled(db.Model):
+    __tablename__ = 'dark_fibre_installed'
+    nren_id: Mapped[int_pk_fkNREN]
+    nren: Mapped[NREN] = relationship(lazy='joined')
+    year: Mapped[int_pk]
+    installed: Mapped[bool]
+    fibre_length_in_country: Mapped[Optional[int]]
+
+
+class FibreLight(db.Model):
+    __tablename__ = 'fibre_light'
+    nren_id: Mapped[int_pk_fkNREN]
+    nren: Mapped[NREN] = relationship(lazy='joined')
+    year: Mapped[int_pk]
+    light_description: Mapped[str]
+
+
+class NetworkMapUrls(db.Model):
+    __tablename__ = 'network_map_urls'
+    nren_id: Mapped[int_pk_fkNREN]
+    nren: Mapped[NREN] = relationship(lazy='joined')
+    year: Mapped[int_pk]
+    urls: Mapped[json_str_list]
+
+
+class MonitoringTools(db.Model):
+    __tablename__ = 'monitoring_tools'
+    nren_id: Mapped[int_pk_fkNREN]
+    nren: Mapped[NREN] = relationship(lazy='joined')
+    year: Mapped[int_pk]
+    tool_descriptions: Mapped[json_str_list]
+    netflow_processing_description: Mapped[str]
+
+
+class PassiveMonitoring(db.Model):
+    __tablename__ = 'passive_monitoring'
+    nren_id: Mapped[int_pk_fkNREN]
+    nren: Mapped[NREN] = relationship(lazy='joined')
+    year: Mapped[int_pk]
+    monitoring: Mapped[bool]
+    method: Mapped[Optional[MonitoringMethod]]
+
+
+class TrafficStatistics(db.Model):
+    __tablename__ = 'traffic_statistics'
+    nren_id: Mapped[int_pk_fkNREN]
+    nren: Mapped[NREN] = relationship(lazy='joined')
+    year: Mapped[int_pk]
+    traffic_statistics: Mapped[bool]
+    urls: Mapped[json_str_list]
+
+
+class SiemVendors(db.Model):
+    __tablename__ = 'siem_vendors'
+    nren_id: Mapped[int_pk_fkNREN]
+    nren: Mapped[NREN] = relationship(lazy='joined')
+    year: Mapped[int_pk]
+    vendor_names: Mapped[json_str_list]
+
+
+class CertificateProviders(db.Model):
+    __tablename__ = 'certificate_providers'
+    nren_id: Mapped[int_pk_fkNREN]
+    nren: Mapped[NREN] = relationship(lazy='joined')
+    year: Mapped[int_pk]
+    provider_names: Mapped[json_str_list]
+
+
+class WeatherMap(db.Model):
+    __tablename__ = 'weather_map'
+    nren_id: Mapped[int_pk_fkNREN]
+    nren: Mapped[NREN] = relationship(lazy='joined')
+    year: Mapped[int_pk]
+    weather_map: Mapped[bool]
+    url: Mapped[str]
+
+
+class PertTeam(db.Model):
+    __tablename__ = 'pert_team'
+    nren_id: Mapped[int_pk_fkNREN]
+    nren: Mapped[NREN] = relationship(lazy='joined')
+    year: Mapped[int_pk]
+    pert_team: Mapped[YesNoPlanned]
+
+
+class AlienWave(db.Model):
+    __tablename__ = 'alien_wave'
+    nren_id: Mapped[int_pk_fkNREN]
+    nren: Mapped[NREN] = relationship(lazy='joined')
+    year: Mapped[int_pk]
+    alien_wave_third_pary: Mapped[Optional[YesNoPlanned]]
+    nr_of_alien_wave_third_party_services: Mapped[Optional[int]]
+    alien_wave_internal: Mapped[Optional[bool]]
+
+
+class Capacity(db.Model):
+    __tablename__ = 'capacity'
+    nren_id: Mapped[int_pk_fkNREN]
+    nren: Mapped[NREN] = relationship(lazy='joined')
+    year: Mapped[int_pk]
+    largest_link_capacity: Mapped[Optional[Decimal]]
+    typical_backbone_capacity: Mapped[Optional[Decimal]]
+
+
+class ExternalConnections(db.Model):
+    __tablename__ = 'external_connections'
+    nren_id: Mapped[int_pk_fkNREN]
+    nren: Mapped[NREN] = relationship(lazy='joined')
+    year: Mapped[int_pk]
+    connections: Mapped[List[ExternalConnection]] = mapped_column(JSON)
+
+
+class NonREPeers(db.Model):
+    __tablename__ = 'non_r_and_e_peers'
+    nren_id: Mapped[int_pk_fkNREN]
+    nren: Mapped[NREN] = relationship(lazy='joined')
+    year: Mapped[int_pk]
+    nr_of_non_r_and_e_peers: Mapped[int]
+
+
+class TrafficRatio(db.Model):
+    __tablename__ = 'traffic_ratio'
+    nren_id: Mapped[int_pk_fkNREN]
+    nren: Mapped[NREN] = relationship(lazy='joined')
+    year: Mapped[int_pk]
+    r_and_e_percentage: Mapped[int]
+    commodity_percentage: Mapped[int]
+
+
+class OpsAutomation(db.Model):
+    __tablename__ = 'ops_automation'
+    nren_id: Mapped[int_pk_fkNREN]
+    nren: Mapped[NREN] = relationship(lazy='joined')
+    year: Mapped[int_pk]
+    ops_automation: Mapped[YesNoPlanned]
+    ops_automation_specifics: Mapped[str]
+
+
+class NetworkFunctionVirtualisation(db.Model):
+    __tablename__ = 'network_function_virtualisation'
+    nren_id: Mapped[int_pk_fkNREN]
+    nren: Mapped[NREN] = relationship(lazy='joined')
+    year: Mapped[int_pk]
+    nfv: Mapped[YesNoPlanned]
+    nfv_specifics: Mapped[json_str_list]
+
 
+class NetworkAutomation(db.Model):
+    __tablename__ = 'network_automation'
     nren_id: Mapped[int_pk_fkNREN]
     nren: Mapped[NREN] = relationship(lazy='joined')
     year: Mapped[int_pk]
-    urls: Mapped[List[str]] = mapped_column(JSON)
+    network_automation: Mapped[YesNoPlanned]
+    network_automation_specifics: Mapped[json_str_list]
diff --git a/compendium_v2/db/model_enums.py b/compendium_v2/db/model_enums.py
new file mode 100644
index 00000000..bda21ddf
--- /dev/null
+++ b/compendium_v2/db/model_enums.py
@@ -0,0 +1,88 @@
+from enum import Enum
+
+
+class FeeType(Enum):
+    flat_fee = "flat_fee"
+    usage_based_fee = "usage_based_fee"
+    combination = "combination"
+    no_charge = "no_charge"
+    other = "other"
+
+
+class UserCategory(Enum):
+    universities = "universities"
+    further_education = "further_education"
+    secondary_schools = "secondary_schools"
+    primary_schools = "primary_schools"
+    institutes = "institutes"
+    cultural = "cultural"
+    hospitals = "hospitals"
+    government = "government"
+    iros = "iros"
+    for_profit_orgs = "for_profit_orgs"
+
+
+class ServiceCategory(Enum):
+    network_services = "network_services"
+    isp_support = "isp_support"
+    security = "security"
+    identity = "identity"
+    collaboration = "collaboration"
+    multimedia = "multimedia"
+    storage_and_hosting = "storage_and_hosting"
+    professional_services = "professional_services"
+
+
+class ConnectivityCoverage(Enum):
+    yes_incl_other = "yes_incl_other"
+    yes_national_nren = "yes_national_nren"
+    sometimes = "sometimes"
+    no_policy = "no_policy"
+    no_financial = "no_financial"
+    no_other = "no_other"
+    unsure = "unsure"
+
+
+class CarryMechanism(Enum):
+    nren_local_loops = "nren_local_loops"
+    regional_nren_backbone = "regional_nren_backbone"
+    commercial_provider_backbone = "commercial_provider_backbone"
+    man = "man"
+    other = "other"
+
+
+class CommercialConnectivityCoverage(Enum):
+    yes_incl_other = "yes_incl_other"
+    yes_national_nren = "yes_national_nren"
+    yes_if_sponsored = "yes_if_sponsored"
+    no_but_direct_peering = "no_but_direct_peering"
+    no_policy = "no_policy"
+    no_financial = "no_financial"
+    no_other = "no_other"
+
+
+class CommarcialChargingLevel(Enum):
+    higher_than_r_e_charges = "higher_than_r_e_charges"
+    same_as_r_e_charges = "same_as_r_e_charges"
+    no_charges_if_r_e_requested = "no_charges_if_r_e_requested"
+    lower_than_r_e_charges = "lower_than_r_e_charges"
+
+
+class ConnectionMethod(Enum):
+    internet_exchange = "internet_exchange"
+    open_exchange = "open_exchange"
+    direct = "direct"
+    geant = "geant"
+    other = "other"
+
+
+class YesNoPlanned(Enum):
+    yes = "yes"
+    no = "no"
+    planned = "planned"
+
+
+class MonitoringMethod(Enum):
+    span_ports = "span_ports"
+    taps = "taps"
+    both = "both"
diff --git a/compendium_v2/migrations/versions/8ff7260ad48f_add_presentation_models.py b/compendium_v2/migrations/versions/8ff7260ad48f_add_presentation_models.py
new file mode 100644
index 00000000..67384a3f
--- /dev/null
+++ b/compendium_v2/migrations/versions/8ff7260ad48f_add_presentation_models.py
@@ -0,0 +1,359 @@
+"""add presentation models
+
+Revision ID: 8ff7260ad48f
+Revises: e17f9e6c90ab
+Create Date: 2023-09-14 15:09:36.301058
+
+"""
+
+# flake8: noqa
+
+from alembic import op
+import sqlalchemy as sa
+from sqlalchemy.dialects import postgresql
+
+# revision identifiers, used by Alembic.
+revision = '8ff7260ad48f'
+down_revision = 'e17f9e6c90ab'
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+    # ### commands auto generated by Alembic - please adjust! ###
+    sa.Enum('yes', 'no', 'planned', name='yesnoplanned').create(op.get_bind())
+    sa.Enum('span_ports', 'taps', 'both', name='monitoringmethod').create(op.get_bind())
+    sa.Enum('higher_than_r_e_charges', 'same_as_r_e_charges', 'no_charges_if_r_e_requested', 'lower_than_r_e_charges', name='commarcialcharginglevel').create(op.get_bind())
+    sa.Enum('yes_incl_other', 'yes_national_nren', 'yes_if_sponsored', 'no_but_direct_peering', 'no_policy', 'no_financial', 'no_other', name='commercialconnectivitycoverage').create(op.get_bind())
+    sa.Enum('nren_local_loops', 'regional_nren_backbone', 'commercial_provider_backbone', 'man', 'other', name='carrymechanism').create(op.get_bind())
+    sa.Enum('yes_incl_other', 'yes_national_nren', 'sometimes', 'no_policy', 'no_financial', 'no_other', 'unsure', name='connectivitycoverage').create(op.get_bind())
+    sa.Enum('network_services', 'isp_support', 'security', 'identity', 'collaboration', 'multimedia', 'storage_and_hosting', 'professional_services', name='servicecategory').create(op.get_bind())
+    sa.Enum('universities', 'further_education', 'secondary_schools', 'primary_schools', 'institutes', 'cultural', 'hospitals', 'government', 'iros', 'for_profit_orgs', name='usercategory').create(op.get_bind())
+    op.create_table('alien_wave',
+    sa.Column('nren_id', sa.Integer(), nullable=False),
+    sa.Column('year', sa.Integer(), nullable=False),
+    sa.Column('alien_wave_third_pary', postgresql.ENUM('yes', 'no', 'planned', name='yesnoplanned', create_type=False), nullable=True),
+    sa.Column('nr_of_alien_wave_third_party_services', sa.Integer(), nullable=True),
+    sa.Column('alien_wave_internal', sa.Boolean(), nullable=True),
+    sa.ForeignKeyConstraint(['nren_id'], ['nren.id'], name=op.f('fk_alien_wave_nren_id_nren')),
+    sa.PrimaryKeyConstraint('nren_id', 'year', name=op.f('pk_alien_wave'))
+    )
+    op.create_table('capacity',
+    sa.Column('nren_id', sa.Integer(), nullable=False),
+    sa.Column('year', sa.Integer(), nullable=False),
+    sa.Column('largest_link_capacity', sa.Numeric(), nullable=True),
+    sa.Column('typical_backbone_capacity', sa.Numeric(), nullable=True),
+    sa.ForeignKeyConstraint(['nren_id'], ['nren.id'], name=op.f('fk_capacity_nren_id_nren')),
+    sa.PrimaryKeyConstraint('nren_id', 'year', name=op.f('pk_capacity'))
+    )
+    op.create_table('central_procurement',
+    sa.Column('nren_id', sa.Integer(), nullable=False),
+    sa.Column('year', sa.Integer(), nullable=False),
+    sa.Column('central_procurement', sa.Boolean(), nullable=False),
+    sa.Column('amount', sa.Numeric(), nullable=True),
+    sa.ForeignKeyConstraint(['nren_id'], ['nren.id'], name=op.f('fk_central_procurement_nren_id_nren')),
+    sa.PrimaryKeyConstraint('nren_id', 'year', name=op.f('pk_central_procurement'))
+    )
+    op.create_table('certificate_providers',
+    sa.Column('nren_id', sa.Integer(), nullable=False),
+    sa.Column('year', sa.Integer(), nullable=False),
+    sa.Column('provider_names', sa.JSON(), nullable=False),
+    sa.ForeignKeyConstraint(['nren_id'], ['nren.id'], name=op.f('fk_certificate_providers_nren_id_nren')),
+    sa.PrimaryKeyConstraint('nren_id', 'year', name=op.f('pk_certificate_providers'))
+    )
+    op.create_table('commercial_charging_level',
+    sa.Column('nren_id', sa.Integer(), nullable=False),
+    sa.Column('year', sa.Integer(), nullable=False),
+    sa.Column('collaboration', postgresql.ENUM('higher_than_r_e_charges', 'same_as_r_e_charges', 'no_charges_if_r_e_requested', 'lower_than_r_e_charges', name='commarcialcharginglevel', create_type=False), nullable=True),
+    sa.Column('service_supplier', postgresql.ENUM('higher_than_r_e_charges', 'same_as_r_e_charges', 'no_charges_if_r_e_requested', 'lower_than_r_e_charges', name='commarcialcharginglevel', create_type=False), nullable=True),
+    sa.Column('direct_peering', postgresql.ENUM('higher_than_r_e_charges', 'same_as_r_e_charges', 'no_charges_if_r_e_requested', 'lower_than_r_e_charges', name='commarcialcharginglevel', create_type=False), nullable=True),
+    sa.ForeignKeyConstraint(['nren_id'], ['nren.id'], name=op.f('fk_commercial_charging_level_nren_id_nren')),
+    sa.PrimaryKeyConstraint('nren_id', 'year', name=op.f('pk_commercial_charging_level'))
+    )
+    op.create_table('commercial_connectivity',
+    sa.Column('nren_id', sa.Integer(), nullable=False),
+    sa.Column('year', sa.Integer(), nullable=False),
+    sa.Column('commercial_r_and_e', postgresql.ENUM('yes_incl_other', 'yes_national_nren', 'yes_if_sponsored', 'no_but_direct_peering', 'no_policy', 'no_financial', 'no_other', name='commercialconnectivitycoverage', create_type=False), nullable=True),
+    sa.Column('commercial_general', postgresql.ENUM('yes_incl_other', 'yes_national_nren', 'yes_if_sponsored', 'no_but_direct_peering', 'no_policy', 'no_financial', 'no_other', name='commercialconnectivitycoverage', create_type=False), nullable=True),
+    sa.Column('commercial_collaboration', postgresql.ENUM('yes_incl_other', 'yes_national_nren', 'yes_if_sponsored', 'no_but_direct_peering', 'no_policy', 'no_financial', 'no_other', name='commercialconnectivitycoverage', create_type=False), nullable=True),
+    sa.Column('commercial_service_provider', postgresql.ENUM('yes_incl_other', 'yes_national_nren', 'yes_if_sponsored', 'no_but_direct_peering', 'no_policy', 'no_financial', 'no_other', name='commercialconnectivitycoverage', create_type=False), nullable=True),
+    sa.Column('university_spin_off', postgresql.ENUM('yes_incl_other', 'yes_national_nren', 'yes_if_sponsored', 'no_but_direct_peering', 'no_policy', 'no_financial', 'no_other', name='commercialconnectivitycoverage', create_type=False), nullable=True),
+    sa.ForeignKeyConstraint(['nren_id'], ['nren.id'], name=op.f('fk_commercial_connectivity_nren_id_nren')),
+    sa.PrimaryKeyConstraint('nren_id', 'year', name=op.f('pk_commercial_connectivity'))
+    )
+    op.create_table('connected_proportion',
+    sa.Column('nren_id', sa.Integer(), nullable=False),
+    sa.Column('year', sa.Integer(), nullable=False),
+    sa.Column('user_category', postgresql.ENUM('universities', 'further_education', 'secondary_schools', 'primary_schools', 'institutes', 'cultural', 'hospitals', 'government', 'iros', 'for_profit_orgs', name='usercategory', create_type=False), nullable=False),
+    sa.Column('coverage', postgresql.ENUM('yes_incl_other', 'yes_national_nren', 'sometimes', 'no_policy', 'no_financial', 'no_other', 'unsure', name='connectivitycoverage', create_type=False), nullable=True),
+    sa.Column('number_connected', sa.Integer(), nullable=True),
+    sa.Column('market_share', sa.Numeric(), nullable=True),
+    sa.Column('users_served', sa.Integer(), nullable=True),
+    sa.ForeignKeyConstraint(['nren_id'], ['nren.id'], name=op.f('fk_connected_proportion_nren_id_nren')),
+    sa.PrimaryKeyConstraint('nren_id', 'year', 'user_category', name=op.f('pk_connected_proportion'))
+    )
+    op.create_table('connection_carrier',
+    sa.Column('nren_id', sa.Integer(), nullable=False),
+    sa.Column('year', sa.Integer(), nullable=False),
+    sa.Column('user_category', postgresql.ENUM('universities', 'further_education', 'secondary_schools', 'primary_schools', 'institutes', 'cultural', 'hospitals', 'government', 'iros', 'for_profit_orgs', name='usercategory', create_type=False), nullable=False),
+    sa.Column('carry_mechanism', postgresql.ENUM('nren_local_loops', 'regional_nren_backbone', 'commercial_provider_backbone', 'man', 'other', name='carrymechanism', create_type=False), nullable=False),
+    sa.ForeignKeyConstraint(['nren_id'], ['nren.id'], name=op.f('fk_connection_carrier_nren_id_nren')),
+    sa.PrimaryKeyConstraint('nren_id', 'year', 'user_category', name=op.f('pk_connection_carrier'))
+    )
+    op.create_table('connectivity_growth',
+    sa.Column('nren_id', sa.Integer(), nullable=False),
+    sa.Column('year', sa.Integer(), nullable=False),
+    sa.Column('user_category', postgresql.ENUM('universities', 'further_education', 'secondary_schools', 'primary_schools', 'institutes', 'cultural', 'hospitals', 'government', 'iros', 'for_profit_orgs', name='usercategory', create_type=False), nullable=False),
+    sa.Column('growth', sa.Numeric(), nullable=False),
+    sa.ForeignKeyConstraint(['nren_id'], ['nren.id'], name=op.f('fk_connectivity_growth_nren_id_nren')),
+    sa.PrimaryKeyConstraint('nren_id', 'year', 'user_category', name=op.f('pk_connectivity_growth'))
+    )
+    op.create_table('connectivity_level',
+    sa.Column('nren_id', sa.Integer(), nullable=False),
+    sa.Column('year', sa.Integer(), nullable=False),
+    sa.Column('user_category', postgresql.ENUM('universities', 'further_education', 'secondary_schools', 'primary_schools', 'institutes', 'cultural', 'hospitals', 'government', 'iros', 'for_profit_orgs', name='usercategory', create_type=False), nullable=False),
+    sa.Column('typical_speed', sa.Integer(), nullable=True),
+    sa.Column('highest_speed', sa.Integer(), nullable=True),
+    sa.Column('highest_speed_proportion', sa.Numeric(), nullable=True),
+    sa.ForeignKeyConstraint(['nren_id'], ['nren.id'], name=op.f('fk_connectivity_level_nren_id_nren')),
+    sa.PrimaryKeyConstraint('nren_id', 'year', 'user_category', name=op.f('pk_connectivity_level'))
+    )
+    op.create_table('connectivity_load',
+    sa.Column('nren_id', sa.Integer(), nullable=False),
+    sa.Column('year', sa.Integer(), nullable=False),
+    sa.Column('user_category', postgresql.ENUM('universities', 'further_education', 'secondary_schools', 'primary_schools', 'institutes', 'cultural', 'hospitals', 'government', 'iros', 'for_profit_orgs', name='usercategory', create_type=False), nullable=False),
+    sa.Column('average_load_from_institutions', sa.Integer(), nullable=True),
+    sa.Column('average_load_to_institutions', sa.Integer(), nullable=True),
+    sa.Column('peak_load_from_institutions', sa.Integer(), nullable=True),
+    sa.Column('peak_load_to_institutions', sa.Integer(), nullable=True),
+    sa.ForeignKeyConstraint(['nren_id'], ['nren.id'], name=op.f('fk_connectivity_load_nren_id_nren')),
+    sa.PrimaryKeyConstraint('nren_id', 'year', 'user_category', name=op.f('pk_connectivity_load'))
+    )
+    op.create_table('crisis_excercises',
+    sa.Column('nren_id', sa.Integer(), nullable=False),
+    sa.Column('year', sa.Integer(), nullable=False),
+    sa.Column('exercise_descriptions', sa.JSON(), nullable=False),
+    sa.ForeignKeyConstraint(['nren_id'], ['nren.id'], name=op.f('fk_crisis_excercises_nren_id_nren')),
+    sa.PrimaryKeyConstraint('nren_id', 'year', name=op.f('pk_crisis_excercises'))
+    )
+    op.create_table('dark_fibre_installed',
+    sa.Column('nren_id', sa.Integer(), nullable=False),
+    sa.Column('year', sa.Integer(), nullable=False),
+    sa.Column('installed', sa.Boolean(), nullable=False),
+    sa.Column('fibre_length_in_country', sa.Integer(), nullable=True),
+    sa.ForeignKeyConstraint(['nren_id'], ['nren.id'], name=op.f('fk_dark_fibre_installed_nren_id_nren')),
+    sa.PrimaryKeyConstraint('nren_id', 'year', name=op.f('pk_dark_fibre_installed'))
+    )
+    op.create_table('dark_fibre_lease',
+    sa.Column('nren_id', sa.Integer(), nullable=False),
+    sa.Column('year', sa.Integer(), nullable=False),
+    sa.Column('iru_or_lease', sa.Boolean(), nullable=False),
+    sa.Column('fibre_length_in_country', sa.Integer(), nullable=True),
+    sa.Column('fibre_length_outside_country', sa.Integer(), nullable=True),
+    sa.Column('iru_duration', sa.Numeric(), nullable=True),
+    sa.ForeignKeyConstraint(['nren_id'], ['nren.id'], name=op.f('fk_dark_fibre_lease_nren_id_nren')),
+    sa.PrimaryKeyConstraint('nren_id', 'year', name=op.f('pk_dark_fibre_lease'))
+    )
+    op.create_table('eosc_listings',
+    sa.Column('nren_id', sa.Integer(), nullable=False),
+    sa.Column('year', sa.Integer(), nullable=False),
+    sa.Column('service_names', sa.JSON(), nullable=False),
+    sa.ForeignKeyConstraint(['nren_id'], ['nren.id'], name=op.f('fk_eosc_listings_nren_id_nren')),
+    sa.PrimaryKeyConstraint('nren_id', 'year', name=op.f('pk_eosc_listings'))
+    )
+    op.create_table('external_connections',
+    sa.Column('nren_id', sa.Integer(), nullable=False),
+    sa.Column('year', sa.Integer(), nullable=False),
+    sa.Column('connections', sa.JSON(), nullable=False),
+    sa.ForeignKeyConstraint(['nren_id'], ['nren.id'], name=op.f('fk_external_connections_nren_id_nren')),
+    sa.PrimaryKeyConstraint('nren_id', 'year', name=op.f('pk_external_connections'))
+    )
+    op.create_table('fibre_light',
+    sa.Column('nren_id', sa.Integer(), nullable=False),
+    sa.Column('year', sa.Integer(), nullable=False),
+    sa.Column('light_description', sa.String(), nullable=False),
+    sa.ForeignKeyConstraint(['nren_id'], ['nren.id'], name=op.f('fk_fibre_light_nren_id_nren')),
+    sa.PrimaryKeyConstraint('nren_id', 'year', name=op.f('pk_fibre_light'))
+    )
+    op.create_table('monitoring_tools',
+    sa.Column('nren_id', sa.Integer(), nullable=False),
+    sa.Column('year', sa.Integer(), nullable=False),
+    sa.Column('tool_descriptions', sa.JSON(), nullable=False),
+    sa.Column('netflow_processing_description', sa.String(), nullable=False),
+    sa.ForeignKeyConstraint(['nren_id'], ['nren.id'], name=op.f('fk_monitoring_tools_nren_id_nren')),
+    sa.PrimaryKeyConstraint('nren_id', 'year', name=op.f('pk_monitoring_tools'))
+    )
+    op.create_table('network_automation',
+    sa.Column('nren_id', sa.Integer(), nullable=False),
+    sa.Column('year', sa.Integer(), nullable=False),
+    sa.Column('network_automation', postgresql.ENUM('yes', 'no', 'planned', name='yesnoplanned', create_type=False), nullable=False),
+    sa.Column('network_automation_specifics', sa.JSON(), nullable=False),
+    sa.ForeignKeyConstraint(['nren_id'], ['nren.id'], name=op.f('fk_network_automation_nren_id_nren')),
+    sa.PrimaryKeyConstraint('nren_id', 'year', name=op.f('pk_network_automation'))
+    )
+    op.create_table('network_function_virtualisation',
+    sa.Column('nren_id', sa.Integer(), nullable=False),
+    sa.Column('year', sa.Integer(), nullable=False),
+    sa.Column('nfv', postgresql.ENUM('yes', 'no', 'planned', name='yesnoplanned', create_type=False), nullable=False),
+    sa.Column('nfv_specifics', sa.JSON(), nullable=False),
+    sa.ForeignKeyConstraint(['nren_id'], ['nren.id'], name=op.f('fk_network_function_virtualisation_nren_id_nren')),
+    sa.PrimaryKeyConstraint('nren_id', 'year', name=op.f('pk_network_function_virtualisation'))
+    )
+    op.create_table('network_map_urls',
+    sa.Column('nren_id', sa.Integer(), nullable=False),
+    sa.Column('year', sa.Integer(), nullable=False),
+    sa.Column('urls', sa.JSON(), nullable=False),
+    sa.ForeignKeyConstraint(['nren_id'], ['nren.id'], name=op.f('fk_network_map_urls_nren_id_nren')),
+    sa.PrimaryKeyConstraint('nren_id', 'year', name=op.f('pk_network_map_urls'))
+    )
+    op.create_table('non_r_and_e_peers',
+    sa.Column('nren_id', sa.Integer(), nullable=False),
+    sa.Column('year', sa.Integer(), nullable=False),
+    sa.Column('nr_of_non_r_and_e_peers', sa.Integer(), nullable=False),
+    sa.ForeignKeyConstraint(['nren_id'], ['nren.id'], name=op.f('fk_non_r_and_e_peers_nren_id_nren')),
+    sa.PrimaryKeyConstraint('nren_id', 'year', name=op.f('pk_non_r_and_e_peers'))
+    )
+    op.create_table('ops_automation',
+    sa.Column('nren_id', sa.Integer(), nullable=False),
+    sa.Column('year', sa.Integer(), nullable=False),
+    sa.Column('ops_automation', postgresql.ENUM('yes', 'no', 'planned', name='yesnoplanned', create_type=False), nullable=False),
+    sa.Column('ops_automation_specifics', sa.String(), nullable=False),
+    sa.ForeignKeyConstraint(['nren_id'], ['nren.id'], name=op.f('fk_ops_automation_nren_id_nren')),
+    sa.PrimaryKeyConstraint('nren_id', 'year', name=op.f('pk_ops_automation'))
+    )
+    op.create_table('passive_monitoring',
+    sa.Column('nren_id', sa.Integer(), nullable=False),
+    sa.Column('year', sa.Integer(), nullable=False),
+    sa.Column('monitoring', sa.Boolean(), nullable=False),
+    sa.Column('method', postgresql.ENUM('span_ports', 'taps', 'both', name='monitoringmethod', create_type=False), nullable=True),
+    sa.ForeignKeyConstraint(['nren_id'], ['nren.id'], name=op.f('fk_passive_monitoring_nren_id_nren')),
+    sa.PrimaryKeyConstraint('nren_id', 'year', name=op.f('pk_passive_monitoring'))
+    )
+    op.create_table('pert_team',
+    sa.Column('nren_id', sa.Integer(), nullable=False),
+    sa.Column('year', sa.Integer(), nullable=False),
+    sa.Column('pert_team', postgresql.ENUM('yes', 'no', 'planned', name='yesnoplanned', create_type=False), nullable=False),
+    sa.ForeignKeyConstraint(['nren_id'], ['nren.id'], name=op.f('fk_pert_team_nren_id_nren')),
+    sa.PrimaryKeyConstraint('nren_id', 'year', name=op.f('pk_pert_team'))
+    )
+    op.create_table('remote_campuses',
+    sa.Column('nren_id', sa.Integer(), nullable=False),
+    sa.Column('year', sa.Integer(), nullable=False),
+    sa.Column('remote_campus_connectivity', sa.Boolean(), nullable=False),
+    sa.Column('connections', sa.JSON(), nullable=False),
+    sa.ForeignKeyConstraint(['nren_id'], ['nren.id'], name=op.f('fk_remote_campuses_nren_id_nren')),
+    sa.PrimaryKeyConstraint('nren_id', 'year', name=op.f('pk_remote_campuses'))
+    )
+    op.create_table('security_controls',
+    sa.Column('nren_id', sa.Integer(), nullable=False),
+    sa.Column('year', sa.Integer(), nullable=False),
+    sa.Column('security_control_descriptions', sa.JSON(), nullable=False),
+    sa.ForeignKeyConstraint(['nren_id'], ['nren.id'], name=op.f('fk_security_controls_nren_id_nren')),
+    sa.PrimaryKeyConstraint('nren_id', 'year', name=op.f('pk_security_controls'))
+    )
+    op.create_table('service_management',
+    sa.Column('nren_id', sa.Integer(), nullable=False),
+    sa.Column('year', sa.Integer(), nullable=False),
+    sa.Column('service_management_framework', sa.Boolean(), nullable=True),
+    sa.Column('service_level_targets', sa.Boolean(), nullable=True),
+    sa.ForeignKeyConstraint(['nren_id'], ['nren.id'], name=op.f('fk_service_management_nren_id_nren')),
+    sa.PrimaryKeyConstraint('nren_id', 'year', name=op.f('pk_service_management'))
+    )
+    op.create_table('service_user_types',
+    sa.Column('nren_id', sa.Integer(), nullable=False),
+    sa.Column('year', sa.Integer(), nullable=False),
+    sa.Column('user_category', postgresql.ENUM('universities', 'further_education', 'secondary_schools', 'primary_schools', 'institutes', 'cultural', 'hospitals', 'government', 'iros', 'for_profit_orgs', name='usercategory', create_type=False), nullable=False),
+    sa.Column('service_category', postgresql.ENUM('network_services', 'isp_support', 'security', 'identity', 'collaboration', 'multimedia', 'storage_and_hosting', 'professional_services', name='servicecategory', create_type=False), nullable=False),
+    sa.ForeignKeyConstraint(['nren_id'], ['nren.id'], name=op.f('fk_service_user_types_nren_id_nren')),
+    sa.PrimaryKeyConstraint('nren_id', 'year', 'user_category', name=op.f('pk_service_user_types'))
+    )
+    op.create_table('siem_vendors',
+    sa.Column('nren_id', sa.Integer(), nullable=False),
+    sa.Column('year', sa.Integer(), nullable=False),
+    sa.Column('vendor_names', sa.JSON(), nullable=False),
+    sa.ForeignKeyConstraint(['nren_id'], ['nren.id'], name=op.f('fk_siem_vendors_nren_id_nren')),
+    sa.PrimaryKeyConstraint('nren_id', 'year', name=op.f('pk_siem_vendors'))
+    )
+    op.create_table('standards',
+    sa.Column('nren_id', sa.Integer(), nullable=False),
+    sa.Column('year', sa.Integer(), nullable=False),
+    sa.Column('audits', sa.Boolean(), nullable=True),
+    sa.Column('audit_specifics', sa.String(), nullable=False),
+    sa.Column('business_continuity_plans', sa.Boolean(), nullable=True),
+    sa.Column('business_continuity_plans_specifics', sa.String(), nullable=False),
+    sa.Column('crisis_management_procedure', sa.Boolean(), nullable=True),
+    sa.ForeignKeyConstraint(['nren_id'], ['nren.id'], name=op.f('fk_standards_nren_id_nren')),
+    sa.PrimaryKeyConstraint('nren_id', 'year', name=op.f('pk_standards'))
+    )
+    op.create_table('traffic_ratio',
+    sa.Column('nren_id', sa.Integer(), nullable=False),
+    sa.Column('year', sa.Integer(), nullable=False),
+    sa.Column('r_and_e_percentage', sa.Integer(), nullable=False),
+    sa.Column('commodity_percentage', sa.Integer(), nullable=False),
+    sa.ForeignKeyConstraint(['nren_id'], ['nren.id'], name=op.f('fk_traffic_ratio_nren_id_nren')),
+    sa.PrimaryKeyConstraint('nren_id', 'year', name=op.f('pk_traffic_ratio'))
+    )
+    op.create_table('traffic_statistics',
+    sa.Column('nren_id', sa.Integer(), nullable=False),
+    sa.Column('year', sa.Integer(), nullable=False),
+    sa.Column('traffic_statistics', sa.Boolean(), nullable=False),
+    sa.Column('urls', sa.JSON(), nullable=False),
+    sa.ForeignKeyConstraint(['nren_id'], ['nren.id'], name=op.f('fk_traffic_statistics_nren_id_nren')),
+    sa.PrimaryKeyConstraint('nren_id', 'year', name=op.f('pk_traffic_statistics'))
+    )
+    op.create_table('weather_map',
+    sa.Column('nren_id', sa.Integer(), nullable=False),
+    sa.Column('year', sa.Integer(), nullable=False),
+    sa.Column('weather_map', sa.Boolean(), nullable=False),
+    sa.Column('url', sa.String(), nullable=False),
+    sa.ForeignKeyConstraint(['nren_id'], ['nren.id'], name=op.f('fk_weather_map_nren_id_nren')),
+    sa.PrimaryKeyConstraint('nren_id', 'year', name=op.f('pk_weather_map'))
+    )
+    # ### end Alembic commands ###
+
+
+def downgrade():
+    # ### commands auto generated by Alembic - please adjust! ###
+    op.drop_table('weather_map')
+    op.drop_table('traffic_statistics')
+    op.drop_table('traffic_ratio')
+    op.drop_table('standards')
+    op.drop_table('siem_vendors')
+    op.drop_table('service_user_types')
+    op.drop_table('service_management')
+    op.drop_table('security_controls')
+    op.drop_table('remote_campuses')
+    op.drop_table('pert_team')
+    op.drop_table('passive_monitoring')
+    op.drop_table('ops_automation')
+    op.drop_table('non_r_and_e_peers')
+    op.drop_table('network_map_urls')
+    op.drop_table('network_function_virtualisation')
+    op.drop_table('network_automation')
+    op.drop_table('monitoring_tools')
+    op.drop_table('fibre_light')
+    op.drop_table('external_connections')
+    op.drop_table('eosc_listings')
+    op.drop_table('dark_fibre_lease')
+    op.drop_table('dark_fibre_installed')
+    op.drop_table('crisis_excercises')
+    op.drop_table('connectivity_load')
+    op.drop_table('connectivity_level')
+    op.drop_table('connectivity_growth')
+    op.drop_table('connection_carrier')
+    op.drop_table('connected_proportion')
+    op.drop_table('commercial_connectivity')
+    op.drop_table('commercial_charging_level')
+    op.drop_table('certificate_providers')
+    op.drop_table('central_procurement')
+    op.drop_table('capacity')
+    op.drop_table('alien_wave')
+    sa.Enum('universities', 'further_education', 'secondary_schools', 'primary_schools', 'institutes', 'cultural', 'hospitals', 'government', 'iros', 'for_profit_orgs', name='usercategory').drop(op.get_bind())
+    sa.Enum('network_services', 'isp_support', 'security', 'identity', 'collaboration', 'multimedia', 'storage_and_hosting', 'professional_services', name='servicecategory').drop(op.get_bind())
+    sa.Enum('yes_incl_other', 'yes_national_nren', 'sometimes', 'no_policy', 'no_financial', 'no_other', 'unsure', name='connectivitycoverage').drop(op.get_bind())
+    sa.Enum('nren_local_loops', 'regional_nren_backbone', 'commercial_provider_backbone', 'man', 'other', name='carrymechanism').drop(op.get_bind())
+    sa.Enum('yes_incl_other', 'yes_national_nren', 'yes_if_sponsored', 'no_but_direct_peering', 'no_policy', 'no_financial', 'no_other', name='commercialconnectivitycoverage').drop(op.get_bind())
+    sa.Enum('higher_than_r_e_charges', 'same_as_r_e_charges', 'no_charges_if_r_e_requested', 'lower_than_r_e_charges', name='commarcialcharginglevel').drop(op.get_bind())
+    sa.Enum('span_ports', 'taps', 'both', name='monitoringmethod').drop(op.get_bind())
+    sa.Enum('yes', 'no', 'planned', name='yesnoplanned').drop(op.get_bind())
+    # ### end Alembic commands ###
diff --git a/compendium_v2/publishers/survey_publisher_2022.py b/compendium_v2/publishers/survey_publisher_2022.py
index 39038d1e..7251db54 100644
--- a/compendium_v2/publishers/survey_publisher_2022.py
+++ b/compendium_v2/publishers/survey_publisher_2022.py
@@ -17,7 +17,7 @@ from sqlalchemy import delete, text
 from collections import defaultdict
 
 import compendium_v2
-from compendium_v2.db.model import FeeType
+from compendium_v2.db.model_enums import FeeType
 from compendium_v2.environment import setup_logging
 from compendium_v2.config import load
 from compendium_v2.publishers.helpers import extract_urls
diff --git a/compendium_v2/publishers/survey_publisher_v2.py b/compendium_v2/publishers/survey_publisher_v2.py
index ebede055..5bb12b78 100644
--- a/compendium_v2/publishers/survey_publisher_v2.py
+++ b/compendium_v2/publishers/survey_publisher_v2.py
@@ -1,19 +1,21 @@
 from decimal import Decimal
+from typing import List
 
 from sqlalchemy import delete, select
 
 from compendium_v2.db import db
-from compendium_v2.db.model import BudgetEntry, ChargingStructure, ECProject, FeeType, FundingSource, \
-    InstitutionURLs, NrenStaff, ParentOrganization, Policy, SubOrganization, TrafficVolume
+from compendium_v2.db.model import BudgetEntry, ChargingStructure, ECProject, ExternalConnections, FundingSource, \
+    InstitutionURLs, NrenStaff, ParentOrganization, Policy, SubOrganization, TrafficVolume, ExternalConnection
+from compendium_v2.db.model_enums import FeeType
 from compendium_v2.db.survey_model import ResponseStatus, SurveyResponse
 
 
-def map_2023(nren, answers):
+def map_2023(nren, answers) -> None:
     year = 2023
 
     for table_class in [BudgetEntry, ChargingStructure, ECProject, FundingSource, InstitutionURLs,
                         NrenStaff, ParentOrganization, Policy, SubOrganization, TrafficVolume]:
-        db.session.execute(delete(table_class).where(table_class.year == 2023))
+        db.session.execute(delete(table_class).where(table_class.year == 2023))  # type: ignore
 
     answers = answers["data"]
     budget = answers.get("budget")
@@ -104,6 +106,22 @@ def map_2023(nren, answers):
                 urls=urls
             ))
 
+    external_connections = answers.get("external_connections")
+    if external_connections:
+        connections: List[ExternalConnection] = []
+        for connection in external_connections:
+            connections.append({
+                'link_name': connection.get('link_name', ''),
+                'capacity': connection.get('capacity'),
+                'from_organization': connection.get('from_organization', ''),
+                'to_organization': connection.get('to_organization', ''),
+                'interconnection_method': connection.get('interconnection_method')
+            })
+        db.session.add(ExternalConnections(
+            nren_id=nren.id, nren=nren, year=year,
+            connections=connections
+        ))
+
 
 def publish(year):
     responses = db.session.scalars(
diff --git a/test/test_survey_publisher_2022.py b/test/test_survey_publisher_2022.py
index e8eaa8b4..8f6d1616 100644
--- a/test/test_survey_publisher_2022.py
+++ b/test/test_survey_publisher_2022.py
@@ -1,6 +1,6 @@
 from sqlalchemy import select
 
-from compendium_v2.db import db, model
+from compendium_v2.db import db, model, model_enums
 from compendium_v2.publishers.survey_publisher_2022 import _cli, FundingSource, \
     StaffQuestion, OrgQuestion, ChargingStructure, ECQuestion
 
@@ -280,11 +280,11 @@ def test_publisher(app_with_survey_db, mocker, dummy_config):
         ).all()
         assert len(charging_structures) == 3
         assert charging_structures[0].nren.name.lower() == 'nren1'
-        assert charging_structures[0].fee_type == model.FeeType.no_charge
+        assert charging_structures[0].fee_type == model_enums.FeeType.no_charge
         assert charging_structures[1].nren.name.lower() == 'nren2'
-        assert charging_structures[1].fee_type == model.FeeType.usage_based_fee
+        assert charging_structures[1].fee_type == model_enums.FeeType.usage_based_fee
         assert charging_structures[2].nren.name.lower() == 'nren3'
-        assert charging_structures[2].fee_type == model.FeeType.other
+        assert charging_structures[2].fee_type == model_enums.FeeType.other
 
         _ec_data = db.session.scalars(
             select(model.ECProject).order_by(model.ECProject.nren_id.asc())
diff --git a/test/test_survey_publisher_v2.py b/test/test_survey_publisher_v2.py
index 68b4b2d0..d0a0a8cb 100644
--- a/test/test_survey_publisher_v2.py
+++ b/test/test_survey_publisher_v2.py
@@ -5,7 +5,7 @@ import os
 from sqlalchemy import func, select
 
 from compendium_v2 import db
-from compendium_v2.db import model
+from compendium_v2.db import model, model_enums
 from compendium_v2.publishers.survey_publisher_v2 import map_2023
 
 
@@ -53,7 +53,7 @@ def test_v2_publisher_full(app):
         assert funding_source.other == Decimal("10")
 
         charging_structure = db.session.scalar(select(model.ChargingStructure.fee_type))
-        assert charging_structure == model.FeeType.usage_based_fee
+        assert charging_structure == model_enums.FeeType.usage_based_fee
 
         staff = db.session.scalar(select(model.NrenStaff))
         assert staff.permanent_fte == Decimal("5.6")
@@ -97,3 +97,28 @@ def test_v2_publisher_full(app):
 
         client_urls = db.session.scalar(select(model.InstitutionURLs))
         assert client_urls.urls == ["http://erse.com", "https://wwe.com"]
+
+        external_connections = db.session.scalar(select(model.ExternalConnections))
+        external_connection_list = external_connections.connections
+        assert len(external_connection_list) == 6
+        assert external_connection_list[0] == {
+            "capacity": "1",
+            "from_organization": "GEANT",
+            "interconnection_method": "geant",
+            "link_name": "GEANT",
+            "to_organization": "MREN"
+        }
+        assert external_connection_list[3] == {
+            "capacity": None,
+            "from_organization": "",
+            "interconnection_method": "other",
+            "link_name": "",
+            "to_organization": ""
+        }
+        assert external_connection_list[4] == {
+            "capacity": "1.1",
+            "from_organization": "",
+            "interconnection_method": None,
+            "link_name": "",
+            "to_organization": "",
+        }
-- 
GitLab


From e94f9a88960474f6b5256c788d127f9fcad83fca Mon Sep 17 00:00:00 2001
From: Remco Tukker <remco.tukker@geant.org>
Date: Fri, 15 Sep 2023 14:57:37 +0200
Subject: [PATCH 2/2] better name for enum file

---
 .../db/{model_enums.py => presentation_model_enums.py}    | 0
 compendium_v2/db/presentation_models.py                   | 5 +++--
 compendium_v2/publishers/excel_parser.py                  | 2 +-
 compendium_v2/publishers/survey_publisher.py              | 2 +-
 compendium_v2/publishers/survey_publisher_old_db_2022.py  | 2 +-
 test/test_db_survey_publisher_2022.py                     | 8 ++++----
 test/test_survey_publisher.py                             | 4 ++--
 7 files changed, 12 insertions(+), 11 deletions(-)
 rename compendium_v2/db/{model_enums.py => presentation_model_enums.py} (100%)

diff --git a/compendium_v2/db/model_enums.py b/compendium_v2/db/presentation_model_enums.py
similarity index 100%
rename from compendium_v2/db/model_enums.py
rename to compendium_v2/db/presentation_model_enums.py
diff --git a/compendium_v2/db/presentation_models.py b/compendium_v2/db/presentation_models.py
index 5464188b..de573493 100644
--- a/compendium_v2/db/presentation_models.py
+++ b/compendium_v2/db/presentation_models.py
@@ -11,8 +11,9 @@ from sqlalchemy.orm import Mapped, mapped_column, relationship
 from sqlalchemy.schema import ForeignKey
 
 from compendium_v2.db import db
-from compendium_v2.db.model_enums import CarryMechanism, CommarcialChargingLevel, UserCategory, ServiceCategory, \
-    ConnectivityCoverage, ConnectionMethod, YesNoPlanned, MonitoringMethod, CommercialConnectivityCoverage, FeeType
+from compendium_v2.db.presentation_model_enums import CarryMechanism, CommarcialChargingLevel, UserCategory, \
+    ConnectivityCoverage, ConnectionMethod, YesNoPlanned, MonitoringMethod, CommercialConnectivityCoverage, \
+    FeeType, ServiceCategory
 
 
 logger = logging.getLogger(__name__)
diff --git a/compendium_v2/publishers/excel_parser.py b/compendium_v2/publishers/excel_parser.py
index 1e964be8..41f7c014 100644
--- a/compendium_v2/publishers/excel_parser.py
+++ b/compendium_v2/publishers/excel_parser.py
@@ -2,7 +2,7 @@ import logging
 import openpyxl
 import os
 
-from compendium_v2.db.model_enums import FeeType
+from compendium_v2.db.presentation_model_enums import FeeType
 from compendium_v2.environment import setup_logging
 
 setup_logging()
diff --git a/compendium_v2/publishers/survey_publisher.py b/compendium_v2/publishers/survey_publisher.py
index 604d2723..6946aa10 100644
--- a/compendium_v2/publishers/survey_publisher.py
+++ b/compendium_v2/publishers/survey_publisher.py
@@ -18,7 +18,7 @@ from compendium_v2.db import db
 from compendium_v2.db.presentation_models import BudgetEntry, ChargingStructure, ECProject, ExternalConnections, \
     InstitutionURLs, NrenStaff, ParentOrganization, Policy, SubOrganization, TrafficVolume, ExternalConnection, \
     FundingSource
-from compendium_v2.db.model_enums import FeeType
+from compendium_v2.db.presentation_model_enums import FeeType
 from compendium_v2.db.survey_models import ResponseStatus, SurveyResponse
 
 
diff --git a/compendium_v2/publishers/survey_publisher_old_db_2022.py b/compendium_v2/publishers/survey_publisher_old_db_2022.py
index 322e06e1..c4c1bb86 100644
--- a/compendium_v2/publishers/survey_publisher_old_db_2022.py
+++ b/compendium_v2/publishers/survey_publisher_old_db_2022.py
@@ -17,7 +17,7 @@ from sqlalchemy import delete, text
 from collections import defaultdict
 
 import compendium_v2
-from compendium_v2.db.model_enums import FeeType
+from compendium_v2.db.presentation_model_enums import FeeType
 from compendium_v2.environment import setup_logging
 from compendium_v2.config import load
 from compendium_v2.publishers.helpers import extract_urls
diff --git a/test/test_db_survey_publisher_2022.py b/test/test_db_survey_publisher_2022.py
index bcec6c98..1d0fbcd1 100644
--- a/test/test_db_survey_publisher_2022.py
+++ b/test/test_db_survey_publisher_2022.py
@@ -1,6 +1,6 @@
 from sqlalchemy import select
 
-from compendium_v2.db import db, presentation_models, model_enums
+from compendium_v2.db import db, presentation_model_enums, presentation_models
 from compendium_v2.publishers.survey_publisher_old_db_2022 import _cli, FundingSource, \
     StaffQuestion, OrgQuestion, ChargingStructure, ECQuestion
 
@@ -282,11 +282,11 @@ def test_publisher(app_with_survey_db, mocker, dummy_config):
         ).all()
         assert len(charging_structures) == 3
         assert charging_structures[0].nren.name.lower() == 'nren1'
-        assert charging_structures[0].fee_type == model_enums.FeeType.no_charge
+        assert charging_structures[0].fee_type == presentation_model_enums.FeeType.no_charge
         assert charging_structures[1].nren.name.lower() == 'nren2'
-        assert charging_structures[1].fee_type == model_enums.FeeType.usage_based_fee
+        assert charging_structures[1].fee_type == presentation_model_enums.FeeType.usage_based_fee
         assert charging_structures[2].nren.name.lower() == 'nren3'
-        assert charging_structures[2].fee_type == model_enums.FeeType.other
+        assert charging_structures[2].fee_type == presentation_model_enums.FeeType.other
 
         _ec_data = db.session.scalars(
             select(presentation_models.ECProject).order_by(presentation_models.ECProject.nren_id.asc())
diff --git a/test/test_survey_publisher.py b/test/test_survey_publisher.py
index b1ca4534..e6cc54f6 100644
--- a/test/test_survey_publisher.py
+++ b/test/test_survey_publisher.py
@@ -5,7 +5,7 @@ import os
 from sqlalchemy import func, select
 
 from compendium_v2 import db
-from compendium_v2.db import presentation_models, model_enums
+from compendium_v2.db import presentation_model_enums, presentation_models
 from compendium_v2.publishers.survey_publisher import _map_2023
 
 
@@ -53,7 +53,7 @@ def test_v2_publisher_full(app):
         assert funding_source.other == Decimal("10")
 
         charging_structure = db.session.scalar(select(presentation_models.ChargingStructure.fee_type))
-        assert charging_structure == model_enums.FeeType.usage_based_fee
+        assert charging_structure == presentation_model_enums.FeeType.usage_based_fee
 
         staff = db.session.scalar(select(presentation_models.NrenStaff))
         assert staff.permanent_fte == Decimal("5.6")
-- 
GitLab