Skip to content

Commit eb9a1d1

Browse files
Merge pull request #1622 from IFRCGo/feature/local-units-app
Add LocalUnit Model & endpoints
2 parents e88acdc + ad02ceb commit eb9a1d1

File tree

12 files changed

+361
-0
lines changed

12 files changed

+361
-0
lines changed

local_units/__init__.py

Whitespace-only changes.

local_units/admin.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from django.contrib.gis import admin
2+
3+
from .models import LocalUnit, LocalUnitType
4+
5+
admin.site.register(LocalUnit, admin.OSMGeoAdmin)
6+
admin.site.register(LocalUnitType, admin.ModelAdmin)

local_units/apps.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from django.apps import AppConfig
2+
3+
4+
class LocalUnits(AppConfig):
5+
default_auto_field = 'django.db.models.BigAutoField'
6+
name = 'local_units'
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
from django.db import transaction
2+
import csv
3+
from django.core.management.base import BaseCommand, CommandError
4+
from django.contrib.gis.geos import Point
5+
6+
from api.models import Country
7+
from ...models import LocalUnit, LocalUnitType
8+
9+
10+
class Command(BaseCommand):
11+
help = "Import LocalUnits data from CSV"
12+
missing_args_message = "Filename is missing. Filename / path to CSV file required. Required headers in CSV: distr_code, GOadm2code, ADM2"
13+
14+
def add_arguments(self, parser):
15+
parser.add_argument('filename', nargs='+', type=str)
16+
17+
@transaction.atomic
18+
def handle(self, *args, **options):
19+
filename = options['filename'][0]
20+
with open(filename) as csvfile:
21+
reader = csv.DictReader(csvfile)
22+
for row in reader:
23+
unit = LocalUnit()
24+
unit.country = Country.objects.get(iso3=row['ISO3'])
25+
unit.type, created = LocalUnitType.objects.get_or_create(
26+
level=row['TYPECODE'],
27+
name=row['TYPENAME']
28+
)
29+
if created:
30+
print(f'New LocalUnitType created: {unit.type.name}')
31+
32+
unit.name_loc = row['NAME_LOC']
33+
unit.name_en = row['NAME_EN']
34+
unit.branch_level = int(row['TYPECODE'])
35+
unit.postcode = int(row['POSTCODE']) if row['POSTCODE'] else None
36+
unit.address_loc = row['ADDRESS_LOC']
37+
unit.address_en = row['ADDRESS_EN']
38+
unit.city_loc = row['CITY_LOC']
39+
unit.city_en = row['CITY_EN']
40+
unit.focal_person_loc = row['FOCAL_PERSON_LOC']
41+
unit.focal_person_en = row['FOCAL_PERSON_EN']
42+
unit.phone = row['TELEPHONE']
43+
unit.email = row['EMAIL']
44+
unit.link = row['WEBSITE']
45+
unit.source_en = row['SOURCE_EN']
46+
unit.source_loc = row['SOURCE_LOC']
47+
unit.location = Point(float(row['LONGITUDE']), float(row['LATITUDE']))
48+
unit.save()
49+
print(f'{unit.name_en} saved')
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# Generated by Django 3.2.16 on 2023-01-10 23:01
2+
3+
import django.contrib.gis.db.models.fields
4+
import django.core.validators
5+
from django.db import migrations, models
6+
import django.db.models.deletion
7+
8+
9+
class Migration(migrations.Migration):
10+
11+
initial = True
12+
13+
dependencies = [
14+
('api', '0160_merge_0159_auto_20221022_1542_0159_auto_20221028_0940'),
15+
]
16+
17+
operations = [
18+
migrations.CreateModel(
19+
name='LocalUnitType',
20+
fields=[
21+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
22+
('level', models.IntegerField(validators=[django.core.validators.MaxValueValidator(10), django.core.validators.MinValueValidator(0)], verbose_name='Level')),
23+
('name', models.CharField(max_length=100, verbose_name='Name')),
24+
],
25+
),
26+
migrations.CreateModel(
27+
name='LocalUnit',
28+
fields=[
29+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
30+
('local_branch_name', models.CharField(max_length=255, verbose_name='Branch name in local language')),
31+
('english_branch_name', models.CharField(max_length=255, verbose_name='Branch name in English')),
32+
('created_at', models.DateTimeField(auto_now=True, verbose_name='Created at')),
33+
('modified_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')),
34+
('draft', models.BooleanField(default=False, verbose_name='Draft')),
35+
('validated', models.BooleanField(default=True, verbose_name='Validated')),
36+
('source_en', models.CharField(blank=True, max_length=500, null=True, verbose_name='Source in Local Language')),
37+
('source_loc', models.CharField(blank=True, max_length=500, null=True, verbose_name='Source in English')),
38+
('address_loc', models.CharField(blank=True, max_length=500, null=True, verbose_name='Address in local language')),
39+
('address_en', models.CharField(blank=True, max_length=500, null=True, verbose_name='Address in English')),
40+
('city_loc', models.CharField(max_length=255, verbose_name='City in local language')),
41+
('city_en', models.CharField(max_length=255, verbose_name='City in English')),
42+
('focal_person_loc', models.CharField(blank=True, max_length=255, null=True, verbose_name='Focal person for local language')),
43+
('focal_person_en', models.CharField(blank=True, max_length=255, null=True, verbose_name='Focal person for English')),
44+
('postcode', models.CharField(max_length=10, null=True, verbose_name='Postal code')),
45+
('phone', models.CharField(blank=True, max_length=20, null=True, verbose_name='Telephone')),
46+
('email', models.EmailField(blank=True, max_length=255, null=True, verbose_name='Email')),
47+
('link', models.URLField(blank=True, max_length=255, null=True, verbose_name='Social link')),
48+
('location', django.contrib.gis.db.models.fields.PointField(srid=4326)),
49+
('country', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='local_unit_country', to='api.country', verbose_name='Country')),
50+
('type', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='local_unit_type', to='local_units.localunittype', verbose_name='Type')),
51+
],
52+
),
53+
]

