Skip to content

Commit

Permalink
add sync API
Browse files Browse the repository at this point in the history
  • Loading branch information
saxix committed Jul 9, 2024
1 parent 1a3ef94 commit fb5b4f5
Show file tree
Hide file tree
Showing 14 changed files with 256 additions and 46 deletions.
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ dependencies = [
"django-adminactions>=2.3.0",
"duckdb>=1.0.0",
"python-calamine>=0.2.0",
"requests>=2.32.3",
"responses>=0.25.3",
]
requires-python = ">=3.11"
readme = "README.md"
Expand Down Expand Up @@ -79,6 +81,7 @@ dev = [
"responses>=0.25.3",
"django-csp>=3.8",
"bandit>=1.7.9",
"pdm-bump>=0.9.0",
]

[tool.pdm.scripts]
Expand Down
33 changes: 4 additions & 29 deletions src/hope_flex_fields/admin/definition.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
import io
import json
import tempfile
from io import StringIO
from json import JSONDecodeError
from pathlib import Path

from django import forms
from django.contrib import messages
from django.contrib.admin import ModelAdmin, register
from django.core.exceptions import ValidationError
from django.core.management import call_command
from django.core.serializers.base import DeserializationError
from django.core.validators import FileExtensionValidator
from django.http import HttpResponse, HttpResponseRedirect
Expand All @@ -22,6 +17,7 @@

from ..forms import FieldDefinitionForm
from ..models import FieldDefinition, get_default_attrs
from ..utils import dumpdata_to_buffer, loaddata_from_buffer


@deconstructible
Expand Down Expand Up @@ -76,41 +72,20 @@ def js_validation(self, obj):

@view()
def export_all(self, request):
buf = StringIO()
call_command(
"dumpdata",
"hope_flex_fields",
use_natural_primary_keys=True,
use_natural_foreign_keys=True,
stdout=buf,
)
buf.seek(0)
return HttpResponse(buf.getvalue(), content_type="application/json")
data = dumpdata_to_buffer()
return HttpResponse(data, content_type="application/json")

@view()
def import_all(self, request):
ctx = self.get_common_context(request, title="Import Configuration")
if request.method == "POST":
form = ImportConfigurationForm(request.POST, request.FILES)
if form.is_valid():
workdir = Path(".").absolute()
kwargs = {
"dir": workdir,
"prefix": "~LOADDATA",
"suffix": ".json",
"delete": False,
}
with tempfile.NamedTemporaryFile(**kwargs) as fdst:
fdst.write(form.files["file"].file.read())
fixture = (workdir / fdst.name).absolute()
out = io.StringIO()
try:
call_command("loaddata", fixture, stdout=out, verbosity=3)
loaddata_from_buffer(form.files["file"].file)
self.message_user(request, "Data successfully imported.")
except DeserializationError as e:
self.message_user(request, str(e), messages.ERROR)
finally:
fixture.unlink()
return HttpResponseRedirect("..")
else:
form = ImportConfigurationForm()
Expand Down
60 changes: 55 additions & 5 deletions src/hope_flex_fields/api/serializers.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,65 @@
from rest_framework import serializers
from strategy_field.utils import fqn

from hope_flex_fields.models import FieldDefinition
from hope_flex_fields.models import DataChecker, FieldDefinition, Fieldset, FlexField


class FieldDefinitionSerializer(serializers.HyperlinkedModelSerializer):
class BaseSerializer(serializers.ModelSerializer):
pass


class FieldDefinitionSerializer(BaseSerializer):
field_type = serializers.SerializerMethodField()

class Meta:
model = FieldDefinition
fields = ["name", "description", "field_type", "attrs", "regex", "validation"]
fields = [
"last_modified",
"name",
"description",
"field_type",
"attrs",
"regex",
"validation",
]

def get_field_type(self, obj: FieldDefinition):
return fqn(obj.field_type)


class FlexFieldSerializer(BaseSerializer):
field = serializers.SlugRelatedField(read_only=True, slug_field="name")

class Meta:
model = FlexField
fields = [
"last_modified",
"name",
"description",
"field",
"fieldset",
"attrs",
"regex",
"validation",
]

