Skip to content

Commit

Permalink
Update APIv2 handling of Tags and UIGroups
Browse files Browse the repository at this point in the history
  - Added eight new endpoints:
      /api/2/projects/:id/tags/
      /api/2/projects/:id/uigroups/
      /api/2/uigroups/
      /api/2/uigroups/
      /api/2/uigroups/:id/
      /api/2/uiitems/
      /api/2/uiitems/:id/
      /api/2/tags/
    See roundware.api2.filters for available params

  - Renamed MasterUI to UIGroup

  - Renamed UIMapping to UIItem

  - Changed all mentions of MasterUI and UIMapping in the codebase

  - Added new fields as per the docs:
    - Asset.loc_alt_text
    - Asset.loc_caption
    - UIItem.parent
    - Tag.location
    - Tag.project

  - Deprecated Tag.relationships into Tag.relationships_old
    It should be accessible via APIv1 as before

  - Added new TagRelationship model: {id,tag,parent}
    - TR.tag points to a Tag object
    - TR.parent points to another TR, or null
    - This allows for complex tag trees

  - Added new Tag.relationships serialization
    Added basic UIGroup and UIItem serialization

  - As per the docs, Tags returns id i/o tag_id

  - Added TagRelationshipAdmin to admin.py

  - Split get_project_tags into *_old and *_new
    IAPI/1 aliases *_old, API/2 aliases *_new

  - Adjusted Django admin to reflect these changes

  - Fixed the APIv2 tests to account for the responses
    More tests for APIv2 should be written!
  • Loading branch information
IllyaMoskvin committed Jun 9, 2016
1 parent cd3a4e7 commit c929b4c
Show file tree
Hide file tree
Showing 29 changed files with 750 additions and 356 deletions.
2 changes: 1 addition & 1 deletion docs/source/docs/api/get_tags.html.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ JSON response is broken into sections for `listen` and `speak` at the top level,
and tag metadata to be different for each mode. Beneath that, there are nodes for each tag category
and then the tags themselves.

`get_tags` response is governed by the `master_ui` and `ui_mapping` objects.
`get_tags` response is governed by the `ui_group` and `ui_item` objects.

### Example Response

Expand Down
2 changes: 1 addition & 1 deletion roundware/api1/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from roundware.lib import dbus_send
from roundware.lib.exception import RoundException
from roundwared import gpsmixer
from roundware.lib.api import (get_project_tags, t, log_event, form_to_request,
from roundware.lib.api import (get_project_tags_old as get_project_tags, t, log_event, form_to_request,
check_for_single_audiotrack, get_parameter_from_request)

logger = logging.getLogger(__name__)
Expand Down
35 changes: 34 additions & 1 deletion roundware/api2/filters.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from roundware.rw.models import Event, Asset, ListeningHistoryItem, Tag
from roundware.rw.models import Event, Asset, ListeningHistoryItem, Tag, TagRelationship, UIItem, UIGroup
from distutils.util import strtobool
import django_filters

Expand Down Expand Up @@ -101,3 +101,36 @@ class TagFilterSet(django_filters.FilterSet):

class Meta:
model = Tag


class TagRelationshipFilterSet(django_filters.FilterSet):
tag_id = django_filters.NumberFilter()
parent_id = django_filters.NumberFilter()

class Meta:
model = TagRelationship


class UIGroupFilterSet(django_filters.FilterSet):
name = django_filters.CharFilter(lookup_type='startswith')
ui_mode = django_filters.TypedChoiceFilter(choices=UIGroup.UI_MODES)
tag_category_id = django_filters.NumberFilter()
select = django_filters.TypedChoiceFilter(choices=UIGroup.SELECT_METHODS)
active = django_filters.TypedChoiceFilter(choices=BOOLEAN_CHOICES, coerce=strtobool)
index = django_filters.NumberFilter()
project_id = django_filters.NumberFilter()

class Meta:
model = UIGroup


class UIItemFilterSet(django_filters.FilterSet):
ui_group_id = django_filters.NumberFilter()
index = django_filters.NumberFilter()
tag_id = django_filters.NumberFilter()
default = django_filters.TypedChoiceFilter(choices=BOOLEAN_CHOICES, coerce=strtobool)
active = django_filters.TypedChoiceFilter(choices=BOOLEAN_CHOICES, coerce=strtobool)
parent_id = django_filters.NumberFilter()

class Meta:
model = UIItem
63 changes: 59 additions & 4 deletions roundware/api2/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
# The Django REST Framework object serializers for the V2 API.
from __future__ import unicode_literals
from roundware.rw.models import (Asset, Event, Envelope, Language, ListeningHistoryItem,
Project, Tag, Session, LocalizedString, Vote)
LocalizedString, Project, Tag, TagRelationship, UIGroup,
UIItem, Session, Vote)
from roundware.lib.api import request_stream, vote_count_by_asset
from rest_framework import serializers
from rest_framework.serializers import ValidationError
Expand Down Expand Up @@ -222,6 +223,16 @@ def create(self, vdata):
return stream


