diff options
| author | Tim Graham <timograham@gmail.com> | 2015-02-10 10:07:44 -0500 |
|---|---|---|
| committer | Tim Graham <timograham@gmail.com> | 2015-02-11 10:29:54 -0500 |
| commit | ad0be620aece4dde2f94383426c284f4afef9fbc (patch) | |
| tree | a3539cd1368e540f2022d3f6d351473addce9ffe /tests/gis_tests/layermap | |
| parent | 5ab327a3894c26f57baabe14084bcce2a71b8af8 (diff) | |
Moved contrib.gis tests out of contrib.
Diffstat (limited to 'tests/gis_tests/layermap')
| -rw-r--r-- | tests/gis_tests/layermap/__init__.py | 0 | ||||
| -rw-r--r-- | tests/gis_tests/layermap/models.py | 98 | ||||
| -rw-r--r-- | tests/gis_tests/layermap/tests.py | 337 |
3 files changed, 435 insertions, 0 deletions
diff --git a/tests/gis_tests/layermap/__init__.py b/tests/gis_tests/layermap/__init__.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/tests/gis_tests/layermap/__init__.py diff --git a/tests/gis_tests/layermap/models.py b/tests/gis_tests/layermap/models.py new file mode 100644 index 0000000000..f2c7882547 --- /dev/null +++ b/tests/gis_tests/layermap/models.py @@ -0,0 +1,98 @@ +from django.contrib.gis.db import models +from django.utils.encoding import python_2_unicode_compatible + + +@python_2_unicode_compatible +class NamedModel(models.Model): + name = models.CharField(max_length=25) + + objects = models.GeoManager() + + class Meta: + abstract = True + + def __str__(self): + return self.name + + +class State(NamedModel): + pass + + +class County(NamedModel): + state = models.ForeignKey(State) + mpoly = models.MultiPolygonField(srid=4269) # Multipolygon in NAD83 + + +class CountyFeat(NamedModel): + poly = models.PolygonField(srid=4269) + + +class City(NamedModel): + name_txt = models.TextField(default='') + name_short = models.CharField(max_length=5) + population = models.IntegerField() + density = models.DecimalField(max_digits=7, decimal_places=1) + dt = models.DateField() + point = models.PointField() + + class Meta: + app_label = 'layermap' + + +class Interstate(NamedModel): + length = models.DecimalField(max_digits=6, decimal_places=2) + path = models.LineStringField() + + class Meta: + app_label = 'layermap' + + +# Same as `City` above, but for testing model inheritance. +class CityBase(NamedModel): + population = models.IntegerField() + density = models.DecimalField(max_digits=7, decimal_places=1) + point = models.PointField() + + +class ICity1(CityBase): + dt = models.DateField() + + class Meta(CityBase.Meta): + pass + + +class ICity2(ICity1): + dt_time = models.DateTimeField(auto_now=True) + + class Meta(ICity1.Meta): + pass + + +class Invalid(models.Model): + point = models.PointField() + + +# Mapping dictionaries for the models above. +co_mapping = { + 'name': 'Name', + # ForeignKey's use another mapping dictionary for the _related_ Model (State in this case). + 'state': {'name': 'State'}, + 'mpoly': 'MULTIPOLYGON', # Will convert POLYGON features into MULTIPOLYGONS. +} + +cofeat_mapping = {'name': 'Name', + 'poly': 'POLYGON', + } + +city_mapping = {'name': 'Name', + 'population': 'Population', + 'density': 'Density', + 'dt': 'Created', + 'point': 'POINT', + } + +inter_mapping = {'name': 'Name', + 'length': 'Length', + 'path': 'LINESTRING', + } diff --git a/tests/gis_tests/layermap/tests.py b/tests/gis_tests/layermap/tests.py new file mode 100644 index 0000000000..85f105f4a0 --- /dev/null +++ b/tests/gis_tests/layermap/tests.py @@ -0,0 +1,337 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +import os +import unittest +from copy import copy +from decimal import Decimal +from unittest import skipUnless + +from django.conf import settings +from django.contrib.gis.gdal import HAS_GDAL +from django.db import connection +from django.test import TestCase, override_settings, skipUnlessDBFeature +from django.utils._os import upath + +if HAS_GDAL: + from django.contrib.gis.utils.layermapping import (LayerMapping, + LayerMapError, InvalidDecimal, InvalidString, MissingForeignKey) + from django.contrib.gis.gdal import DataSource + + from .models import ( + City, County, CountyFeat, Interstate, ICity1, ICity2, Invalid, State, + city_mapping, co_mapping, cofeat_mapping, inter_mapping) + + +shp_path = os.path.realpath(os.path.join(os.path.dirname(upath(__file__)), os.pardir, 'data')) +city_shp = os.path.join(shp_path, 'cities', 'cities.shp') +co_shp = os.path.join(shp_path, 'counties', 'counties.shp') +inter_shp = os.path.join(shp_path, 'interstates', 'interstates.shp') +invalid_shp = os.path.join(shp_path, 'invalid', 'emptypoints.shp') + +# Dictionaries to hold what's expected in the county shapefile. +NAMES = ['Bexar', 'Galveston', 'Harris', 'Honolulu', 'Pueblo'] +NUMS = [1, 2, 1, 19, 1] # Number of polygons for each. +STATES = ['Texas', 'Texas', 'Texas', 'Hawaii', 'Colorado'] + + +@skipUnless(HAS_GDAL, "LayerMapTest needs GDAL support") +@skipUnlessDBFeature("gis_enabled") +class LayerMapTest(TestCase): + + def test_init(self): + "Testing LayerMapping initialization." + + # Model field that does not exist. + bad1 = copy(city_mapping) + bad1['foobar'] = 'FooField' + + # Shapefile field that does not exist. + bad2 = copy(city_mapping) + bad2['name'] = 'Nombre' + + # Nonexistent geographic field type. + bad3 = copy(city_mapping) + bad3['point'] = 'CURVE' + + # Incrementing through the bad mapping dictionaries and + # ensuring that a LayerMapError is raised. + for bad_map in (bad1, bad2, bad3): + with self.assertRaises(LayerMapError): + LayerMapping(City, city_shp, bad_map) + + # A LookupError should be thrown for bogus encodings. + with self.assertRaises(LookupError): + LayerMapping(City, city_shp, city_mapping, encoding='foobar') + + def test_simple_layermap(self): + "Test LayerMapping import of a simple point shapefile." + # Setting up for the LayerMapping. + lm = LayerMapping(City, city_shp, city_mapping) + lm.save() + + # There should be three cities in the shape file. + self.assertEqual(3, City.objects.count()) + + # Opening up the shapefile, and verifying the values in each + # of the features made it to the model. + ds = DataSource(city_shp) + layer = ds[0] + for feat in layer: + city = City.objects.get(name=feat['Name'].value) + self.assertEqual(feat['Population'].value, city.population) + self.assertEqual(Decimal(str(feat['Density'])), city.density) + self.assertEqual(feat['Created'].value, city.dt) + + # Comparing the geometries. + pnt1, pnt2 = feat.geom, city.point + self.assertAlmostEqual(pnt1.x, pnt2.x, 5) + self.assertAlmostEqual(pnt1.y, pnt2.y, 5) + + def test_layermap_strict(self): + "Testing the `strict` keyword, and import of a LineString shapefile." + # When the `strict` keyword is set an error encountered will force + # the importation to stop. + with self.assertRaises(InvalidDecimal): + lm = LayerMapping(Interstate, inter_shp, inter_mapping) + lm.save(silent=True, strict=True) + Interstate.objects.all().delete() + + # This LayerMapping should work b/c `strict` is not set. + lm = LayerMapping(Interstate, inter_shp, inter_mapping) + lm.save(silent=True) + + # Two interstate should have imported correctly. + self.assertEqual(2, Interstate.objects.count()) + + # Verifying the values in the layer w/the model. + ds = DataSource(inter_shp) + + # Only the first two features of this shapefile are valid. + valid_feats = ds[0][:2] + for feat in valid_feats: + istate = Interstate.objects.get(name=feat['Name'].value) + + if feat.fid == 0: + self.assertEqual(Decimal(str(feat['Length'])), istate.length) + elif feat.fid == 1: + # Everything but the first two decimal digits were truncated, + # because the Interstate model's `length` field has decimal_places=2. + self.assertAlmostEqual(feat.get('Length'), float(istate.length), 2) + + for p1, p2 in zip(feat.geom, istate.path): + self.assertAlmostEqual(p1[0], p2[0], 6) + self.assertAlmostEqual(p1[1], p2[1], 6) + + def county_helper(self, county_feat=True): + "Helper function for ensuring the integrity of the mapped County models." + for name, n, st in zip(NAMES, NUMS, STATES): + # Should only be one record b/c of `unique` keyword. + c = County.objects.get(name=name) + self.assertEqual(n, len(c.mpoly)) + self.assertEqual(st, c.state.name) # Checking ForeignKey mapping. + + # Multiple records because `unique` was not set. + if county_feat: + qs = CountyFeat.objects.filter(name=name) + self.assertEqual(n, qs.count()) + + def test_layermap_unique_multigeometry_fk(self): + "Testing the `unique`, and `transform`, geometry collection conversion, and ForeignKey mappings." + # All the following should work. + try: + # Telling LayerMapping that we want no transformations performed on the data. + lm = LayerMapping(County, co_shp, co_mapping, transform=False) + + # Specifying the source spatial reference system via the `source_srs` keyword. + lm = LayerMapping(County, co_shp, co_mapping, source_srs=4269) + lm = LayerMapping(County, co_shp, co_mapping, source_srs='NAD83') + + # Unique may take tuple or string parameters. + for arg in ('name', ('name', 'mpoly')): + lm = LayerMapping(County, co_shp, co_mapping, transform=False, unique=arg) + except Exception: + self.fail('No exception should be raised for proper use of keywords.') + + # Testing invalid params for the `unique` keyword. + for e, arg in ((TypeError, 5.0), (ValueError, 'foobar'), (ValueError, ('name', 'mpolygon'))): + self.assertRaises(e, LayerMapping, County, co_shp, co_mapping, transform=False, unique=arg) + + # No source reference system defined in the shapefile, should raise an error. + if connection.features.supports_transform: + self.assertRaises(LayerMapError, LayerMapping, County, co_shp, co_mapping) + + # Passing in invalid ForeignKey mapping parameters -- must be a dictionary + # mapping for the model the ForeignKey points to. + bad_fk_map1 = copy(co_mapping) + bad_fk_map1['state'] = 'name' + bad_fk_map2 = copy(co_mapping) + bad_fk_map2['state'] = {'nombre': 'State'} + self.assertRaises(TypeError, LayerMapping, County, co_shp, bad_fk_map1, transform=False) + self.assertRaises(LayerMapError, LayerMapping, County, co_shp, bad_fk_map2, transform=False) + + # There exist no State models for the ForeignKey mapping to work -- should raise + # a MissingForeignKey exception (this error would be ignored if the `strict` + # keyword is not set). + lm = LayerMapping(County, co_shp, co_mapping, transform=False, unique='name') + self.assertRaises(MissingForeignKey, lm.save, silent=True, strict=True) + + # Now creating the state models so the ForeignKey mapping may work. + State.objects.bulk_create([ + State(name='Colorado'), State(name='Hawaii'), State(name='Texas') + ]) + + # If a mapping is specified as a collection, all OGR fields that + # are not collections will be converted into them. For example, + # a Point column would be converted to MultiPoint. Other things being done + # w/the keyword args: + # `transform=False`: Specifies that no transform is to be done; this + # has the effect of ignoring the spatial reference check (because the + # county shapefile does not have implicit spatial reference info). + # + # `unique='name'`: Creates models on the condition that they have + # unique county names; geometries from each feature however will be + # appended to the geometry collection of the unique model. Thus, + # all of the various islands in Honolulu county will be in in one + # database record with a MULTIPOLYGON type. + lm = LayerMapping(County, co_shp, co_mapping, transform=False, unique='name') + lm.save(silent=True, strict=True) + + # A reference that doesn't use the unique keyword; a new database record will + # created for each polygon. + lm = LayerMapping(CountyFeat, co_shp, cofeat_mapping, transform=False) + lm.save(silent=True, strict=True) + + # The county helper is called to ensure integrity of County models. + self.county_helper() + + def test_test_fid_range_step(self): + "Tests the `fid_range` keyword and the `step` keyword of .save()." + # Function for clearing out all the counties before testing. + def clear_counties(): + County.objects.all().delete() + + State.objects.bulk_create([ + State(name='Colorado'), State(name='Hawaii'), State(name='Texas') + ]) + + # Initializing the LayerMapping object to use in these tests. + lm = LayerMapping(County, co_shp, co_mapping, transform=False, unique='name') + + # Bad feature id ranges should raise a type error. + bad_ranges = (5.0, 'foo', co_shp) + for bad in bad_ranges: + self.assertRaises(TypeError, lm.save, fid_range=bad) + + # Step keyword should not be allowed w/`fid_range`. + fr = (3, 5) # layer[3:5] + self.assertRaises(LayerMapError, lm.save, fid_range=fr, step=10) + lm.save(fid_range=fr) + + # Features IDs 3 & 4 are for Galveston County, Texas -- only + # one model is returned because the `unique` keyword was set. + qs = County.objects.all() + self.assertEqual(1, qs.count()) + self.assertEqual('Galveston', qs[0].name) + + # Features IDs 5 and beyond for Honolulu County, Hawaii, and + # FID 0 is for Pueblo County, Colorado. + clear_counties() + lm.save(fid_range=slice(5, None), silent=True, strict=True) # layer[5:] + lm.save(fid_range=slice(None, 1), silent=True, strict=True) # layer[:1] + + # Only Pueblo & Honolulu counties should be present because of + # the `unique` keyword. Have to set `order_by` on this QuerySet + # or else MySQL will return a different ordering than the other dbs. + qs = County.objects.order_by('name') + self.assertEqual(2, qs.count()) + hi, co = tuple(qs) + hi_idx, co_idx = tuple(map(NAMES.index, ('Honolulu', 'Pueblo'))) + self.assertEqual('Pueblo', co.name) + self.assertEqual(NUMS[co_idx], len(co.mpoly)) + self.assertEqual('Honolulu', hi.name) + self.assertEqual(NUMS[hi_idx], len(hi.mpoly)) + + # Testing the `step` keyword -- should get the same counties + # regardless of we use a step that divides equally, that is odd, + # or that is larger than the dataset. + for st in (4, 7, 1000): + clear_counties() + lm.save(step=st, strict=True) + self.county_helper(county_feat=False) + + def test_model_inheritance(self): + "Tests LayerMapping on inherited models. See #12093." + icity_mapping = {'name': 'Name', + 'population': 'Population', + 'density': 'Density', + 'point': 'POINT', + 'dt': 'Created', + } + + # Parent model has geometry field. + lm1 = LayerMapping(ICity1, city_shp, icity_mapping) + lm1.save() + + # Grandparent has geometry field. + lm2 = LayerMapping(ICity2, city_shp, icity_mapping) + lm2.save() + + self.assertEqual(6, ICity1.objects.count()) + self.assertEqual(3, ICity2.objects.count()) + + def test_invalid_layer(self): + "Tests LayerMapping on invalid geometries. See #15378." + invalid_mapping = {'point': 'POINT'} + lm = LayerMapping(Invalid, invalid_shp, invalid_mapping, + source_srs=4326) + lm.save(silent=True) + + def test_charfield_too_short(self): + mapping = copy(city_mapping) + mapping['name_short'] = 'Name' + lm = LayerMapping(City, city_shp, mapping) + with self.assertRaises(InvalidString): + lm.save(silent=True, strict=True) + + def test_textfield(self): + "Tests that String content fits also in a TextField" + mapping = copy(city_mapping) + mapping['name_txt'] = 'Name' + lm = LayerMapping(City, city_shp, mapping) + lm.save(silent=True, strict=True) + self.assertEqual(City.objects.count(), 3) + self.assertEqual(City.objects.get(name='Houston').name_txt, "Houston") + + def test_encoded_name(self): + """ Test a layer containing utf-8-encoded name """ + city_shp = os.path.join(shp_path, 'ch-city', 'ch-city.shp') + lm = LayerMapping(City, city_shp, city_mapping) + lm.save(silent=True, strict=True) + self.assertEqual(City.objects.count(), 1) + self.assertEqual(City.objects.all()[0].name, "Zürich") + + +class OtherRouter(object): + def db_for_read(self, model, **hints): + return 'other' + + def db_for_write(self, model, **hints): + return self.db_for_read(model, **hints) + + def allow_relation(self, obj1, obj2, **hints): + return None + + def allow_migrate(self, db, model): + return True + + +@skipUnless(HAS_GDAL, "LayerMapRouterTest needs GDAL support") +@skipUnlessDBFeature("gis_enabled") +@override_settings(DATABASE_ROUTERS=[OtherRouter()]) +class LayerMapRouterTest(TestCase): + + @unittest.skipUnless(len(settings.DATABASES) > 1, 'multiple databases required') + def test_layermapping_default_db(self): + lm = LayerMapping(City, city_shp, city_mapping) + self.assertEqual(lm.using, 'other') |