def get_field_type(self, obj):
return fqn(obj)

class FieldsetSerializer(BaseSerializer):
# fields = serializers.SlugRelatedField(
# many=True, read_only=True, slug_field="name"
# )
fields = FlexFieldSerializer(many=True)

class Meta:
model = Fieldset
fields = ["last_modified", "name", "description", "extends", "fields"]


class DataCheckerSerializer(BaseSerializer):
fieldsets = serializers.SlugRelatedField(
many=True, read_only=True, slug_field="name"
)

class Meta:
model = DataChecker
fields = ["last_modified", "name", "description", "fieldsets"]
44 changes: 39 additions & 5 deletions src/hope_flex_fields/api/views.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,44 @@
from rest_framework import permissions, viewsets
from django.http import HttpResponse

from ..models import FieldDefinition
from .serializers import FieldDefinitionSerializer
from rest_framework import viewsets

from ..config import CONFIG
from ..models import DataChecker, FieldDefinition, Fieldset, FlexField
from ..utils import dumpdata_to_buffer
from .serializers import (
DataCheckerSerializer,
FieldDefinitionSerializer,
FieldsetSerializer,
FlexFieldSerializer,
)

class FieldDefinitionViewSet(viewsets.ModelViewSet):

class Base:
permission_classes = CONFIG["API_PERMISSION_CLASSES"]
authentication_classes = CONFIG["API_AUTHENTICATION_CLASSES"]


class FieldDefinitionViewSet(Base, viewsets.ModelViewSet):
queryset = FieldDefinition.objects.all().order_by("pk")
serializer_class = FieldDefinitionSerializer
permission_classes = [permissions.IsAuthenticated]


class FieldsetViewSet(Base, viewsets.ModelViewSet):
queryset = Fieldset.objects.all().order_by("pk")
serializer_class = FieldsetSerializer


class FlexFieldViewSet(Base, viewsets.ModelViewSet):
queryset = FlexField.objects.all().order_by("pk")
serializer_class = FlexFieldSerializer


class DataCheckerViewSet(Base, viewsets.ModelViewSet):
queryset = DataChecker.objects.all().order_by("pk")
serializer_class = DataCheckerSerializer


class SyncViewSet(Base, viewsets.ModelViewSet):

def list(self, request, *args, **kwargs):
return HttpResponse(dumpdata_to_buffer(), content_type="application/json")
13 changes: 13 additions & 0 deletions src/hope_flex_fields/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from django.conf import settings
from django.utils.module_loading import import_string

CONFIG = {
"API_PERMISSION_CLASSES": ["rest_framework.permissions.IsAuthenticated"],
"API_AUTHENTICATION_CLASSES": ["rest_framework.authentication.BasicAuthentication"],
"API_TOKEN": "",
"MASTER_URL": None,
}

CONFIG.update(**getattr(settings, "FLEX_FIELDS_CONFIG", {}))
for entry in ["API_AUTHENTICATION_CLASSES", "API_PERMISSION_CLASSES"]:
CONFIG[entry] = [import_string(m) for m in CONFIG[entry]]
1 change: 1 addition & 0 deletions src/hope_flex_fields/models/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ class FlexForm(forms.Form):


class AbstractField(models.Model):
last_modified = models.DateTimeField(auto_now=True)
name = models.CharField(max_length=255)
description = models.TextField(max_length=500, blank=True, null=True, default="")
attrs = models.JSONField(default=dict, blank=True, null=False)
Expand Down
2 changes: 2 additions & 0 deletions src/hope_flex_fields/models/datachecker.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ def get_by_natural_key(self, name):


class DataCheckerFieldset(models.Model):
last_modified = models.DateTimeField(auto_now=True)
checker = models.ForeignKey(
"DataChecker", on_delete=models.CASCADE, related_name="members"
)
Expand All @@ -78,6 +79,7 @@ class DataCheckerFieldset(models.Model):


class DataChecker(ValidatorMixin, models.Model):
last_modified = models.DateTimeField(auto_now=True)
name = models.CharField(max_length=255, unique=True)
description = models.TextField(blank=True)
fieldsets = models.ManyToManyField(Fieldset, through=DataCheckerFieldset)
Expand Down
1 change: 1 addition & 0 deletions src/hope_flex_fields/models/definition.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@


