Skip to content

Commit

Permalink
Implement flexible format validators
Browse files Browse the repository at this point in the history
This commit adds flexible format validators.

There is one new query: `all_format_validators` that enables querying
all available format validators.

A text- or textarea-question can have one or multiple format_validators
assigned.

When querying a question, there is a new `FormatValidatorsConnection`,
containing all FormatValidator-objects assigned on the question.

For this there is a new `QuestionValidator` that ensures, referenced
format_validators exist.

`translate_value()` has been moved to `core.utils`, as this is used in the
`data_source`- and `form`-app.

There is a new extension-point `format_validators.py`.

When adding the tests for this, it became obvious, that there was something
wrong with the parameters for `DataSource`. As I was touching those tests
anyway, I fixed this as well.

Added commented out extension volume to docker-compose.yml. Also added
forgotten data_sources extension volume.

Additionally this commit also cleans up the snapshots for document and
question as there were quite a lot old unused snapshots.

Closes projectcaluma#360
  • Loading branch information
open-dynaMIX committed May 6, 2019
1 parent e1331e3 commit 885b962
Show file tree
Hide file tree
Showing 20 changed files with 1,406 additions and 782 deletions.
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,15 @@ Per default no CORS headers are set but can be configured with following options
* `CORS_ORIGIN_ALLOW_ALL`: If True, the whitelist will not be used and all origins will be accepted. (default: False)
* `CORS_ORIGIN_WHITELIST`: A list of origin hostnames that are authorized to make cross-site HTTP requests.

#### FormatValidators
FormatValidator classes can validate input data for answers, based on rules set on the question.