local_units/migrations/__init__.py

Whitespace-only changes.

local_units/models.py

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
from django.contrib.gis.db import models
2+
from django.core.validators import MaxValueValidator, MinValueValidator
3+
from django.utils.translation import gettext_lazy as _
4+
5+
from api.models import Country
6+
7+
8+
class LocalUnitType(models.Model):
9+
level = models.IntegerField(
10+
verbose_name=_('Level'),
11+
validators=[
12+
MaxValueValidator(10),
13+
MinValueValidator(0)
14+
]
15+
)
16+
name = models.CharField(
17+
max_length=100,
18+
verbose_name=_('Name')
19+
)
20+
21+
22+
class LocalUnit(models.Model):
23+
country = models.ForeignKey(
24+
Country, on_delete=models.SET_NULL, verbose_name=_('Country'),
25+
related_name='local_unit_country', null=True
26+
)
27+
type = models.ForeignKey(
28+
LocalUnitType, on_delete=models.SET_NULL, verbose_name=_('Type'),
29+
related_name='local_unit_type', null=True
30+
)
31+
local_branch_name = models.CharField(
32+
max_length=255,
33+
verbose_name=_('Branch name in local language')
34+
)
35+
english_branch_name = models.CharField(
36+
max_length=255,
37+
verbose_name=_('Branch name in English')
38+
)
39+
40+
created_at = models.DateTimeField(
41+
verbose_name=_('Created at'),
42+
auto_now=True
43+
)
44+
modified_at = models.DateTimeField(
45+
verbose_name=_('Updated at'),
46+
auto_now=True
47+
)
48+
draft = models.BooleanField(default=False, verbose_name=_('Draft'))
49+
validated = models.BooleanField(default=True, verbose_name=_('Validated'))
50+
source_en = models.CharField(
51+
max_length=500,
52+
blank=True,
53+
null=True,
54+
verbose_name=_('Source in Local Language')
55+
)
56+
source_loc = models.CharField(
57+
max_length=500,
58+
blank=True,
59+
null=True,
60+
verbose_name=_('Source in English')
61+
)
62+
address_loc = models.CharField(
63+
max_length=500,
64+
blank=True,
65+
null=True,
66+
verbose_name=_('Address in local language')
67+
)
68+
address_en = models.CharField(
69+
max_length=500,
70+
blank=True,
71+
null=True,
72+
verbose_name=_('Address in English')
73+
)
74+
city_loc = models.CharField(max_length=255, verbose_name=_('City in local language'))
75+
city_en = models.CharField(max_length=255, verbose_name=_('City in English'))
76+
focal_person_loc = models.CharField(
77+
max_length=255,
78+
blank=True,
79+
null=True,
80+
verbose_name=_('Focal person for local language')
81+
)
82+
focal_person_en = models.CharField(
83+
max_length=255,
84+
blank=True,
85+
null=True,
86+
verbose_name=_('Focal person for English')
87+
)
88+
postcode = models.CharField(max_length=10, null=True, verbose_name=_('Postal code'))
89+
phone = models.CharField(
90+
max_length=20,
91+
blank=True,
92+
null=True,
93+
verbose_name=_('Telephone')
94+
)
95+
email = models.EmailField(
96+
max_length=255,
97+
blank=True,
98+
null=True,
99+
verbose_name=_('Email')
100+
)
101+
link = models.URLField(
102+
max_length=255,
103+
blank=True,
104+
null=True,
105+
verbose_name=_('Social link')
106+
)
107+
location = models.PointField()

