From 50938ce419811bb06fff209322cfb07780cadab1 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Mon, 3 Feb 2025 16:53:07 -0500 Subject: [PATCH] refact(geo): add extra properties, created, updated attrs; help text --- chord_metadata_service/geo/descriptions.py | 25 ++++++++--- .../geo/migrations/0001_v11_0_0.py | 18 ++++---- chord_metadata_service/geo/models.py | 42 ++++++++++++++----- chord_metadata_service/geo/schemas.py | 2 + chord_metadata_service/geo/serializers.py | 12 ++++-- .../geo/tests/test_serializers.py | 2 + .../phenopackets/migrations/0012_v11_0_0.py | 2 +- chord_metadata_service/restapi/validators.py | 3 ++ 8 files changed, 79 insertions(+), 27 deletions(-) diff --git a/chord_metadata_service/geo/descriptions.py b/chord_metadata_service/geo/descriptions.py index 90f86d283..0f739030c 100644 --- a/chord_metadata_service/geo/descriptions.py +++ b/chord_metadata_service/geo/descriptions.py @@ -1,4 +1,17 @@ -__all__ = ["GEO_LOCATION"] +__all__ = [ + "PROP_LABEL", + "PROP_CITY", + "PROP_COUNTRY", + "PROP_ISO3166_ALPHA_3", + "PROP_PRECISION", + "GEO_LOCATION", +] + +PROP_LABEL = "Address or other human-readable location name." +PROP_CITY = "Optional name of the city where this location rests." +PROP_COUNTRY = "Optional name of the country where this location rests." +PROP_ISO3166_ALPHA_3 = "Optional ISO 3166-1 alpha 3 country code (three letters)." +PROP_PRECISION = "Optional, human-readable indication of how precise this location is (e.g., \"city\")." GEO_LOCATION = { "description": "A GeoJSON-compatible object representing a geographic location.", @@ -20,11 +33,11 @@ "schema block." ), "properties": { - "label": "Address or other human-readable location name.", - "city": "Optional name of the city where this location rests.", - "country": "Optional name of the country where this location rests.", - "ISO3166alpha3": "Optional ISO 3166-1 alpha 3 country code (three letters).", - "precision": "Optional, human-readable indication of how precise this location is (e.g., \"city\")." + "label": PROP_LABEL, + "city": PROP_CITY, + "country": PROP_COUNTRY, + "ISO3166alpha3": PROP_ISO3166_ALPHA_3, + "precision": PROP_PRECISION, }, }, }, diff --git a/chord_metadata_service/geo/migrations/0001_v11_0_0.py b/chord_metadata_service/geo/migrations/0001_v11_0_0.py index 7a49f9f1b..765ef954c 100644 --- a/chord_metadata_service/geo/migrations/0001_v11_0_0.py +++ b/chord_metadata_service/geo/migrations/0001_v11_0_0.py @@ -1,5 +1,6 @@ -# Generated by Django 5.0.11 on 2025-01-29 22:17 +# Generated by Django 5.0.11 on 2025-02-03 20:14 +import chord_metadata_service.restapi.validators import django.contrib.gis.db.models.fields from django.db import migrations, models @@ -16,12 +17,15 @@ class Migration(migrations.Migration): name='GeoLocation', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('point', django.contrib.gis.db.models.fields.PointField(srid=4326)), - ('label', models.TextField(blank=True)), - ('city', models.TextField(blank=True)), - ('country', models.TextField(blank=True)), - ('iso_a3_code', models.CharField(choices=[('ABW', 'Aruba'), ('AFG', 'Afghanistan'), ('AGO', 'Angola'), ('AIA', 'Anguilla'), ('ALA', 'Åland Islands'), ('ALB', 'Albania'), ('AND', 'Andorra'), ('ARE', 'United Arab Emirates'), ('ARG', 'Argentina'), ('ARM', 'Armenia'), ('ASM', 'American Samoa'), ('ATA', 'Antarctica'), ('ATF', 'French Southern Territories'), ('ATG', 'Antigua and Barbuda'), ('AUS', 'Australia'), ('AUT', 'Austria'), ('AZE', 'Azerbaijan'), ('BDI', 'Burundi'), ('BEL', 'Belgium'), ('BEN', 'Benin'), ('BES', 'Bonaire, Sint Eustatius and Saba'), ('BFA', 'Burkina Faso'), ('BGD', 'Bangladesh'), ('BGR', 'Bulgaria'), ('BHR', 'Bahrain'), ('BHS', 'Bahamas'), ('BIH', 'Bosnia and Herzegovina'), ('BLM', 'Saint Barthélemy'), ('BLR', 'Belarus'), ('BLZ', 'Belize'), ('BMU', 'Bermuda'), ('BOL', 'Bolivia, Plurinational State of'), ('BRA', 'Brazil'), ('BRB', 'Barbados'), ('BRN', 'Brunei Darussalam'), ('BTN', 'Bhutan'), ('BVT', 'Bouvet Island'), ('BWA', 'Botswana'), ('CAF', 'Central African Republic'), ('CAN', 'Canada'), ('CCK', 'Cocos (Keeling) Islands'), ('CHE', 'Switzerland'), ('CHL', 'Chile'), ('CHN', 'China'), ('CIV', "Côte d'Ivoire"), ('CMR', 'Cameroon'), ('COD', 'Congo, The Democratic Republic of the'), ('COG', 'Congo'), ('COK', 'Cook Islands'), ('COL', 'Colombia'), ('COM', 'Comoros'), ('CPV', 'Cabo Verde'), ('CRI', 'Costa Rica'), ('CUB', 'Cuba'), ('CUW', 'Curaçao'), ('CXR', 'Christmas Island'), ('CYM', 'Cayman Islands'), ('CYP', 'Cyprus'), ('CZE', 'Czechia'), ('DEU', 'Germany'), ('DJI', 'Djibouti'), ('DMA', 'Dominica'), ('DNK', 'Denmark'), ('DOM', 'Dominican Republic'), ('DZA', 'Algeria'), ('ECU', 'Ecuador'), ('EGY', 'Egypt'), ('ERI', 'Eritrea'), ('ESH', 'Western Sahara'), ('ESP', 'Spain'), ('EST', 'Estonia'), ('ETH', 'Ethiopia'), ('FIN', 'Finland'), ('FJI', 'Fiji'), ('FLK', 'Falkland Islands (Malvinas)'), ('FRA', 'France'), ('FRO', 'Faroe Islands'), ('FSM', 'Micronesia, Federated States of'), ('GAB', 'Gabon'), ('GBR', 'United Kingdom'), ('GEO', 'Georgia'), ('GGY', 'Guernsey'), ('GHA', 'Ghana'), ('GIB', 'Gibraltar'), ('GIN', 'Guinea'), ('GLP', 'Guadeloupe'), ('GMB', 'Gambia'), ('GNB', 'Guinea-Bissau'), ('GNQ', 'Equatorial Guinea'), ('GRC', 'Greece'), ('GRD', 'Grenada'), ('GRL', 'Greenland'), ('GTM', 'Guatemala'), ('GUF', 'French Guiana'), ('GUM', 'Guam'), ('GUY', 'Guyana'), ('HKG', 'Hong Kong'), ('HMD', 'Heard Island and McDonald Islands'), ('HND', 'Honduras'), ('HRV', 'Croatia'), ('HTI', 'Haiti'), ('HUN', 'Hungary'), ('IDN', 'Indonesia'), ('IMN', 'Isle of Man'), ('IND', 'India'), ('IOT', 'British Indian Ocean Territory'), ('IRL', 'Ireland'), ('IRN', 'Iran, Islamic Republic of'), ('IRQ', 'Iraq'), ('ISL', 'Iceland'), ('ISR', 'Israel'), ('ITA', 'Italy'), ('JAM', 'Jamaica'), ('JEY', 'Jersey'), ('JOR', 'Jordan'), ('JPN', 'Japan'), ('KAZ', 'Kazakhstan'), ('KEN', 'Kenya'), ('KGZ', 'Kyrgyzstan'), ('KHM', 'Cambodia'), ('KIR', 'Kiribati'), ('KNA', 'Saint Kitts and Nevis'), ('KOR', 'Korea, Republic of'), ('KWT', 'Kuwait'), ('LAO', "Lao People's Democratic Republic"), ('LBN', 'Lebanon'), ('LBR', 'Liberia'), ('LBY', 'Libya'), ('LCA', 'Saint Lucia'), ('LIE', 'Liechtenstein'), ('LKA', 'Sri Lanka'), ('LSO', 'Lesotho'), ('LTU', 'Lithuania'), ('LUX', 'Luxembourg'), ('LVA', 'Latvia'), ('MAC', 'Macao'), ('MAF', 'Saint Martin (French part)'), ('MAR', 'Morocco'), ('MCO', 'Monaco'), ('MDA', 'Moldova, Republic of'), ('MDG', 'Madagascar'), ('MDV', 'Maldives'), ('MEX', 'Mexico'), ('MHL', 'Marshall Islands'), ('MKD', 'North Macedonia'), ('MLI', 'Mali'), ('MLT', 'Malta'), ('MMR', 'Myanmar'), ('MNE', 'Montenegro'), ('MNG', 'Mongolia'), ('MNP', 'Northern Mariana Islands'), ('MOZ', 'Mozambique'), ('MRT', 'Mauritania'), ('MSR', 'Montserrat'), ('MTQ', 'Martinique'), ('MUS', 'Mauritius'), ('MWI', 'Malawi'), ('MYS', 'Malaysia'), ('MYT', 'Mayotte'), ('NAM', 'Namibia'), ('NCL', 'New Caledonia'), ('NER', 'Niger'), ('NFK', 'Norfolk Island'), ('NGA', 'Nigeria'), ('NIC', 'Nicaragua'), ('NIU', 'Niue'), ('NLD', 'Netherlands'), ('NOR', 'Norway'), ('NPL', 'Nepal'), ('NRU', 'Nauru'), ('NZL', 'New Zealand'), ('OMN', 'Oman'), ('PAK', 'Pakistan'), ('PAN', 'Panama'), ('PCN', 'Pitcairn'), ('PER', 'Peru'), ('PHL', 'Philippines'), ('PLW', 'Palau'), ('PNG', 'Papua New Guinea'), ('POL', 'Poland'), ('PRI', 'Puerto Rico'), ('PRK', "Korea, Democratic People's Republic of"), ('PRT', 'Portugal'), ('PRY', 'Paraguay'), ('PSE', 'Palestine, State of'), ('PYF', 'French Polynesia'), ('QAT', 'Qatar'), ('REU', 'Réunion'), ('ROU', 'Romania'), ('RUS', 'Russian Federation'), ('RWA', 'Rwanda'), ('SAU', 'Saudi Arabia'), ('SDN', 'Sudan'), ('SEN', 'Senegal'), ('SGP', 'Singapore'), ('SGS', 'South Georgia and the South Sandwich Islands'), ('SHN', 'Saint Helena, Ascension and Tristan da Cunha'), ('SJM', 'Svalbard and Jan Mayen'), ('SLB', 'Solomon Islands'), ('SLE', 'Sierra Leone'), ('SLV', 'El Salvador'), ('SMR', 'San Marino'), ('SOM', 'Somalia'), ('SPM', 'Saint Pierre and Miquelon'), ('SRB', 'Serbia'), ('SSD', 'South Sudan'), ('STP', 'Sao Tome and Principe'), ('SUR', 'Suriname'), ('SVK', 'Slovakia'), ('SVN', 'Slovenia'), ('SWE', 'Sweden'), ('SWZ', 'Eswatini'), ('SXM', 'Sint Maarten (Dutch part)'), ('SYC', 'Seychelles'), ('SYR', 'Syrian Arab Republic'), ('TCA', 'Turks and Caicos Islands'), ('TCD', 'Chad'), ('TGO', 'Togo'), ('THA', 'Thailand'), ('TJK', 'Tajikistan'), ('TKL', 'Tokelau'), ('TKM', 'Turkmenistan'), ('TLS', 'Timor-Leste'), ('TON', 'Tonga'), ('TTO', 'Trinidad and Tobago'), ('TUN', 'Tunisia'), ('TUR', 'Türkiye'), ('TUV', 'Tuvalu'), ('TWN', 'Taiwan, Province of China'), ('TZA', 'Tanzania, United Republic of'), ('UGA', 'Uganda'), ('UKR', 'Ukraine'), ('UMI', 'United States Minor Outlying Islands'), ('URY', 'Uruguay'), ('USA', 'United States'), ('UZB', 'Uzbekistan'), ('VAT', 'Holy See (Vatican City State)'), ('VCT', 'Saint Vincent and the Grenadines'), ('VEN', 'Venezuela, Bolivarian Republic of'), ('VGB', 'Virgin Islands, British'), ('VIR', 'Virgin Islands, U.S.'), ('VNM', 'Viet Nam'), ('VUT', 'Vanuatu'), ('WLF', 'Wallis and Futuna'), ('WSM', 'Samoa'), ('YEM', 'Yemen'), ('ZAF', 'South Africa'), ('ZMB', 'Zambia'), ('ZWE', 'Zimbabwe')], default=None, max_length=3, null=True)), - ('precision', models.TextField(blank=True)), + ('point', django.contrib.gis.db.models.fields.PointField(help_text='Point (coordinates) specifying a precise geographic location.', srid=4326)), + ('label', models.TextField(blank=True, default='', help_text='Address or other human-readable location name.')), + ('city', models.TextField(blank=True, default='', help_text='Optional name of the city where this location rests.')), + ('country', models.TextField(blank=True, default='', help_text='Optional name of the country where this location rests.')), + ('iso_a3_code', models.CharField(choices=[('ABW', 'Aruba'), ('AFG', 'Afghanistan'), ('AGO', 'Angola'), ('AIA', 'Anguilla'), ('ALA', 'Åland Islands'), ('ALB', 'Albania'), ('AND', 'Andorra'), ('ARE', 'United Arab Emirates'), ('ARG', 'Argentina'), ('ARM', 'Armenia'), ('ASM', 'American Samoa'), ('ATA', 'Antarctica'), ('ATF', 'French Southern Territories'), ('ATG', 'Antigua and Barbuda'), ('AUS', 'Australia'), ('AUT', 'Austria'), ('AZE', 'Azerbaijan'), ('BDI', 'Burundi'), ('BEL', 'Belgium'), ('BEN', 'Benin'), ('BES', 'Bonaire, Sint Eustatius and Saba'), ('BFA', 'Burkina Faso'), ('BGD', 'Bangladesh'), ('BGR', 'Bulgaria'), ('BHR', 'Bahrain'), ('BHS', 'Bahamas'), ('BIH', 'Bosnia and Herzegovina'), ('BLM', 'Saint Barthélemy'), ('BLR', 'Belarus'), ('BLZ', 'Belize'), ('BMU', 'Bermuda'), ('BOL', 'Bolivia, Plurinational State of'), ('BRA', 'Brazil'), ('BRB', 'Barbados'), ('BRN', 'Brunei Darussalam'), ('BTN', 'Bhutan'), ('BVT', 'Bouvet Island'), ('BWA', 'Botswana'), ('CAF', 'Central African Republic'), ('CAN', 'Canada'), ('CCK', 'Cocos (Keeling) Islands'), ('CHE', 'Switzerland'), ('CHL', 'Chile'), ('CHN', 'China'), ('CIV', "Côte d'Ivoire"), ('CMR', 'Cameroon'), ('COD', 'Congo, The Democratic Republic of the'), ('COG', 'Congo'), ('COK', 'Cook Islands'), ('COL', 'Colombia'), ('COM', 'Comoros'), ('CPV', 'Cabo Verde'), ('CRI', 'Costa Rica'), ('CUB', 'Cuba'), ('CUW', 'Curaçao'), ('CXR', 'Christmas Island'), ('CYM', 'Cayman Islands'), ('CYP', 'Cyprus'), ('CZE', 'Czechia'), ('DEU', 'Germany'), ('DJI', 'Djibouti'), ('DMA', 'Dominica'), ('DNK', 'Denmark'), ('DOM', 'Dominican Republic'), ('DZA', 'Algeria'), ('ECU', 'Ecuador'), ('EGY', 'Egypt'), ('ERI', 'Eritrea'), ('ESH', 'Western Sahara'), ('ESP', 'Spain'), ('EST', 'Estonia'), ('ETH', 'Ethiopia'), ('FIN', 'Finland'), ('FJI', 'Fiji'), ('FLK', 'Falkland Islands (Malvinas)'), ('FRA', 'France'), ('FRO', 'Faroe Islands'), ('FSM', 'Micronesia, Federated States of'), ('GAB', 'Gabon'), ('GBR', 'United Kingdom'), ('GEO', 'Georgia'), ('GGY', 'Guernsey'), ('GHA', 'Ghana'), ('GIB', 'Gibraltar'), ('GIN', 'Guinea'), ('GLP', 'Guadeloupe'), ('GMB', 'Gambia'), ('GNB', 'Guinea-Bissau'), ('GNQ', 'Equatorial Guinea'), ('GRC', 'Greece'), ('GRD', 'Grenada'), ('GRL', 'Greenland'), ('GTM', 'Guatemala'), ('GUF', 'French Guiana'), ('GUM', 'Guam'), ('GUY', 'Guyana'), ('HKG', 'Hong Kong'), ('HMD', 'Heard Island and McDonald Islands'), ('HND', 'Honduras'), ('HRV', 'Croatia'), ('HTI', 'Haiti'), ('HUN', 'Hungary'), ('IDN', 'Indonesia'), ('IMN', 'Isle of Man'), ('IND', 'India'), ('IOT', 'British Indian Ocean Territory'), ('IRL', 'Ireland'), ('IRN', 'Iran, Islamic Republic of'), ('IRQ', 'Iraq'), ('ISL', 'Iceland'), ('ISR', 'Israel'), ('ITA', 'Italy'), ('JAM', 'Jamaica'), ('JEY', 'Jersey'), ('JOR', 'Jordan'), ('JPN', 'Japan'), ('KAZ', 'Kazakhstan'), ('KEN', 'Kenya'), ('KGZ', 'Kyrgyzstan'), ('KHM', 'Cambodia'), ('KIR', 'Kiribati'), ('KNA', 'Saint Kitts and Nevis'), ('KOR', 'Korea, Republic of'), ('KWT', 'Kuwait'), ('LAO', "Lao People's Democratic Republic"), ('LBN', 'Lebanon'), ('LBR', 'Liberia'), ('LBY', 'Libya'), ('LCA', 'Saint Lucia'), ('LIE', 'Liechtenstein'), ('LKA', 'Sri Lanka'), ('LSO', 'Lesotho'), ('LTU', 'Lithuania'), ('LUX', 'Luxembourg'), ('LVA', 'Latvia'), ('MAC', 'Macao'), ('MAF', 'Saint Martin (French part)'), ('MAR', 'Morocco'), ('MCO', 'Monaco'), ('MDA', 'Moldova, Republic of'), ('MDG', 'Madagascar'), ('MDV', 'Maldives'), ('MEX', 'Mexico'), ('MHL', 'Marshall Islands'), ('MKD', 'North Macedonia'), ('MLI', 'Mali'), ('MLT', 'Malta'), ('MMR', 'Myanmar'), ('MNE', 'Montenegro'), ('MNG', 'Mongolia'), ('MNP', 'Northern Mariana Islands'), ('MOZ', 'Mozambique'), ('MRT', 'Mauritania'), ('MSR', 'Montserrat'), ('MTQ', 'Martinique'), ('MUS', 'Mauritius'), ('MWI', 'Malawi'), ('MYS', 'Malaysia'), ('MYT', 'Mayotte'), ('NAM', 'Namibia'), ('NCL', 'New Caledonia'), ('NER', 'Niger'), ('NFK', 'Norfolk Island'), ('NGA', 'Nigeria'), ('NIC', 'Nicaragua'), ('NIU', 'Niue'), ('NLD', 'Netherlands'), ('NOR', 'Norway'), ('NPL', 'Nepal'), ('NRU', 'Nauru'), ('NZL', 'New Zealand'), ('OMN', 'Oman'), ('PAK', 'Pakistan'), ('PAN', 'Panama'), ('PCN', 'Pitcairn'), ('PER', 'Peru'), ('PHL', 'Philippines'), ('PLW', 'Palau'), ('PNG', 'Papua New Guinea'), ('POL', 'Poland'), ('PRI', 'Puerto Rico'), ('PRK', "Korea, Democratic People's Republic of"), ('PRT', 'Portugal'), ('PRY', 'Paraguay'), ('PSE', 'Palestine, State of'), ('PYF', 'French Polynesia'), ('QAT', 'Qatar'), ('REU', 'Réunion'), ('ROU', 'Romania'), ('RUS', 'Russian Federation'), ('RWA', 'Rwanda'), ('SAU', 'Saudi Arabia'), ('SDN', 'Sudan'), ('SEN', 'Senegal'), ('SGP', 'Singapore'), ('SGS', 'South Georgia and the South Sandwich Islands'), ('SHN', 'Saint Helena, Ascension and Tristan da Cunha'), ('SJM', 'Svalbard and Jan Mayen'), ('SLB', 'Solomon Islands'), ('SLE', 'Sierra Leone'), ('SLV', 'El Salvador'), ('SMR', 'San Marino'), ('SOM', 'Somalia'), ('SPM', 'Saint Pierre and Miquelon'), ('SRB', 'Serbia'), ('SSD', 'South Sudan'), ('STP', 'Sao Tome and Principe'), ('SUR', 'Suriname'), ('SVK', 'Slovakia'), ('SVN', 'Slovenia'), ('SWE', 'Sweden'), ('SWZ', 'Eswatini'), ('SXM', 'Sint Maarten (Dutch part)'), ('SYC', 'Seychelles'), ('SYR', 'Syrian Arab Republic'), ('TCA', 'Turks and Caicos Islands'), ('TCD', 'Chad'), ('TGO', 'Togo'), ('THA', 'Thailand'), ('TJK', 'Tajikistan'), ('TKL', 'Tokelau'), ('TKM', 'Turkmenistan'), ('TLS', 'Timor-Leste'), ('TON', 'Tonga'), ('TTO', 'Trinidad and Tobago'), ('TUN', 'Tunisia'), ('TUR', 'Türkiye'), ('TUV', 'Tuvalu'), ('TWN', 'Taiwan, Province of China'), ('TZA', 'Tanzania, United Republic of'), ('UGA', 'Uganda'), ('UKR', 'Ukraine'), ('UMI', 'United States Minor Outlying Islands'), ('URY', 'Uruguay'), ('USA', 'United States'), ('UZB', 'Uzbekistan'), ('VAT', 'Holy See (Vatican City State)'), ('VCT', 'Saint Vincent and the Grenadines'), ('VEN', 'Venezuela, Bolivarian Republic of'), ('VGB', 'Virgin Islands, British'), ('VIR', 'Virgin Islands, U.S.'), ('VNM', 'Viet Nam'), ('VUT', 'Vanuatu'), ('WLF', 'Wallis and Futuna'), ('WSM', 'Samoa'), ('YEM', 'Yemen'), ('ZAF', 'South Africa'), ('ZMB', 'Zambia'), ('ZWE', 'Zimbabwe')], default=None, help_text='Optional ISO 3166-1 alpha 3 country code (three letters).', max_length=3, null=True)), + ('precision', models.TextField(blank=True, default='', help_text='Optional ISO 3166-1 alpha 3 country code (three letters).')), + ('extra_properties', models.JSONField(blank=True, default=dict, help_text='Extra properties that do not have a predefined field in the database.', validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': '/chord_metadata_service/schemas/phenopacket/extra_properties', '$schema': 'http://json-schema.org/draft-07/schema#', 'type': 'object'}, formats=None)])), + ('created', models.DateTimeField(auto_now_add=True)), + ('updated', models.DateTimeField(auto_now=True)), ], ), ] diff --git a/chord_metadata_service/geo/models.py b/chord_metadata_service/geo/models.py index f2facd664..027156369 100644 --- a/chord_metadata_service/geo/models.py +++ b/chord_metadata_service/geo/models.py @@ -1,6 +1,9 @@ -from django.contrib.gis.db import models as geo_models +from django.contrib.gis.db import models from django.contrib.gis.geos import Point +from chord_metadata_service.restapi.validators import base_extra_properties_validator + +from . import descriptions as d from .constants import ISO_3166_1_ALPHA_3_COUNTRY_CODE_CHOICES @@ -9,7 +12,7 @@ ] -class GeoLocation(geo_models.Model): +class GeoLocation(models.Model): """ Model describing a specific geographical location. Heavily inspired by the Progenetix GeoLocation schema block: https://schemablocks.org/schema_pages/Progenetix/GeoLocation/ @@ -17,19 +20,38 @@ class GeoLocation(geo_models.Model): # geometry: # - serializes into a GeoJSON geometry object when rendering any instances as JSON - point = geo_models.PointField(spatial_index=True) + point = models.PointField( + spatial_index=True, help_text="Point (coordinates) specifying a precise geographic location." + ) # metadata / free-text data: # - serializes into a GeoJSON properties object when rendering any instances as JSON - label = geo_models.TextField(blank=True) - city = geo_models.TextField(blank=True) - country = geo_models.TextField(blank=True) - iso_a3_code = geo_models.CharField( - max_length=3, choices=ISO_3166_1_ALPHA_3_COUNTRY_CODE_CHOICES, null=True, default=None + label = models.TextField(blank=True, default="", help_text=d.PROP_LABEL) + city = models.TextField(blank=True, default="", help_text=d.PROP_CITY) + country = models.TextField(blank=True, default="", help_text=d.PROP_COUNTRY) + iso_a3_code = models.CharField( + max_length=3, + choices=ISO_3166_1_ALPHA_3_COUNTRY_CODE_CHOICES, + null=True, + default=None, + help_text=d.PROP_ISO3166_ALPHA_3, + ) + precision = models.TextField(blank=True, default="", help_text=d.PROP_ISO3166_ALPHA_3) + + # properties (mapping to other properties in the GeoJSON object) which do not map to any of the above fields: + extra_properties = models.JSONField( + blank=True, + default=dict, + validators=[base_extra_properties_validator], + help_text="Extra properties that do not have a predefined field in the database.", ) - precision = geo_models.TextField(blank=True) - # TODO: extra properties + # ------------------------------------------------------------------------------------------------------------------ + + created = models.DateTimeField(auto_now_add=True) + updated = models.DateTimeField(auto_now=True) + + # ------------------------------------------------------------------------------------------------------------------ def __str__(self): # noinspection PyTypeChecker diff --git a/chord_metadata_service/geo/schemas.py b/chord_metadata_service/geo/schemas.py index c78f3b272..8080319db 100644 --- a/chord_metadata_service/geo/schemas.py +++ b/chord_metadata_service/geo/schemas.py @@ -41,6 +41,7 @@ }, }, "required": ["type", "coordinates"], + "additionalProperties": False, # Geometry must be just type and coordinates }, "properties": { "type": "object", @@ -51,6 +52,7 @@ "ISO3166alpha3": enum_of(constants.ISO_3166_1_ALPHA_3_COUNTRY_CODES), "precision": base_type(SchemaTypes.STRING), }, + "additionalProperties": True, # Explicitly allow "extra properties" } }, "required": ["type", "geometry", "properties"], diff --git a/chord_metadata_service/geo/serializers.py b/chord_metadata_service/geo/serializers.py index 49eec1e0e..afcda0a1d 100644 --- a/chord_metadata_service/geo/serializers.py +++ b/chord_metadata_service/geo/serializers.py @@ -33,6 +33,9 @@ class GeoLocationPropertiesSerializer(serializers.Serializer): precision: serializers.CharField(required=False, allow_blank=True) +GEO_LOCATION_PREDEF_ATTRS = ("label", "city", "country", "iso_a3_code", "precision") + + class GeoLocationSerializer(serializers.Serializer): type = serializers.CharField(validators=[type_is_feature]) @@ -51,8 +54,11 @@ def to_representation(self, instance: GeoLocation): "coordinates": list(instance.point.coords), }, "properties": { - MODEL_ATTRS_TO_PREDEF_PROPS[k]: getattr(instance, k) - for k in ("label", "city", "country", "iso_a3_code", "precision") - if getattr(instance, k) + **({k: v for k, v in instance.extra_properties.items() if k not in GEO_LOCATION_PREDEF_ATTRS}), + **({ + MODEL_ATTRS_TO_PREDEF_PROPS[k]: getattr(instance, k) + for k in GEO_LOCATION_PREDEF_ATTRS + if getattr(instance, k) + }), }, } diff --git a/chord_metadata_service/geo/tests/test_serializers.py b/chord_metadata_service/geo/tests/test_serializers.py index 94eee635b..c28278625 100644 --- a/chord_metadata_service/geo/tests/test_serializers.py +++ b/chord_metadata_service/geo/tests/test_serializers.py @@ -1,12 +1,14 @@ from .constants import KINGSTON_GEOM_JSON, GeoLocationTestCase from ..serializers import GeoLocationPropertiesSerializer, GeoLocationSerializer + VALID_GEO_LOCATION_PROPERTIES = ( {}, {"label": "my location"}, { "country": "Canada", "ISO3166alpha3": "CDN", + "my_cool_extra_prop": 5324, }, { "label": "David's Hometown", diff --git a/chord_metadata_service/phenopackets/migrations/0012_v11_0_0.py b/chord_metadata_service/phenopackets/migrations/0012_v11_0_0.py index fa44f65ae..8451c666b 100644 --- a/chord_metadata_service/phenopackets/migrations/0012_v11_0_0.py +++ b/chord_metadata_service/phenopackets/migrations/0012_v11_0_0.py @@ -1,4 +1,4 @@ -# Generated by Django 5.0.11 on 2025-01-30 20:55 +# Generated by Django 5.0.11 on 2025-02-03 20:14 import django.db.models.deletion from django.db import migrations, models diff --git a/chord_metadata_service/restapi/validators.py b/chord_metadata_service/restapi/validators.py index e956a290c..4a01591dc 100644 --- a/chord_metadata_service/restapi/validators.py +++ b/chord_metadata_service/restapi/validators.py @@ -5,6 +5,7 @@ ONTOLOGY_CLASS, ONTOLOGY_CLASS_LIST, KEY_VALUE_OBJECT, + EXTRA_PROPERTIES_SCHEMA, ) @@ -14,6 +15,7 @@ "ontology_validator", "ontology_list_validator", "key_value_validator", + "base_extra_properties_validator", ] @@ -52,3 +54,4 @@ def deconstruct(self): ontology_validator = JsonSchemaValidator(ONTOLOGY_CLASS) ontology_list_validator = JsonSchemaValidator(ONTOLOGY_CLASS_LIST) key_value_validator = JsonSchemaValidator(KEY_VALUE_OBJECT) +base_extra_properties_validator = JsonSchemaValidator(EXTRA_PROPERTIES_SCHEMA)