class FieldDefinitionManager(models.Manager):

def get_by_natural_key(self, name):
return self.get(name=name)

Expand Down
1 change: 1 addition & 0 deletions src/hope_flex_fields/models/fieldset.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ def get_by_natural_key(self, name):


class Fieldset(ValidatorMixin, models.Model):
last_modified = models.DateTimeField(auto_now=True)
name = models.CharField(max_length=255, unique=True)
description = models.TextField(blank=True)
extends = models.ForeignKey("self", null=True, blank=True, on_delete=models.CASCADE)
Expand Down
39 changes: 39 additions & 0 deletions src/hope_flex_fields/utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import inspect
import io
import tempfile
from io import StringIO
from pathlib import Path

from django import forms
from django.core.management import call_command
from django.forms.fields import DateTimeFormatsIterator
from django.utils.text import slugify

Expand Down Expand Up @@ -43,3 +48,37 @@ def get_kwargs_from_formfield(field: forms.Field):
value = [str(v) for v in value]
ret[attr_name] = value
return ret


def dumpdata_to_buffer():
buf = StringIO()
call_command(
"dumpdata",
"hope_flex_fields",
use_natural_primary_keys=True,
use_natural_foreign_keys=True,
stdout=buf,
)
buf.seek(0)
return buf.getvalue()


def loaddata_from_buffer(buf):
workdir = Path(".").absolute()
kwargs = {
"dir": workdir,
"prefix": "~LOADDATA",
"suffix": ".json",
"delete": False,
}
with tempfile.NamedTemporaryFile(**kwargs) as fdst:
fdst.write(buf.getvalue())
fixture = (workdir / fdst.name).absolute()
out = io.StringIO()
try:
call_command("loaddata", fixture, stdout=out, verbosity=3)
except Exception:
raise
finally:
fixture.unlink()
return out.getvalue()
7 changes: 6 additions & 1 deletion tests/admin/data.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
{
"model": "hope_flex_fields.fielddefinition",
"fields": {
"last_modified": "2024-07-08T13:16:07.112Z",
"name": "imported-intfield",
"description": "",
"attrs": {
Expand All @@ -15,6 +16,7 @@
{
"model": "hope_flex_fields.fielddefinition",
"fields": {
"last_modified": "2024-07-08T13:16:07.112Z",
"name": "imported-floatfield",
"description": "",
"attrs": {
Expand All @@ -28,12 +30,14 @@
{
"model": "hope_flex_fields.fieldset",
"fields": {
"name": "imported-fieldset"
"name": "imported-fieldset",
"last_modified": "2024-07-08T13:16:07.112Z"
}
},
{
"model": "hope_flex_fields.flexfield",
"fields": {
"last_modified": "2024-07-08T13:16:07.112Z",
"name": "imported-int",
"description": "",
"attrs": {
Expand All @@ -54,6 +58,7 @@
{
"model": "hope_flex_fields.flexfield",
"fields": {
"last_modified": "2024-07-08T13:16:07.112Z",
"name": "imported-float",
"description": "",
"attrs": {
Expand Down
Empty file removed tests/extra/demoapp/demo/api.py
Empty file.
14 changes: 12 additions & 2 deletions tests/extra/demoapp/demo/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,20 @@

from rest_framework import routers

from hope_flex_fields.api.views import FieldDefinitionViewSet
from hope_flex_fields.api.views import (
DataCheckerViewSet,
FieldDefinitionViewSet,
FieldsetViewSet,
FlexFieldViewSet,
SyncViewSet,
)

router = routers.DefaultRouter()
router.register(r"fields", FieldDefinitionViewSet)
router.register(r"field", FieldDefinitionViewSet)
router.register(r"fieldset", FieldsetViewSet)
router.register(r"datachecker", DataCheckerViewSet)
router.register(r"flexfield", FlexFieldViewSet)
router.register(r"sync", SyncViewSet, basename="sync")


urlpatterns = [
Expand Down
Loading

0 comments on commit fb5b4f5

Please sign in to comment.