class TagRelationshipSerializer(serializers.ModelSerializer):
class Meta:
model = TagRelationship

def to_representation(self, obj):
result = super(TagRelationshipSerializer, self).to_representation(obj)
# TODO: Determine if anything needs to be serialized here
return result


class TagSerializer(serializers.ModelSerializer):
class Meta:
model = Tag
Expand All @@ -232,13 +243,57 @@ def to_representation(self, obj):
session = None
if "session" in self.context:
session = self.context["session"]

# TODO: determine who is using these loc_* fields - not in spec doc!
# TODO: `filter` field is also not in spec doc
for field in ["loc_msg", "loc_description"]:
result[field] = _select_localized_string(result[field], session=session)
# field renaming
result["tag_id"] = result["id"]
del result["id"]

# field renaming - spec doc asks for `id`
# however, all other responses return *_id format
# TODO: determine if `id` should be renamed to `tag_id`

# result["tag_id"] = result["id"]
# del result["id"]

del result["relationships_old"]

tagrelationships = TagRelationship.objects.filter(tag=result["id"])
serializer = TagRelationshipSerializer(tagrelationships, many=True)

result["relationships"] = serializer.data

return result

class UIItemSerializer(serializers.ModelSerializer):
class Meta:
model = UIItem

def to_representation(self, obj):
result = super(UIItemSerializer, self).to_representation(obj)
# TODO: Determine if anything needs to be serialized here
return result

class UIGroupSerializer(serializers.ModelSerializer):
class Meta:
model = UIGroup

def to_representation(self, obj):
result = super(UIGroupSerializer, self).to_representation(obj)
# find correct localized strings
session = None
if "session" in self.context:
session = self.context["session"]

for field in ["header_text_loc"]:
result[field] = _select_localized_string(result[field], session=session)

uiitems = UIItem.objects.filter(ui_group=result["id"])
serializer = UIItemSerializer(uiitems, many=True)

result["ui_items"] = serializer.data

return result

class UserSerializer(serializers.Serializer):
username = serializers.CharField(max_length=255, required=False)
Expand Down
4 changes: 4 additions & 0 deletions roundware/api2/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@
router.register(r'tags', views.TagViewSet)
router.register(r'users', views.UserViewSet)
router.register(r'envelopes', views.EnvelopeViewSet)
router.register(r'tagrelationships', views.TagRelationshipViewSet)
router.register(r'uigroups', views.UIGroupViewSet)
router.register(r'uiitems', views.UIItemViewSet)


