Skip to content

Commit

Permalink
Merge pull request #2334 from szok/transition-api
Browse files Browse the repository at this point in the history
Transition API
  • Loading branch information
szok committed Apr 7, 2016
2 parents d7f73e9 + 028c862 commit 9fd5acc
Show file tree
Hide file tree
Showing 13 changed files with 499 additions and 17 deletions.
1 change: 0 additions & 1 deletion .gitignore
Expand Up @@ -75,5 +75,4 @@ src/ralph/var/
debian/ralph-core*

.idea

!src/ralph/lib
3 changes: 2 additions & 1 deletion src/ralph/api/__init__.py
@@ -1,9 +1,10 @@
from ralph.api.serializers import RalphAPISerializer
from ralph.api.viewsets import RalphAPIViewSet
from ralph.api.viewsets import RalphAPIViewSet, RalphReadOnlyAPIViewSet
from ralph.api.routers import router

__all__ = [
'RalphAPISerializer',
'RalphAPIViewSet',
'RalphReadOnlyAPIViewSet',
'router',
]
18 changes: 17 additions & 1 deletion src/ralph/api/fields.py
@@ -1,6 +1,6 @@
from collections import OrderedDict

from rest_framework.fields import ChoiceField, Field
from rest_framework.fields import ChoiceField, Field, MultipleChoiceField


class StrField(Field):
Expand Down Expand Up @@ -56,3 +56,19 @@ def to_internal_value(self, data):
except KeyError:
pass
return super(ReversedChoiceField, self).to_internal_value(data)


class ModelMultipleChoiceField(MultipleChoiceField):
"""
Multiple Model Choices Field for Django Rest Framework
Changes list of integer data to Django Model queryset
"""
def __init__(self, *args, **kwargs):
self.model = kwargs['choices'].queryset.model
super().__init__(*args, **kwargs)

def to_internal_value(self, data):
if data:
return self.model.objects.filter(pk__in=data)
return self.model.objects.none()
8 changes: 8 additions & 0 deletions src/ralph/api/viewsets.py
Expand Up @@ -137,3 +137,11 @@ class RalphAPIViewSet(
metaclass=RalphAPIViewSetMetaclass
):
pass


class RalphReadOnlyAPIViewSet(
RalphAPIViewSetMixin,
viewsets.ReadOnlyModelViewSet,
metaclass=RalphAPIViewSetMetaclass
):
pass
27 changes: 25 additions & 2 deletions src/ralph/lib/external_services/models.py
@@ -1,11 +1,14 @@
# -*- coding: utf-8 -*-
import logging
import uuid
from datetime import date

from dateutil.parser import parse
from dj.choices import Choices
from django.contrib.auth import get_user_model
from django.contrib.contenttypes.models import ContentType
from django.db import models
from django.db.models.query import QuerySet
from django.utils.translation import ugettext_lazy as _
from django_extensions.db.fields.json import JSONField

