Skip to content

Commit

Permalink
Merge pull request #72 from Souleymane-T/feature-add-ip-fields
Browse files Browse the repository at this point in the history
Add new field type IP
  • Loading branch information
lcognat committed Feb 15, 2021
2 parents e845263 + 04ed50a commit df5be56
Show file tree
Hide file tree
Showing 9 changed files with 213 additions and 6 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

### Added

- nothing added
- New field type `ip`

### Changed

Expand Down
1 change: 0 additions & 1 deletion concrete_datastore/api/v1/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,6 @@ def make_serializer_class(
if field.type.startswith("rel_"):
_fields += ['{}_uid'.format(name)]
fk_read_only_fields += [name]

class Meta:
model = concrete.models[meta_model.get_model_name().lower()]
fields = _fields
Expand Down
10 changes: 9 additions & 1 deletion concrete_datastore/concrete/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -850,6 +850,15 @@ class Meta:

args.update({'on_delete': getattr(models, on_delete_rule)})

elif field.f_type in ('GenericIPAddressField',):
#: If blank is True, null should be too
#: https://docs.djangoproject.com/fr/3.1/ref/models/fields/#genericipaddressfield
if args.get('blank', False) is True:
args['null'] = True
else:
args.setdefault('blank', False)
args['null'] = False

elif field.f_type in ('DateTimeField',):
if args.get('null', False) is True:
args['null'] = True
Expand All @@ -870,7 +879,6 @@ class Meta:
# Copy args to not alter the real field.f_args
args = args.copy()
args.pop('null', None)

attrs.update({field_name: getattr(models, field.f_type)(**args)})
if meta_model.get_model_name() != divider:
if meta_model.get_model_name() == "User":
Expand Down
3 changes: 3 additions & 0 deletions concrete_datastore/parsers/constants.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# coding: utf-8
from __future__ import unicode_literals, absolute_import, print_function

AUTHORIZED_IP_PROTOCOLS = ("ipv4", "ipv6", "ipv4_6")
PROTOCOL_EQUIVALENCE = {"ipv4": "IPv4", "ipv6": "IPv6", "ipv4_6": "both"}

STD_MODEL_SPECIFIER_KEYS = (
"std.specifier",
Expand Down Expand Up @@ -92,6 +94,7 @@
'email': 'EmailField',
'fk': 'ForeignKey',
'm2m': 'ManyToManyField',
'ip': 'GenericIPAddressField',
}


Expand Down
16 changes: 15 additions & 1 deletion concrete_datastore/parsers/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# coding: utf-8
from concrete_datastore.parsers.constants import VERSIONS_ATTRIBUTES
from concrete_datastore.parsers.constants import (
VERSIONS_ATTRIBUTES,
AUTHORIZED_IP_PROTOCOLS,
)


class ModelManagerGenericException(Exception):
Expand Down Expand Up @@ -33,6 +36,17 @@ class UnknownDatamodelVersionError(ModelManagerGenericException):
message = 'Key "version" of manifest should be 1.0.0.'


class UnknownIPProtocol(ModelManagerGenericException):
code = 'UnknownIPProtocol'

def __init__(self, protocol, source, field_name, *args, **kwargs):
self.message = (
f"Unknow protocol '{protocol}' for model {source}"
f" and field {field_name}. "
f"Authorized protocols are {AUTHORIZED_IP_PROTOCOLS}"
)


class MissingRelationForModel(ModelManagerGenericException):
code = 'MISSING_RELATION'

Expand Down
18 changes: 17 additions & 1 deletion concrete_datastore/parsers/meta.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from copy import deepcopy
from keyword import iskeyword
from six import with_metaclass

from django.core.validators import validate_ipv46_address
from concrete_datastore.parsers.exceptions import (
UnknownDatamodelVersionError,
MissingRelationForModel,
Expand All @@ -19,13 +19,16 @@
MissingKeyForDefinition,
ProtectedModelNameError,
ProtectedFieldNameError,
UnknownIPProtocol,
)
from concrete_datastore.parsers.validators import validate_specifier
from concrete_datastore.parsers.constants import (
VERSIONS_ATTRIBUTES,
CONCRETE_USER_PROTECTED_FIELDS,
CONCRETE_MODELS_PROTECTED_FIELDS,
CONCRETE_CUSTOM_MODELS,
PROTOCOL_EQUIVALENCE,
AUTHORIZED_IP_PROTOCOLS,
)
from concrete_datastore.parsers.models import Model
from concrete_datastore.parsers.fields import (
Expand Down Expand Up @@ -249,6 +252,7 @@ def get_meta_models(self):
'date',
'int',
'fk',
'ip',
]
#: we should consider only simple types
]
Expand Down Expand Up @@ -342,6 +346,18 @@ def update_specifier_data(
attributes.update({'null': allow_empty, 'blank': allow_empty})
if datatype == 'char':
attributes.setdefault('max_length', 250)
if datatype == 'ip':
attributes.setdefault('protocol', 'ipv4_6')
protocol = attributes['protocol'].lower()
if protocol not in AUTHORIZED_IP_PROTOCOLS:
raise UnknownIPProtocol(
protocol, model_name, spec[self.element_name]
)
default_ip = attributes.get('default')
if default_ip:
#: Raise Validation Error if the default IP is invalid
validate_ipv46_address(default_ip)
attributes['protocol'] = PROTOCOL_EQUIVALENCE[protocol]
if datatype in ['fk', 'm2m']:
field_type = (
f'rel_{"single" if datatype == "fk" else "iterable"}'
Expand Down
18 changes: 18 additions & 0 deletions tests/migrations/0007_project_ip_address.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 2.2.18 on 2021-02-15 15:37

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('concrete', '0006_publicmodelmanagerretrieve'),
]

operations = [
migrations.AddField(
model_name='project',
name='ip_address',
field=models.GenericIPAddressField(blank=True, null=True, protocol='ipv4'),
),
]
135 changes: 135 additions & 0 deletions tests/tests_api_v1_1/test_api_v1_1_ip_field.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
# coding: utf-8
from mock import MagicMock
import uuid
from rest_framework.test import APITestCase
from collections import OrderedDict
from rest_framework import status
import pendulum

from concrete_datastore.api.v1.filters import (
FilterSupportingOrBackend,
FilterSupportingRangeBackend,
)
from concrete_datastore.concrete.models import (
User,
UserConfirmation,
Project,
Skill,
DefaultDivider,
DIVIDER_MODEL,
Category,
)
from django.test import override_settings
from concrete_datastore.api.v1.datetime import format_datetime


@override_settings(DEBUG=True)
class GenericIpAdressFieldTestCase(APITestCase):
def setUp(self):
self.user = User.objects.create_user(
'johndoe@netsach.org'
# 'John',
# 'Doe',
)
self.user.set_password('plop')
self.user.save()
UserConfirmation.objects.create(user=self.user, confirmed=True).save()
url = '/api/v1.1/auth/login/'
resp = self.client.post(
url, {"email": "johndoe@netsach.org", "password": "plop"}
)
self.token = resp.data['token']

def test_success_create_with_valid_ip(self):
url_projects = '/api/v1.1/project/'

self.assertEqual(Project.objects.count(), 0)

# CREATE a valid project and ensure that request is valid(201)
resp = self.client.post(
url_projects,
{
"name": "Projects2",
# "date_creation": timezone.now(),
"ip_address": "127.0.0.1",
"description": "description de mon projet",
"skills": [],
"members": [],
},
HTTP_AUTHORIZATION='Token {}'.format(self.token),
)
self.assertEqual(
resp.status_code, status.HTTP_201_CREATED, msg=resp.content
)
self.assertEqual(Project.objects.count(), 1)

# Retrieve the project by filtering
resp = self.client.get(
f'{url_projects}?ip_address=127.0.0.1',
HTTP_AUTHORIZATION='Token {}'.format(self.token),
)
self.assertEqual(
resp.status_code, status.HTTP_200_OK, msg=resp.content
)
self.assertEqual(resp.json()['objects_count'], 1)

def test_failure_create_with_invalid_ip(self):
url_projects = '/api/v1.1/project/'

self.assertEqual(Project.objects.count(), 0)

# CREATE a valid project with an invalid IP address
resp = self.client.post(
url_projects,
{
"name": "Projects2",
# "date_creation": timezone.now(),
"ip_address": "This is the ip of my project",
"description": "description de mon projet",
"skills": [],
"members": [],
},
HTTP_AUTHORIZATION='Token {}'.format(self.token),
)
self.assertEqual(
resp.status_code, status.HTTP_400_BAD_REQUEST, msg=resp.content
)
self.assertEqual(
resp.json(),
{
'ip_address': [
'Enter a valid IPv4 address.',
'Enter a valid IPv4 or IPv6 address.',
]
},
msg=resp.content,
)
self.assertEqual(Project.objects.count(), 0)

def test_failure_create_with_ipv6(self):
url_projects = '/api/v1.1/project/'

self.assertEqual(Project.objects.count(), 0)

#: Create with an ipv6 IP rather than an ipv4
resp = self.client.post(
url_projects,
{
"name": "Projects2",
# "date_creation": timezone.now(),
"ip_address": "2001:0db8:85a3:0000:0000:8a2e:0370:7334",
"description": "description de mon projet",
"skills": [],
"members": [],
},
HTTP_AUTHORIZATION='Token {}'.format(self.token),
)
self.assertEqual(
resp.status_code, status.HTTP_400_BAD_REQUEST, msg=resp.content
)
self.assertEqual(
resp.json(),
{'ip_address': ['Enter a valid IPv4 address.']},
msg=resp.content,
)
self.assertEqual(Project.objects.count(), 0)
16 changes: 15 additions & 1 deletion tests/unittest_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -450,7 +450,12 @@
"ext.m_retrieve_minimum_level": 'anonymous',
"ext.m_list_display": ['name', 'archived'],
"ext.m_search_fields": ['name'],
"ext.m_filter_fields": ['name', 'archived', 'expected_skills'],
"ext.m_filter_fields": [
'name',
'archived',
'expected_skills',
'ip_address',
],
"std.fields": [
{
"std.name": "name",
Expand All @@ -461,6 +466,15 @@
"ext.f_type": "CharField",
"ext.f_args": {'max_length': 200},
},
{
"std.name": "ip_address",
"std.specifier": "Field",
"std.verbose_name": "IP",
"std.description": "IP of the project",
"std.type": "data",
"ext.f_type": "GenericIPAddressField",
"ext.f_args": {'protocol': 'ipv4', 'blank': True},
},
{
"std.name": "archived",
"std.specifier": "Field",
Expand Down

0 comments on commit df5be56

Please sign in to comment.