urlpatterns = patterns('',
url(r'^', include(router.urls)),
Expand Down
120 changes: 110 additions & 10 deletions roundware/api2/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@
from django.shortcuts import get_object_or_404
from django.http import Http404
from roundware.rw.models import (Asset, Event, Envelope, ListeningHistoryItem, Project,
Session, Tag, UserProfile)
Session, Tag, TagRelationship, UIGroup, UIItem, UserProfile)
from roundware.api2 import serializers
from roundware.api2.filters import EventFilterSet, AssetFilterSet, ListeningHistoryItemFilterSet, TagFilterSet
from roundware.lib.api import (get_project_tags, modify_stream, move_listener, heartbeat,
from roundware.api2.filters import (EventFilterSet, AssetFilterSet, ListeningHistoryItemFilterSet,
TagFilterSet, TagRelationshipFilterSet, UIGroupFilterSet,
UIItemFilterSet)
from roundware.lib.api import (get_project_tags_new as get_project_tags, modify_stream, move_listener, heartbeat,
skip_ahead, add_asset_to_envelope, get_current_streaming_asset,
save_asset_from_request, vote_asset,
vote_count_by_asset, log_event)
Expand Down Expand Up @@ -244,14 +246,21 @@ def retrieve(self, request, pk=None):

@detail_route(methods=['get'])
def tags(self, request, pk=None):
session = None
if "session_id" in request.query_params:
session = get_object_or_404(Session, pk=request.query_params["session_id"])
tags = get_project_tags(s=session)
else:
raise ParseError("session_id is required")
project = get_object_or_404(Project, pk=pk)
tags = get_project_tags(p=project)
return Response(tags)

tags = get_project_tags(p=pk)
serializer = serializers.TagSerializer(tags, context={"session": session}, many=True)
return Response({"tags":serializer.data})

@detail_route(methods=['get'])
def uigroups(self, request, pk=None):
params = request.query_params.copy()
params["project_id"] = pk
uigroups = UIGroupFilterSet(params)
serializer = serializers.UIGroupSerializer(uigroups, many=True)
return Response({"ui_groups":serializer.data})

@detail_route(methods=['get'])
def assets(self, request, pk=None):
Expand Down Expand Up @@ -352,7 +361,7 @@ def list(self, request):
"""
tags = TagFilterSet(request.query_params)
serializer = serializers.TagSerializer(tags, many=True)
return Response(serializer.data)
return Response({"tags":serializer.data})

def retrieve(self, request, pk=None):
"""
Expand All @@ -372,6 +381,97 @@ def retrieve(self, request, pk=None):
return Response(serializer.data)


class TagRelationshipViewSet(viewsets.ViewSet):
"""
API V2: api/2/tagrelationships/
api/2/tagrelationships/:id/
"""
queryset = TagRelationship.objects.all()
permission_classes = (IsAuthenticated,)

def list(self, request):
"""
GET api/2/tagrelationships/ - Provides list of TagRelationships filtered by parameters
"""
tagrelationships = TagRelationshipFilterSet(request.query_params)
serializer = serializers.TagRelationshipSerializer(tagrelationships, many=True)
return Response(serializer.data)

def retrieve(self, request, pk=None):
"""
GET api/2/tagrelationships/:id/ - Get TagRelationship by id
"""
try:
tagrelationship = TagRelationship.objects.get(pk=pk)
except TagRelationship.DoesNotExist:
raise Http404("TagRelationship not found")
# session_id not needed, because no localization..?
serializer = serializers.TagRelationshipSerializer(tagrelationship)
return Response(serializer.data)


class UIGroupViewSet(viewsets.ViewSet):
"""
API V2: api/2/uigroups/
api/2/uigroups/:uigroup_id/
"""
queryset = UIGroup.objects.all()
permission_classes = (IsAuthenticated,)

def list(self, request):
"""
GET api/2/uigroups/ - Provides list of uigroups filtered by parameters
"""
uigroups = UIGroupFilterSet(request.query_params)
serializer = serializers.UIGroupSerializer(uigroups, many=True)
return Response({"ui_groups":serializer.data})

def retrieve(self, request, pk=None):
"""
GET api/2/uigroups/:id/ - Get uigroup by id
"""
try:
uigroup = UIGroup.objects.get(pk=pk)
except UIGroup.DoesNotExist:
raise Http404("UIGroup not found")
session = None
if "session_id" in request.query_params:
try:
session = Session.objects.get(pk=request.query_params["session_id"])
except:
raise ParseError("Session not found")
serializer = serializers.UIGroupSerializer(uigroup, context={"session": session})
return Response(serializer.data)

class UIItemViewSet(viewsets.ViewSet):
"""
API V2: api/2/uiitems/
api/2/uiitems/:uiitem_id/
"""
queryset = UIItem.objects.all()
permission_classes = (IsAuthenticated,)

def list(self, request):
"""
GET api/2/uiitems/ - Provides list of uiitems filtered by parameters
"""
uiitems = UIItemFilterSet(request.query_params)
serializer = serializers.UIItemSerializer(uiitems, many=True)
return Response(serializer.data)

def retrieve(self, request, pk=None):
"""
GET api/2/uiitems/:id/ - Get uiitem by id
"""
try:
uiitem = UIItem.objects.get(pk=pk)
except UIItem.DoesNotExist:
raise Http404("UIItem not found")
# session_id not needed, because no localization..?
serializer = serializers.UIItemSerializer(uiitem)
return Response(serializer.data)


class UserViewSet(viewsets.ViewSet):
"""
API V2: api/2/users/
Expand Down
48 changes: 32 additions & 16 deletions roundware/lib/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,29 +33,29 @@ def t(msg, field, language):
pass
return msg


def get_project_tags(p=None, s=None):
# This function only used by API/2 to keep backwards compatability
def get_project_tags_old(p=None, s=None):
if s is None and p is None:
raise RoundException("Must pass either a project or a session")
language = models.Language.objects.filter(language_code='en')[0]
if s is not None:
p = s.project
language = s.language

m = models.MasterUI.objects.filter(project=p)
uigroups = models.UIGroup.objects.filter(project=p)
modes = {}

for masterui in m:
if masterui.active:
mappings = models.UIMapping.objects.filter(
master_ui=masterui, active=True)
header = t("", masterui.header_text_loc, language)
for uigroup in uigroups:
if uigroup.active:
mappings = models.UIItem.objects.filter(
ui_group=uigroup, active=True)
header = t("", uigroup.header_text_loc, language)

masterD = {'name': masterui.name,
masterD = {'name': uigroup.name,
'header_text': header,
'code': masterui.tag_category.name,
'select': masterui.get_select_display(),
'order': masterui.index
'code': uigroup.tag_category.name,
'select': uigroup.get_select_display(),
'order': uigroup.index
}
masterOptionsList = []

Expand All @@ -70,19 +70,35 @@ def get_project_tags(p=None, s=None):
# {'tag_id':self.tag.id,'order':self.index,'value':self.tag.value}

masterOptionsList.append({'tag_id': mapping.tag.id, 'order': mapping.index, 'data': mapping.tag.data,
'relationships': mapping.tag.get_relationships(),
'relationships': mapping.tag.get_relationships_old(),
'description': mapping.tag.description, 'shortcode': mapping.tag.value,
'loc_description': loc_desc,
'value': t("", mapping.tag.loc_msg, language)})
masterD["options"] = masterOptionsList
masterD["defaults"] = default
if masterui.ui_mode not in modes:
modes[masterui.ui_mode] = [masterD, ]
if uigroup.ui_mode not in modes:
modes[uigroup.ui_mode] = [masterD, ]
else:
modes[masterui.ui_mode].append(masterD)
modes[uigroup.ui_mode].append(masterD)

return modes

# This function is used in API/2 (aliased as get_project_tags)
def get_project_tags_new(p=None, s=None):

if s is None and p is None:
raise RoundException("Must pass either a project or a session")

language = models.Language.objects.filter(language_code='en')[0]

if s is not None and p is None:
p = s.project
language = s.language

tags = models.Tag.objects.filter(project=p)

return tags


# @profile(stats=True)
def request_stream(request):
Expand Down
Loading

0 comments on commit c929b4c

Please sign in to comment.