Skip to content
This repository was archived by the owner on Oct 15, 2020. It is now read-only.

Commit 10fffaf

Browse files
Merge pull request #157 from BaseAdresseNationale/housenumber-positions
Replace HouseNumber.center by HouseNumber.positions
2 parents 8170189 + a523376 commit 10fffaf

16 files changed

+327
-250
lines changed

ban/core/models.py

+9-8
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ class BaseModel(BaseResource, BaseVersioned):
2323
class Model(ResourceModel, Versioned, metaclass=BaseModel):
2424
resource_fields = ['version', 'created_at', 'created_by', 'modified_at',
2525
'modified_by', 'attributes']
26+
exclude_for_collection = ['version', 'created_at', 'created_by',
27+
'modified_at', 'modified_by']
2628
# 'version' is validated by us.
2729
resource_schema = {'version': {'required': False},
2830
'created_at': {'readonly': True},
@@ -56,6 +58,7 @@ def __str__(self):
5658
class Municipality(NamedModel):
5759
identifiers = ['siren', 'insee']
5860
resource_fields = ['name', 'alias', 'insee', 'siren', 'postcodes']
61+
exclude_for_version = ['postcodes']
5962

6063
insee = db.CharField(max_length=5, unique=True)
6164
siren = db.CharField(max_length=9, unique=True, null=True)
@@ -135,9 +138,9 @@ def housenumbers(self):
135138
class HouseNumber(Model):
136139
identifiers = ['cia', 'laposte', 'ign']
137140
resource_fields = ['number', 'ordinal', 'parent', 'cia', 'laposte',
138-
'ancestors', 'center', 'ign', 'postcode']
141+
'ancestors', 'positions', 'ign', 'postcode']
139142
resource_schema = {'cia': {'readonly': True},
140-
'center': {'readonly': True}}
143+
'positions': {'readonly': True}}
141144

142145
number = db.CharField(max_length=16, null=True)
143146
ordinal = db.CharField(max_length=16, null=True)
@@ -167,10 +170,8 @@ def compute_cia(self):
167170
self.number, self.ordinal)
168171

169172
@property
170-
def center(self):
171-
position = self.position_set.first()
172-
return (position.center.geojson
173-
if position and position.center else None)
173+
def positions_extended(self):
174+
return list(self.positions.as_resource_list())
174175

175176
@property
176177
def ancestors_extended(self):
@@ -224,13 +225,13 @@ class Position(Model):
224225

225226
name = db.CharField(max_length=200, null=True)
226227
center = db.PointField(verbose_name=_("center"), null=True, index=True)
227-
housenumber = db.ForeignKeyField(HouseNumber)
228+
housenumber = db.ForeignKeyField(HouseNumber, related_name='positions')
228229
parent = db.ForeignKeyField('self', related_name='children', null=True)
229230
source = db.CharField(max_length=64, null=True)
230231
kind = db.CharField(max_length=64, choices=KIND)
231232
positioning = db.CharField(max_length=32, choices=POSITIONING)
232233
ign = db.CharField(max_length=24, null=True, unique=True)
233-
comment = peewee.TextField(null=True)
234+
comment = db.TextField(null=True)
234235