There are a variety of base FormatValidators ready to use. There is also an [extension point](#formatvalidator-classes) for them.

List of built-in base FormatValidators:

* email

#### Extension points

Caluma is meant to be used as a service with a clean API hence it doesn't provide a Django app.
Expand Down Expand Up @@ -246,6 +255,27 @@ see [docker-compose.yml](https://github.com/projectcaluma/caluma/blob/master/doc

Afterwards you can configure it in `VALIDATION_CLASSES` as `caluma.extensions.validations.CustomValidation`.

##### FormatValidator classes

Custom FormatValidator classes can be created to validate input data for answers, based
on rules set on the question.
q
There are some [core FormatValidators](#formatvalidators) you can use.

A custom FormatValidator looks like this:

```python
from caluma.form.format_validators import BaseFormatValidator


class MyFormatValidator(BaseFormatValidator):
slug = "my-slug"
name = {"en": "englisch name", "de": "Deutscher Name"}
regex = r"(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$)"
error_msg = {"en": "Not valid", "de": "Nicht gültig"}
```


## File question and answers
In order to make use of Calumas file question and answer, you need to set up a storage provider.

Expand Down
16 changes: 16 additions & 0 deletions caluma/core/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from django.conf import settings
from django.utils import translation


def translate_value(value):
"""Translate a string.
:param value: dict or string
:return: translated value or original string
"""
lang = translation.get_language()
if lang in value:
return value[lang]
if settings.LANGUAGE_CODE in value:
return value[settings.LANGUAGE_CODE]
return value
21 changes: 4 additions & 17 deletions caluma/data_source/data_source_handlers.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
from collections import namedtuple

from django.conf import settings
from django.utils import translation
from django.utils.module_loading import import_string

from caluma.core.utils import translate_value

DataSource = namedtuple("DataSource", ["name", "info"])


Expand All @@ -19,15 +20,6 @@ def is_iterable_and_no_string(value):
return False


def translate_value(value, lang):
if isinstance(value, dict):
if lang in value:
return value[lang]
if settings.LANGUAGE_CODE in value:
return value[settings.LANGUAGE_CODE]
return value


class Data:
def __init__(self, data):
self.data = data
Expand All @@ -42,10 +34,7 @@ def load(self):
elif isinstance(self.data[1], dict):
return (
str(self.data[0]),
str(
translate_value(self.data[1], translation.get_language())
or self.data[0]
),
str(translate_value(self.data[1]) or self.data[0]),
)
return str(self.data[0]), str(self.data[1])

Expand All @@ -63,9 +52,7 @@ def get_data_sources(dic=False):
if dic:
return {ds.__name__: ds for ds in data_source_classes}
return [
DataSource(
name=ds.__name__, info=translate_value(ds.info, translation.get_language())
)
DataSource(name=ds.__name__, info=translate_value(ds.info))
for ds in data_source_classes
]

Expand Down
1 change: 1 addition & 0 deletions caluma/extensions/format_validators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# To be overwritten for format validator extensions point
1 change: 1 addition & 0 deletions caluma/form/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ class QuestionFactory(DjangoModelFactory):
configuration = {}
meta = {}
is_archived = False
format_validators = []

row_form = Maybe(
"is_table", yes_declaration=SubFactory(FormFactory), no_declaration=None
Expand Down
77 changes: 77 additions & 0 deletions caluma/form/format_validators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import re
from collections import namedtuple

from django.conf import settings
from django.utils.module_loading import import_string
from rest_framework.exceptions import ValidationError

from caluma.core.utils import translate_value


class BaseFormatValidator:
r"""Basic format validator class to be extended by any format validator implementation.
A custom format validator class could look like this:
```
>>> from caluma.form.format_validators import BaseFormatValidator
...
...
... class CustomFormatValidator(BaseFormatValidator):
... slug = "email"
... name = {"en": "E-mail", "de": "E-Mail"}
... regex = r"(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$)"
... error_msg = {"en": "Not an e-mail address", "de": "Keine E-Mail adresse"}
```
"""

def __init__(self):
if not all(
[self.slug, self.regex, self.name, self.error_msg]
): # pragma: no cover
raise NotImplementedError("Missing properties!")

def validate(self, value, document):
if not re.match(self.regex, value):
raise ValidationError(translate_value(self.error_msg))


class EMailFormatValidator(BaseFormatValidator):
slug = "email"
name = {"en": "E-mail", "de": "E-Mail"}
regex = r"(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$)"
error_msg = {"en": "Not an e-mail address", "de": "Keine E-Mail adresse"}


base_format_validators = [EMailFormatValidator]


FormatValidator = namedtuple("FormatValidator", ["slug", "name", "regex", "error_msg"])


def get_format_validators(include=None, dic=False):
"""Get all FormatValidators.
:param include: List of FormatValidators to include
:param dic: Should return a dict
:return: List of FormatValidator-objects if dic False otherwise dict
"""

format_validator_classes = [
import_string(cls) for cls in settings.FORMAT_VALIDATOR_CLASSES
] + base_format_validators
if include is not None:
format_validator_classes = [
fvc for fvc in format_validator_classes if fvc.slug in include
]
if dic:
return {ds.slug: ds for ds in format_validator_classes}
return [
FormatValidator(
slug=ds.slug,
name=translate_value(ds.name),
regex=ds.regex,
error_msg=translate_value(ds.error_msg),
)
for ds in format_validator_classes
]
24 changes: 24 additions & 0 deletions caluma/form/migrations/0015_question_format_validators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.20 on 2019-05-06 06:55
from __future__ import unicode_literals

import django.contrib.postgres.fields
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [("form", "0014_auto_20190429_0910")]

operations = [
migrations.AddField(
model_name="question",
name="format_validators",
field=django.contrib.postgres.fields.ArrayField(
base_field=models.CharField(max_length=255),
blank=True,
default=list,
size=None,
),
)
]
5 changes: 4 additions & 1 deletion caluma/form/models.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from django.contrib.postgres.fields import JSONField
from django.contrib.postgres.fields import ArrayField, JSONField
from django.db import models, transaction
from django.db.models.signals import post_init
from django.dispatch import receiver
Expand Down Expand Up @@ -104,6 +104,9 @@ class Question(SlugModel):
related_name="+",
help_text="Reference this question has been copied from",
)
format_validators = ArrayField(
models.CharField(max_length=255), blank=True, default=list
)

@property
def max_length(self):
Expand Down
40 changes: 38 additions & 2 deletions caluma/form/schema.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import graphene
from graphene import ConnectionField, relay
from graphene.types import generic
from graphene import Connection, ConnectionField, relay
from graphene.types import ObjectType, generic
from graphene_django.rest_framework import serializer_converter

from ..core.filters import DjangoFilterConnectionField, DjangoFilterSetConnectionField
Expand All @@ -10,6 +10,7 @@
from ..data_source.data_source_handlers import get_data_source_data
from ..data_source.schema import DataSourceDataConnection
from . import filters, models, serializers
from .format_validators import get_format_validators


class QuestionJexl(graphene.String):
Expand Down Expand Up @@ -106,9 +107,25 @@ def get_queryset(cls, queryset, info):
return Question.get_queryset(queryset, info)


class FormatValidator(ObjectType):
slug = graphene.String(required=True)
name = graphene.String(required=True)
regex = graphene.String(required=True)
error_msg = graphene.String(required=True)


class FormatValidatorConnection(Connection):
class Meta:
node = FormatValidator


class TextQuestion(QuestionQuerysetMixin, DjangoObjectType):
max_length = graphene.Int()
placeholder = graphene.String()
format_validators = graphene.ConnectionField(FormatValidatorConnection)

def resolve_format_validators(self, info):
return get_format_validators(include=self.format_validators)

class Meta:
model = models.Question
Expand All @@ -129,6 +146,10 @@ class Meta:
class TextareaQuestion(QuestionQuerysetMixin, DjangoObjectType):
max_length = graphene.Int()
placeholder = graphene.String()
format_validators = graphene.ConnectionField(FormatValidatorConnection)

def resolve_format_validators(self, info):
return get_format_validators(include=self.format_validators)

class Meta:
model = models.Question
Expand Down Expand Up @@ -159,6 +180,7 @@ class Meta:
"sub_form",
"placeholder",
"static_content",
"format_validators",
)
use_connection = False
interfaces = (Question, graphene.Node)
Expand All @@ -180,6 +202,7 @@ class Meta:
"sub_form",
"placeholder",
"static_content",
"format_validators",
)
use_connection = False
interfaces = (Question, graphene.Node)
Expand All @@ -200,6 +223,7 @@ class Meta:
"row_form",
"sub_form",
"placeholder",
"format_validators",
)
use_connection = False
interfaces = (Question, graphene.Node)
Expand All @@ -222,6 +246,7 @@ class Meta:
"sub_form",
"placeholder",
"static_content",
"format_validators",
)
use_connection = False
interfaces = (Question, graphene.Node)
Expand All @@ -244,6 +269,7 @@ class Meta:
"sub_form",
"placeholder",
"static_content",
"format_validators",
)
use_connection = False
interfaces = (Question, graphene.Node)
Expand All @@ -265,6 +291,7 @@ class Meta:
"row_form",
"sub_form",
"static_content",
"format_validators",
)
use_connection = False
interfaces = (Question, graphene.Node)
Expand All @@ -286,6 +313,7 @@ class Meta:
"row_form",
"sub_form",
"static_content",
"format_validators",
)
use_connection = False
interfaces = (Question, graphene.Node)
Expand All @@ -303,6 +331,7 @@ class Meta:
"sub_form",
"placeholder",
"static_content",
"format_validators",
)
use_connection = False
interfaces = (Question, graphene.Node)
Expand All @@ -320,6 +349,7 @@ class Meta:
"row_form",
"placeholder",
"static_content",
"format_validators",
)
use_connection = False
interfaces = (Question, graphene.Node)
Expand All @@ -338,6 +368,7 @@ class Meta:
"sub_form",
"placeholder",
"static_content",
"format_validators",
)
use_connection = False
interfaces = (Question, graphene.Node)
Expand All @@ -355,6 +386,7 @@ class Meta:
"sub_form",
"placeholder",
"is_required",
"format_validators",
)
use_connection = False
interfaces = (Question, graphene.Node)
Expand Down Expand Up @@ -802,3 +834,7 @@ class Query(object):
all_documents = DjangoFilterConnectionField(
Document, filterset_class=filters.DocumentFilterSet
)
all_format_validators = ConnectionField(FormatValidatorConnection)

def resolve_all_format_validators(self, info):
return get_format_validators()
Loading

0 comments on commit 885b962

Please sign in to comment.