Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Annotation api #65

Merged
merged 20 commits into from Mar 3, 2017
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
31 changes: 31 additions & 0 deletions core/migrations/0004_create_users_tokens.py
@@ -0,0 +1,31 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals

import os
import binascii

from django.db import migrations


def forwards_func(apps, schema_editor):
User = apps.get_model('core', 'User')
Token = apps.get_model('authtoken', 'Token')
for user in User.objects.all().iterator():
Token.objects.get_or_create(
user=user,
defaults=dict(key=binascii.hexlify(os.urandom(20)).decode()))

def reverse_func(apps, schema_editor):
pass


class Migration(migrations.Migration):

dependencies = [
('core', '0003_statisticcache'),
('authtoken', '0002_auto_20160226_1747'),
]

operations = [
migrations.RunPython(forwards_func, reverse_func),
]
8 changes: 8 additions & 0 deletions core/models.py
@@ -1,5 +1,8 @@
from django.db import models
from django.contrib.auth.models import AbstractUser
from django.db.models.signals import post_save
from django.dispatch import receiver
from rest_framework.authtoken.models import Token

from legacy.models import Sample, Series, PlatformProbe, Platform, Analysis
from tags.models import (Tag, SeriesTag, SerieValidation,
Expand All @@ -15,6 +18,11 @@ class User(AbstractUser):
class Meta:
db_table = 'auth_user'

@receiver(post_save, sender=User)
def create_auth_token(sender, instance=None, created=False, **kwargs):
if created:
Token.objects.create(user=instance)


ATTR_PER_SAMPLE = 32
ATTR_PER_SERIE = 4
Expand Down
63 changes: 63 additions & 0 deletions starapi/serializers.py
@@ -1,7 +1,10 @@
from funcy import all, compact, map

from rest_framework import serializers

from legacy.models import Platform, Series, Analysis, MetaAnalysis, PlatformProbe
from tags.models import SerieAnnotation, Tag

from .fields import S3Field


Expand Down Expand Up @@ -41,12 +44,72 @@ def __init__(self, *args, **kwargs):
self.fields['specie'].required = True
self.fields['specie'].allow_blank = False


class SerieAnnotationSerializer(serializers.ModelSerializer):
class Meta:
model = SerieAnnotation
exclude = ['series_tag', 'created_on', 'modified_on', ]


class SampleAnnotationValidator(serializers.Serializer):
series = serializers.PrimaryKeyRelatedField(queryset=Series.objects.all())
tag = serializers.PrimaryKeyRelatedField(queryset=Tag.objects.all())
column = serializers.CharField(max_length=512, required=False, allow_blank=True, default='')
regex = serializers.CharField(max_length=512, required=False, default='')
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove column and regex fields

comment = serializers.CharField(max_length=512, required=False, default='')
values = serializers.JSONField()
# values field is a json object with sample_id as keys and tag value as values

def validate_values(self, value):
if not all(isinstance(v, (unicode, str)) for v in value.values()):
raise serializers.ValidationError(
"Values should be a dict with serie tag id as a key and tag value as a value")

if not all(isinstance(v, (unicode, str, int)) for v in value.keys()):
raise serializers.ValidationError(
"Values should be a dict with serie tag id as a key and tag value as a value")

return value

def validate(self, data):
samples = data['series'].samples.all()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Samples are defined by serie and platform.


gsm_to_id = {s.gsm_name.upper(): s.id for s in samples}
id_to_gsm = {v: k for k, v in gsm_to_id.iteritems()}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove support for ids, only work with gsms. Do not play with case.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is also funcy.flip() function for this. But you won't need this code at all )


try:
values = {
gsm_to_id[key.upper()]
if isinstance(key, (unicode, str)) and key.upper().startswith('GSM')
else
int(key): value
for key, value in data['values'].iteritems()
}
except KeyError as err:
raise serializers.ValidationError(
"There is samples with id {0} which doesn't belongs to series {1}"
.format(err.message, data['series'].id))
except ValueError as err:
raise serializers.ValidationError(unicode(err))

tagged_samples_ids = set(values.keys())
all_samples_ids = set(data['series'].samples.values_list('id', flat=True))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Totally unneeded query. Use samples from above. Or use .values_list() above.

missing_annotations = all_samples_ids - tagged_samples_ids
extra_annotations = tagged_samples_ids - all_samples_ids

if missing_annotations == extra_annotations == set():
data['values'] = values
return data

missing_text = "There are samples with ids {0} which are missing their annotation"\
.format(map(id_to_gsm, missing_annotations)) \
if missing_annotations else None
extra_text = "There are samples with ids {0} which doesn't belongs to series {1}"\
.format(list(extra_annotations), data['series'].id) \
if extra_annotations else None

raise serializers.ValidationError(compact([missing_text, extra_text]))

class TagSerializer(serializers.ModelSerializer):
class Meta:
model = Tag
Expand Down
40 changes: 37 additions & 3 deletions starapi/viewsets.py
Expand Up @@ -2,24 +2,28 @@

from cacheops import cached_as
from django.http import JsonResponse, HttpResponse
from funcy import walk_keys
from django.db import transaction
from funcy import walk_keys, first
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Return empty line.

from pandas.computation.ops import UndefinedVariableError
from rest_framework import viewsets, exceptions
from rest_framework.decorators import list_route
from rest_framework.permissions import AllowAny
from rest_framework.permissions import AllowAny, IsAuthenticatedOrReadOnly
from rest_framework.renderers import CoreJSONRenderer
from rest_framework.response import Response
from rest_framework.schemas import SchemaGenerator
from rest_framework.serializers import ValidationError
from rest_framework.views import APIView
from rest_framework_swagger import renderers

from tags.models import SerieAnnotation, Tag, SampleAnnotation
from tags.models import SerieAnnotation, Tag, SampleAnnotation, SeriesTag
from tags.misc import save_annotation, save_validation
from legacy.models import Platform, Series, Analysis, MetaAnalysis, PlatformProbe
from s3field.ops import frame_dumps
from analysis.analysis import get_analysis_df
from .serializers import (PlatformSerializer, SeriesSerializer, AnalysisSerializer,
AnalysisParamSerializer, SerieAnnotationSerializer,
TagSerializer, MetaAnalysisSerializer, PlatformProbeSerializer,
SampleAnnotationValidator,
)


Expand Down Expand Up @@ -74,6 +78,7 @@ class SerieAnnotationViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = SerieAnnotationSerializer

class SampleAnnotationViewSet(viewsets.ViewSet):
permission_classes = [IsAuthenticatedOrReadOnly]
KEYS = {
'serie_annotation__tag__tag_name': 'tag_name',
'serie_annotation__tag_id': 'tag_id',
Expand All @@ -88,6 +93,35 @@ class SampleAnnotationViewSet(viewsets.ViewSet):
'annotation': 'annotation',
}

@transaction.atomic
def create(self, request):
user_id = request.user.id

serializer = SampleAnnotationValidator(
data=request.data)
serializer.is_valid(raise_exception=True)

series = serializer.validated_data['series']
tag = serializer.validated_data['tag']

series_tag = first(SeriesTag.objects.filter(series=series, tag=tag))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use Django's QuerySet.first().

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also specify .order_by('id'), now this code is unstable.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also MAJOR error not filtering by platform here. SeriesTag is defined by these 3 things: serie, platform, tag. And filter by is_active=True.


if not series_tag:
# create annotation
save_annotation(user_id, serializer.validated_data, True)
else:
# create validation
if series_tag.created_by.id == user_id:
raise ValidationError(
{'non_field_errors': ["You can't validate your own annotation"]})
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't it non field error by default?


res, err = save_validation(user_id, series_tag, serializer.validated_data, True)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You get orphan ValidationJob here, clean it up.

if not res:
raise ValidationError(
{'non_field_errors': [err]})

return Response(status=201)

def list(self, request, format=None):
"""
Download all samples annotations.
Expand Down
5 changes: 5 additions & 0 deletions stargeo/settings.py
Expand Up @@ -50,6 +50,7 @@
'rest_framework',
'rest_framework_swagger',
'starapi',
'rest_framework.authtoken',
)
if DEBUG:
INSTALLED_APPS += ('debug_toolbar', 'django_extensions')
Expand Down Expand Up @@ -273,6 +274,10 @@
'rest_framework.renderers.JSONRenderer',
),
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination',
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework.authentication.SessionAuthentication',
'rest_framework.authentication.TokenAuthentication',
),
'PAGE_SIZE': 10
}

Expand Down