local_units/serializers.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import json
2+
from rest_framework.serializers import ModelSerializer, SerializerMethodField
3+
4+
from .models import LocalUnit, LocalUnitType
5+
from api.models import Country
6+
7+
8+
class CountrySerializer(ModelSerializer):
9+
10+
class Meta:
11+
model = Country
12+
fields = (
13+
'name', 'iso3'
14+
)
15+
16+
class LocalUnitTypeSerializer(ModelSerializer):
17+
18+
class Meta:
19+
model = LocalUnitType
20+
fields = (
21+
'name', 'level'
22+
)
23+
24+
class LocalUnitSerializer(ModelSerializer):
25+
location = SerializerMethodField()
26+
country = CountrySerializer()
27+
type = LocalUnitTypeSerializer()
28+
class Meta:
29+
model = LocalUnit
30+
fields = [
31+
'local_branch_name', 'english_branch_name', 'type', 'country',
32+
'created_at', 'modified_at', 'draft', 'validated', 'postcode',
33+
'address_loc', 'address_en', 'city_loc', 'city_en', 'link',
34+
'location', 'focal_person_loc', 'focal_person_en',
35+
'source_loc', 'source_en'
36+
# 'email', 'phone',
37+
]
38+
39+
def get_location(self, unit):
40+
return json.loads(unit.location.geojson)
41+
42+
def get_country(self, unit):
43+
return {'country'}
44+
45+
def get_type(self, unit):
46+
return {'type'}

local_units/test_views.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import factory
2+
from django.test import TestCase
3+
from django.contrib.gis.geos import Point
4+
5+
from .models import LocalUnit, LocalUnitType
6+
from api.models import Country, Region
7+
8+
9+
class LocalUnitFactory(factory.django.DjangoModelFactory):
10+
class Meta:
11+
model = LocalUnit
12+
13+
location = Point(12, 38)
14+
15+
16+
class TestLocalUnitsListView(TestCase):
17+
def setUp(self):
18+
region = Region.objects.create(name=2)
19+
country = Country.objects.create(name='Nepal', iso3='NLP', region=region)
20+
country_1 = Country.objects.create(name='Philippines', iso3='PHL', region=region)
21+
type = LocalUnitType.objects.create(level=0, name='Level 0')
22+
type_1 = LocalUnitType.objects.create(level=1, name='Level 1')
23+
LocalUnitFactory.create_batch(5, country=country, type=type)
24+
LocalUnitFactory.create_batch(5, country=country_1, type=type_1)
25+
26+
def test_list(self):
27+
response = self.client.get('/api/v2/local-unit/')
28+
self.assertEqual(response.status_code, 200)
29+
self.assertEqual(response.data['count'], 10)
30+
self.assertEqual(response.data['results'][0]['location']['coordinates'], [12, 38])
31+
self.assertEqual(response.data['results'][0]['country']['name'], 'Nepal')
32+
self.assertEqual(response.data['results'][0]['country']['iso3'], 'NLP')
33+
self.assertEqual(response.data['results'][0]['type']['name'], 'Level 0')
34+
self.assertEqual(response.data['results'][0]['type']['level'], 0)
35+
36+
def test_filter(self):
37+
response = self.client.get('/api/v2/local-unit/?country__name=Nepal')
38+
self.assertEqual(response.status_code, 200)
39+
self.assertEqual(response.data['count'], 5)
40+
41+
response = self.client.get('/api/v2/local-unit/?country__name=Philippines')
42+
self.assertEqual(response.status_code, 200)
43+
self.assertEqual(response.data['count'], 5)
44+
45+
response = self.client.get('/api/v2/local-unit/?country__name=Belgium')
46+
self.assertEqual(response.status_code, 200)
47+
self.assertEqual(response.data['count'], 0)
48+
49+
50+
class TestLocalUnitsDetailView(TestCase):
51+
def setUp(self):
52+
region = Region.objects.create(name=2)
53+
country = Country.objects.create(name='Nepal', iso3='NLP', region=region)
54+
type = LocalUnitType.objects.create(level=0, name='Level 0')
55+
LocalUnitFactory.create_batch(2, country=country, type=type)
56+
57+
def test_detail(self):
58+
local_unit = LocalUnit.objects.all().first()
59+
response = self.client.get(f'/api/v2/local-unit/{local_unit.id}/')
60+
self.assertEqual(response.status_code, 200)
61+
self.assertEqual(response.data['location']['coordinates'], [12, 38])
62+
self.assertEqual(response.data['country']['name'], 'Nepal')
63+
self.assertEqual(response.data['country']['iso3'], 'NLP')
64+
self.assertEqual(response.data['type']['name'], 'Level 0')
65+
self.assertEqual(response.data['type']['level'], 0)

local_units/views.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
from rest_framework.generics import (
2+
ListAPIView, RetrieveAPIView
3+
)
4+
from django_filters import rest_framework as filters
5+
6+
from .models import LocalUnit
7+
from .serializers import LocalUnitSerializer
8+
9+
10+
class LocalUnitFilters(filters.FilterSet):
11+
class Meta:
12+
model = LocalUnit
13+
fields = ('country', 'country__name')
14+
15+
16+
class LocalUnitListAPIView(ListAPIView):
17+
queryset = LocalUnit.objects.all()
18+
serializer_class = LocalUnitSerializer
19+
filterset_class = LocalUnitFilters
20+
search_fields = ('name',)
21+
22+
23+
class LocalUnitDetailAPIView(RetrieveAPIView):
24+
queryset = LocalUnit.objects.all()
25+
serializer_class = LocalUnitSerializer

0 commit comments

Comments
 (0)