Expand Down Expand Up @@ -137,8 +140,21 @@ def dump_obj_to_jsonable(cls, obj):
Dump obj to JSON-acceptable format
"""
result = obj
if isinstance(obj, (list, tuple)):
if isinstance(obj, (list, tuple, set)):
result = [cls.dump_obj_to_jsonable(p) for p in obj]
elif isinstance(obj, QuerySet):
result = {
'__django_queryset': True,
'value': [i.pk for i in obj],
'content_type_id': ContentType.objects.get_for_model(
obj.model
).pk
}
elif isinstance(obj, date):
result = {
'__date': True,
'value': str(obj)
}
elif isinstance(obj, dict):
result = {}
for k, v in obj.items():
Expand All @@ -161,7 +177,14 @@ def _restore_django_models(cls, obj):
if isinstance(obj, (list, tuple)):
result = [cls._restore_django_models(p) for p in obj]
elif isinstance(obj, dict):
if obj.get('__django_model') is True:
if obj.get('__date') is True:
result = parse(obj.get('value')).date()
elif obj.get('__django_queryset') is True:
ct = ContentType.objects.get_for_id(obj['content_type_id'])
result = ct.model_class().objects.filter(
pk__in=obj.get('value')
)
elif obj.get('__django_model') is True:
ct = ContentType.objects.get_for_id(obj['content_type_id'])
result = ct.get_object_for_this_type(pk=obj['object_pk'])
else:
Expand Down
23 changes: 23 additions & 0 deletions src/ralph/lib/transitions/api/routers.py
@@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
from django.conf.urls import url

from ralph.api import router
from ralph.lib.transitions.api.views import (
TransitionActionViewSet,
TransitionJobViewSet,
TransitionModelViewSet,
TransitionView,
TransitionViewSet
)

router.register(r'transitions', TransitionViewSet)
router.register(r'transitions-action', TransitionActionViewSet)
router.register(r'transitions-model', TransitionModelViewSet)
router.register(r'transitions-job', TransitionJobViewSet)


urlpatterns = [url(
r'^transition/(?P<transition_pk>[0-9]+)/(?P<obj_pk>\w+)$',
TransitionView.as_view(),
name='transition-view'
)]
39 changes: 39 additions & 0 deletions src/ralph/lib/transitions/api/serializers.py
@@ -0,0 +1,39 @@
# -*- coding: utf-8 -*-
from rest_framework import serializers

from ralph.api import RalphAPISerializer
from ralph.lib.transitions.models import (
Action,
Transition,
TransitionJob,
TransitionModel
)


class TransitionModelSerializer(RalphAPISerializer):

model = serializers.CharField(source='content_type.model')

class Meta:
model = TransitionModel
exclude = ('content_type',)


class TransitionActionSerializer(RalphAPISerializer):

class Meta:
model = Action
exclude = ('content_type',)


class TransitionSerializer(RalphAPISerializer):

class Meta:
model = Transition


class TransitionJobSerializer(RalphAPISerializer):

class Meta:
model = TransitionJob
exclude = ('content_type',)
145 changes: 145 additions & 0 deletions src/ralph/lib/transitions/api/views.py
@@ -0,0 +1,145 @@
# -*- coding: utf-8 -*-
from django import forms
from django.core.urlresolvers import reverse
from rest_framework import serializers
from rest_framework.response import Response
from rest_framework.views import APIView

from ralph.api import RalphReadOnlyAPIViewSet
from ralph.api.fields import ModelMultipleChoiceField
from ralph.lib.transitions.api.serializers import (
TransitionActionSerializer,
TransitionJobSerializer,
TransitionModelSerializer,
TransitionSerializer
)
from ralph.lib.transitions.models import (
Action,
run_transition,
Transition,
TransitionJob,
TransitionModel
)
from ralph.lib.transitions.views import collect_actions

FIELD_MAP = {
forms.CharField: (serializers.CharField, [
'max_length', 'initial', 'required'
]),
forms.BooleanField: (serializers.BooleanField, ['initial', 'required']),
forms.URLField: (serializers.URLField, ['initial', 'required']),
forms.IntegerField: (serializers.IntegerField, ['initial', 'required']),
forms.DecimalField: (serializers.DecimalField, ['initial', 'required']),
forms.DateField: (serializers.DateField, ['initial', 'required']),
forms.DateTimeField: (serializers.DateTimeField, ['initial', 'required']),
forms.TimeField: (serializers.TimeField, ['initial', 'required']),
forms.ModelMultipleChoiceField: (ModelMultipleChoiceField, [
'initial', 'required', 'choices'
]),
forms.ModelChoiceField: (serializers.ChoiceField, [
'initial', 'required', 'choices'
]),
forms.ChoiceField: (serializers.ChoiceField, [
'initial', 'required', 'choices'
]),
}


class TransitionJobViewSet(RalphReadOnlyAPIViewSet):
queryset = TransitionJob.objects.all()
serializer_class = TransitionJobSerializer


class TransitionModelViewSet(RalphReadOnlyAPIViewSet):
queryset = TransitionModel.objects.all()
serializer_class = TransitionModelSerializer


class TransitionActionViewSet(RalphReadOnlyAPIViewSet):
queryset = Action.objects.all()
serializer_class = TransitionActionSerializer


class TransitionViewSet(RalphReadOnlyAPIViewSet):
queryset = Transition.objects.all()
serializer_class = TransitionSerializer
prefetch_related = ['actions']


class TransitionView(APIView):

def dispatch(self, request, transition_pk, obj_pk, *args, **kwargs):
self.transition = Transition.objects.get(
pk=transition_pk
)
self.obj = self.transition.model.content_type.get_object_for_this_type(
pk=obj_pk
)
self.actions, self.return_attachment = collect_actions(
self.obj, self.transition
)
return super().dispatch(request, *args, **kwargs)

def get_fields(self):
fields = {}
fields_name_map = {}
for action in self.actions:
action_fields = getattr(action, 'form_fields', {})
for name, options in action_fields.items():
field_class, field_attr = FIELD_MAP.get(
options['field'].__class__, None
)
attrs = {
name: getattr(
options['field'], name, None
) for name in field_attr
}
fields_name_map[name] = '{}__{}'.format(action.__name__, name)
fields[name] = field_class(**attrs)
return fields, fields_name_map

def get_serializer_class(self):
class_name = 'TransitionSerializer{}'.format(
self.obj.__class__.__name__
)
class_attrs, _ = self.get_fields()
serializer_class = type(
class_name, (serializers.Serializer,), class_attrs
)

return serializer_class

def get_serializer(self, only_class=False):
return self.get_serializer_class()()

def add_function_name_to_data(self, data):
result = {}
_, fields_name_map = self.get_fields()
for k, v in data.items():
result[fields_name_map.get(k)] = v
return result

def post(self, request, *args, **kwargs):
serializer = self.get_serializer_class()(data=request.data)
result = {'status': False}
if serializer.is_valid():
data = self.add_function_name_to_data(serializer.validated_data)
transition_result = run_transition(
[self.obj],
self.transition,
self.transition.model.field_name,
data,
request=request
)
if self.transition.is_async:
result['job_ids'] = [
reverse('transitionjob-detail', args=(i,))
for i in transition_result
]
result['status'] = True if transition_result else False
else:
result['status'] = transition_result[0]
else:
result['errors'] = serializer.errors

return Response(result)
2 changes: 1 addition & 1 deletion src/ralph/lib/transitions/async.py
Expand Up @@ -144,7 +144,7 @@ def _perform_async_transition(transition_job):

# save obj and history
_post_transition_instance_processing(
obj, transition, transition_job.params, func_history_kwargs,
obj, transition, transition_job.params['data'], func_history_kwargs,
user=transition_job.user, attachment=attachment,
)
transition_job.success()
2 changes: 1 addition & 1 deletion src/ralph/lib/transitions/models.py
Expand Up @@ -162,7 +162,7 @@ def _check_instances_for_transition(instances, transition):
Args:
instances: Objects to checks.
transition: The transition object or a string.
transition: The transition object.
Raises:
TransitionNotAllowedError: An error ocurred when one or more of
Expand Down

0 comments on commit 9fd5acc

Please sign in to comment.