235236
class Meta:
236237
indexes = (

ban/core/resource.py

+24-11
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,8 @@ def as_resource_list(self):
3434

3535
class BaseResource(peewee.BaseModel):
3636

37-
def include_field_for_compact(cls, name):
38-
if name in ['version', 'created_at', 'created_by', 'modified_at',
39-
'modified_by']:
37+
def include_field_for_collection(cls, name):
38+
if name in cls.exclude_for_collection:
4039
return False
4140
attr = getattr(cls, name, None)
4241
exclude = (db.ManyToManyField, peewee.ReverseRelationDescriptor,
@@ -49,6 +48,8 @@ def __new__(mcs, name, bases, attrs, **kwargs):
4948
# Inherit and extend instead of replacing.
5049
resource_fields = attrs.pop('resource_fields', None)
5150
resource_schema = attrs.pop('resource_schema', None)
51+
exclude_for_collection = attrs.pop('exclude_for_collection', None)
52+
exclude_for_version = attrs.pop('exclude_for_version', None)
5253
cls = super().__new__(mcs, name, bases, attrs, **kwargs)
5354
if resource_fields is not None:
5455
inherited = getattr(cls, 'resource_fields', {})
@@ -58,10 +59,20 @@ def __new__(mcs, name, bases, attrs, **kwargs):
5859
inherited = getattr(cls, 'resource_schema', {})
5960
resource_schema.update(inherited)
6061
cls.resource_schema = resource_schema
61-
cls.extended_fields = cls.resource_fields
62-
cls.compact_fields = [
63-
n for n in cls.extended_fields
64-
if mcs.include_field_for_compact(cls, n)] + ['resource']
62+
if exclude_for_collection is not None:
63+
inherited = getattr(cls, 'exclude_for_collection', [])
64+
exclude_for_collection.extend(inherited)
65+
cls.exclude_for_collection = exclude_for_collection
66+
if exclude_for_version is not None:
67+
inherited = getattr(cls, 'exclude_for_version', [])
68+
exclude_for_version.extend(inherited)
69+
cls.exclude_for_version = exclude_for_version
70+
cls.collection_fields = [
71+
n for n in cls.resource_fields
72+
if mcs.include_field_for_collection(cls, n)] + ['resource']
73+
cls.versioned_fields = [
74+
n for n in cls.resource_fields
75+
if n not in cls.exclude_for_version]
6576
cls.build_resource_schema()
6677
return cls
6778

@@ -70,6 +81,8 @@ class ResourceModel(db.Model, metaclass=BaseResource):
7081
resource_fields = ['id']
7182
identifiers = []
7283
resource_schema = {'id': {'readonly': True}}
84+
exclude_for_collection = []
85+
exclude_for_version = []
7386

7487
id = db.CharField(max_length=50, unique=True, null=False)
7588

@@ -91,7 +104,7 @@ def build_resource_schema(cls):
91104
"""Map Peewee models to Cerberus validation schema."""
92105
schema = dict(cls.resource_schema)
93106
for name, field in cls._meta.fields.items():
94-
if name not in cls.extended_fields:
107+
if name not in cls.resource_fields:
95108
continue
96109
if field.primary_key:
97110
continue
@@ -133,17 +146,17 @@ def resource(self):
133146
@property
134147
def as_resource(self):
135148
"""Resource plus relations."""
136-
return {f: self.extended_field(f) for f in self.extended_fields}
149+
return {f: self.extended_field(f) for f in self.resource_fields}
137150

138151
@property
139152
def as_relation(self):
140153
"""Resources plus relation references without metadata."""
141-
return {f: self.compact_field(f) for f in self.compact_fields}
154+
return {f: self.compact_field(f) for f in self.collection_fields}
142155

143156
@property
144157
def as_version(self):
145158
"""Resources plus relations references and metadata."""
146-
return {f: self.compact_field(f) for f in self.extended_fields}
159+
return {f: self.compact_field(f) for f in self.versioned_fields}
147160

148161
def extended_field(self, name):
149162
value = getattr(self, '{}_extended'.format(name), getattr(self, name))

ban/core/validators.py

+5
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ def _validate_type_point(self, field, value):
2020
if not isinstance(value, (str, list, tuple, Point)):
2121
self._error(field, 'Invalid Point: {}'.format(value))
2222

23+
def _validate_type_string(self, field, value):
24+
if value is None and not self.schema[field].get('required'):
25+
return True
26+
return super()._validate_type_string(field, value)
27+
2328
def _validate_type_foreignkey(self, field, value):
2429
if not isinstance(value, int):
2530
self._error(field, 'No matching resource for {}'.format(value))

ban/core/versioning.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ def store_version(self):
7575
old = None
7676
if self.version > 1:
7777
old = self.load_version(self.version - 1)
78-
old.close_period(new.period[0])
78+
old.close_period(new.period.lower)
7979
if Diff.ACTIVE:
8080
Diff.create(old=old, new=new, created_at=self.modified_at)
8181

ban/db/fields.py

+16-3
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
__all__ = ['PointField', 'ForeignKeyField', 'CharField', 'IntegerField',
1212
'HStoreField', 'UUIDField', 'ArrayField', 'DateTimeField',
1313
'BooleanField', 'BinaryJSONField', 'PostCodeField', 'FantoirField',
14-
'ManyToManyField', 'PasswordField', 'DateRangeField']
14+
'ManyToManyField', 'PasswordField', 'DateRangeField', 'TextField']
1515

1616

1717
lonlat_pattern = re.compile('^[\[\(]{1}(?P<lon>-?\d{,3}(:?\.\d*)?), ?(?P<lat>-?\d{,3}(\.\d*)?)[\]\)]{1}$') # noqa
@@ -99,6 +99,8 @@ class ForeignKeyField(peewee.ForeignKeyField):
9999
schema_type = 'foreignkey'
100100

101101
def coerce(self, value):
102+
if not value:
103+
return None
102104
if isinstance(value, peewee.Model):
103105
value = value.pk
104106
elif isinstance(value, dict):
@@ -117,10 +119,19 @@ def _get_related_name(self):
117119
class CharField(peewee.CharField):
118120
schema_type = 'string'
119121

120-
def db_value(self, value):
122+
def coerce(self, value):
121123
if self.null and not value:
122124
return None
123-
return super().db_value(value)
125+
return super().coerce(value)
126+
127+
128+
class TextField(peewee.TextField):
129+
schema_type = 'string'
130+
131+
def coerce(self, value):
132+
if self.null and not value:
133+
return None
134+
return super().coerce(value)
124135

125136

126137
class IntegerField(peewee.IntegerField):
@@ -177,6 +188,8 @@ class FantoirField(CharField):
177188
max_length = 9
178189

179190
def coerce(self, value):
191+
if not value:
192+
return None
180193
value = str(value)
181194
if len(value) == 10:
182195
value = value[:9]

ban/db/model.py

+6
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,9 @@ def first(cls, *expressions):
4444
qs = qs.where(*expressions)
4545
# See https://github.com/coleifer/peewee/commit/eeb6d4d727da8536906a00c490f94352465e90bb # noqa
4646
return qs.limit(1).first()
47+
48+
def __setattr__(self, name, value):
49+
attr = getattr(self.__class__, name, None)
50+
if attr and hasattr(attr, 'coerce'):
51+
value = attr.coerce(value)
52+
return super().__setattr__(name, value)

ban/http/resources.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ def on_get(self, req, resp, **params):
9494
"""Get {resource} collection."""
9595
qs = self.get_collection(req, resp, **params)
9696
qs = self.get_where_clause(req, qs)
97-
self.collection(req, resp, qs.as_resource())
97+
self.collection(req, resp, qs.as_resource_list())
9898

9999
@auth.protect
100100
@app.endpoint(path='/{identifier}')
@@ -279,7 +279,7 @@ def get_collection(self, req, resp, **kwargs):
279279
def on_get_positions(self, req, resp, *args, **kwargs):
280280
"""Retrieve {resource} positions."""
281281
instance = self.get_object(**kwargs)
282-
qs = instance.position_set.as_resource_list()
282+
qs = instance.positions.as_resource_list()
283283
self.collection(req, resp, qs)
284284

285285

ban/tests/http/test_housenumber.py

+5-5
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ def test_get_housenumber_collection(get, url):
7171
resp = get(url('housenumber'))
7272
assert resp.json['total'] == 5
7373
for obj in objs:
74-
assert json.loads(dumps(obj.as_resource)) in resp.json['collection']
74+
assert json.loads(dumps(obj.as_relation)) in resp.json['collection']
7575

7676

7777
@authorize
@@ -82,7 +82,7 @@ def test_get_housenumber_collection_can_be_filtered_by_bbox(get, url):
8282
resp = get(url('housenumber', query_string=bbox))
8383
assert resp.json['total'] == 1
8484
# JSON transform internals tuples to lists.
85-
resource = position.housenumber.as_resource
85+
resource = position.housenumber.as_relation
8686
assert resp.json['collection'][0] == json.loads(dumps(resource))
8787

8888

@@ -142,16 +142,16 @@ def test_housenumber_with_two_positions_is_not_duplicated_in_bbox(get, url):
142142
resp = get(url('housenumber', query_string=bbox))
143143
assert resp.json['total'] == 1
144144
# JSON transform internals tuples to lists.
145-
data = json.loads(dumps(position.housenumber.as_resource))
145+
data = json.loads(dumps(position.housenumber.as_relation))
146146
assert resp.json['collection'][0] == data
147147

148148

149149
@authorize
150150
def test_get_housenumber_with_position(get, url):
151151
housenumber = HouseNumberFactory()
152-
PositionFactory(housenumber=housenumber, center=(1, 1))
152+
position = PositionFactory(housenumber=housenumber, center=(1, 1))
153153
resp = get(url('housenumber-resource', identifier=housenumber.id))
154-
assert resp.json['center'] == {'coordinates': [1, 1], 'type': 'Point'}
154+
assert resp.json['positions'] == [json.loads(dumps(position.as_relation))]
155155

156156

157157
@authorize

ban/tests/http/test_position.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -392,7 +392,7 @@ def test_get_position_collection_can_be_filtered_by_bbox(get, url):
392392
resp = get(url('position', query_string=bbox))
393393
assert resp.json['total'] == 1
394394
# JSON transform internals tuples to lists.
395-
resource = position.as_resource
395+
resource = position.as_relation
396396
assert resp.json['collection'][0] == json.loads(dumps(resource))
397397

398398

ban/tests/http/test_postcode.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,8 @@ def test_postcode_select_use_default_orderby(get, url):
4545
assert resp.status == falcon.HTTP_200
4646
assert resp.json['total'] == 3
4747
assert resp.json['collection'][0]['code'] == '90101'
48-
assert resp.json['collection'][1]['municipality']['insee'] == "90002"
49-
assert resp.json['collection'][2]['municipality']['insee'] == "90001"
48+
assert resp.json['collection'][1]['municipality'] == mun1.id
49+
assert resp.json['collection'][2]['municipality'] == mun2.id
5050

5151

5252
@authorize

ban/tests/test_diff.py

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
from .factories import MunicipalityFactory
2+
3+
4+
def test_only_changed_columns_should_be_in_diff():
5+
municipality = MunicipalityFactory(name='Moret-sur-Loing', insee='77316',
6+
siren='987654321')
7+
municipality.name = 'Orvanne'
8+
municipality.siren = '123456789'
9+
# "Changed" with same value.
10+
municipality.insee = '77316'
11+
municipality.increment_version()
12+
municipality.save()
13+
diff = municipality.versions[1].diff
14+
assert len(diff.diff) == 2 # name, siren
15+
assert diff.diff['name']['old'] == 'Moret-sur-Loing'
16+
assert diff.diff['name']['new'] == 'Orvanne'
17+
assert diff.diff['siren']['old'] == '987654321'
18+
assert diff.diff['siren']['new'] == '123456789'
19+
municipality.insee = '77319'
20+
municipality.increment_version()
21+
municipality.save()
22+
diff = municipality.versions[2].diff
23+
assert len(diff.diff) == 1 # insee
24+
assert diff.diff['insee']['old'] == '77316'
25+
assert diff.diff['insee']['new'] == '77319'
26+
27+
28+
def test_adding_value_to_arrayfield_column():
29+
municipality = MunicipalityFactory(name='Moret-sur-Loing')
30+
municipality.alias = ['Orvanne']
31+
municipality.increment_version()
32+
municipality.save()
33+
diff = municipality.versions[1].diff
34+
assert len(diff.diff) == 1 # name, siren
35+
assert diff.diff['alias']['old'] is None
36+
assert diff.diff['alias']['new'] == ['Orvanne']

ban/tests/test_managers.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from ban.core import models
22

3-
from .factories import MunicipalityFactory, GroupFactory
3+
from .factories import GroupFactory, MunicipalityFactory
44

55

66
def test_municipality_as_resource():

0 commit comments

Comments
 (0)