Skip to content
Snippets Groups Projects
Commit 53f83d7b authored by Jonathan Séguin's avatar Jonathan Séguin
Browse files

Merge branch '126-explore-graphql-as-separate-api' into 'master'

Resolve "Explore GraphQL as separate API"

See merge request !77
parents 7aa66d23 de5b6023
No related branches found
No related tags found
1 merge request!77Resolve "Explore GraphQL as separate API"
...@@ -51,6 +51,7 @@ INSTALLED_APPS = [ ...@@ -51,6 +51,7 @@ INSTALLED_APPS = [
'chartjs', 'chartjs',
'private_storage', 'private_storage',
'loginas', 'loginas',
'graphene_django',
'portal' 'portal'
] ]
...@@ -238,6 +239,7 @@ USE_I18N = True ...@@ -238,6 +239,7 @@ USE_I18N = True
USE_L10N = True USE_L10N = True
USE_TZ = True USE_TZ = True
ENV_PATH = os.path.abspath(os.path.dirname(__file__)) ENV_PATH = os.path.abspath(os.path.dirname(__file__))
MEDIA_ROOT = os.path.join(ENV_PATH, 'media/') MEDIA_ROOT = os.path.join(ENV_PATH, 'media/')
MEDIA_URL = "/media/" MEDIA_URL = "/media/"
......
...@@ -13,12 +13,14 @@ Including another URLconf ...@@ -13,12 +13,14 @@ Including another URLconf
1. Import the include() function: from django.urls import include, path 1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
""" """
import private_storage.urls
from django.conf import settings from django.conf import settings
from django.conf.urls.static import static from django.conf.urls.static import static
from django.contrib import admin from django.contrib import admin
from django.urls import include, path from django.urls import include, path
from django.views.decorators.csrf import csrf_exempt
import private_storage.urls from portal.graphql_schema import schema
from portal.graphql_schema import PrivateGraphQLView
urlpatterns = [ urlpatterns = [
path('', include('portal.urls')), path('', include('portal.urls')),
...@@ -27,4 +29,5 @@ urlpatterns = [ ...@@ -27,4 +29,5 @@ urlpatterns = [
path('private-media/', include(private_storage.urls)), path('private-media/', include(private_storage.urls)),
path("admin/", include('loginas.urls')), path("admin/", include('loginas.urls')),
path('oauth/', include('social_django.urls', namespace='social')), path('oauth/', include('social_django.urls', namespace='social')),
path("graphql/", csrf_exempt(PrivateGraphQLView.as_view(graphiql=True, schema=schema))),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
import graphene
from django.contrib.auth import get_user_model
from graphene_django.types import DjangoObjectType
from graphene_django.views import GraphQLView
from portal.models import DataFile, DataSet, Profile
from portal.views import TokenLoginMixin
# Protect graphql API page
class PrivateGraphQLView(TokenLoginMixin, GraphQLView):
pass
# Create a GraphQL type for the various models
class DataFileType(DjangoObjectType):
class Meta:
model = DataFile
fields = "__all__"
class DataSetType(DjangoObjectType):
class Meta:
model = DataSet
fields = "__all__"
class ProfileSetType(DjangoObjectType):
class Meta:
model = Profile
class UserType(DjangoObjectType):
class Meta:
model = get_user_model()
# Create a Query type
class Query(graphene.ObjectType):
datafile = graphene.Field(DataFileType, id=graphene.String(), dbid=graphene.Int())
dataset = graphene.Field(DataSetType, id=graphene.String())
datafiles = graphene.List(DataFileType, key=graphene.String(), value=graphene.String(), dataset=graphene.String())
datasets = graphene.List(DataSetType)
def resolve_datafile(self, info, **kwargs):
id = kwargs.get('id')
dbid = kwargs.get('dbid')
if id is not None:
return DataFile.objects.get(iric_data_id=id)
elif dbid is not None:
return DataFile.objects.get(pk=dbid)
def resolve_dataset(self, info, **kwargs):
id = kwargs.get('id')
if id is not None:
return DataSet.objects.get(iric_data_id=id)
return None
def resolve_datafiles(self, info, **kwargs):
# qs = DataFile.objects.accessible_to_profile(self.request.user.profile)
qs = DataFile.objects
key = kwargs.get('key', None)
value = kwargs.get('value', None)
dataset = kwargs.get('dataset', None)
if key and value:
qs = qs.filter(annotations__contains={key: value})
elif key:
qs = qs.filter(annotations__has_key=key)
elif value:
qs = qs.filter(annotations__values__icontains=value)
if dataset:
qs = qs.filter(datasets__iric_data_id=dataset)
return qs.distinct()
def resolve_datasets(self, info, **kwargs):
return DataSet.objects.all()
class Mutation(graphene.ObjectType):
pass
schema = graphene.Schema(query=Query, auto_camelcase=False)
...@@ -3,7 +3,7 @@ import binascii ...@@ -3,7 +3,7 @@ import binascii
import hashlib import hashlib
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import User from django.contrib.auth import get_user_model
from django.contrib.postgres.fields import HStoreField from django.contrib.postgres.fields import HStoreField
from django.db import models from django.db import models
from django.db.models import Q from django.db.models import Q
...@@ -14,11 +14,11 @@ from private_storage.fields import PrivateFileField ...@@ -14,11 +14,11 @@ from private_storage.fields import PrivateFileField
from portal.storage import HashStorage from portal.storage import HashStorage
class ProfileManager(models.Manager): class ProfileManager(models.Manager):
def exclude_profile(self, profile): def exclude_profile(self, profile):
return super().get_queryset().exclude(id=profile.id) return super().get_queryset().exclude(id=profile.id)
class Profile(models.Model): class Profile(models.Model):
"""example Documentation: """example Documentation:
https://simpleisbetterthancomplex.com/tutorial/2016/07/22/how-to-extend-django-user-model.html#onetoone https://simpleisbetterthancomplex.com/tutorial/2016/07/22/how-to-extend-django-user-model.html#onetoone
...@@ -31,7 +31,7 @@ class Profile(models.Model): ...@@ -31,7 +31,7 @@ class Profile(models.Model):
accountname = models.CharField(max_length=150, null=True, blank=True) accountname = models.CharField(max_length=150, null=True, blank=True)
unix_username = models.CharField(max_length=150, null=True, blank=True, verbose_name=_("Unix username")) unix_username = models.CharField(max_length=150, null=True, blank=True, verbose_name=_("Unix username"))
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile') user = models.OneToOneField(get_user_model(), on_delete=models.CASCADE, related_name='profile')
labs = models.ManyToManyField('Lab', related_name='members') labs = models.ManyToManyField('Lab', related_name='members')
api_token = models.CharField(max_length=64, null=True, unique=True, blank=False, default=None) api_token = models.CharField(max_length=64, null=True, unique=True, blank=False, default=None)
......
...@@ -34,4 +34,3 @@ class HashStorage(PrivateStorage): ...@@ -34,4 +34,3 @@ class HashStorage(PrivateStorage):
if not os.listdir(hashdir): if not os.listdir(hashdir):
# clean up # clean up
os.rmdir(hashdir) os.rmdir(hashdir)
...@@ -16,8 +16,12 @@ ...@@ -16,8 +16,12 @@
</div> </div>
<p> <p>
{% trans 'Use this token to authenticate your API calls by inserting it in the request header under "Iric-Auth-Token".' %} {% trans 'Use this token to authenticate your API calls by inserting it in the request header under "Iric-Auth-Token".' %}
Ex.
</p> </p>
<p>
{% trans 'Two APIs exists to query IRIC Data :' %}
</p>
<h3> {% trans 'IRIC Data API v1 (python requests example)' %} </h3>
<div class="col-10 alert alert-secondary"> <div class="col-10 alert alert-secondary">
<pre class='mb-0'><code>import requests <pre class='mb-0'><code>import requests
url = '{{ request.scheme }}://{{ request.get_host }}/api/v1/my-datasets/list/json/' url = '{{ request.scheme }}://{{ request.get_host }}/api/v1/my-datasets/list/json/'
...@@ -25,6 +29,24 @@ headers = {'Iric-Auth-Token': '{{user.profile.api_token}}'} ...@@ -25,6 +29,24 @@ headers = {'Iric-Auth-Token': '{{user.profile.api_token}}'}
r = requests.get(url, headers=headers) r = requests.get(url, headers=headers)
r.json()</code></pre> r.json()</code></pre>
</div> </div>
<h3> {% trans 'GraphQL API (python requests example)' %} </h3>
<p>
<i>{% trans 'NOTE: A GraphQL sandbox is also available online' %} : <a href="{{ request.scheme }}://{{ request.get_host }}/graphql/">{{ request.scheme }}://{{ request.get_host }}/graphql/<a></i>
</p>
<div class="col-10 alert alert-secondary">
<pre class='mb-0'><code>import requests
url = '{{ request.scheme }}://{{ request.get_host }}/graphql/'
headers = {'Iric-Auth-Token': '{{user.profile.api_token}}'}
query = """{
datafiles {
filename
}
}
"""
r = requests.post(url, headers=headers, json={'query': query})
r.json()</code></pre>
</div>
</div> </div>
</div> </div>
......
...@@ -8,19 +8,18 @@ from .views.public import IndexView ...@@ -8,19 +8,18 @@ from .views.public import IndexView
from .views.secure.alert import (AlertCreateView, AlertDeleteView, from .views.secure.alert import (AlertCreateView, AlertDeleteView,
AlertJSONListView, AlertListView, AlertJSONListView, AlertListView,
AlertUpdateView) AlertUpdateView)
from .views.secure.api import (APIView, from .views.secure.api import (AdminDataSetJSONListView, APIView,
AdminDataSetJSONListView,
DataFileAnnotationJSONView, DataFileAnnotationJSONView,
DataFileDeleteJSONView,
DataFileLookupJSONListView, DataFileLookupJSONListView,
DataFileMetadataJSONView, DataFileMetadataJSONView,
UserDataSetJSONListView, UserDataSetJSONListView)
DataFileDeleteJSONView)
from .views.secure.dashboard import AdminDashboardView, DashboardView from .views.secure.dashboard import AdminDashboardView, DashboardView
from .views.secure.datafile import (DataFileAnnotateView, from .views.secure.datafile import (DataFileAnnotateView,
DataFileAnnotationDownloadView, DataFileAnnotationDownloadView,
DataFileBatchAddAnnotation, DataFileBatchAddAnnotation,
DataFileBatchDelete,
DataFileBatchRemoveAnnotation, DataFileBatchRemoveAnnotation,
DataFileBatchDelete, ########### xj
DataFileDeleteView, DataFileDetailsView, DataFileDeleteView, DataFileDetailsView,
DataFileDownloadView, DataFileDownloadView,
DataFilesJSONListView, DataFilesView, DataFilesJSONListView, DataFilesView,
......
import copy import copy
from django.contrib.auth.mixins import UserPassesTestMixin from django.contrib.auth import authenticate, login
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from django.contrib.messages.views import SuccessMessageMixin from django.contrib.messages.views import SuccessMessageMixin
from django.http import JsonResponse from django.http import JsonResponse
from django.utils import translation from django.utils import translation
...@@ -9,11 +10,9 @@ from django.views.generic import ListView ...@@ -9,11 +10,9 @@ from django.views.generic import ListView
from django.views.generic.base import ContextMixin from django.views.generic.base import ContextMixin
from django.views.generic.detail import BaseDetailView, SingleObjectMixin from django.views.generic.detail import BaseDetailView, SingleObjectMixin
from django.views.generic.edit import ModelFormMixin from django.views.generic.edit import ModelFormMixin
from django.contrib.auth import login, authenticate from portal.models import (AppSettings, DataFile, Lab, Log, Profile,
from django.contrib.auth.mixins import LoginRequiredMixin ShareGroup)
from ..models import (AppSettings, DataFile, DataSet, Lab, Log, Profile,
ShareGroup)
class TokenLoginMixin(LoginRequiredMixin): class TokenLoginMixin(LoginRequiredMixin):
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
...@@ -23,7 +22,7 @@ class TokenLoginMixin(LoginRequiredMixin): ...@@ -23,7 +22,7 @@ class TokenLoginMixin(LoginRequiredMixin):
login(request, user) login(request, user)
else: else:
return JsonResponse({'error': _('Malformed authorization header or invalid token')}, status=401) return JsonResponse({'error': _('Malformed authorization header or invalid token')}, status=401)
return super().dispatch(request, *args, **kwargs) return super().dispatch(request, *args, **kwargs)
......
...@@ -2,18 +2,16 @@ import logging ...@@ -2,18 +2,16 @@ import logging
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.db.models import Q from django.db.models import Q
from django.shortcuts import get_object_or_404
from django.views.generic import TemplateView
from django.http import JsonResponse from django.http import JsonResponse
from django.views.generic.detail import BaseDetailView from django.shortcuts import get_object_or_404
from django.utils.decorators import method_decorator
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from portal.views import TokenLoginMixin, SlugMixin
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from django.utils.decorators import method_decorator from django.views.generic import TemplateView
from django.views.generic.detail import BaseDetailView
from ...models import DataFile, DataSet, Profile from portal.models import DataFile, DataSet, Profile
from ...views import JSONListView, JSONView, StaffViewMixin, ActivePageViewMixin from portal.views import (ActivePageViewMixin, JSONListView, JSONView,
SlugMixin, TokenLoginMixin)
logger = logging.getLogger('debug') logger = logging.getLogger('debug')
...@@ -105,7 +103,7 @@ class DataFileDeleteJSONView(TokenLoginMixin, SlugMixin, BaseDetailView): ...@@ -105,7 +103,7 @@ class DataFileDeleteJSONView(TokenLoginMixin, SlugMixin, BaseDetailView):
def delete(self, request, *args, **kwargs): def delete(self, request, *args, **kwargs):
try: try:
o = self.get_object() o = self.get_object()
except Exception as e: except Exception:
return JsonResponse({'error': _('File cannot be found')}, status=403) return JsonResponse({'error': _('File cannot be found')}, status=403)
if o in DataFile.objects.editable_by_profile(self.request.user.profile).all(): if o in DataFile.objects.editable_by_profile(self.request.user.profile).all():
......
aniso8601==7.0.0
autopep8==1.4.3 autopep8==1.4.3
certifi==2018.11.29 certifi==2018.11.29
cffi==1.14.3 cffi==1.14.3
...@@ -12,10 +13,15 @@ django-loginas==0.3.8 ...@@ -12,10 +13,15 @@ django-loginas==0.3.8
django-model-utils==3.1.2 django-model-utils==3.1.2
django-private-storage==2.2 django-private-storage==2.2
flake8==3.6.0 flake8==3.6.0
graphene==2.1.8
graphene-django==2.15.0
graphql-core==2.3.2
graphql-relay==2.0.1
idna==2.8 idna==2.8
mccabe==0.6.1 mccabe==0.6.1
oauthlib==3.1.0 oauthlib==3.1.0
Pillow==8.1.0 Pillow==5.3.0
promise==2.3
psycopg2-binary==2.8.4 psycopg2-binary==2.8.4
pyasn1==0.4.4 pyasn1==0.4.4
pyasn1-modules==0.2.2 pyasn1-modules==0.2.2
...@@ -29,8 +35,11 @@ python3-openid==3.2.0 ...@@ -29,8 +35,11 @@ python3-openid==3.2.0
pytz==2018.7 pytz==2018.7
requests==2.21.0 requests==2.21.0
requests-oauthlib==1.3.0 requests-oauthlib==1.3.0
Rx==1.6.1
singledispatch==3.4.0.3
six==1.12.0 six==1.12.0
social-auth-app-django==3.1.0 social-auth-app-django==3.1.0
social-auth-core==3.3.3 social-auth-core==3.3.3
SQLAlchemy==0.8.3 SQLAlchemy==0.8.3
text-unidecode==1.3
urllib3==1.24.1 urllib3==1.24.1
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment